From 8fcdc4f1277dabc3aa81fe4c56b2b38b8cded913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Malbr=C3=A1n?= Date: Sun, 17 Mar 2013 23:05:53 -0300 Subject: [PATCH] Lots of Find in Files improvements --- src/base-config/keyboard.json | 84 +-- src/command/Commands.js | 17 +- src/command/DefaultMenus.js | 30 +- src/command/Menus.js | 10 +- src/htmlContent/search-dialog.html | 7 + src/htmlContent/search-results.html | 16 + src/nls/root/strings.js | 27 +- src/project/ProjectManager.js | 23 +- src/search/FindInFiles.js | 842 +++++++++++++++++++++------- src/search/FindReplace.js | 8 +- test/spec/FindReplace-test.js | 106 ++-- 11 files changed, 860 insertions(+), 310 deletions(-) create mode 100644 src/htmlContent/search-dialog.html create mode 100644 src/htmlContent/search-results.html diff --git a/src/base-config/keyboard.json b/src/base-config/keyboard.json index bacaf2ea012..9790a476207 100644 --- a/src/base-config/keyboard.json +++ b/src/base-config/keyboard.json @@ -59,39 +59,6 @@ "platform": "mac" } ], - "edit.find": [ - "Ctrl-F" - ], - "edit.findInFiles": [ - "Ctrl-Shift-F" - ], - "edit.findNext": [ - { - "key": "F3" - }, - { - "key": "Cmd-G", - "platform": "mac" - } - ], - "edit.findPrevious": [ - { - "key": "Shift-F3" - }, - { - "key": "Cmd-Shift-G", - "platform": "mac" - } - ], - "edit.replace": [ - { - "key": "Ctrl-H" - }, - { - "key": "Cmd-Alt-F", - "platform": "mac" - } - ], "edit.indent": [ { "key": "Ctrl-]" @@ -142,6 +109,57 @@ "platform": "mac" } ], + "search.find": [ + "Ctrl-F" + ], + "search.findNext": [ + { + "key": "F3" + }, + { + "key": "Cmd-G", + "platform": "mac" + } + ], + "search.findPrevious": [ + { + "key": "Shift-F3" + }, + { + "key": "Cmd-Shift-G", + "platform": "mac" + } + ], + "search.replace": [ + { + "key": "Ctrl-H" + }, + { + "key": "Cmd-Alt-F", + "platform": "mac" + } + ], + "search.findInFiles": [ + "Ctrl-Shift-F" + ], + "search.nextResult": [ + { + "key": "F4" + }, + { + "key": "Cmd-J", + "platform": "mac" + } + ], + "search.previousResult": [ + { + "key": "Shift-F4" + }, + { + "key": "Cmd-Shift-J", + "platform": "mac" + } + ], "view.hideSidebar": [ "Ctrl-Shift-H" ], diff --git a/src/command/Commands.js b/src/command/Commands.js index e61cc7fd84b..0375ed69f6c 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -57,12 +57,6 @@ define(function (require, exports, module) { exports.EDIT_PASTE = "edit.paste"; exports.EDIT_SELECT_ALL = "edit.selectAll"; exports.EDIT_SELECT_LINE = "edit.selectLine"; - exports.EDIT_FIND = "edit.find"; - exports.EDIT_FIND_IN_FILES = "edit.findInFiles"; - exports.EDIT_FIND_IN_SUBTREE = "edit.findInSubtree"; - exports.EDIT_FIND_NEXT = "edit.findNext"; - exports.EDIT_FIND_PREVIOUS = "edit.findPrevious"; - exports.EDIT_REPLACE = "edit.replace"; exports.EDIT_INDENT = "edit.indent"; exports.EDIT_UNINDENT = "edit.unindent"; exports.EDIT_DUPLICATE = "edit.duplicate"; @@ -73,6 +67,17 @@ define(function (require, exports, module) { exports.EDIT_LINE_DOWN = "edit.lineDown"; exports.TOGGLE_CLOSE_BRACKETS = "edit.autoCloseBrackets"; + // SEARCH + exports.SEARCH_FIND = "search.find"; + exports.SEARCH_FIND_NEXT = "search.findNext"; + exports.SEARCH_FIND_PREVIOUS = "search.findPrevious"; + exports.SEARCH_REPLACE = "search.replace"; + exports.SEARCH_FIND_IN_FILES = "search.findInFiles"; + exports.SEARCH_FIND_IN_SUBTREE = "search.findInSubtree"; + exports.SEARCH_FIND_IN_WORKING_SET = "search.findInWorkingSet"; + exports.SEARCH_NEXT_RESULT = "search.nextResult"; + exports.SEARCH_PREVIOUS_RESULT = "search.previousResult"; + // VIEW exports.VIEW_HIDE_SIDEBAR = "view.hideSidebar"; exports.VIEW_INCREASE_FONT_SIZE = "view.increaseFontSize"; diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 1d8c0527cc8..c9077e49c4d 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -77,15 +77,6 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.EDIT_SELECT_ALL); menu.addMenuItem(Commands.EDIT_SELECT_LINE); menu.addMenuDivider(); - menu.addMenuItem(Commands.EDIT_FIND); - menu.addMenuItem(Commands.EDIT_FIND_IN_FILES); - menu.addMenuItem(Commands.EDIT_FIND_NEXT); - - menu.addMenuItem(Commands.EDIT_FIND_PREVIOUS); - - menu.addMenuDivider(); - menu.addMenuItem(Commands.EDIT_REPLACE); - menu.addMenuDivider(); menu.addMenuItem(Commands.EDIT_INDENT); menu.addMenuItem(Commands.EDIT_UNINDENT); menu.addMenuItem(Commands.EDIT_DUPLICATE); @@ -98,6 +89,22 @@ define(function (require, exports, module) { menu.addMenuDivider(); menu.addMenuItem(Commands.TOGGLE_CLOSE_BRACKETS); + /* + * Search menu + */ + menu = Menus.addMenu(Strings.SEARCH_MENU, Menus.AppMenuBar.SEARCH_MENU); + menu.addMenuItem(Commands.SEARCH_FIND); + menu.addMenuItem(Commands.SEARCH_FIND_NEXT); + menu.addMenuItem(Commands.SEARCH_FIND_PREVIOUS); + menu.addMenuDivider(); + menu.addMenuItem(Commands.SEARCH_REPLACE); + menu.addMenuDivider(); + menu.addMenuItem(Commands.SEARCH_FIND_IN_FILES); + menu.addMenuItem(Commands.SEARCH_FIND_IN_WORKING_SET); + menu.addMenuItem(Commands.SEARCH_FIND_IN_SUBTREE); + menu.addMenuItem(Commands.SEARCH_NEXT_RESULT); + menu.addMenuItem(Commands.SEARCH_PREVIOUS_RESULT); + /* * View menu */ @@ -116,7 +123,6 @@ define(function (require, exports, module) { menu = Menus.addMenu(Strings.NAVIGATE_MENU, Menus.AppMenuBar.NAVIGATE_MENU); menu.addMenuItem(Commands.NAVIGATE_QUICK_OPEN); menu.addMenuItem(Commands.NAVIGATE_GOTO_LINE); - menu.addMenuItem(Commands.NAVIGATE_GOTO_DEFINITION); menu.addMenuItem(Commands.NAVIGATE_GOTO_JSLINT_ERROR); menu.addMenuDivider(); @@ -176,7 +182,7 @@ define(function (require, exports, module) { project_cmenu.addMenuItem(Commands.FILE_NEW_FOLDER); project_cmenu.addMenuItem(Commands.FILE_RENAME); project_cmenu.addMenuDivider(); - project_cmenu.addMenuItem(Commands.EDIT_FIND_IN_SUBTREE); + project_cmenu.addMenuItem(Commands.SEARCH_FIND_IN_SUBTREE); var working_set_cmenu = Menus.registerContextMenu(Menus.ContextMenuIds.WORKING_SET_MENU); working_set_cmenu.addMenuItem(Commands.FILE_CLOSE); @@ -184,7 +190,7 @@ define(function (require, exports, module) { working_set_cmenu.addMenuItem(Commands.FILE_RENAME); working_set_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); working_set_cmenu.addMenuDivider(); - working_set_cmenu.addMenuItem(Commands.EDIT_FIND_IN_SUBTREE); + working_set_cmenu.addMenuItem(Commands.SEARCH_FIND_IN_SUBTREE); working_set_cmenu.addMenuDivider(); working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_BY_ADDED); working_set_cmenu.addMenuItem(Commands.SORT_WORKINGSET_BY_NAME); diff --git a/src/command/Menus.js b/src/command/Menus.js index a227f4492ff..6ffb47859f9 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -43,6 +43,7 @@ define(function (require, exports, module) { var AppMenuBar = { FILE_MENU : "file-menu", EDIT_MENU : "edit-menu", + SEARCH_MENU : "search-menu", VIEW_MENU : "view-menu", NAVIGATE_MENU : "navigate-menu", HELP_MENU : "help-menu" @@ -77,10 +78,15 @@ define(function (require, exports, module) { FILE_SAVE_COMMANDS: {sectionMarker: Commands.FILE_SAVE}, FILE_LIVE: {sectionMarker: Commands.FILE_LIVE_FILE_PREVIEW}, + EDIT_UNDO_REDO_COMMANDS: {sectionMarker: Commands.EDIT_UNDO}, + EDIT_TEXT_COMMANDS: {sectionMarker: Commands.EDIT_CUT}, EDIT_SELECTION_COMMANDS: {sectionMarker: Commands.EDIT_SELECT_ALL}, - EDIT_FIND: {sectionMarker: Commands.EDIT_FIND}, - EDIT_REPLACE_COMMANDS: {sectionMarker: Commands.EDIT_REPLACE}, EDIT_MODIFY_SELECTION: {sectionMarker: Commands.EDIT_INDENT}, + EDIT_COMMENT_SELECTION: {sectionMarker: Commands.EDIT_LINE_COMMENT}, + + SEARCH_FIND_COMMANDS: {sectionMarker: Commands.SEARCH_FIND}, + SEARCH_REPLACE_COMMANDS: {sectionMarker: Commands.SEARCH_REPLACE}, + SEARCH_FIND_IN_FILES_COMMANDS: {sectionMarker: Commands.SEARCH_FIND_IN_FILES}, VIEW_HIDESHOW_COMMANDS: {sectionMarker: Commands.VIEW_HIDE_SIDEBAR}, VIEW_FONTSIZE_COMMANDS: {sectionMarker: Commands.VIEW_INCREASE_FONT_SIZE}, diff --git a/src/htmlContent/search-dialog.html b/src/htmlContent/search-dialog.html new file mode 100644 index 00000000000..bbe242dce0a --- /dev/null +++ b/src/htmlContent/search-dialog.html @@ -0,0 +1,7 @@ +{{CMD_FIND}}: + +
+ {{{label}}} + ({{SEARCH_REGEXP_INFO}}) +
+
diff --git a/src/htmlContent/search-results.html b/src/htmlContent/search-results.html new file mode 100644 index 00000000000..0083b724e5b --- /dev/null +++ b/src/htmlContent/search-results.html @@ -0,0 +1,16 @@ + + + {{#searchList}} + + + + {{#items}} + + + + + + {{/items}} + {{/searchList}} + +
{{{filename}}}
{{line}}{{pre}}{{highlight}}{{post}}
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index b99250cd089..2036725d8a2 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -111,15 +111,18 @@ define({ "FIND_IN_FILES_TITLE" : "for \"{4}\" {5} - {0} {1} in {2} {3}", "FIND_IN_FILES_SCOPED" : "in {0}", - "FIND_IN_FILES_NO_SCOPE" : "in project", + "FIND_IN_FILES_WORKING_SET" : "in working set", + "FIND_IN_FILES_PROJECT" : "in project", "FIND_IN_FILES_FILE" : "file", "FIND_IN_FILES_FILES" : "files", "FIND_IN_FILES_MATCH" : "match", "FIND_IN_FILES_MATCHES" : "matches", "FIND_IN_FILES_MORE_THAN" : "More than ", - "FIND_IN_FILES_MAX" : " (showing the first {0} matches)", + "FIND_IN_FILES_PAGING" : " (showing matches {0} to {1})", + "FIND_IN_FILES_LESS" : " Less", + "FIND_IN_FILES_MORE" : " More", "FIND_IN_FILES_FILE_PATH" : "File: {0}", - "FIND_IN_FILES_LINE" : "line: {0}", + "FIND_IN_FILES_LINE" : "line: {0}", "ERROR_FETCHING_UPDATE_INFO_TITLE" : "Error getting update info", "ERROR_FETCHING_UPDATE_INFO_MSG" : "There was a problem getting the latest update information from the server. Please make sure you are connected to the internet and try again.", @@ -188,12 +191,6 @@ define({ "CMD_PASTE" : "Paste", "CMD_SELECT_ALL" : "Select All", "CMD_SELECT_LINE" : "Select Line", - "CMD_FIND" : "Find", - "CMD_FIND_IN_FILES" : "Find in Files", - "CMD_FIND_IN_SUBTREE" : "Find in\u2026", - "CMD_FIND_NEXT" : "Find Next", - "CMD_FIND_PREVIOUS" : "Find Previous", - "CMD_REPLACE" : "Replace", "CMD_INDENT" : "Indent", "CMD_UNINDENT" : "Unindent", "CMD_DUPLICATE" : "Duplicate", @@ -204,6 +201,18 @@ define({ "CMD_LINE_DOWN" : "Move Line Down", "CMD_TOGGLE_CLOSE_BRACKETS" : "Auto Close Braces", + // Search menu commands + "SEARCH_MENU" : "Search", + "CMD_FIND" : "Find", + "CMD_FIND_NEXT" : "Find Next", + "CMD_FIND_PREVIOUS" : "Find Previous", + "CMD_REPLACE" : "Replace", + "CMD_FIND_IN_FILES" : "Find in Project", + "CMD_FIND_IN_WORKING_SET" : "Find in Working Set", + "CMD_FIND_IN_SUBTREE" : "Find in File/Subtree", + "CMD_NEXT_RESULT" : "Next Result", + "CMD_PREVIOUS_RESULT" : "Previous Result", + // View menu commands "VIEW_MENU" : "View", "CMD_HIDE_SIDEBAR" : "Hide Sidebar", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 708aae7062a..c70ea8b83da 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -180,9 +180,9 @@ define(function (require, exports, module) { } /** - * Returns the FileEntry or DirectoryEntry corresponding to the selected item, or null - * if no item is selected. - * + * Returns the FileEntry or DirectoryEntry corresponding to the item selected in the file tree, or null + * if no item is selected in the tree (though the working set may still have a selection; use + * getSidebarSelectedItem() to get the selection regardless of whether it's in the tree or working set). * @return {?Entry} */ function getSelectedItem() { @@ -192,6 +192,22 @@ define(function (require, exports, module) { } return null; } + + /** + * Returns the FileEntry or DirectoryEntry corresponding to the item selected in the sidebar panel, whether in + * the file tree OR in the working set; or null if no item is selected anywhere in the sidebar. + * @return {?Entry} + */ + function getSidebarSelectedItem() { + // Prefer file tree selection, else use working set selection + var selectedEntry = getSelectedItem(); + if (!selectedEntry) { + var doc = DocumentManager.getCurrentDocument(); + selectedEntry = (doc && doc.file); + } + return selectedEntry; + } + function _fileViewFocusChange() { _redraw(true); @@ -1386,6 +1402,7 @@ define(function (require, exports, module) { exports.shouldShow = shouldShow; exports.openProject = openProject; exports.getSelectedItem = getSelectedItem; + exports.getSidebarSelectedItem = getSidebarSelectedItem; exports.getInitialProjectPath = getInitialProjectPath; exports.isWelcomeProjectPath = isWelcomeProjectPath; exports.updateWelcomeProjectPath = updateWelcomeProjectPath; diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 106981ac661..8a62800744a 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ -/*global define, $, PathUtils, window */ +/*global define, $, PathUtils, Mustache, window */ /* * Adds a "find in files" command to allow the user to find all occurances of a string in all files in @@ -33,7 +33,6 @@ * FUTURE: * - Proper UI for both dialog and results * - Refactor dialog class and share with Quick File Open - * - Search files in working set that are *not* in the project * - Handle matches that span mulitple lines * - Refactor UI from functionality to enable unit testing */ @@ -42,27 +41,72 @@ define(function (require, exports, module) { "use strict"; - var Async = require("utils/Async"), - CommandManager = require("command/CommandManager"), - Commands = require("command/Commands"), - Strings = require("strings"), - StringUtils = require("utils/StringUtils"), - ProjectManager = require("project/ProjectManager"), - DocumentManager = require("document/DocumentManager"), - EditorManager = require("editor/EditorManager"), - FileIndexManager = require("project/FileIndexManager"), - KeyEvent = require("utils/KeyEvent"), - AppInit = require("utils/AppInit"), - StatusBar = require("widgets/StatusBar"), - ModalBar = require("widgets/ModalBar").ModalBar; - - var searchResults = []; + var Async = require("utils/Async"), + NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + FileUtils = require("file/FileUtils"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + Strings = require("strings"), + StringUtils = require("utils/StringUtils"), + ProjectManager = require("project/ProjectManager"), + FileViewController = require("project/FileViewController"), + FileIndexManager = require("project/FileIndexManager"), + DocumentManager = require("document/DocumentManager"), + EditorManager = require("editor/EditorManager"), + KeyEvent = require("utils/KeyEvent"), + AppInit = require("utils/AppInit"), + CollectionUtils = require("utils/CollectionUtils"), + StatusBar = require("widgets/StatusBar"), + ModalBar = require("widgets/ModalBar").ModalBar, + SearchDialogTemplate = require("text!htmlContent/search-dialog.html"), + SearchResultsTemplate = require("text!htmlContent/search-results.html"); + + var RESULTS_PER_PAGE = 100, + FIND_IN_FILE_MAX = 300; + + /** + * Map of all the last search results + * @type {Object., collapsed: boolean}>} + */ + var _searchResults = {}; + + /** @type {Array.} Keeps a copy of the search files sorted by name and with the selected file first */ + var _searchFiles = []; + + /** @type {Document} The current editor used to register the change and deleted events */ + var _currentDocument = null; + + /** @type {string} The current search query */ + var _currentQuery = ""; + + /** @type {RegExp} The current regular expresion created from the search query */ + var _currentQueryExpr = ""; + + /** @type {Array.} An array of the files where it should look or null/empty to search the entire project */ + var _currentScope = null; + + /** @type {number} The index of the first result that is displayed */ + var _currentStart = 0; + + /** @type {boolean} True if the matches in a file reached FIND_IN_FILE_MAX */ + var _maxHitsFoundInFile = false; + + /** @type {boolean} Tracks the automatically selection of the first result after the search is complete */ + var _gotoFirstResult = false; - var FIND_IN_FILES_MAX = 100, - maxHitsFoundInFile = false, - currentQuery = "", - currentScope; + /** @type {$.Element} jQuery elements used in the search results */ + var $searchResults, + $searchSummary, + $searchContent, + $selectedRow; + + /** + * @private + * Returns a regular expression from the given query and shows an error in the modal-bar if it was invalid + * @param {!string} query - The query from the modal-bar input + * @return {RegExp} + */ function _getQueryRegExp(query) { // Clear any pending RegEx error message $(".modal-bar .message").css("display", "inline-block"); @@ -96,15 +140,19 @@ define(function (require, exports, module) { /** * Returns label text to indicate the search scope. Already HTML-escaped. - * @param {?Entry} scope + * @param {?Array.} scope */ function _labelForScope(scope) { var projName = ProjectManager.getProjectRoot().name; if (scope) { - var displayPath = StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(scope.fullPath)); - return StringUtils.format(Strings.FIND_IN_FILES_SCOPED, displayPath); + if (scope.length === 1) { + var displayPath = StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(scope[0].fullPath)); + return StringUtils.format(Strings.FIND_IN_FILES_SCOPED, displayPath); + } else { + return Strings.FIND_IN_FILES_WORKING_SET; + } } else { - return Strings.FIND_IN_FILES_NO_SCOPE; + return Strings.FIND_IN_FILES_PROJECT; } } @@ -113,18 +161,17 @@ define(function (require, exports, module) { // class that everyone can use. /** - * FindInFilesDialog class - * @constructor - * - */ + * FindInFilesDialog class + * @constructor + */ function FindInFilesDialog() { this.closed = false; this.result = null; // $.Deferred } /** - * Closes the search dialog and resolves the promise that showDialog returned - */ + * Closes the search dialog and resolves the promise that showDialog returned + */ FindInFilesDialog.prototype._close = function (value) { if (this.closed) { return; @@ -137,26 +184,26 @@ define(function (require, exports, module) { }; /** - * Shows the search dialog - * @param {?string} initialString Default text to prepopulate the search field with - * @param {?Entry} scope Search scope, or null to search whole proj - * @returns {$.Promise} that is resolved with the string to search for - */ + * Shows the search dialog + * @param {?string} initialString Default text to prepopulate the search field with + * @param {?Entry} scope Search scope, or null to search whole proj + * @returns {$.Promise} that is resolved with the string to search for + */ FindInFilesDialog.prototype.showDialog = function (initialString, scope) { // Note the prefix label is a simple "Find:" - the "in ..." part comes after the text field - var dialogHTML = Strings.CMD_FIND + - ":  " + - "
(" + Strings.SEARCH_REGEXP_INFO + ")
"; - this.result = new $.Deferred(); + var templateVars = { + value: initialString || "", + label: _labelForScope(scope) + }; + var dialogHTML = Mustache.render(SearchDialogTemplate, $.extend(templateVars, Strings)); + + this.result = new $.Deferred(); this.modalBar = new ModalBar(dialogHTML, false); - var $searchField = $("input#findInFilesInput"); + + var $searchField = $("input#searchInput"); var that = this; - $searchField.attr("value", initialString || ""); $searchField.get(0).select(); - - $("#findInFilesScope").html(_labelForScope(scope)); - $searchField.bind("keydown", function (event) { if (event.keyCode === KeyEvent.DOM_VK_RETURN || event.keyCode === KeyEvent.DOM_VK_ESCAPE) { // Enter/Return key or Esc key event.stopPropagation(); @@ -185,6 +232,13 @@ define(function (require, exports, module) { }; + /** + * @private + * Searches throught the contents an returns an array of matches + * @param {!string} contents + * @param {!RegExp} queryExpr + * @return {Array.<{start: {line:number,ch:number}, end: {line:number,ch:number}, line: string}>} + */ function _getSearchMatches(contents, queryExpr) { // Quick exit if not found if (contents.search(queryExpr) === -1) { @@ -196,13 +250,12 @@ define(function (require, exports, module) { var matchStart; var matches = []; - var match; var lines = StringUtils.getLines(contents); while ((match = queryExpr.exec(contents)) !== null) { - var lineNum = StringUtils.offsetToLineNum(lines, match.index); - var line = lines[lineNum]; - var ch = match.index - contents.lastIndexOf("\n", match.index) - 1; // 0-based index + var lineNum = StringUtils.offsetToLineNum(lines, match.index); + var line = lines[lineNum]; + var ch = match.index - contents.lastIndexOf("\n", match.index) - 1; // 0-based index var matchLength = match[0].length; // Don't store more than 200 chars per line @@ -210,38 +263,81 @@ define(function (require, exports, module) { matches.push({ start: {line: lineNum, ch: ch}, - end: {line: lineNum, ch: ch + matchLength}, - line: line + end: {line: lineNum, ch: ch + matchLength}, + line: line }); // We have the max hits in just this 1 file. Stop searching this file. // This fixed issue #1829 where code hangs on too many hits. - if (matches.length >= FIND_IN_FILES_MAX) { + if (matches.length >= FIND_IN_FILE_MAX) { queryExpr.lastIndex = 0; - maxHitsFoundInFile = true; + _maxHitsFoundInFile = true; break; } } return matches; } + + /** + * @private + * Searches and stores the match results for the given file, if there are matches + * @param {!string} fullPath + * @param {!string} contents + * @param {!RegExp} queryExpr + */ + function _addSearchMatches(fullPath, contents, queryExpr) { + var matches = _getSearchMatches(contents, queryExpr); - function _showSearchResults(searchResults, query, scope) { - var $searchResultsDiv = $("#search-results"); + if (matches && matches.length) { + _searchResults[fullPath] = { + matches: matches, + collapsed: false + }; + } + } + + /** + * @private + * Sorts the file keys to show the selected result first and the rest sorted by path + */ + function _sortResultFiles() { + var selectedEntry = ProjectManager.getSidebarSelectedItem(); + _searchFiles = Object.keys(_searchResults); - if (searchResults && searchResults.length) { - var $resultTable = $("") - .append(""); + _searchFiles.sort(function (key1, key2) { + if (selectedEntry.fullPath === key1) { + return -1; + } else if (selectedEntry.fullPath === key2) { + return 1; + } else { + return key1.toLocaleLowerCase().localeCompare(key2.toLocaleLowerCase()); + } + }); + } + + + /** + * @private + * Shows the results in a table and adds the necesary event listeners + */ + function _showSearchResults() { + if (!$.isEmptyObject(_searchResults)) { - // Count the total number of matches - var numMatches = 0; - searchResults.forEach(function (item) { + // Count the total number of files and matches + var numFiles = 0, numMatches = 0; + CollectionUtils.forEach(_searchResults, function (item) { + numFiles++; numMatches += item.matches.length; }); + if (_currentStart > numMatches || _currentStart < 0) { + return; + } + // Show result summary in header var numMatchesStr = ""; - if (maxHitsFoundInFile) { + if (_maxHitsFoundInFile) { numMatchesStr = Strings.FIND_IN_FILES_MORE_THAN; } numMatchesStr += String(numMatches); @@ -251,166 +347,291 @@ define(function (require, exports, module) { Strings.FIND_IN_FILES_TITLE, numMatchesStr, (numMatches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, - searchResults.length, - (searchResults.length > 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE), - StringUtils.htmlEscape(query), - scope ? _labelForScope(scope) : "" + numFiles, + (numFiles > 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE), + StringUtils.htmlEscape(_currentQuery), + _currentScope ? _labelForScope(_currentScope) : "" ); - $("#search-result-summary") + // The last result index displayed + var last = _currentStart + RESULTS_PER_PAGE > numMatches ? numMatches : _currentStart + RESULTS_PER_PAGE; + + // Insert the search summary + $searchSummary .html(summary + - (numMatches > FIND_IN_FILES_MAX ? StringUtils.format(Strings.FIND_IN_FILES_MAX, FIND_IN_FILES_MAX) : "")) + (numMatches > RESULTS_PER_PAGE ? StringUtils.format(Strings.FIND_IN_FILES_PAGING, _currentStart + 1, last) : "") + + (_currentStart > 0 ? Strings.FIND_IN_FILES_LESS : "") + + (last < numMatches ? Strings.FIND_IN_FILES_MORE : "")) .prepend(" "); // putting a normal space before the "-" is not enough - var resultsDisplayed = 0; + // Create the results template search list + var searchList = []; + var resultsDisplayed = 0, i; + var searchItems, item, match; - searchResults.forEach(function (item) { - if (item && resultsDisplayed < FIND_IN_FILES_MAX) { - var makeCell = function (content) { - return $("") - .append("") - .click(function () { - // Clicking file section header collapses/expands result rows for that file - var $fileHeader = $(this); - $fileHeader.nextUntil(".file-section").toggle(); - - var $triangle = $(".disclosure-triangle", $fileHeader); - $triangle.toggleClass("expanded").toggleClass("collapsed"); - }) - .appendTo($resultTable); + + // All the matches can be displayed + } else if (resultsDisplayed < last) { + i = 0; + + // We can't display more items by now. Break the loop + } else { + return true; + } + + if (i >= 0 && i < item.matches.length) { + // Add a row for each match in the file + searchItems = []; + while (i < item.matches.length && resultsDisplayed < last) { + match = item.matches[i]; + searchItems.push({ + file: searchList.length, + item: i, + line: StringUtils.format(Strings.FIND_IN_FILES_LINE, (match.start.line + 1)), + pre: match.line.substr(0, match.start.ch), + highlight: match.line.substring(match.start.ch, match.end.ch), + post: match.line.substr(match.end.ch), + start: match.start, + end: match.end + }); + resultsDisplayed++; + i++; + } - // Add row for each match in file - item.matches.forEach(function (match) { - if (resultsDisplayed < FIND_IN_FILES_MAX) { - var $row = $("") - .append(makeCell(" ")) // Indent - .append(makeCell(StringUtils.format(Strings.FIND_IN_FILES_LINE, (match.start.line + 1)))) - .append(makeCell(highlightMatch(match.line, match.start.ch, match.end.ch))) - .appendTo($resultTable); - - $row.click(function () { - CommandManager.execute(Commands.FILE_OPEN, {fullPath: item.fullPath}) - .done(function (doc) { - // Opened document is now the current main editor - EditorManager.getCurrentFullEditor().setSelection(match.start, match.end, true); - }); - }); - resultsDisplayed++; - } + // Add a row for each file + searchList.push({ + file: searchList.length, + filename: StringUtils.breakableUrl(StringUtils.htmlEscape(fullPath)), + fullPath: fullPath, + items: searchItems }); - } }); - $("#search-results .table-container") + // Insert the search results + $searchContent .empty() - .append($resultTable) + .append(Mustache.render(SearchResultsTemplate, {searchList: searchList})) .scrollTop(0); // otherwise scroll pos from previous contents is remembered - $("#search-results .close") + $searchResults.find(".close") .one("click", function () { - $searchResultsDiv.hide(); + $searchResults.hide(); EditorManager.resizeEditor(); }); - $searchResultsDiv.show(); + // The link to go the previous page + $searchResults.find(".find-less") + .one("click", function () { + _currentStart -= RESULTS_PER_PAGE; + _showSearchResults(); + }); + + // The link to go to the next page + $searchResults.find(".find-more") + .one("click", function () { + _currentStart += RESULTS_PER_PAGE; + _showSearchResults(); + }); + + // Add the click and double click event directly on the table parent + $searchContent + .off(".searchList") // Remove the old events. Needed for paging + .on("click.searchList", function (e) { + var $row = $(e.target).closest("tr"); + + if ($row.length) { + if ($selectedRow) { + $selectedRow.removeClass("selected"); + } + $row.addClass("selected"); + $selectedRow = $row; + + var searchItem = searchList[$row.data("file")]; + var fullPath = searchItem.fullPath; + + // This is a file title row, expand/collapse on click + if ($row.hasClass("file-section")) { + // Clicking file section header collapses/expands result rows for that file + $row.nextUntil(".file-section").toggle(); + + var $triangle = $(".disclosure-triangle", $row); + $triangle.toggleClass("expanded").toggleClass("collapsed"); + + _searchResults[fullPath].collapsed = !_searchResults[fullPath].collapsed; + + // This is a file row, show result click + } else { + // Grab the required item data + var item = searchItem.items[$row.data("item")]; + + 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); + }); + } + } + + // Add the file to the working set on double click + }).on("dblclick.searchList", function (e) { + var $row = $(e.target).closest("tr"); + if ($row.length && !$row.hasClass("file-section")) { + // Grab the required item data + var item = searchList[$row.data("file")]; + + FileViewController.addToWorkingSetAndSelect(item.fullPath); + } + + // Restore the collapsed files + }).find(".file-section").each(function () { + var searchItem = searchList[$(this).data("file")]; + var fullPath = searchItem.fullPath; + + if (_searchResults[fullPath].collapsed) { + _searchResults[fullPath].collapsed = false; + $(this).trigger("click"); + } + }); + + $searchResults.show(); + EditorManager.resizeEditor(); + + // Select the first result if the current file is the first result in the list + if (_gotoFirstResult && _searchResults[_currentDocument.file.fullPath]) { + $searchContent.find("tr:nth-child(2)").trigger("click"); + _gotoFirstResult = false; + } + } else { - $searchResultsDiv.hide(); + $searchResults.hide(); + EditorManager.resizeEditor(); } - - EditorManager.resizeEditor(); } /** - * @param {!FileInfo} fileInfo File in question - * @param {?Entry} scope Search scope, or null if whole project + * @private + * Returns true if there is no scope or if the file is within one of the folder or is one of the files + * @param {!FileInfo} fileInfo - File in question + * @param {?Array.} scope - Search scope, or null/empty if whole project * @return {boolean} */ - function inScope(fileInfo, scope) { - if (scope) { - if (scope.isDirectory) { - // Dirs always have trailing slash, so we don't have to worry about being - // a substring of another dir name - return fileInfo.fullPath.indexOf(scope.fullPath) === 0; - } else { - return fileInfo.fullPath === scope.fullPath; - } + function _inScope(fileInfo, scope) { + if (scope && scope.length) { + return scope.some(function (item) { + if (item.isDirectory) { + // Dirs always have trailing slash, so we don't have to worry about being + // a substring of another dir name + return fileInfo.fullPath.indexOf(item.fullPath) === 0; + } else { + return fileInfo.fullPath === item.fullPath; + } + }); } return true; } /** - * Displays a non-modal embedded dialog above the code mirror editor that allows the user to do - * a find operation across all files in the project. - * @param {?Entry} scope Project file/subfolder to search within; else searches whole project. - */ - function doFindInFiles(scope) { - + * @private + * Adds the files in the working set that arent in the project to the file index list + * @param {!Array.} fileListResult + */ + function _addNonProjectFiles(fileListResult) { + var result = false; + var workingSet = DocumentManager.getWorkingSet(); + + workingSet.forEach(function (item) { + result = fileListResult.some(function (fileInfo) { + return fileInfo.fullPath === item.fullPath; + }); + if (!result) { + fileListResult.push(item); + } + }); + } + + /** + * @private + * Displays a non-modal embedded dialog above the code mirror editor that allows the user to do + * a find operation across all files in the project or a given scope. + * @param {?Array.} scope - An array of file/subfolder to search within; else searches whole project. + */ + function _handleFindInFiles(scope) { var dialog = new FindInFilesDialog(); // Default to searching for the current selection var currentEditor = EditorManager.getActiveEditor(); var initialString = currentEditor && currentEditor.getSelectedText(); - - currentQuery = ""; - currentScope = scope; - searchResults = []; - maxHitsFoundInFile = false; + + // Reset the private variables + _searchResults = {}; + _searchFiles = []; + _currentQuery = ""; + _currentQueryExpr = ""; + _currentScope = scope; + _currentStart = 0; + _maxHitsFoundInFile = false; + _gotoFirstResult = true; dialog.showDialog(initialString, scope) .done(function (query) { if (query) { - currentQuery = query; - var queryExpr = _getQueryRegExp(query); - if (!queryExpr) { + _currentQuery = query; + _currentQueryExpr = _getQueryRegExp(query); + + if (!_currentQueryExpr) { return; } + StatusBar.showBusyIndicator(true); FileIndexManager.getFileInfoList("all") .done(function (fileListResult) { + _addNonProjectFiles(fileListResult); + Async.doInParallel(fileListResult, function (fileInfo) { var result = new $.Deferred(); - if (!inScope(fileInfo, scope)) { + if (!_inScope(fileInfo, scope)) { result.resolve(); } else { // Search one file - DocumentManager.getDocumentForPath(fileInfo.fullPath) - .done(function (doc) { - var matches = _getSearchMatches(doc.getText(), queryExpr); - - if (matches && matches.length) { - searchResults.push({ - fullPath: fileInfo.fullPath, - matches: matches - }); - } - result.resolve(); - }) - .fail(function (error) { - // Error reading this file. This is most likely because the file isn't a text file. - // Resolve here so we move on to the next file. - result.resolve(); - }); + var file; + if (DocumentManager.findInWorkingSet(fileInfo.fullPath) > -1) { + file = DocumentManager.getDocumentForPath(fileInfo.fullPath); + } else { + var fileEntry = new NativeFileSystem.FileEntry(fileInfo.fullPath); + file = FileUtils.readAsText(fileEntry); + } + file.done(function (doc) { + var text = typeof doc === "object" ? doc.getText() : doc; + _addSearchMatches(fileInfo.fullPath, text, _currentQueryExpr); + result.resolve(); + + }).fail(function (error) { + // Error reading this file. This is most likely because the file isn't a text file. + // Resolve here so we move on to the next file. + result.resolve(); + }); } return result.promise(); }) .done(function () { - // Done searching all files: show results - _showSearchResults(searchResults, query, scope); + // Done searching all files: sort the files and show results + _sortResultFiles(); + _showSearchResults(); StatusBar.hideBusyIndicator(); }) .fail(function () { @@ -422,37 +643,282 @@ define(function (require, exports, module) { }); } - /** Search within the file/subtree defined by the sidebar selection */ - function doFindInSubtree() { - // Prefer project tree selection, else use working set selection - var selectedEntry = ProjectManager.getSelectedItem(); - if (!selectedEntry) { - var doc = DocumentManager.getCurrentDocument(); - selectedEntry = (doc && doc.file); + /** + * @private + * Search within the file/subtree inside the project defined by the sidebar selection + */ + function _handleFindInSubtree() { + var selectedEntry = ProjectManager.getSidebarSelectedItem(); + _handleFindInFiles(selectedEntry ? [selectedEntry] : null); + } + + /** + * @private + * Search within all the files in the working set inside the project + */ + function _handleFindInWorkingSet() { + _handleFindInFiles(DocumentManager.getWorkingSet()); + } + + + + /** + * @private + * Triggers a click on the given row element and scrolls to it if needed + * @param {$.Element} $row - A table row jQuery element + */ + function _triggerClick($row) { + // If this row is a ile header, just select it + if ($row.hasClass("file-section")) { + if ($selectedRow) { + $selectedRow.removeClass("selected"); + } + $row.addClass("selected"); + $selectedRow = $row; + } else { + $row.trigger("click"); } - doFindInFiles(selectedEntry); + // Scroll to show the element at the top or bottom of the results, if needed + var tableScroll = $searchContent.scrollTop(), + tableHeight = $searchContent.outerHeight(), + rowTop = $row.offset().top + tableScroll - $searchContent.offset().top, + rowHeight = $row.outerHeight(); + + if (tableScroll + tableHeight - rowHeight < rowTop) { + $searchContent.scrollTop(rowTop - tableHeight + rowHeight); + } else if (tableScroll > rowTop) { + $searchContent.scrollTop(rowTop); + } } + /** + * @private + * Selects the next result in the table or the first of the next page + */ + function _handleNextResult() { + var $row; + if (!$selectedRow || !$selectedRow.length) { + $row = $searchContent.find("tr").first(); + } else { + $row = $selectedRow.next(); + if (!$row.length) { + _currentStart += RESULTS_PER_PAGE; + _showSearchResults(); + $row = $searchContent.find("tr").first(); + } + } + _triggerClick($row); + } - // Initialize items dependent on HTML DOM - AppInit.htmlReady(function () { - var $searchResults = $("#search-results"), - $searchContent = $("#search-results .table-container"); - }); - + /** + * @private + * Selects the previous result in the table or the last of the previous page + */ + function _handlePreviousResult() { + var $row; + if (!$selectedRow || !$selectedRow.length) { + $row = $searchContent.find("tr").last(); + } else { + $row = $selectedRow.prev(); + if (!$row.length) { + _currentStart -= RESULTS_PER_PAGE; + _showSearchResults(); + $row = $searchContent.find("tr").last(); + } + } + _triggerClick($row); + } + + + + /** + * @private + * Shows the search results and tryes to restore the previous scroll and selection + */ + function _restoreSearchResults() { + var scrollTop = $searchContent.scrollTop(); + var index = $selectedRow ? $selectedRow.index() : null; + + _showSearchResults(); + + $searchContent.scrollTop(scrollTop); + if ($selectedRow) { + $selectedRow = $searchContent.find("tr:eq(" + index + ")"); + $selectedRow.addClass("selected"); + } + } + + /** + * @private + * Move the search results from the previous path to the new one and update the results list + * @param {!$.Event} event + * @param {!string} oldName + * @param {!string} newName + */ function _fileNameChangeHandler(event, oldName, newName) { - if ($("#search-results").is(":visible")) { + if ($searchResults.is(":visible")) { + var resultsChanged = false; + // Update the search results - searchResults.forEach(function (item) { - item.fullPath = item.fullPath.replace(oldName, newName); + CollectionUtils.forEach(_searchResults, function (item, key) { + if (key.match(oldName)) { + _searchResults[key.replace(oldName, newName)] = item; + delete _searchResults[key]; + resultsChanged = true; + } }); - _showSearchResults(searchResults, currentQuery, currentScope); + + // Restore the reesults if needed + if (resultsChanged) { + _sortResultFiles(); + _restoreSearchResults(); + } + } + } + + /** + * @private + * Update the result matches every time the content of a file changes + * @param {!$.Event} event + * @param {!Document} doc - The Document that changed, should be the current one + * @param {!Object} change - A linked list as described in the Document constructor + * @param {?boolean} resultsChanged - True when search results changed from a file change + */ + function _fileChangeHandler(event, doc, change, resultsChanged) { + if ($searchResults.is(":visible")) { + var fullPath = doc.file.fullPath; + + // There is no from or to positions, so the entire file changed, we must search all over again + if (!change.from || !change.to) { + _addSearchMatches(fullPath, doc.getText(), _currentQueryExpr); + _restoreSearchResults(); + + } else { + // Lests get only the lines that changed + var i, lines = [], diff; + for (i = 0; i < change.text.length; i++) { + lines.push(doc.getLine(change.from.line + i)); + } + + // If the change is a delete, define the lines difference as a negative value + if (change.from.line !== change.to.line && change.text.length === 1 && !change.text[0].length) { + diff = change.from.line - change.to.line; + // If the change was a replacement, define the lines difference as a positive value + } else if (change.from.line !== change.to.line) { + diff = change.from.line - change.to.line; + // If the change was an addition, define the lines difference as the lines added minus 1 + } else { + diff = lines.length - 1; + } + + var start = 0, howMany = 0; + if (_searchResults[fullPath]) { + // Lets search the last match before a replacement, the amount of matches deleted and update + // the lines values for all the matches after the change + _searchResults[fullPath].matches.forEach(function (item) { + if (item.end.line < change.from.line) { + start++; + } else if (item.end.line <= change.to.line) { + howMany++; + } else { + item.start.line += diff; + item.end.line += diff; + } + }); + + // Delete the lines that where deleted or replaced + if (howMany > 0) { + _searchResults[fullPath].matches.splice(start, howMany); + } + resultsChanged = true; + } + + // Searches only over the lines that changed + var matches = _getSearchMatches(lines.join("\r\n"), _currentQueryExpr); + if (matches && matches.length) { + // Updates the lines, since the text didnt started on the first line + matches.forEach(function (value, key) { + matches[key].start.line += change.from.line; + matches[key].end.line += change.from.line; + }); + + // If the file index exists, add the new matches to the file at the start index found before + if (_searchResults[fullPath]) { + Array.prototype.splice.apply(_searchResults[fullPath].matches, [start, 0].concat(matches)); + // If not, add the matches to a new file index + } else { + _searchResults[fullPath] = { + matches: matches, + collapsed: false + }; + } + resultsChanged = true; + } + + // All the matches where deleted, remove the file from the results + if (_searchResults[fullPath] && !_searchResults[fullPath].matches.length) { + delete _searchResults[fullPath]; + resultsChanged = true; + } + + // This is link to the next change objet, so we need to keep searching + if (change.next) { + _fileChangeHandler(event, doc, change.next, resultsChanged); + + // If not we can show the results, but only if something changed + } else if (resultsChanged) { + _sortResultFiles(); + _restoreSearchResults(); + } + } } } - $(DocumentManager).on("fileNameChange", _fileNameChangeHandler); + /** + * @private + * Update the results to delete the results from the deleted file + */ + function _fileDeletedHandler() { + if ($searchResults.is(":visible")) { + if (_searchResults[_currentDocument.file.fullPath]) { + delete _searchResults[_currentDocument.file.fullPath]; + + _sortResultFiles(); + _restoreSearchResults(); + } + } + } + + /** + * @private + * Updates the event listeners when the current document changes + */ + function _currentDocumentChangeHandler() { + $(_currentDocument).off(".findInFiles"); + + _currentDocument = DocumentManager.getCurrentDocument(); + $(_currentDocument) + .on("change.findInFiles", _fileChangeHandler) + .on("deleted.findInFiles", _fileDeletedHandler); + } + + + // Initialize items dependent on HTML DOM + AppInit.htmlReady(function () { + $searchResults = $("#search-results"); + $searchSummary = $("#search-result-summary"); + $searchContent = $("#search-results .table-container"); + }); + + // Initialize: register listeners + $(DocumentManager).on("fileNameChange", _fileNameChangeHandler); + $(DocumentManager).on("currentDocumentChange", _currentDocumentChangeHandler); - CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, doFindInFiles); - CommandManager.register(Strings.CMD_FIND_IN_SUBTREE, Commands.EDIT_FIND_IN_SUBTREE, doFindInSubtree); + // Initialize: command handlers + CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.SEARCH_FIND_IN_FILES, _handleFindInFiles); + CommandManager.register(Strings.CMD_FIND_IN_SUBTREE, Commands.SEARCH_FIND_IN_SUBTREE, _handleFindInSubtree); + CommandManager.register(Strings.CMD_FIND_IN_WORKING_SET, Commands.SEARCH_FIND_IN_WORKING_SET, _handleFindInWorkingSet); + CommandManager.register(Strings.CMD_NEXT_RESULT, Commands.SEARCH_NEXT_RESULT, _handleNextResult); + CommandManager.register(Strings.CMD_PREVIOUS_RESULT, Commands.SEARCH_PREVIOUS_RESULT, _handlePreviousResult); }); diff --git a/src/search/FindReplace.js b/src/search/FindReplace.js index 1d5c1f7295f..418a933ab88 100644 --- a/src/search/FindReplace.js +++ b/src/search/FindReplace.js @@ -387,8 +387,8 @@ define(function (require, exports, module) { } } - CommandManager.register(Strings.CMD_FIND, Commands.EDIT_FIND, _launchFind); - CommandManager.register(Strings.CMD_FIND_NEXT, Commands.EDIT_FIND_NEXT, _findNext); - CommandManager.register(Strings.CMD_REPLACE, Commands.EDIT_REPLACE, _replace); - CommandManager.register(Strings.CMD_FIND_PREVIOUS, Commands.EDIT_FIND_PREVIOUS, _findPrevious); + CommandManager.register(Strings.CMD_FIND, Commands.SEARCH_FIND, _launchFind); + CommandManager.register(Strings.CMD_FIND_NEXT, Commands.SEARCH_FIND_NEXT, _findNext); + CommandManager.register(Strings.CMD_REPLACE, Commands.SEARCH_REPLACE, _replace); + CommandManager.register(Strings.CMD_FIND_PREVIOUS, Commands.SEARCH_FIND_PREVIOUS, _findPrevious); }); diff --git a/test/spec/FindReplace-test.js b/test/spec/FindReplace-test.js index 4447f86b8bf..8e707fde391 100644 --- a/test/spec/FindReplace-test.js +++ b/test/spec/FindReplace-test.js @@ -165,24 +165,24 @@ define(function (require, exports, module) { it("should find all case-insensitive matches", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); expectHighlightedMatches(fooExpectedMatches); expectSelection(fooExpectedMatches[0]); expect(myEditor.centerOnCursor.calls.length).toEqual(1); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(fooExpectedMatches[1]); expect(myEditor.centerOnCursor.calls.length).toEqual(2); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(fooExpectedMatches[2]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(fooExpectedMatches[3]); expectHighlightedMatches(fooExpectedMatches); // no change in highlights // wraparound - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(fooExpectedMatches[0]); expect(myEditor.centerOnCursor.calls.length).toEqual(5); }); @@ -195,27 +195,27 @@ define(function (require, exports, module) { ]; myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("Foo"); expectHighlightedMatches(expectedSelections); expectSelection(expectedSelections[0]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[1]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[2]); // note the lowercase "foo()" is NOT matched // wraparound - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[0]); }); it("should Find Next after search bar closed, including wraparound", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); pressEnter(); @@ -226,23 +226,23 @@ define(function (require, exports, module) { expect(myEditor.centerOnCursor.calls.length).toEqual(1); // Simple linear Find Next - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: 31}, end: {line: LINE_FIRST_REQUIRE, ch: 34}}); expect(myEditor.centerOnCursor.calls.length).toEqual(2); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: 6, ch: 17}, end: {line: 6, ch: 20}}); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: 8, ch: 8}, end: {line: 8, ch: 11}}); // Wrap around to first result - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: 8}, end: {line: LINE_FIRST_REQUIRE, ch: 11}}); }); it("should Find Previous after search bar closed, including wraparound", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); pressEnter(); @@ -251,22 +251,22 @@ define(function (require, exports, module) { expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: 8}, end: {line: LINE_FIRST_REQUIRE, ch: 11}}); // Wrap around to last result - CommandManager.execute(Commands.EDIT_FIND_PREVIOUS); + CommandManager.execute(Commands.SEARCH_FIND_PREVIOUS); expectSelection({start: {line: 8, ch: 8}, end: {line: 8, ch: 11}}); // Simple linear Find Previous - CommandManager.execute(Commands.EDIT_FIND_PREVIOUS); + CommandManager.execute(Commands.SEARCH_FIND_PREVIOUS); expectSelection({start: {line: 6, ch: 17}, end: {line: 6, ch: 20}}); - CommandManager.execute(Commands.EDIT_FIND_PREVIOUS); + CommandManager.execute(Commands.SEARCH_FIND_PREVIOUS); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: 31}, end: {line: LINE_FIRST_REQUIRE, ch: 34}}); - CommandManager.execute(Commands.EDIT_FIND_PREVIOUS); + CommandManager.execute(Commands.SEARCH_FIND_PREVIOUS); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: 8}, end: {line: LINE_FIRST_REQUIRE, ch: 11}}); }); it("shouldn't Find Next after search bar reopened", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); pressEnter(); @@ -274,19 +274,19 @@ define(function (require, exports, module) { // Open search bar a second time myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSearchBarOpen(); expect(myEditor).toHaveCursorPosition(0, 0); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expect(myEditor).toHaveCursorPosition(0, 0); }); it("should open search bar on Find Next with no previous search", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSearchBarOpen(); expect(myEditor).toHaveCursorPosition(0, 0); @@ -295,7 +295,7 @@ define(function (require, exports, module) { it("should select-all without affecting search state if Find invoked while search bar open", function () { // #2478 myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); // position cursor first @@ -306,7 +306,7 @@ define(function (require, exports, module) { expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 11); // cursor left at end of last good match ("foo") // Invoke Find a 2nd time - this time while search bar is open - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSearchBarOpen(); expect(getSearchField().val()).toEqual("foobar"); @@ -324,7 +324,7 @@ define(function (require, exports, module) { it("should re-search from original position when text changes", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("baz"); @@ -344,14 +344,14 @@ define(function (require, exports, module) { it("should re-search from original position when text changes, even after Find Next", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("foo"); expectSelection(fooExpectedMatches[0]); // get search highlight down below where the "bar" match will be - CommandManager.execute(Commands.EDIT_FIND_NEXT); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(fooExpectedMatches[2]); enterSearchText("bar"); @@ -361,7 +361,7 @@ define(function (require, exports, module) { it("should extend original selection when appending to prepopulated text", function () { myEditor.setSelection({line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expect(getSearchField().val()).toEqual("require"); var requireExpectedMatches = [ @@ -385,7 +385,7 @@ define(function (require, exports, module) { it("should collapse selection when appending to prepopulated text causes no result", function () { myEditor.setSelection({line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); enterSearchText("requireX"); @@ -396,7 +396,7 @@ define(function (require, exports, module) { it("should clear selection, return cursor to start after backspacing to empty query", function () { myEditor.setCursorPos(2, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("require"); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); @@ -413,7 +413,7 @@ define(function (require, exports, module) { it("should go to next on Enter with prepopulated text & no Find Nexts", function () { myEditor.setSelection({line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); pressEnter(); @@ -426,7 +426,7 @@ define(function (require, exports, module) { it("shouldn't change selection on Esc with prepopulated text & no Find Nexts", function () { myEditor.setSelection({line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); pressEscape(); @@ -439,10 +439,10 @@ define(function (require, exports, module) { it("shouldn't change selection on Enter with prepopulated text & after Find Next", function () { myEditor.setSelection({line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_PAREN}}); pressEnter(); @@ -454,7 +454,7 @@ define(function (require, exports, module) { it("shouldn't change selection on Enter after typing text, no Find Nexts", function () { myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 0); enterSearchText("require"); @@ -469,13 +469,13 @@ define(function (require, exports, module) { it("shouldn't change selection on Enter after typing text & Find Next", function () { myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 0); enterSearchText("require"); expectSelection({start: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE, ch: CH_REQUIRE_PAREN}}); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection({start: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_START}, end: {line: LINE_FIRST_REQUIRE + 1, ch: CH_REQUIRE_PAREN}}); pressEnter(); @@ -487,10 +487,10 @@ define(function (require, exports, module) { it("should no-op on Find Next with blank search", function () { myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 0); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 0); // no change }); @@ -498,7 +498,7 @@ define(function (require, exports, module) { it("should no-op on Enter with blank search", function () { myEditor.setCursorPos(LINE_FIRST_REQUIRE, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); expect(myEditor).toHaveCursorPosition(LINE_FIRST_REQUIRE, 0); pressEnter(); @@ -521,21 +521,21 @@ define(function (require, exports, module) { ]; myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("/Ba./"); expectHighlightedMatches(expectedSelections); expectSelection(expectedSelections[0]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[1]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[2]); - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[3]); // wraparound - CommandManager.execute(Commands.EDIT_FIND_NEXT); + CommandManager.execute(Commands.SEARCH_FIND_NEXT); expectSelection(expectedSelections[0]); }); @@ -545,7 +545,7 @@ define(function (require, exports, module) { ]; myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("/foo/"); expectHighlightedMatches(expectedSelections); @@ -561,7 +561,7 @@ define(function (require, exports, module) { ]; myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("/foo/i"); expectHighlightedMatches(expectedSelections); @@ -575,7 +575,7 @@ define(function (require, exports, module) { ]; myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); // This should be interpreted as a non-RegExp search, which actually does have a result thanks to "modules/Bar" enterSearchText("/Ba"); @@ -586,7 +586,7 @@ define(function (require, exports, module) { it("shouldn't choke on invalid regexp", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); // This is interpreted as a regexp (has both "/"es) but is invalid; should show error message enterSearchText("/+/"); @@ -598,7 +598,7 @@ define(function (require, exports, module) { it("shouldn't choke on empty regexp", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("//"); expectHighlightedMatches([]); @@ -608,7 +608,7 @@ define(function (require, exports, module) { it("shouldn't freeze on /.*/ regexp", function () { myEditor.setCursorPos(0, 0); - CommandManager.execute(Commands.EDIT_FIND); + CommandManager.execute(Commands.SEARCH_FIND); enterSearchText("/.*/"); pressEnter();
").html(content); - }; - - // shorthand function name - var esc = StringUtils.htmlEscape; - - var highlightMatch = function (line, start, end) { - return esc(line.substr(0, start)) + "" + esc(line.substring(start, end)) + "" + esc(line.substr(end)); - }; + _searchFiles.some(function (fullPath) { + item = _searchResults[fullPath]; + + // Skip the items that will not fit in the results page + if (resultsDisplayed + item.matches.length < _currentStart) { + resultsDisplayed += item.matches.length; + i = -1; + + // Only the first matches will be displayed filling the remaining space of the table + } else if (resultsDisplayed < _currentStart) { + i = _currentStart - resultsDisplayed; + resultsDisplayed = _currentStart; - // Add row for file name - var displayFileName = StringUtils.format(Strings.FIND_IN_FILES_FILE_PATH, - StringUtils.breakableUrl(esc(item.fullPath))); - $("
" + displayFileName + "