/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/search/SearchResultsView.js
604 строки
26 KB
Nitesh Kumar
LSP Find References Feature (#14693)
15 апр 2019, 13:42
15 апр 2019, 13:42
317992f
Код
Авторство
О чём код?
/* * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /* * Panel showing search results for a Find/Replace in Files operation. */ define(function (require, exports, module) { "use strict"; var CommandManager = require("command/CommandManager"), EventDispatcher = require("utils/EventDispatcher"), Commands = require("command/Commands"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), ProjectManager = require("project/ProjectManager"), FileViewController = require("project/FileViewController"), FileUtils = require("file/FileUtils"), FindUtils = require("search/FindUtils"), WorkspaceManager = require("view/WorkspaceManager"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), HealthLogger = require("utils/HealthLogger"), _ = require("thirdparty/lodash"), Mustache = require("thirdparty/mustache/mustache"), searchPanelTemplate = require("text!htmlContent/search-panel.html"), searchResultsTemplate = require("text!htmlContent/search-results.html"), searchSummaryTemplate = require("text!htmlContent/search-summary.html"); /** * @const * The maximum results to show per page. * @type {number} */ var RESULTS_PER_PAGE = 100; /** * @const * Debounce time for document changes updating the search results view. * @type {number} */ var UPDATE_TIMEOUT = 400; /** * @constructor * Handles the search results panel. * Dispatches the following events: * replaceBatch - when the "Replace" button is clicked. * close - when the panel is closed. * * @param {SearchModel} model The model that this view is showing. * @param {string} panelID The CSS ID to use for the panel. * @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). * @param {string} type type to identify if it is reference search or string match serach */ function SearchResultsView(model, panelID, panelName, type) { var panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100); this._$summary = this._panel.$panel.find(".title"); this._$table = this._panel.$panel.find(".table-container"); this._model = model; this._searchResultsType = type; } EventDispatcher.makeEventDispatcher(SearchResultsView.prototype); /** @type {SearchModel} The search results model we're viewing. */ SearchResultsView.prototype._model = null; /** * Array with content used in the Results Panel * @type {Array.<{fileIndex: number, filename: string, fullPath: string, items: Array.<Object>}>} */ SearchResultsView.prototype._searchList = []; /** @type {Panel} Bottom panel holding the search results */ SearchResultsView.prototype._panel = null; /** @type {?string} The full path of the file that was open in the main editor on the initial search */ SearchResultsView.prototype._initialFilePath = null; /** @type {number} The index of the first result that is displayed */ SearchResultsView.prototype._currentStart = 0; /** @type {boolean} Used to remake the replace all summary after it is changed */ SearchResultsView.prototype._allChecked = false; /** @type {$.Element} The currently selected row */ SearchResultsView.prototype._$selectedRow = null; /** @type {$.Element} The element where the title is placed */ SearchResultsView.prototype._$summary = null; /** @type {$.Element} The table that holds the results */ SearchResultsView.prototype._$table = null; /** @type {number} The ID we use for timeouts when handling model changes. */ SearchResultsView.prototype._timeoutID = null; /** @type {string} The Id we use to check if it is reference search or match search */ SearchResultsView.prototype._searchResultsType = null; /** * @private * Handles when model changes. Updates the view, buffering changes if necessary so as not to churn too much. */ SearchResultsView.prototype._handleModelChange = function (quickChange) { // If this is a replace, to avoid complications with updating, just close ourselves if we hear about // a results model change after we've already shown the results initially. // TODO: notify user, re-do search in file if (this._model.isReplace) { this.close(); return; } var self = this; if (this._timeoutID) { window.clearTimeout(this._timeoutID); } if (quickChange) { this._timeoutID = window.setTimeout(function () { self._updateResults(); self._timeoutID = null; }, UPDATE_TIMEOUT); } else { this._updateResults(); } }; /** * @private * Adds the listeners for close, prev, next, first, last and check all */ SearchResultsView.prototype._addPanelListeners = function () { var self = this; this._panel.$panel .off(".searchResults") // Remove the old events .on("dblclick.searchResults", ".toolbar", function() { self.close(); }) .on("click.searchResults", ".close", function () { self.close(); }) // The link to go the first page .on("click.searchResults", ".first-page:not(.disabled)", function () { self._currentStart = 0; self._render(); HealthLogger.searchDone(HealthLogger.SEARCH_FIRST_PAGE); }) // The link to go the previous page .on("click.searchResults", ".prev-page:not(.disabled)", function () { self._currentStart -= RESULTS_PER_PAGE; self._render(); HealthLogger.searchDone(HealthLogger.SEARCH_PREV_PAGE); }) // The link to go to the next page .on("click.searchResults", ".next-page:not(.disabled)", function () { self.trigger('getNextPage'); HealthLogger.searchDone(HealthLogger.SEARCH_NEXT_PAGE); }) // The link to go to the last page .on("click.searchResults", ".last-page:not(.disabled)", function () { self.trigger('getLastPage'); HealthLogger.searchDone(HealthLogger.SEARCH_LAST_PAGE); }) // Add the file to the working set on double click .on("dblclick.searchResults", ".table-container tr:not(.file-section)", function (e) { var item = self._searchList[$(this).data("file-index")]; FileViewController.openFileAndAddToWorkingSet(item.fullPath); }) // Add the click event listener directly on the table parent .on("click.searchResults .table-container", function (e) { var $row = $(e.target).closest("tr"); if ($row.length) { if (self._$selectedRow) { self._$selectedRow.removeClass("selected"); } $row.addClass("selected"); self._$selectedRow = $row; var searchItem = self._searchList[$row.data("file-index")], fullPath = searchItem.fullPath; // This is a file title row, expand/collapse on click if ($row.hasClass("file-section")) { var $titleRows, collapsed = !self._model.results[fullPath].collapsed; if (e.metaKey || e.ctrlKey) { //Expand all / Collapse all $titleRows = $(e.target).closest("table").find(".file-section"); } else { // Clicking the file section header collapses/expands result rows for that file $titleRows = $row; } $titleRows.each(function () { fullPath = self._searchList[$(this).data("file-index")].fullPath; searchItem = self._model.results[fullPath]; if (searchItem.collapsed !== collapsed) { searchItem.collapsed = collapsed; $(this).nextUntil(".file-section").toggle(); $(this).find(".disclosure-triangle").toggleClass("expanded"); } }); //In Expand/Collapse all, reset all search results 'collapsed' flag to same value(true/false). if (e.metaKey || e.ctrlKey) { FindUtils.setCollapseResults(collapsed); _.forEach(self._model.results, function (item) { item.collapsed = collapsed; }); } // This is a file row, show the result on click } else { // Grab the required item data var item = searchItem.items[$row.data("item-index")]; CommandManager.execute(Commands.FILE_OPEN, {fullPath: fullPath}) .done(function (doc) { // Opened document is now the current main editor EditorManager.getCurrentFullEditor().setSelection(item.start, item.end, true); }); } } }); function updateHeaderCheckbox($checkAll) { var $allFileRows = self._panel.$panel.find(".file-section"), $checkedFileRows = $allFileRows.filter(function (index) { return $(this).find(".check-one-file").is(":checked"); }); if ($checkedFileRows.length === $allFileRows.length) { $checkAll.prop("checked", true); } } function updateFileAndHeaderCheckboxes($clickedRow, isChecked) { var $firstMatch = ($clickedRow.data("item-index") === 0) ? $clickedRow : $clickedRow.prevUntil(".file-section").last(), $fileRow = $firstMatch.prev(), $siblingRows = $fileRow.nextUntil(".file-section"), $fileCheckbox = $fileRow.find(".check-one-file"), $checkAll = self._panel.$panel.find(".check-all"); if (isChecked) { if (!$fileCheckbox.is(":checked")) { var $checkedSibilings = $siblingRows.filter(function (index) { return $(this).find(".check-one").is(":checked"); }); if ($checkedSibilings.length === $siblingRows.length) { $fileCheckbox.prop("checked", true); if (!$checkAll.is(":checked")) { updateHeaderCheckbox($checkAll); } } } } else { if ($checkAll.is(":checked")) { $checkAll.prop("checked", false); } if ($fileCheckbox.is(":checked")) { $fileCheckbox.prop("checked", false); } } } // Add the Click handlers for replace functionality if required if (this._model.isReplace) { this._panel.$panel .on("click.searchResults", ".check-all", function (e) { var isChecked = $(this).is(":checked"); _.forEach(self._model.results, function (results) { results.matches.forEach(function (match) { match.isChecked = isChecked; }); }); self._$table.find(".check-one").prop("checked", isChecked); self._$table.find(".check-one-file").prop("checked", isChecked); self._allChecked = isChecked; }) .on("click.searchResults", ".check-one-file", function (e) { var isChecked = $(this).is(":checked"), $row = $(e.target).closest("tr"), item = self._searchList[$row.data("file-index")], $matchRows = $row.nextUntil(".file-section"), $checkAll = self._panel.$panel.find(".check-all"); if (item) { self._model.results[item.fullPath].matches.forEach(function (match) { match.isChecked = isChecked; }); } $matchRows.find(".check-one").prop("checked", isChecked); if (!isChecked) { if ($checkAll.is(":checked")) { $checkAll.prop("checked", false); } } else if (!$checkAll.is(":checked")) { updateHeaderCheckbox($checkAll); } e.stopPropagation(); }) .on("click.searchResults", ".check-one", function (e) { var $row = $(e.target).closest("tr"), item = self._searchList[$row.data("file-index")], match = self._model.results[item.fullPath].matches[$row.data("match-index")]; match.isChecked = $(this).is(":checked"); updateFileAndHeaderCheckboxes($row, match.isChecked); e.stopPropagation(); }) .on("click.searchResults", ".replace-checked", function (e) { self.trigger("replaceBatch"); }); } }; /** * @private * Shows the Results Summary */ SearchResultsView.prototype._showSummary = function () { var count = this._model.countFilesMatches(), lastIndex = this._getLastIndex(count.matches), typeStr = (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, filesStr, summary; if(this._searchResultsType === "reference") { typeStr = (count.matches > 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES; } filesStr = StringUtils.format( Strings.FIND_NUM_FILES, count.files, (count.files > 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE) ); // This text contains some formatting, so all the strings are assumed to be already escaped summary = StringUtils.format( Strings.FIND_TITLE_SUMMARY, this._model.exceedsMaximum ? Strings.FIND_IN_FILES_MORE_THAN : "", String(count.matches), typeStr, filesStr ); this._$summary.html(Mustache.render(searchSummaryTemplate, { query: (this._model.queryInfo && this._model.queryInfo.query && this._model.queryInfo.query.toString()) || "", replaceWith: this._model.replaceText, titleLabel: this._model.isReplace ? Strings.FIND_REPLACE_TITLE_LABEL : Strings.FIND_TITLE_LABEL, scope: this._model.scope ? " " + FindUtils.labelForScope(this._model.scope) + " " : "", summary: summary, allChecked: this._allChecked, hasPages: count.matches > RESULTS_PER_PAGE, results: StringUtils.format(Strings.FIND_IN_FILES_PAGING, this._currentStart + 1, lastIndex), hasPrev: this._currentStart > 0, hasNext: lastIndex < count.matches, replace: this._model.isReplace, Strings: Strings })); }; /** * @private * Shows the current set of results. */ SearchResultsView.prototype._render = function () { var searchItems, match, i, item, multiLine, count = this._model.countFilesMatches(), searchFiles = this._model.prioritizeOpenFile(this._initialFilePath), lastIndex = this._getLastIndex(count.matches), matchesCounter = 0, showMatches = false, allInFileChecked = true, self = this; this._showSummary(); this._searchList = []; // Iterates throuh the files to display the results sorted by filenamess. The loop ends as soon as // we filled the results for one page searchFiles.some(function (fullPath) { showMatches = true; item = self._model.results[fullPath]; // Since the amount of matches on this item plus the amount of matches we skipped until // now is still smaller than the first match that we want to display, skip these. if (matchesCounter + item.matches.length < self._currentStart) { matchesCounter += item.matches.length; showMatches = false; // If we still haven't skipped enough items to get to the first match, but adding the // item matches to the skipped ones is greater the the first match we want to display, // then we can display the matches from this item skipping the first ones } else if (matchesCounter < self._currentStart) { i = self._currentStart - matchesCounter; matchesCounter = self._currentStart; // If we already skipped enough matches to get to the first match to display, we can start // displaying from the first match of this item } else if (matchesCounter < lastIndex) { i = 0; // We can't display more items by now. Break the loop } else { return true; } if (showMatches && i < item.matches.length) { // Add a row for each match in the file searchItems = []; allInFileChecked = true; // Add matches until we get to the last match of this item, or filling the page while (i < item.matches.length && matchesCounter < lastIndex) { match = item.matches[i]; multiLine = match.start.line !== match.end.line; searchItems.push({ fileIndex: self._searchList.length, itemIndex: searchItems.length, matchIndex: i, line: match.start.line + 1, pre: match.line.substr(0, match.start.ch - match.highlightOffset), highlight: match.line.substring(match.start.ch - match.highlightOffset, multiLine ? undefined : match.end.ch - match.highlightOffset), post: multiLine ? "\u2026" : match.line.substr(match.end.ch - match.highlightOffset), start: match.start, end: match.end, isChecked: match.isChecked, isCollapsed: item.collapsed }); if (!match.isChecked) { allInFileChecked = false; } matchesCounter++; i++; } // Add a row for each file var relativePath = FileUtils.getDirectoryPath(ProjectManager.makeProjectRelativeIfPossible(fullPath)), directoryPath = FileUtils.getDirectoryPath(relativePath), displayFileName = StringUtils.format( Strings.FIND_IN_FILES_FILE_PATH, StringUtils.breakableUrl(FileUtils.getBaseName(fullPath)), StringUtils.breakableUrl(directoryPath), directoryPath ? "—" : "" ); self._searchList.push({ fileIndex: self._searchList.length, filename: displayFileName, fullPath: fullPath, isChecked: allInFileChecked, items: searchItems, isCollapsed: item.collapsed }); } }); // Insert the search results this._$table .empty() .append(Mustache.render(searchResultsTemplate, { replace: this._model.isReplace, searchList: this._searchList, Strings: Strings })); if (this._$selectedRow) { this._$selectedRow.removeClass("selected"); this._$selectedRow = null; } this._panel.show(); this._$table.scrollTop(0); // Otherwise scroll pos from previous contents is remembered }; /** * Updates the results view after a model change, preserving scroll position and selection. */ SearchResultsView.prototype._updateResults = function () { // In general this shouldn't get called if the panel is closed, but in case some // asynchronous process kicks this (e.g. a debounced model change), we double-check. if (this._panel.isVisible()) { var scrollTop = this._$table.scrollTop(), index = this._$selectedRow ? this._$selectedRow.index() : null, numMatches = this._model.countFilesMatches().matches; if (this._currentStart > numMatches) { this._currentStart = this._getLastCurrentStart(numMatches); } this._render(); this._$table.scrollTop(scrollTop); if (index) { this._$selectedRow = this._$table.find("tr:eq(" + index + ")"); this._$selectedRow.addClass("selected"); } } }; /** * @private * Returns one past the last result index displayed for the current page. * @param {number} numMatches * @return {number} */ SearchResultsView.prototype._getLastIndex = function (numMatches) { return Math.min(this._currentStart + RESULTS_PER_PAGE, numMatches); }; /** * Shows the next page of the resultrs view if possible */ SearchResultsView.prototype.showNextPage = function () { this._currentStart += RESULTS_PER_PAGE; this._render(); }; /** * Shows the last page of the results view. */ SearchResultsView.prototype.showLastPage = function () { this._currentStart = this._getLastCurrentStart(); this._render(); }; /** * @private * Returns the last possible current start based on the given number of matches * @param {number=} numMatches * @return {number} */ SearchResultsView.prototype._getLastCurrentStart = function (numMatches) { numMatches = numMatches || this._model.countFilesMatches().matches; return Math.floor((numMatches - 1) / RESULTS_PER_PAGE) * RESULTS_PER_PAGE; }; /** * Opens the results panel and displays the current set of results from the model. */ SearchResultsView.prototype.open = function () { // Clear out any paging/selection state. this._currentStart = 0; this._$selectedRow = null; this._allChecked = true; // Save the currently open document's fullpath, if any, so we can sort it to the top of the result list. var currentDoc = DocumentManager.getCurrentDocument(); this._initialFilePath = currentDoc ? currentDoc.file.fullPath : null; this._render(); // Listen for user interaction events with the panel and change events from the model. this._addPanelListeners(); this._model.on("change.SearchResultsView", this._handleModelChange.bind(this)); }; /** * Hides the Search Results Panel and unregisters listeners. */ SearchResultsView.prototype.close = function () { if (this._panel && this._panel.isVisible()) { this._$table.empty(); this._panel.hide(); this._panel.$panel.off(".searchResults"); this._model.off("change.SearchResultsView"); this.trigger("close"); } }; // Public API exports.SearchResultsView = SearchResultsView; });