diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/debugger/views/filter-view.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/debugger/views/filter-view.js')
-rw-r--r-- | devtools/client/debugger/views/filter-view.js | 925 |
1 files changed, 925 insertions, 0 deletions
diff --git a/devtools/client/debugger/views/filter-view.js b/devtools/client/debugger/views/filter-view.js new file mode 100644 index 000000000..460b1201c --- /dev/null +++ b/devtools/client/debugger/views/filter-view.js @@ -0,0 +1,925 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* import-globals-from ../debugger-controller.js */ +/* import-globals-from ../debugger-view.js */ +/* import-globals-from ../utils.js */ +/* globals document, window */ +"use strict"; + +/** + * Functions handling the filtering UI. + */ +function FilterView(DebuggerController, DebuggerView) { + dumpn("FilterView was instantiated"); + + this.Parser = DebuggerController.Parser; + + this.DebuggerView = DebuggerView; + this.FilteredSources = new FilteredSourcesView(DebuggerView); + this.FilteredFunctions = new FilteredFunctionsView(DebuggerController.SourceScripts, + DebuggerController.Parser, + DebuggerView); + + this._onClick = this._onClick.bind(this); + this._onInput = this._onInput.bind(this); + this._onKeyPress = this._onKeyPress.bind(this); + this._onBlur = this._onBlur.bind(this); +} + +FilterView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the FilterView"); + + this._searchbox = document.getElementById("searchbox"); + this._searchboxHelpPanel = document.getElementById("searchbox-help-panel"); + this._filterLabel = document.getElementById("filter-label"); + this._globalOperatorButton = document.getElementById("global-operator-button"); + this._globalOperatorLabel = document.getElementById("global-operator-label"); + this._functionOperatorButton = document.getElementById("function-operator-button"); + this._functionOperatorLabel = document.getElementById("function-operator-label"); + this._tokenOperatorButton = document.getElementById("token-operator-button"); + this._tokenOperatorLabel = document.getElementById("token-operator-label"); + this._lineOperatorButton = document.getElementById("line-operator-button"); + this._lineOperatorLabel = document.getElementById("line-operator-label"); + this._variableOperatorButton = document.getElementById("variable-operator-button"); + this._variableOperatorLabel = document.getElementById("variable-operator-label"); + + this._fileSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("fileSearchKey")); + this._globalSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("globalSearchKey")); + this._filteredFunctionsKey = ShortcutUtils.prettifyShortcut(document.getElementById("functionSearchKey")); + this._tokenSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("tokenSearchKey")); + this._lineSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("lineSearchKey")); + this._variableSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("variableSearchKey")); + + this._searchbox.addEventListener("click", this._onClick, false); + this._searchbox.addEventListener("select", this._onInput, false); + this._searchbox.addEventListener("input", this._onInput, false); + this._searchbox.addEventListener("keypress", this._onKeyPress, false); + this._searchbox.addEventListener("blur", this._onBlur, false); + + let placeholder = L10N.getFormatStr("emptySearchText", this._fileSearchKey); + this._searchbox.setAttribute("placeholder", placeholder); + + this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG); + this._functionOperatorButton.setAttribute("label", SEARCH_FUNCTION_FLAG); + this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG); + this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG); + this._variableOperatorButton.setAttribute("label", SEARCH_VARIABLE_FLAG); + + this._filterLabel.setAttribute("value", + L10N.getFormatStr("searchPanelFilter", this._fileSearchKey)); + this._globalOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelGlobal", this._globalSearchKey)); + this._functionOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelFunction", this._filteredFunctionsKey)); + this._tokenOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelToken", this._tokenSearchKey)); + this._lineOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelGoToLine", this._lineSearchKey)); + this._variableOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelVariable", this._variableSearchKey)); + + this.FilteredSources.initialize(); + this.FilteredFunctions.initialize(); + + this._addCommands(); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the FilterView"); + + this._searchbox.removeEventListener("click", this._onClick, false); + this._searchbox.removeEventListener("select", this._onInput, false); + this._searchbox.removeEventListener("input", this._onInput, false); + this._searchbox.removeEventListener("keypress", this._onKeyPress, false); + this._searchbox.removeEventListener("blur", this._onBlur, false); + + this.FilteredSources.destroy(); + this.FilteredFunctions.destroy(); + }, + + /** + * Add commands that XUL can fire. + */ + _addCommands: function () { + XULUtils.addCommands(document.getElementById("debuggerCommands"), { + fileSearchCommand: () => this._doFileSearch(), + globalSearchCommand: () => this._doGlobalSearch(), + functionSearchCommand: () => this._doFunctionSearch(), + tokenSearchCommand: () => this._doTokenSearch(), + lineSearchCommand: () => this._doLineSearch(), + variableSearchCommand: () => this._doVariableSearch(), + variablesFocusCommand: () => this._doVariablesFocus() + }); + }, + + /** + * Gets the entered operator and arguments in the searchbox. + * @return array + */ + get searchData() { + let operator = "", args = []; + + let rawValue = this._searchbox.value; + let rawLength = rawValue.length; + let globalFlagIndex = rawValue.indexOf(SEARCH_GLOBAL_FLAG); + let functionFlagIndex = rawValue.indexOf(SEARCH_FUNCTION_FLAG); + let variableFlagIndex = rawValue.indexOf(SEARCH_VARIABLE_FLAG); + let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG); + let lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG); + + // This is not a global, function or variable search, allow file/line flags. + if (globalFlagIndex != 0 && functionFlagIndex != 0 && variableFlagIndex != 0) { + // Token search has precedence over line search. + if (tokenFlagIndex != -1) { + operator = SEARCH_TOKEN_FLAG; + args.push(rawValue.slice(0, tokenFlagIndex)); // file + args.push(rawValue.substr(tokenFlagIndex + 1, rawLength)); // token + } else if (lineFlagIndex != -1) { + operator = SEARCH_LINE_FLAG; + args.push(rawValue.slice(0, lineFlagIndex)); // file + args.push(+rawValue.substr(lineFlagIndex + 1, rawLength) || 0); // line + } else { + args.push(rawValue); + } + } + // Global searches dissalow the use of file or line flags. + else if (globalFlagIndex == 0) { + operator = SEARCH_GLOBAL_FLAG; + args.push(rawValue.slice(1)); + } + // Function searches dissalow the use of file or line flags. + else if (functionFlagIndex == 0) { + operator = SEARCH_FUNCTION_FLAG; + args.push(rawValue.slice(1)); + } + // Variable searches dissalow the use of file or line flags. + else if (variableFlagIndex == 0) { + operator = SEARCH_VARIABLE_FLAG; + args.push(rawValue.slice(1)); + } + + return [operator, args]; + }, + + /** + * Returns the current search operator. + * @return string + */ + get searchOperator() { + return this.searchData[0]; + }, + + /** + * Returns the current search arguments. + * @return array + */ + get searchArguments() { + return this.searchData[1]; + }, + + /** + * Clears the text from the searchbox and any changed views. + */ + clearSearch: function () { + this._searchbox.value = ""; + this.clearViews(); + + this.FilteredSources.clearView(); + this.FilteredFunctions.clearView(); + }, + + /** + * Clears all the views that may pop up when searching. + */ + clearViews: function () { + this.DebuggerView.GlobalSearch.clearView(); + this.FilteredSources.clearView(); + this.FilteredFunctions.clearView(); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Performs a line search if necessary. + * (Jump to lines in the currently visible source). + * + * @param number aLine + * The source line number to jump to. + */ + _performLineSearch: function (aLine) { + // Make sure we're actually searching for a valid line. + if (aLine) { + this.DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center"); + } + }, + + /** + * Performs a token search if necessary. + * (Search for tokens in the currently visible source). + * + * @param string aToken + * The source token to find. + */ + _performTokenSearch: function (aToken) { + // Make sure we're actually searching for a valid token. + if (!aToken) { + return; + } + this.DebuggerView.editor.find(aToken); + }, + + /** + * The click listener for the search container. + */ + _onClick: function () { + // If there's some text in the searchbox, displaying a panel would + // interfere with double/triple click default behaviors. + if (!this._searchbox.value) { + this._searchboxHelpPanel.openPopup(this._searchbox); + } + }, + + /** + * The input listener for the search container. + */ + _onInput: function () { + this.clearViews(); + + // Make sure we're actually searching for something. + if (!this._searchbox.value) { + return; + } + + // Perform the required search based on the specified operator. + switch (this.searchOperator) { + case SEARCH_GLOBAL_FLAG: + // Schedule a global search for when the user stops typing. + this.DebuggerView.GlobalSearch.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_FUNCTION_FLAG: + // Schedule a function search for when the user stops typing. + this.FilteredFunctions.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_VARIABLE_FLAG: + // Schedule a variable search for when the user stops typing. + this.DebuggerView.Variables.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_TOKEN_FLAG: + // Schedule a file+token search for when the user stops typing. + this.FilteredSources.scheduleSearch(this.searchArguments[0]); + this._performTokenSearch(this.searchArguments[1]); + break; + case SEARCH_LINE_FLAG: + // Schedule a file+line search for when the user stops typing. + this.FilteredSources.scheduleSearch(this.searchArguments[0]); + this._performLineSearch(this.searchArguments[1]); + break; + default: + // Schedule a file only search for when the user stops typing. + this.FilteredSources.scheduleSearch(this.searchArguments[0]); + break; + } + }, + + /** + * The key press listener for the search container. + */ + _onKeyPress: function (e) { + // This attribute is not implemented in Gecko at this time, see bug 680830. + e.char = String.fromCharCode(e.charCode); + + // Perform the required action based on the specified operator. + let [operator, args] = this.searchData; + let isGlobalSearch = operator == SEARCH_GLOBAL_FLAG; + let isFunctionSearch = operator == SEARCH_FUNCTION_FLAG; + let isVariableSearch = operator == SEARCH_VARIABLE_FLAG; + let isTokenSearch = operator == SEARCH_TOKEN_FLAG; + let isLineSearch = operator == SEARCH_LINE_FLAG; + let isFileOnlySearch = !operator && args.length == 1; + + // Depending on the pressed keys, determine to correct action to perform. + let actionToPerform; + + // Meta+G and Ctrl+N focus next matches. + if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) { + actionToPerform = "selectNext"; + } + // Meta+Shift+G and Ctrl+P focus previous matches. + else if ((e.char == "G" && e.metaKey) || e.char == "p" && e.ctrlKey) { + actionToPerform = "selectPrev"; + } + // Return, enter, down and up keys focus next or previous matches, while + // the escape key switches focus from the search container. + else switch (e.keyCode) { + case KeyCodes.DOM_VK_RETURN: + var isReturnKey = true; + // If the shift key is pressed, focus on the previous result + actionToPerform = e.shiftKey ? "selectPrev" : "selectNext"; + break; + case KeyCodes.DOM_VK_DOWN: + actionToPerform = "selectNext"; + break; + case KeyCodes.DOM_VK_UP: + actionToPerform = "selectPrev"; + break; + } + + // If there's no action to perform, or no operator, file line or token + // were specified, then this is either a broken or empty search. + if (!actionToPerform || (!operator && !args.length)) { + this.DebuggerView.editor.dropSelection(); + return; + } + + e.preventDefault(); + e.stopPropagation(); + + // Jump to the next/previous entry in the global search, or perform + // a new global search immediately + if (isGlobalSearch) { + let targetView = this.DebuggerView.GlobalSearch; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } + return; + } + + // Jump to the next/previous entry in the function search, perform + // a new function search immediately, or clear it. + if (isFunctionSearch) { + let targetView = this.FilteredFunctions; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } else { + if (!targetView.selectedItem) { + targetView.selectedIndex = 0; + } + this.clearSearch(); + } + return; + } + + // Perform a new variable search immediately. + if (isVariableSearch) { + let targetView = this.DebuggerView.Variables; + if (isReturnKey) { + targetView.scheduleSearch(args[0], 0); + } + return; + } + + // Jump to the next/previous entry in the file search, perform + // a new file search immediately, or clear it. + if (isFileOnlySearch) { + let targetView = this.FilteredSources; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } else { + if (!targetView.selectedItem) { + targetView.selectedIndex = 0; + } + this.clearSearch(); + } + return; + } + + // Jump to the next/previous instance of the currently searched token. + if (isTokenSearch) { + let methods = { selectNext: "findNext", selectPrev: "findPrev" }; + this.DebuggerView.editor[methods[actionToPerform]](); + return; + } + + // Increment/decrement the currently searched caret line. + if (isLineSearch) { + let [, line] = args; + let amounts = { selectNext: 1, selectPrev: -1 }; + + // Modify the line number and jump to it. + line += !isReturnKey ? amounts[actionToPerform] : 0; + let lineCount = this.DebuggerView.editor.lineCount(); + let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line; + this._doSearch(SEARCH_LINE_FLAG, lineTarget); + return; + } + }, + + /** + * The blur listener for the search container. + */ + _onBlur: function () { + this.clearViews(); + }, + + /** + * Called when a filtering key sequence was pressed. + * + * @param string aOperator + * The operator to use for filtering. + */ + _doSearch: function (aOperator = "", aText = "") { + this._searchbox.focus(); + this._searchbox.value = ""; // Need to clear value beforehand. Bug 779738. + + if (aText) { + this._searchbox.value = aOperator + aText; + return; + } + if (this.DebuggerView.editor.somethingSelected()) { + this._searchbox.value = aOperator + this.DebuggerView.editor.getSelection(); + return; + } + + let content = this.DebuggerView.editor.getText(); + if (content.length < this.DebuggerView.LARGE_FILE_SIZE && + SEARCH_AUTOFILL.indexOf(aOperator) != -1) { + let cursor = this.DebuggerView.editor.getCursor(); + let location = this.DebuggerView.Sources.selectedItem.attachment.source.url; + let source = this.Parser.get(content, location); + let identifier = source.getIdentifierAt({ line: cursor.line + 1, column: cursor.ch }); + + if (identifier && identifier.name) { + this._searchbox.value = aOperator + identifier.name; + this._searchbox.select(); + this._searchbox.selectionStart += aOperator.length; + return; + } + } + this._searchbox.value = aOperator; + }, + + /** + * Called when the source location filter key sequence was pressed. + */ + _doFileSearch: function () { + this._doSearch(); + this._searchboxHelpPanel.openPopup(this._searchbox); + }, + + /** + * Called when the global search filter key sequence was pressed. + */ + _doGlobalSearch: function () { + this._doSearch(SEARCH_GLOBAL_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source function filter key sequence was pressed. + */ + _doFunctionSearch: function () { + this._doSearch(SEARCH_FUNCTION_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source token filter key sequence was pressed. + */ + _doTokenSearch: function () { + this._doSearch(SEARCH_TOKEN_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source line filter key sequence was pressed. + */ + _doLineSearch: function () { + this._doSearch(SEARCH_LINE_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the variable search filter key sequence was pressed. + */ + _doVariableSearch: function () { + this._doSearch(SEARCH_VARIABLE_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the variables focus key sequence was pressed. + */ + _doVariablesFocus: function () { + this.DebuggerView.showInstrumentsPane(); + this.DebuggerView.Variables.focusFirstVisibleItem(); + }, + + _searchbox: null, + _searchboxHelpPanel: null, + _globalOperatorButton: null, + _globalOperatorLabel: null, + _functionOperatorButton: null, + _functionOperatorLabel: null, + _tokenOperatorButton: null, + _tokenOperatorLabel: null, + _lineOperatorButton: null, + _lineOperatorLabel: null, + _variableOperatorButton: null, + _variableOperatorLabel: null, + _fileSearchKey: "", + _globalSearchKey: "", + _filteredFunctionsKey: "", + _tokenSearchKey: "", + _lineSearchKey: "", + _variableSearchKey: "", +}; + +/** + * Functions handling the filtered sources UI. + */ +function FilteredSourcesView(DebuggerView) { + dumpn("FilteredSourcesView was instantiated"); + + this.DebuggerView = DebuggerView; + + this._onClick = this._onClick.bind(this); + this._onSelect = this._onSelect.bind(this); +} + +FilteredSourcesView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the FilteredSourcesView"); + + this.anchor = document.getElementById("searchbox"); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("click", this._onClick, false); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the FilteredSourcesView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("click", this._onClick, false); + this.anchor = null; + }, + + /** + * Schedules searching for a source. + * + * @param string aToken + * The function to search for. + * @param number aWait + * The amount of milliseconds to wait until draining. + */ + scheduleSearch: function (aToken, aWait) { + // The amount of time to wait for the requests to settle. + let maxDelay = FILE_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("sources-search", delay, () => this._doSearch(aToken)); + }, + + /** + * Finds file matches in all the displayed sources. + * + * @param string aToken + * The string to search for. + */ + _doSearch: function (aToken, aStore = []) { + // Don't continue filtering if the searched token is an empty string. + // In contrast with function searching, in this case we don't want to + // show a list of all the files when no search token was supplied. + if (!aToken) { + return; + } + + for (let item of this.DebuggerView.Sources.items) { + let lowerCaseLabel = item.attachment.label.toLowerCase(); + let lowerCaseToken = aToken.toLowerCase(); + if (lowerCaseLabel.match(lowerCaseToken)) { + aStore.push(item); + } + + // Once the maximum allowed number of results is reached, proceed + // with building the UI immediately. + if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { + this._syncView(aStore); + return; + } + } + + // Couldn't reach the maximum allowed number of results, but that's ok, + // continue building the UI. + this._syncView(aStore); + }, + + /** + * Updates the list of sources displayed in this container. + * + * @param array aSearchResults + * The results array, containing search details for each source. + */ + _syncView: function (aSearchResults) { + // If there are no matches found, keep the popup hidden and avoid + // creating the view. + if (!aSearchResults.length) { + window.emit(EVENTS.FILE_SEARCH_MATCH_NOT_FOUND); + return; + } + + for (let item of aSearchResults) { + let url = item.attachment.source.url; + + if (url) { + // Create the element node for the location item. + let itemView = this._createItemView( + SourceUtils.trimUrlLength(item.attachment.label), + SourceUtils.trimUrlLength(url, 0, "start") + ); + + // Append a location item to this container for each match. + this.push([itemView], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + url: url + } + }); + } + } + + // There's at least one item displayed in this container. Don't select it + // automatically if not forced (by tests) or in tandem with an + // operator. + if (this._autoSelectFirstItem || this.DebuggerView.Filtering.searchOperator) { + this.selectedIndex = 0; + } + this.hidden = false; + + // Signal that file search matches were found and displayed. + window.emit(EVENTS.FILE_SEARCH_MATCH_FOUND); + }, + + /** + * The click listener for this container. + */ + _onClick: function (e) { + let locationItem = this.getItemForElement(e.target); + if (locationItem) { + this.selectedItem = locationItem; + this.DebuggerView.Filtering.clearSearch(); + } + }, + + /** + * The select listener for this container. + * + * @param object aItem + * The item associated with the element to select. + */ + _onSelect: function ({ detail: locationItem }) { + if (locationItem) { + let source = queries.getSourceByURL(DebuggerController.getState(), + locationItem.attachment.url); + this.DebuggerView.setEditorLocation(source.actor, undefined, { + noCaret: true, + noDebug: true + }); + } + } +}); + +/** + * Functions handling the function search UI. + */ +function FilteredFunctionsView(SourceScripts, Parser, DebuggerView) { + dumpn("FilteredFunctionsView was instantiated"); + + this.SourceScripts = SourceScripts; + this.Parser = Parser; + this.DebuggerView = DebuggerView; + + this._onClick = this._onClick.bind(this); + this._onSelect = this._onSelect.bind(this); +} + +FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the FilteredFunctionsView"); + + this.anchor = document.getElementById("searchbox"); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("click", this._onClick, false); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the FilteredFunctionsView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("click", this._onClick, false); + this.anchor = null; + }, + + /** + * Schedules searching for a function in all of the sources. + * + * @param string aToken + * The function to search for. + * @param number aWait + * The amount of milliseconds to wait until draining. + */ + scheduleSearch: function (aToken, aWait) { + // The amount of time to wait for the requests to settle. + let maxDelay = FUNCTION_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("function-search", delay, () => { + // Start fetching as many sources as possible, then perform the search. + let actors = this.DebuggerView.Sources.values; + let sourcesFetched = DebuggerController.dispatch(actions.getTextForSources(actors)); + sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); + }); + }, + + /** + * Finds function matches in all the sources stored in the cache, and groups + * them by location and line number. + * + * @param string aToken + * The string to search for. + * @param array aSources + * An array of [url, text] tuples for each source. + */ + _doSearch: function (aToken, aSources, aStore = []) { + // Continue parsing even if the searched token is an empty string, to + // cache the syntax tree nodes generated by the reflection API. + + // Make sure the currently displayed source is parsed first. Once the + // maximum allowed number of results are found, parsing will be halted. + let currentActor = this.DebuggerView.Sources.selectedValue; + let currentSource = aSources.filter(([actor]) => actor == currentActor)[0]; + aSources.splice(aSources.indexOf(currentSource), 1); + aSources.unshift(currentSource); + + // If not searching for a specific function, only parse the displayed source, + // which is now the first item in the sources array. + if (!aToken) { + aSources.splice(1); + } + + for (let [actor, contents] of aSources) { + let item = this.DebuggerView.Sources.getItemByValue(actor); + let url = item.attachment.source.url; + if (!url) { + continue; + } + + let parsedSource = this.Parser.get(contents, url); + let sourceResults = parsedSource.getNamedFunctionDefinitions(aToken); + + for (let scriptResult of sourceResults) { + for (let parseResult of scriptResult) { + aStore.push({ + sourceUrl: scriptResult.sourceUrl, + scriptOffset: scriptResult.scriptOffset, + functionName: parseResult.functionName, + functionLocation: parseResult.functionLocation, + inferredName: parseResult.inferredName, + inferredChain: parseResult.inferredChain, + inferredLocation: parseResult.inferredLocation + }); + + // Once the maximum allowed number of results is reached, proceed + // with building the UI immediately. + if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { + this._syncView(aStore); + return; + } + } + } + } + + // Couldn't reach the maximum allowed number of results, but that's ok, + // continue building the UI. + this._syncView(aStore); + }, + + /** + * Updates the list of functions displayed in this container. + * + * @param array aSearchResults + * The results array, containing search details for each source. + */ + _syncView: function (aSearchResults) { + // If there are no matches found, keep the popup hidden and avoid + // creating the view. + if (!aSearchResults.length) { + window.emit(EVENTS.FUNCTION_SEARCH_MATCH_NOT_FOUND); + return; + } + + for (let item of aSearchResults) { + // Some function expressions don't necessarily have a name, but the + // parser provides us with an inferred name from an enclosing + // VariableDeclarator, AssignmentExpression, ObjectExpression node. + if (item.functionName && item.inferredName && + item.functionName != item.inferredName) { + let s = " " + L10N.getStr("functionSearchSeparatorLabel") + " "; + item.displayedName = item.inferredName + s + item.functionName; + } + // The function doesn't have an explicit name, but it could be inferred. + else if (item.inferredName) { + item.displayedName = item.inferredName; + } + // The function only has an explicit name. + else { + item.displayedName = item.functionName; + } + + // Some function expressions have unexpected bounds, since they may not + // necessarily have an associated name defining them. + if (item.inferredLocation) { + item.actualLocation = item.inferredLocation; + } else { + item.actualLocation = item.functionLocation; + } + + // Create the element node for the function item. + let itemView = this._createItemView( + SourceUtils.trimUrlLength(item.displayedName + "()"), + SourceUtils.trimUrlLength(item.sourceUrl, 0, "start"), + (item.inferredChain || []).join(".") + ); + + // Append a function item to this container for each match. + this.push([itemView], { + index: -1, /* specifies on which position should the item be appended */ + attachment: item + }); + } + + // There's at least one item displayed in this container. Don't select it + // automatically if not forced (by tests). + if (this._autoSelectFirstItem) { + this.selectedIndex = 0; + } + this.hidden = false; + + // Signal that function search matches were found and displayed. + window.emit(EVENTS.FUNCTION_SEARCH_MATCH_FOUND); + }, + + /** + * The click listener for this container. + */ + _onClick: function (e) { + let functionItem = this.getItemForElement(e.target); + if (functionItem) { + this.selectedItem = functionItem; + this.DebuggerView.Filtering.clearSearch(); + } + }, + + /** + * The select listener for this container. + */ + _onSelect: function ({ detail: functionItem }) { + if (functionItem) { + let sourceUrl = functionItem.attachment.sourceUrl; + let actor = queries.getSourceByURL(DebuggerController.getState(), sourceUrl).actor; + let scriptOffset = functionItem.attachment.scriptOffset; + let actualLocation = functionItem.attachment.actualLocation; + + this.DebuggerView.setEditorLocation(actor, actualLocation.start.line, { + charOffset: scriptOffset, + columnOffset: actualLocation.start.column, + align: "center", + noDebug: true + }); + } + }, + + _searchTimeout: null, + _searchFunction: null, + _searchedToken: "" +}); + +DebuggerView.Filtering = new FilterView(DebuggerController, DebuggerView); |