diff --git a/src/search/QuickOpen.js b/src/search/QuickOpen.js index 4a7bfde2d13..9766c11dd17 100644 --- a/src/search/QuickOpen.js +++ b/src/search/QuickOpen.js @@ -79,6 +79,11 @@ define(function (require, exports, module) { /** @type {boolean} */ var dialogOpen = false; + + /** + * The currently open quick open dialog. + */ + var _curDialog; /** Object representing a search result with associated metadata (added as extra ad hoc fields) */ function SearchResult(label) { @@ -88,8 +93,7 @@ define(function (require, exports, module) { /** * Defines API for new QuickOpen plug-ins */ - function QuickOpenPlugin(name, fileTypes, done, search, match, itemFocus, itemSelect, resultsFormatter) { - + function QuickOpenPlugin(name, fileTypes, done, search, match, itemFocus, itemSelect, resultsFormatter) { this.name = name; this.fileTypes = fileTypes; this.done = done; @@ -151,6 +155,19 @@ define(function (require, exports, module) { */ function QuickNavigateDialog() { this.$searchField = undefined; // defined when showDialog() is called + + // Bind event handlers + this._handleItemSelect = this._handleItemSelect.bind(this); + this._handleItemFocus = this._handleItemFocus.bind(this); + this._handleKeyUp = this._handleKeyUp.bind(this); + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleResultsReady = this._handleResultsReady.bind(this); + this._handleBlur = this._handleBlur.bind(this); + this._handleDocumentMouseDown = this._handleDocumentMouseDown.bind(this); + + // Bind callbacks from smart-autocomplete + this._filterCallback = this._filterCallback.bind(this); + this._resultsFormatterCallback = this._resultsFormatterCallback.bind(this); } /** @@ -211,10 +228,10 @@ define(function (require, exports, module) { } // Smart Autocomplete uses this assumption internally: index of DOM node in results list container - // exactly matches index of search result in list returned by _handleFilter() + // exactly matches index of search result in list returned by _filterCallback() var index = $(domItem).index(); - // This is just the last return value of _handleFilter(), which smart autocomplete helpfully caches + // This is just the last return value of _filterCallback(), which smart autocomplete helpfully caches var lastFilterResult = $("input#quickOpenSearch").data("smart-autocomplete").rawResults; return lastFilterResult[index]; } @@ -227,7 +244,7 @@ define(function (require, exports, module) { * that may have not matched anything in in the list, but may have information * for carrying out an action (e.g. go to line). */ - QuickNavigateDialog.prototype._handleItemSelect = function (selectedDOMItem) { + QuickNavigateDialog.prototype._handleItemSelect = function (e, selectedDOMItem) { // This is a work-around to select first item when a selection event occurs // (usually from pressing the enter key) and no item is selected in the list. @@ -274,7 +291,7 @@ define(function (require, exports, module) { * Opens the file specified by selected item if there is no current plug-in, otherwise defers handling * to the currentPlugin */ - QuickNavigateDialog.prototype._handleItemFocus = function (selectedDOMItem) { + QuickNavigateDialog.prototype._handleItemFocus = function (e, selectedDOMItem) { var selectedItem = domItemToSearchResult(selectedDOMItem); if (currentPlugin) { @@ -373,7 +390,7 @@ define(function (require, exports, module) { /** * Give visual clue when there are no results */ - QuickNavigateDialog.prototype._handleResultsReady = function (results) { + QuickNavigateDialog.prototype._handleResultsReady = function (e, results) { var isNoResults = (results.length === 0 && !this._isValidLineNumberQuery(this.$searchField.val())); this.$searchField.toggleClass("no-results", isNoResults); }; @@ -383,7 +400,6 @@ define(function (require, exports, module) { * searching is done. */ QuickNavigateDialog.prototype._close = function () { - if (!dialogOpen) { return; } @@ -417,7 +433,7 @@ define(function (require, exports, module) { $(".smart_autocomplete_container").remove(); - $(window.document).off("mousedown", this.handleDocumentMouseDown); + $(window.document).off("mousedown", this._handleDocumentMouseDown); }; /** @@ -717,7 +733,14 @@ define(function (require, exports, module) { return filteredList; } - function _handleFilter(query) { + /** + * Handles changes to the current query in the search field. + * @param {string} query The new query. + * @return {Array} The filtered list of results. + */ + QuickNavigateDialog.prototype._filterCallback = function (query) { + this._updateDialogLabel(query); + var curDoc = DocumentManager.getCurrentDocument(); if (curDoc) { var filename = _filenameFromPath(curDoc.file.fullPath, true); @@ -737,8 +760,7 @@ define(function (require, exports, module) { // No plugin: use default file search mode currentPlugin = null; return searchFileList(query); - } - + }; /** * Formats item's label as properly escaped HTML text, highlighting sections that match 'query'. @@ -804,8 +826,13 @@ define(function (require, exports, module) { return "
  • " + displayName + "
    " + displayPath + "
  • "; } - function _handleResultsFormatter(item) { - var query = $("input#quickOpenSearch").val(); + /** + * Formats the entry for the given item to be displayed in the dropdown. + * @param {Object} item The item to be displayed. + * @return {string} The HTML to be displayed. + */ + QuickNavigateDialog.prototype._resultsFormatterCallback = function (item) { + var query = this.$searchField.val(); var formatter; @@ -817,30 +844,60 @@ define(function (require, exports, module) { formatter = _filenameResultsFormatter; } return formatter(item, query); - } + }; - function setSearchFieldValue(prefix, initialString) { + /** + * Sets the value in the search field, updating the current mode and label based on the + * given prefix. + * @param {string} prefix The prefix that determines which mode we're in: must be empty (for file search), + * "@" for go to definition, or ":" for go to line. + * @param {string} initialString The query string to search for (without the prefix). + */ + QuickNavigateDialog.prototype.setSearchFieldValue = function (prefix, initialString) { prefix = prefix || ""; initialString = initialString || ""; initialString = prefix + initialString; - var $field = $("input#quickOpenSearch"); - if ($field) { - $field.val(initialString); - $field.get(0).setSelectionRange(prefix.length, initialString.length); - - // Kick smart-autocomplete to update (it only listens for keyboard events) - // (due to #1855, this will only pop up results list; it won't auto-"focus" the first result) - $field.trigger("keyIn", [initialString]); + var $field = this.$searchField; + $field.val(initialString); + $field.get(0).setSelectionRange(prefix.length, initialString.length); + + // Kick smart-autocomplete to update (it only listens for keyboard events) + // (due to #1855, this will only pop up results list; it won't auto-"focus" the first result) + $field.trigger("keyIn", [initialString]); + + this._updateDialogLabel(initialString); + }; + + /** + * Sets the dialog label based on the type of the given query. + * @param {string} query The user's current query. + */ + QuickNavigateDialog.prototype._updateDialogLabel = function (query) { + var prefix = (query.length > 0 ? query.charAt(0) : ""); + + // Update the dialog label based on the current prefix. + var dialogLabel = ""; + switch (prefix) { + case ":": + dialogLabel = Strings.CMD_GOTO_LINE; + break; + case "@": + dialogLabel = Strings.CMD_GOTO_DEFINITION; + break; + default: + dialogLabel = Strings.CMD_QUICK_OPEN; + break; } - } + $(".find-dialog-label", this.dialog).text(dialogLabel); + }; /** * Close the dialog when the user clicks outside of it. Smart-autocomplete listens for this and automatically closes its popup, * but we want to close the whole search "dialog." (And we can't just piggyback on the popup closing event, since there are cases * where the popup closes that we want the dialog to remain open (e.g. deleting search term via backspace). */ - QuickNavigateDialog.prototype.handleDocumentMouseDown = function (e) { + QuickNavigateDialog.prototype._handleDocumentMouseDown = function (e) { if ($(this.dialog).find(e.target).length === 0 && $(".smart_autocomplete_container").find(e.target).length === 0) { this._close(); } else { @@ -852,21 +909,25 @@ define(function (require, exports, module) { } } }; + + /** + * Close the dialog when it loses focus. + */ + QuickNavigateDialog.prototype._handleBlur = function (e) { + this._close(); + }; /** * Shows the search dialog and initializes the auto suggestion list with filenames from the current project */ QuickNavigateDialog.prototype.showDialog = function (prefix, initialString) { - var that = this; - if (dialogOpen) { return; } dialogOpen = true; // Global listener to hide search bar & popup - this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this); - $(window.document).on("mousedown", this.handleDocumentMouseDown); + $(window.document).on("mousedown", this._handleDocumentMouseDown); // Ty TODO: disabled for now while file switching is disabled in _handleItemFocus @@ -881,14 +942,13 @@ define(function (require, exports, module) { } else { origSelection = null; } - - // Show the search bar ("dialog") - var dialogHTML = Strings.CMD_QUICK_OPEN + ": "; - that._createDialogDiv(dialogHTML); - that.$searchField = $("input#quickOpenSearch"); + // Show the search bar ("dialog") + var dialogHTML = ": "; + this._createDialogDiv(dialogHTML); + this.$searchField = $("input#quickOpenSearch"); - that.$searchField.smartAutoComplete({ + this.$searchField.smartAutoComplete({ source: [], maxResults: 20, minCharLimit: 0, @@ -896,22 +956,22 @@ define(function (require, exports, module) { forceSelect: false, typeAhead: false, // won't work right now because smart auto complete // using internal raw results instead of filtered results for matching - filter: _handleFilter, - resultFormatter: _handleResultsFormatter + filter: this._filterCallback, + resultFormatter: this._resultsFormatterCallback }); - that.$searchField.bind({ - resultsReady: function (e, results) { that._handleResultsReady(results); }, - itemSelect: function (e, selectedItem) { that._handleItemSelect(selectedItem); }, - itemFocus: function (e, selectedItem) { that._handleItemFocus(selectedItem); }, - keydown: function (e) { that._handleKeyDown(e); }, - keyup: function (e, query) { that._handleKeyUp(e); }, - blur: function (e) { that._close(); } + this.$searchField.bind({ + resultsReady: this._handleResultsReady, + itemSelect: this._handleItemSelect, + itemFocus: this._handleItemFocus, + keydown: this._handleKeyDown, + keyup: this._handleKeyUp, + blur: this._handleBlur // Note: lostFocus event DOESN'T work because auto smart complete catches the key up from shift-command-o and immediately // triggers lostFocus }); - setSearchFieldValue(prefix, initialString); + this.setSearchFieldValue(prefix, initialString); // Start fetching the file list, which will be needed the first time the user enters an un-prefixed query. If FileIndexManager's // caches are out of date, this list might take some time to asynchronously build. See searchFileList() for how this is handled. @@ -936,10 +996,10 @@ define(function (require, exports, module) { */ function beginSearch(prefix, initialString) { if (dialogOpen) { - setSearchFieldValue(prefix, initialString); + _curDialog.setSearchFieldValue(prefix, initialString); } else { - var dialog = new QuickNavigateDialog(); - dialog.showDialog(prefix, initialString); + _curDialog = new QuickNavigateDialog(); + _curDialog.showDialog(prefix, initialString); } }