diff options
Diffstat (limited to 'devtools/client/debugger/views')
-rw-r--r-- | devtools/client/debugger/views/filter-view.js | 925 | ||||
-rw-r--r-- | devtools/client/debugger/views/global-search-view.js | 756 | ||||
-rw-r--r-- | devtools/client/debugger/views/options-view.js | 215 | ||||
-rw-r--r-- | devtools/client/debugger/views/stack-frames-classic-view.js | 141 | ||||
-rw-r--r-- | devtools/client/debugger/views/stack-frames-view.js | 283 | ||||
-rw-r--r-- | devtools/client/debugger/views/toolbar-view.js | 287 | ||||
-rw-r--r-- | devtools/client/debugger/views/variable-bubble-view.js | 321 | ||||
-rw-r--r-- | devtools/client/debugger/views/watch-expressions-view.js | 303 | ||||
-rw-r--r-- | devtools/client/debugger/views/workers-view.js | 55 |
9 files changed, 3286 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); diff --git a/devtools/client/debugger/views/global-search-view.js b/devtools/client/debugger/views/global-search-view.js new file mode 100644 index 000000000..c6a627971 --- /dev/null +++ b/devtools/client/debugger/views/global-search-view.js @@ -0,0 +1,756 @@ +/* -*- 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 global search UI. + */ +function GlobalSearchView(DebuggerController, DebuggerView) { + dumpn("GlobalSearchView was instantiated"); + + this.SourceScripts = DebuggerController.SourceScripts; + this.DebuggerView = DebuggerView; + + this._onHeaderClick = this._onHeaderClick.bind(this); + this._onLineClick = this._onLineClick.bind(this); + this._onMatchClick = this._onMatchClick.bind(this); +} + +GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the GlobalSearchView"); + + this.widget = new SimpleListWidget(document.getElementById("globalsearch")); + this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); + + this.emptyText = L10N.getStr("noMatchingStringsText"); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the GlobalSearchView"); + }, + + /** + * Sets the results container hidden or visible. It's hidden by default. + * @param boolean aFlag + */ + set hidden(aFlag) { + this.widget.setAttribute("hidden", aFlag); + this._splitter.setAttribute("hidden", aFlag); + }, + + /** + * Gets the visibility state of the global search container. + * @return boolean + */ + get hidden() { + return this.widget.getAttribute("hidden") == "true" || + this._splitter.getAttribute("hidden") == "true"; + }, + + /** + * Hides and removes all items from this search container. + */ + clearView: function () { + this.hidden = true; + this.empty(); + }, + + /** + * Selects the next found item in this container. + * Does not change the currently focused node. + */ + selectNext: function () { + let totalLineResults = LineResults.size(); + if (!totalLineResults) { + return; + } + if (++this._currentlyFocusedMatch >= totalLineResults) { + this._currentlyFocusedMatch = 0; + } + this._onMatchClick({ + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) + }); + }, + + /** + * Selects the previously found item in this container. + * Does not change the currently focused node. + */ + selectPrev: function () { + let totalLineResults = LineResults.size(); + if (!totalLineResults) { + return; + } + if (--this._currentlyFocusedMatch < 0) { + this._currentlyFocusedMatch = totalLineResults - 1; + } + this._onMatchClick({ + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) + }); + }, + + /** + * Schedules searching for a string in all of the sources. + * + * @param string aToken + * The string 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 = GLOBAL_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("global-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 string matches in all the sources stored in the controller's cache, + * and groups them by url 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) { + // Don't continue filtering if the searched token is an empty string. + if (!aToken) { + this.clearView(); + return; + } + + // Search is not case sensitive, prepare the actual searched token. + let lowerCaseToken = aToken.toLowerCase(); + let tokenLength = aToken.length; + + // Create a Map containing search details for each source. + let globalResults = new GlobalResults(); + + // Search for the specified token in each source's text. + for (let [actor, text] of aSources) { + let item = this.DebuggerView.Sources.getItemByValue(actor); + let url = item.attachment.source.url; + if (!url) { + continue; + } + + // Verify that the search token is found anywhere in the source. + if (!text.toLowerCase().includes(lowerCaseToken)) { + continue; + } + // ...and if so, create a Map containing search details for each line. + let sourceResults = new SourceResults(actor, + globalResults, + this.DebuggerView.Sources); + + // Search for the specified token in each line's text. + text.split("\n").forEach((aString, aLine) => { + // Search is not case sensitive, prepare the actual searched line. + let lowerCaseLine = aString.toLowerCase(); + + // Verify that the search token is found anywhere in this line. + if (!lowerCaseLine.includes(lowerCaseToken)) { + return; + } + // ...and if so, create a Map containing search details for each word. + let lineResults = new LineResults(aLine, sourceResults); + + // Search for the specified token this line's text. + lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { + let prevLength = aPrev.length; + let currLength = aCurr.length; + + // Everything before the token is unmatched. + let unmatched = aString.substr(prevLength, currLength); + lineResults.add(unmatched); + + // The lowered-case line was split by the lowered-case token. So, + // get the actual matched text from the original line's text. + if (aIndex != aArray.length - 1) { + let matched = aString.substr(prevLength + currLength, tokenLength); + let range = { start: prevLength + currLength, length: matched.length }; + lineResults.add(matched, range, true); + } + + // Continue with the next sub-region in this line's text. + return aPrev + aToken + aCurr; + }, ""); + + if (lineResults.matchCount) { + sourceResults.add(lineResults); + } + }); + + if (sourceResults.matchCount) { + globalResults.add(sourceResults); + } + } + + // Rebuild the results, then signal if there are any matches. + if (globalResults.matchCount) { + this.hidden = false; + this._currentlyFocusedMatch = -1; + this._createGlobalResultsUI(globalResults); + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); + } else { + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); + } + }, + + /** + * Creates global search results entries and adds them to this container. + * + * @param GlobalResults aGlobalResults + * An object containing all source results, grouped by source location. + */ + _createGlobalResultsUI: function (aGlobalResults) { + let i = 0; + + for (let sourceResults of aGlobalResults) { + if (i++ == 0) { + this._createSourceResultsUI(sourceResults); + } else { + // Dispatch subsequent document manipulation operations, to avoid + // blocking the main thread when a large number of search results + // is found, thus giving the impression of faster searching. + Services.tm.currentThread.dispatch({ run: + this._createSourceResultsUI.bind(this, sourceResults) + }, 0); + } + } + }, + + /** + * Creates source search results entries and adds them to this container. + * + * @param SourceResults aSourceResults + * An object containing all the matched lines for a specific source. + */ + _createSourceResultsUI: function (aSourceResults) { + // Create the element node for the source results item. + let container = document.createElement("hbox"); + aSourceResults.createView(container, { + onHeaderClick: this._onHeaderClick, + onLineClick: this._onLineClick, + onMatchClick: this._onMatchClick + }); + + // Append a source results item to this container. + let item = this.push([container], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + sourceResults: aSourceResults + } + }); + }, + + /** + * The click listener for a results header. + */ + _onHeaderClick: function (e) { + let sourceResultsItem = SourceResults.getItemForElement(e.target); + sourceResultsItem.instance.toggle(e); + }, + + /** + * The click listener for a results line. + */ + _onLineClick: function (e) { + let lineResultsItem = LineResults.getItemForElement(e.target); + this._onMatchClick({ target: lineResultsItem.firstMatch }); + }, + + /** + * The click listener for a result match. + */ + _onMatchClick: function (e) { + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + + let target = e.target; + let sourceResultsItem = SourceResults.getItemForElement(target); + let lineResultsItem = LineResults.getItemForElement(target); + + sourceResultsItem.instance.expand(); + this._currentlyFocusedMatch = LineResults.indexOfElement(target); + this._scrollMatchIntoViewIfNeeded(target); + this._bounceMatch(target); + + let actor = sourceResultsItem.instance.actor; + let line = lineResultsItem.instance.line; + + this.DebuggerView.setEditorLocation(actor, line + 1, { noDebug: true }); + + let range = lineResultsItem.lineData.range; + let cursor = this.DebuggerView.editor.getOffset({ line: line, ch: 0 }); + let [ anchor, head ] = this.DebuggerView.editor.getPosition( + cursor + range.start, + cursor + range.start + range.length + ); + + this.DebuggerView.editor.setSelection(anchor, head); + }, + + /** + * Scrolls a match into view if not already visible. + * + * @param nsIDOMNode aMatch + * The match to scroll into view. + */ + _scrollMatchIntoViewIfNeeded: function (aMatch) { + this.widget.ensureElementIsVisible(aMatch); + }, + + /** + * Starts a bounce animation for a match. + * + * @param nsIDOMNode aMatch + * The match to start a bounce animation for. + */ + _bounceMatch: function (aMatch) { + Services.tm.currentThread.dispatch({ run: () => { + aMatch.addEventListener("transitionend", function onEvent() { + aMatch.removeEventListener("transitionend", onEvent); + aMatch.removeAttribute("focused"); + }); + aMatch.setAttribute("focused", ""); + }}, 0); + aMatch.setAttribute("focusing", ""); + }, + + _splitter: null, + _currentlyFocusedMatch: -1, + _forceExpandResults: false +}); + +DebuggerView.GlobalSearch = new GlobalSearchView(DebuggerController, DebuggerView); + +/** + * An object containing all source results, grouped by source location. + * Iterable via "for (let [location, sourceResults] of globalResults) { }". + */ +function GlobalResults() { + this._store = []; + SourceResults._itemsByElement = new Map(); + LineResults._itemsByElement = new Map(); +} + +GlobalResults.prototype = { + /** + * Adds source results to this store. + * + * @param SourceResults aSourceResults + * An object containing search results for a specific source. + */ + add: function (aSourceResults) { + this._store.push(aSourceResults); + }, + + /** + * Gets the number of source results in this store. + */ + get matchCount() { + return this._store.length; + } +}; + +/** + * An object containing all the matched lines for a specific source. + * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". + * + * @param string aActor + * The target source actor id. + * @param GlobalResults aGlobalResults + * An object containing all source results, grouped by source location. + */ +function SourceResults(aActor, aGlobalResults, sourcesView) { + let item = sourcesView.getItemByValue(aActor); + this.actor = aActor; + this.label = item.attachment.source.url; + this._globalResults = aGlobalResults; + this._store = []; +} + +SourceResults.prototype = { + /** + * Adds line results to this store. + * + * @param LineResults aLineResults + * An object containing search results for a specific line. + */ + add: function (aLineResults) { + this._store.push(aLineResults); + }, + + /** + * Gets the number of line results in this store. + */ + get matchCount() { + return this._store.length; + }, + + /** + * Expands the element, showing all the added details. + */ + expand: function () { + this._resultsContainer.removeAttribute("hidden"); + this._arrow.setAttribute("open", ""); + }, + + /** + * Collapses the element, hiding all the added details. + */ + collapse: function () { + this._resultsContainer.setAttribute("hidden", "true"); + this._arrow.removeAttribute("open"); + }, + + /** + * Toggles between the element collapse/expand state. + */ + toggle: function (e) { + this.expanded ^= 1; + }, + + /** + * Gets this element's expanded state. + * @return boolean + */ + get expanded() { + return this._resultsContainer.getAttribute("hidden") != "true" && + this._arrow.hasAttribute("open"); + }, + + /** + * Sets this element's expanded state. + * @param boolean aFlag + */ + set expanded(aFlag) { + this[aFlag ? "expand" : "collapse"](); + }, + + /** + * Gets the element associated with this item. + * @return nsIDOMNode + */ + get target() { + return this._target; + }, + + /** + * Customization function for creating this item's UI. + * + * @param nsIDOMNode aElementNode + * The element associated with the displayed item. + * @param object aCallbacks + * An object containing all the necessary callback functions: + * - onHeaderClick + * - onMatchClick + */ + createView: function (aElementNode, aCallbacks) { + this._target = aElementNode; + + let arrow = this._arrow = document.createElement("box"); + arrow.className = "arrow"; + + let locationNode = document.createElement("label"); + locationNode.className = "plain dbg-results-header-location"; + locationNode.setAttribute("value", this.label); + + let matchCountNode = document.createElement("label"); + matchCountNode.className = "plain dbg-results-header-match-count"; + matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); + + let resultsHeader = this._resultsHeader = document.createElement("hbox"); + resultsHeader.className = "dbg-results-header"; + resultsHeader.setAttribute("align", "center"); + resultsHeader.appendChild(arrow); + resultsHeader.appendChild(locationNode); + resultsHeader.appendChild(matchCountNode); + resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); + + let resultsContainer = this._resultsContainer = document.createElement("vbox"); + resultsContainer.className = "dbg-results-container"; + resultsContainer.setAttribute("hidden", "true"); + + // Create lines search results entries and add them to this container. + // Afterwards, if the number of matches is reasonable, expand this + // container automatically. + for (let lineResults of this._store) { + lineResults.createView(resultsContainer, aCallbacks); + } + if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { + this.expand(); + } + + let resultsBox = document.createElement("vbox"); + resultsBox.setAttribute("flex", "1"); + resultsBox.appendChild(resultsHeader); + resultsBox.appendChild(resultsContainer); + + aElementNode.id = "source-results-" + this.actor; + aElementNode.className = "dbg-source-results"; + aElementNode.appendChild(resultsBox); + + SourceResults._itemsByElement.set(aElementNode, { instance: this }); + }, + + actor: "", + _globalResults: null, + _store: null, + _target: null, + _arrow: null, + _resultsHeader: null, + _resultsContainer: null +}; + +/** + * An object containing all the matches for a specific line. + * Iterable via "for (let chunk of lineResults) { }". + * + * @param number aLine + * The target line in the source. + * @param SourceResults aSourceResults + * An object containing all the matched lines for a specific source. + */ +function LineResults(aLine, aSourceResults) { + this.line = aLine; + this._sourceResults = aSourceResults; + this._store = []; + this._matchCount = 0; +} + +LineResults.prototype = { + /** + * Adds string details to this store. + * + * @param string aString + * The text contents chunk in the line. + * @param object aRange + * An object containing the { start, length } of the chunk. + * @param boolean aMatchFlag + * True if the chunk is a matched string, false if just text content. + */ + add: function (aString, aRange, aMatchFlag) { + this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); + this._matchCount += aMatchFlag ? 1 : 0; + }, + + /** + * Gets the number of word results in this store. + */ + get matchCount() { + return this._matchCount; + }, + + /** + * Gets the element associated with this item. + * @return nsIDOMNode + */ + get target() { + return this._target; + }, + + /** + * Customization function for creating this item's UI. + * + * @param nsIDOMNode aElementNode + * The element associated with the displayed item. + * @param object aCallbacks + * An object containing all the necessary callback functions: + * - onMatchClick + * - onLineClick + */ + createView: function (aElementNode, aCallbacks) { + this._target = aElementNode; + + let lineNumberNode = document.createElement("label"); + lineNumberNode.className = "plain dbg-results-line-number"; + lineNumberNode.classList.add("devtools-monospace"); + lineNumberNode.setAttribute("value", this.line + 1); + + let lineContentsNode = document.createElement("hbox"); + lineContentsNode.className = "dbg-results-line-contents"; + lineContentsNode.classList.add("devtools-monospace"); + lineContentsNode.setAttribute("flex", "1"); + + let lineString = ""; + let lineLength = 0; + let firstMatch = null; + + for (let lineChunk of this._store) { + let { string, range, match } = lineChunk; + lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); + lineLength += string.length; + + let lineChunkNode = document.createElement("label"); + lineChunkNode.className = "plain dbg-results-line-contents-string"; + lineChunkNode.setAttribute("value", lineString); + lineChunkNode.setAttribute("match", match); + lineContentsNode.appendChild(lineChunkNode); + + if (match) { + this._entangleMatch(lineChunkNode, lineChunk); + lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); + firstMatch = firstMatch || lineChunkNode; + } + if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { + lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); + break; + } + } + + this._entangleLine(lineContentsNode, firstMatch); + lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); + + let searchResult = document.createElement("hbox"); + searchResult.className = "dbg-search-result"; + searchResult.appendChild(lineNumberNode); + searchResult.appendChild(lineContentsNode); + + aElementNode.appendChild(searchResult); + }, + + /** + * Handles a match while creating the view. + * @param nsIDOMNode aNode + * @param object aMatchChunk + */ + _entangleMatch: function (aNode, aMatchChunk) { + LineResults._itemsByElement.set(aNode, { + instance: this, + lineData: aMatchChunk + }); + }, + + /** + * Handles a line while creating the view. + * @param nsIDOMNode aNode + * @param nsIDOMNode aFirstMatch + */ + _entangleLine: function (aNode, aFirstMatch) { + LineResults._itemsByElement.set(aNode, { + instance: this, + firstMatch: aFirstMatch, + ignored: true + }); + }, + + /** + * An nsIDOMNode label with an ellipsis value. + */ + _ellipsis: (function () { + let label = document.createElement("label"); + label.className = "plain dbg-results-line-contents-string"; + label.setAttribute("value", ELLIPSIS); + return label; + })(), + + line: 0, + _sourceResults: null, + _store: null, + _target: null +}; + +/** + * A generator-iterator over the global, source or line results. + */ +GlobalResults.prototype[Symbol.iterator] = +SourceResults.prototype[Symbol.iterator] = +LineResults.prototype[Symbol.iterator] = function* () { + yield* this._store; +}; + +/** + * Gets the item associated with the specified element. + * + * @param nsIDOMNode aElement + * The element used to identify the item. + * @return object + * The matched item, or null if nothing is found. + */ +SourceResults.getItemForElement = +LineResults.getItemForElement = function (aElement) { + return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); +}; + +/** + * Gets the element associated with a particular item at a specified index. + * + * @param number aIndex + * The index used to identify the item. + * @return nsIDOMNode + * The matched element, or null if nothing is found. + */ +SourceResults.getElementAtIndex = +LineResults.getElementAtIndex = function (aIndex) { + for (let [element, item] of this._itemsByElement) { + if (!item.ignored && !aIndex--) { + return element; + } + } + return null; +}; + +/** + * Gets the index of an item associated with the specified element. + * + * @param nsIDOMNode aElement + * The element to get the index for. + * @return number + * The index of the matched element, or -1 if nothing is found. + */ +SourceResults.indexOfElement = +LineResults.indexOfElement = function (aElement) { + let count = 0; + for (let [element, item] of this._itemsByElement) { + if (element == aElement) { + return count; + } + if (!item.ignored) { + count++; + } + } + return -1; +}; + +/** + * Gets the number of cached items associated with a specified element. + * + * @return number + * The number of key/value pairs in the corresponding map. + */ +SourceResults.size = +LineResults.size = function () { + let count = 0; + for (let [, item] of this._itemsByElement) { + if (!item.ignored) { + count++; + } + } + return count; +}; diff --git a/devtools/client/debugger/views/options-view.js b/devtools/client/debugger/views/options-view.js new file mode 100644 index 000000000..2fb5b0600 --- /dev/null +++ b/devtools/client/debugger/views/options-view.js @@ -0,0 +1,215 @@ +/* -*- 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"; + +// A time interval sufficient for the options popup panel to finish hiding +// itself. +const POPUP_HIDDEN_DELAY = 100; // ms + +/** + * Functions handling the options UI. + */ +function OptionsView(DebuggerController, DebuggerView) { + dumpn("OptionsView was instantiated"); + + this.DebuggerController = DebuggerController; + this.DebuggerView = DebuggerView; + + this._toggleAutoPrettyPrint = this._toggleAutoPrettyPrint.bind(this); + this._togglePauseOnExceptions = this._togglePauseOnExceptions.bind(this); + this._toggleIgnoreCaughtExceptions = this._toggleIgnoreCaughtExceptions.bind(this); + this._toggleShowPanesOnStartup = this._toggleShowPanesOnStartup.bind(this); + this._toggleShowVariablesOnlyEnum = this._toggleShowVariablesOnlyEnum.bind(this); + this._toggleShowVariablesFilterBox = this._toggleShowVariablesFilterBox.bind(this); + this._toggleShowOriginalSource = this._toggleShowOriginalSource.bind(this); + this._toggleAutoBlackBox = this._toggleAutoBlackBox.bind(this); +} + +OptionsView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the OptionsView"); + + this._button = document.getElementById("debugger-options"); + this._autoPrettyPrint = document.getElementById("auto-pretty-print"); + this._pauseOnExceptionsItem = document.getElementById("pause-on-exceptions"); + this._ignoreCaughtExceptionsItem = document.getElementById("ignore-caught-exceptions"); + this._showPanesOnStartupItem = document.getElementById("show-panes-on-startup"); + this._showVariablesOnlyEnumItem = document.getElementById("show-vars-only-enum"); + this._showVariablesFilterBoxItem = document.getElementById("show-vars-filter-box"); + this._showOriginalSourceItem = document.getElementById("show-original-source"); + this._autoBlackBoxItem = document.getElementById("auto-black-box"); + + this._autoPrettyPrint.setAttribute("checked", Prefs.autoPrettyPrint); + this._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions); + this._ignoreCaughtExceptionsItem.setAttribute("checked", Prefs.ignoreCaughtExceptions); + this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup); + this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible); + this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible); + this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled); + this._autoBlackBoxItem.setAttribute("checked", Prefs.autoBlackBox); + + this._addCommands(); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the OptionsView"); + // Nothing to do here yet. + }, + + /** + * Add commands that XUL can fire. + */ + _addCommands: function () { + XULUtils.addCommands(document.getElementById("debuggerCommands"), { + toggleAutoPrettyPrint: () => this._toggleAutoPrettyPrint(), + togglePauseOnExceptions: () => this._togglePauseOnExceptions(), + toggleIgnoreCaughtExceptions: () => this._toggleIgnoreCaughtExceptions(), + toggleShowPanesOnStartup: () => this._toggleShowPanesOnStartup(), + toggleShowOnlyEnum: () => this._toggleShowVariablesOnlyEnum(), + toggleShowVariablesFilterBox: () => this._toggleShowVariablesFilterBox(), + toggleShowOriginalSource: () => this._toggleShowOriginalSource(), + toggleAutoBlackBox: () => this._toggleAutoBlackBox() + }); + }, + + /** + * Listener handling the 'gear menu' popup showing event. + */ + _onPopupShowing: function () { + this._button.setAttribute("open", "true"); + window.emit(EVENTS.OPTIONS_POPUP_SHOWING); + }, + + /** + * Listener handling the 'gear menu' popup hiding event. + */ + _onPopupHiding: function () { + this._button.removeAttribute("open"); + }, + + /** + * Listener handling the 'gear menu' popup hidden event. + */ + _onPopupHidden: function () { + window.emit(EVENTS.OPTIONS_POPUP_HIDDEN); + }, + + /** + * Listener handling the 'auto pretty print' menuitem command. + */ + _toggleAutoPrettyPrint: function () { + Prefs.autoPrettyPrint = + this._autoPrettyPrint.getAttribute("checked") == "true"; + }, + + /** + * Listener handling the 'pause on exceptions' menuitem command. + */ + _togglePauseOnExceptions: function () { + Prefs.pauseOnExceptions = + this._pauseOnExceptionsItem.getAttribute("checked") == "true"; + + this.DebuggerController.activeThread.pauseOnExceptions( + Prefs.pauseOnExceptions, + Prefs.ignoreCaughtExceptions); + }, + + _toggleIgnoreCaughtExceptions: function () { + Prefs.ignoreCaughtExceptions = + this._ignoreCaughtExceptionsItem.getAttribute("checked") == "true"; + + this.DebuggerController.activeThread.pauseOnExceptions( + Prefs.pauseOnExceptions, + Prefs.ignoreCaughtExceptions); + }, + + /** + * Listener handling the 'show panes on startup' menuitem command. + */ + _toggleShowPanesOnStartup: function () { + Prefs.panesVisibleOnStartup = + this._showPanesOnStartupItem.getAttribute("checked") == "true"; + }, + + /** + * Listener handling the 'show non-enumerables' menuitem command. + */ + _toggleShowVariablesOnlyEnum: function () { + let pref = Prefs.variablesOnlyEnumVisible = + this._showVariablesOnlyEnumItem.getAttribute("checked") == "true"; + + this.DebuggerView.Variables.onlyEnumVisible = pref; + }, + + /** + * Listener handling the 'show variables searchbox' menuitem command. + */ + _toggleShowVariablesFilterBox: function () { + let pref = Prefs.variablesSearchboxVisible = + this._showVariablesFilterBoxItem.getAttribute("checked") == "true"; + + this.DebuggerView.Variables.searchEnabled = pref; + }, + + /** + * Listener handling the 'show original source' menuitem command. + */ + _toggleShowOriginalSource: function () { + let pref = Prefs.sourceMapsEnabled = + this._showOriginalSourceItem.getAttribute("checked") == "true"; + + // Don't block the UI while reconfiguring the server. + window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => { + // The popup panel needs more time to hide after triggering onpopuphidden. + window.setTimeout(() => { + this.DebuggerController.reconfigureThread({ + useSourceMaps: pref, + autoBlackBox: Prefs.autoBlackBox + }); + }, POPUP_HIDDEN_DELAY); + }); + }, + + /** + * Listener handling the 'automatically black box minified sources' menuitem + * command. + */ + _toggleAutoBlackBox: function () { + let pref = Prefs.autoBlackBox = + this._autoBlackBoxItem.getAttribute("checked") == "true"; + + // Don't block the UI while reconfiguring the server. + window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => { + // The popup panel needs more time to hide after triggering onpopuphidden. + window.setTimeout(() => { + this.DebuggerController.reconfigureThread({ + useSourceMaps: Prefs.sourceMapsEnabled, + autoBlackBox: pref + }); + }, POPUP_HIDDEN_DELAY); + }); + }, + + _button: null, + _pauseOnExceptionsItem: null, + _showPanesOnStartupItem: null, + _showVariablesOnlyEnumItem: null, + _showVariablesFilterBoxItem: null, + _showOriginalSourceItem: null, + _autoBlackBoxItem: null +}; + +DebuggerView.Options = new OptionsView(DebuggerController, DebuggerView); diff --git a/devtools/client/debugger/views/stack-frames-classic-view.js b/devtools/client/debugger/views/stack-frames-classic-view.js new file mode 100644 index 000000000..df1b93088 --- /dev/null +++ b/devtools/client/debugger/views/stack-frames-classic-view.js @@ -0,0 +1,141 @@ +/* -*- 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 */ +"use strict"; + +/* + * Functions handling the stackframes classic list UI. + * Controlled by the DebuggerView.StackFrames isntance. + */ +function StackFramesClassicListView(DebuggerController, DebuggerView) { + dumpn("StackFramesClassicListView was instantiated"); + + this.DebuggerView = DebuggerView; + this._onSelect = this._onSelect.bind(this); +} + +StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the StackFramesClassicListView"); + + this.widget = new SideMenuWidget(document.getElementById("callstack-list")); + this.widget.addEventListener("select", this._onSelect, false); + + this.emptyText = L10N.getStr("noStackFramesText"); + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + + // This view's contents are also mirrored in a different container. + this._mirror = this.DebuggerView.StackFrames; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the StackFramesClassicListView"); + + this.widget.removeEventListener("select", this._onSelect, false); + }, + + /** + * Adds a frame in this stackframes container. + * + * @param string aTitle + * The frame title (function name). + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + */ + addFrame: function (aTitle, aUrl, aLine, aDepth) { + // Create the element node for the stack frame item. + let frameView = this._createFrameView.apply(this, arguments); + + // Append a stack frame item to this container. + this.push([frameView], { + attachment: { + depth: aDepth + } + }); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aTitle + * The frame title to be displayed in the list. + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @return nsIDOMNode + * The stack frame view. + */ + _createFrameView: function (aTitle, aUrl, aLine, aDepth) { + let container = document.createElement("hbox"); + container.id = "classic-stackframe-" + aDepth; + container.className = "dbg-classic-stackframe"; + container.setAttribute("flex", "1"); + + let frameTitleNode = document.createElement("label"); + frameTitleNode.className = "plain dbg-classic-stackframe-title"; + frameTitleNode.setAttribute("value", aTitle); + frameTitleNode.setAttribute("crop", "center"); + + let frameDetailsNode = document.createElement("hbox"); + frameDetailsNode.className = "plain dbg-classic-stackframe-details"; + + let frameUrlNode = document.createElement("label"); + frameUrlNode.className = "plain dbg-classic-stackframe-details-url"; + frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); + frameUrlNode.setAttribute("crop", "center"); + frameDetailsNode.appendChild(frameUrlNode); + + let frameDetailsSeparator = document.createElement("label"); + frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep"; + frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG); + frameDetailsNode.appendChild(frameDetailsSeparator); + + let frameLineNode = document.createElement("label"); + frameLineNode.className = "plain dbg-classic-stackframe-details-line"; + frameLineNode.setAttribute("value", aLine); + frameDetailsNode.appendChild(frameLineNode); + + container.appendChild(frameTitleNode); + container.appendChild(frameDetailsNode); + + return container; + }, + + /** + * The select listener for the stackframes container. + */ + _onSelect: function (e) { + let stackframeItem = this.selectedItem; + if (stackframeItem) { + // The container is not empty and an actual item was selected. + // Mirror the selected item in the breadcrumbs list. + let depth = stackframeItem.attachment.depth; + this._mirror.selectedItem = e => e.attachment.depth == depth; + } + }, + + _mirror: null +}); + +DebuggerView.StackFramesClassicList = new StackFramesClassicListView(DebuggerController, + DebuggerView); diff --git a/devtools/client/debugger/views/stack-frames-view.js b/devtools/client/debugger/views/stack-frames-view.js new file mode 100644 index 000000000..244f97b3d --- /dev/null +++ b/devtools/client/debugger/views/stack-frames-view.js @@ -0,0 +1,283 @@ +/* -*- 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 stackframes UI. + */ +function StackFramesView(DebuggerController, DebuggerView) { + dumpn("StackFramesView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.DebuggerView = DebuggerView; + + this._onStackframeRemoved = this._onStackframeRemoved.bind(this); + this._onSelect = this._onSelect.bind(this); + this._onScroll = this._onScroll.bind(this); + this._afterScroll = this._afterScroll.bind(this); + this._getStackAsString = this._getStackAsString.bind(this); +} + +StackFramesView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the StackFramesView"); + + this._popupset = document.getElementById("debuggerPopupset"); + + this.widget = new BreadcrumbsWidget(document.getElementById("stackframes")); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("scroll", this._onScroll, true); + this.widget.setAttribute("context", "stackFramesContextMenu"); + window.addEventListener("resize", this._onScroll, true); + + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + + // This view's contents are also mirrored in a different container. + this._mirror = this.DebuggerView.StackFramesClassicList; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the StackFramesView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("scroll", this._onScroll, true); + window.removeEventListener("resize", this._onScroll, true); + }, + + /** + * Adds a frame in this stackframes container. + * + * @param string aTitle + * The frame title (function name). + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + */ + addFrame: function (aFrame, aLine, aColumn, aDepth, aIsBlackBoxed) { + let { source } = aFrame; + + // The source may not exist in the source listing yet because it's + // an unnamed eval source, which we hide, so we need to add it + if (!DebuggerView.Sources.getItemByValue(source.actor)) { + DebuggerView.Sources.addSource(source, { force: true }); + } + + let location = DebuggerView.Sources.getDisplayURL(source); + let title = StackFrameUtils.getFrameTitle(aFrame); + + // Blackboxed stack frames are collapsed into a single entry in + // the view. By convention, only the first frame is displayed. + if (aIsBlackBoxed) { + if (this._prevBlackBoxedUrl == location) { + return; + } + this._prevBlackBoxedUrl = location; + } else { + this._prevBlackBoxedUrl = null; + } + + // Create the element node for the stack frame item. + let frameView = this._createFrameView( + title, location, aLine, aDepth, aIsBlackBoxed + ); + + // Append a stack frame item to this container. + this.push([frameView], { + index: 0, /* specifies on which position should the item be appended */ + attachment: { + title: title, + url: location, + line: aLine, + depth: aDepth, + column: aColumn + }, + // Make sure that when the stack frame item is removed, the corresponding + // mirrored item in the classic list is also removed. + finalize: this._onStackframeRemoved + }); + + // Mirror this newly inserted item inside the "Call Stack" tab. + this._mirror.addFrame(title, location, aLine, aDepth); + }, + + _getStackAsString: function () { + return [...this].map(frameItem => { + const { attachment: { title, url, line, column }} = frameItem; + return title + "@" + url + ":" + line + ":" + column; + }).join("\n"); + }, + + addCopyContextMenu: function () { + let menupopup = document.createElement("menupopup"); + let menuitem = document.createElement("menuitem"); + + menupopup.id = "stackFramesContextMenu"; + menuitem.id = "copyStackMenuItem"; + + menuitem.setAttribute("label", "Copy"); + menuitem.addEventListener("command", () => { + let stack = this._getStackAsString(); + clipboardHelper.copyString(stack); + }, false); + menupopup.appendChild(menuitem); + this._popupset.appendChild(menupopup); + }, + + /** + * Selects the frame at the specified depth in this container. + * @param number aDepth + */ + set selectedDepth(aDepth) { + this.selectedItem = aItem => aItem.attachment.depth == aDepth; + }, + + /** + * Gets the currently selected stack frame's depth in this container. + * This will essentially be the opposite of |selectedIndex|, which deals + * with the position in the view, where the last item added is actually + * the bottommost, not topmost. + * @return number + */ + get selectedDepth() { + return this.selectedItem.attachment.depth; + }, + + /** + * Specifies if the active thread has more frames that need to be loaded. + */ + dirty: false, + + /** + * Customization function for creating an item's UI. + * + * @param string aTitle + * The frame title to be displayed in the list. + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + * @return nsIDOMNode + * The stack frame view. + */ + _createFrameView: function (aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { + let container = document.createElement("hbox"); + container.id = "stackframe-" + aDepth; + container.className = "dbg-stackframe"; + + let frameDetails = SourceUtils.trimUrlLength( + SourceUtils.getSourceLabel(aUrl), + STACK_FRAMES_SOURCE_URL_MAX_LENGTH, + STACK_FRAMES_SOURCE_URL_TRIM_SECTION); + + if (aIsBlackBoxed) { + container.classList.add("dbg-stackframe-black-boxed"); + } else { + let frameTitleNode = document.createElement("label"); + frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag"; + frameTitleNode.setAttribute("value", aTitle); + container.appendChild(frameTitleNode); + + frameDetails += SEARCH_LINE_FLAG + aLine; + } + + let frameDetailsNode = document.createElement("label"); + frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id"; + frameDetailsNode.setAttribute("value", frameDetails); + container.appendChild(frameDetailsNode); + + return container; + }, + + /** + * Function called each time a stack frame item is removed. + * + * @param object aItem + * The corresponding item. + */ + _onStackframeRemoved: function (aItem) { + dumpn("Finalizing stackframe item: " + aItem.stringify()); + + // Remove the mirrored item in the classic list. + let depth = aItem.attachment.depth; + this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth)); + + // Forget the previously blackboxed stack frame url. + this._prevBlackBoxedUrl = null; + }, + + /** + * The select listener for the stackframes container. + */ + _onSelect: function (e) { + let stackframeItem = this.selectedItem; + if (stackframeItem) { + // The container is not empty and an actual item was selected. + let depth = stackframeItem.attachment.depth; + + // Mirror the selected item in the classic list. + this.suppressSelectionEvents = true; + this._mirror.selectedItem = e => e.attachment.depth == depth; + this.suppressSelectionEvents = false; + + DebuggerController.StackFrames.selectFrame(depth); + } + }, + + /** + * The scroll listener for the stackframes container. + */ + _onScroll: function () { + // Update the stackframes container only if we have to. + if (!this.dirty) { + return; + } + // Allow requests to settle down first. + setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll); + }, + + /** + * Requests the addition of more frames from the controller. + */ + _afterScroll: function () { + let scrollPosition = this.widget.getAttribute("scrollPosition"); + let scrollWidth = this.widget.getAttribute("scrollWidth"); + + // If the stackframes container scrolled almost to the end, with only + // 1/10 of a breadcrumb remaining, load more content. + if (scrollPosition - scrollWidth / 10 < 1) { + this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1); + this.dirty = false; + + // Loads more stack frames from the debugger server cache. + DebuggerController.StackFrames.addMoreFrames(); + } + }, + + _mirror: null, + _prevBlackBoxedUrl: null +}); + +DebuggerView.StackFrames = new StackFramesView(DebuggerController, DebuggerView); diff --git a/devtools/client/debugger/views/toolbar-view.js b/devtools/client/debugger/views/toolbar-view.js new file mode 100644 index 000000000..d76275a71 --- /dev/null +++ b/devtools/client/debugger/views/toolbar-view.js @@ -0,0 +1,287 @@ +/* -*- 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 */ +"use strict"; + +/** + * Functions handling the toolbar view: close button, expand/collapse button, + * pause/resume and stepping buttons etc. + */ +function ToolbarView(DebuggerController, DebuggerView) { + dumpn("ToolbarView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.ThreadState = DebuggerController.ThreadState; + this.DebuggerController = DebuggerController; + this.DebuggerView = DebuggerView; + + this._onTogglePanesActivated = this._onTogglePanesActivated.bind(this); + this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this); + this._onResumePressed = this._onResumePressed.bind(this); + this._onStepOverPressed = this._onStepOverPressed.bind(this); + this._onStepInPressed = this._onStepInPressed.bind(this); + this._onStepOutPressed = this._onStepOutPressed.bind(this); +} + +ToolbarView.prototype = { + get activeThread() { + return this.DebuggerController.activeThread; + }, + + get resumptionWarnFunc() { + return this.DebuggerController._ensureResumptionOrder; + }, + + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the ToolbarView"); + + this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle"); + this._resumeButton = document.getElementById("resume"); + this._stepOverButton = document.getElementById("step-over"); + this._stepInButton = document.getElementById("step-in"); + this._stepOutButton = document.getElementById("step-out"); + this._resumeOrderTooltip = new Tooltip(document); + this._resumeOrderTooltip.defaultPosition = TOOLBAR_ORDER_POPUP_POSITION; + + let resumeKey = ShortcutUtils.prettifyShortcut(document.getElementById("resumeKey")); + let stepOverKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOverKey")); + let stepInKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepInKey")); + let stepOutKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOutKey")); + this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey); + this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey); + this._pausePendingTooltip = L10N.getStr("pausePendingButtonTooltip"); + this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey); + this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey); + this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey); + + this._instrumentsPaneToggleButton.addEventListener("mousedown", + this._onTogglePanesActivated, false); + this._instrumentsPaneToggleButton.addEventListener("keydown", + this._onTogglePanesPressed, false); + this._resumeButton.addEventListener("mousedown", this._onResumePressed, false); + this._stepOverButton.addEventListener("mousedown", this._onStepOverPressed, false); + this._stepInButton.addEventListener("mousedown", this._onStepInPressed, false); + this._stepOutButton.addEventListener("mousedown", this._onStepOutPressed, false); + + this._stepOverButton.setAttribute("tooltiptext", this._stepOverTooltip); + this._stepInButton.setAttribute("tooltiptext", this._stepInTooltip); + this._stepOutButton.setAttribute("tooltiptext", this._stepOutTooltip); + this._toggleButtonsState({ enabled: false }); + + this._addCommands(); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the ToolbarView"); + + this._instrumentsPaneToggleButton.removeEventListener("mousedown", + this._onTogglePanesActivated, false); + this._instrumentsPaneToggleButton.removeEventListener("keydown", + this._onTogglePanesPressed, false); + this._resumeButton.removeEventListener("mousedown", this._onResumePressed, false); + this._stepOverButton.removeEventListener("mousedown", this._onStepOverPressed, false); + this._stepInButton.removeEventListener("mousedown", this._onStepInPressed, false); + this._stepOutButton.removeEventListener("mousedown", this._onStepOutPressed, false); + }, + + /** + * Add commands that XUL can fire. + */ + _addCommands: function () { + XULUtils.addCommands(document.getElementById("debuggerCommands"), { + resumeCommand: this.getCommandHandler("resumeCommand"), + stepOverCommand: this.getCommandHandler("stepOverCommand"), + stepInCommand: this.getCommandHandler("stepInCommand"), + stepOutCommand: this.getCommandHandler("stepOutCommand") + }); + }, + + /** + * Retrieve the callback associated with the provided debugger command. + * + * @param {String} command + * The debugger command id. + * @return {Function} the corresponding callback. + */ + getCommandHandler: function (command) { + switch (command) { + case "resumeCommand": + return () => this._onResumePressed(); + case "stepOverCommand": + return () => this._onStepOverPressed(); + case "stepInCommand": + return () => this._onStepInPressed(); + case "stepOutCommand": + return () => this._onStepOutPressed(); + default: + return () => {}; + } + }, + + /** + * Display a warning when trying to resume a debuggee while another is paused. + * Debuggees must be unpaused in a Last-In-First-Out order. + * + * @param string aPausedUrl + * The URL of the last paused debuggee. + */ + showResumeWarning: function (aPausedUrl) { + let label = L10N.getFormatStr("resumptionOrderPanelTitle", aPausedUrl); + let defaultStyle = "default-tooltip-simple-text-colors"; + this._resumeOrderTooltip.setTextContent({ messages: [label] }); + this._resumeOrderTooltip.show(this._resumeButton); + }, + + /** + * Sets the resume button state based on the debugger active thread. + * + * @param string aState + * Either "paused", "attached", or "breakOnNext". + * @param boolean hasLocation + * True if we are paused at a specific JS location + */ + toggleResumeButtonState: function (aState, hasLocation) { + // Intermidiate state after pressing the pause button and waiting + // for the next script execution to happen. + if (aState == "breakOnNext") { + this._resumeButton.setAttribute("break-on-next", "true"); + this._resumeButton.disabled = true; + this._resumeButton.setAttribute("tooltiptext", this._pausePendingTooltip); + return; + } + + this._resumeButton.removeAttribute("break-on-next"); + this._resumeButton.disabled = false; + + // If we're paused, check and show a resume label on the button. + if (aState == "paused") { + this._resumeButton.setAttribute("checked", "true"); + this._resumeButton.setAttribute("tooltiptext", this._resumeTooltip); + + // Only enable the stepping buttons if we are paused at a + // specific location. After bug 789430, we'll always be paused + // at a location, but currently you can pause the entire engine + // at any point without knowing the location. + if (hasLocation) { + this._toggleButtonsState({ enabled: true }); + } + } + // If we're attached, do the opposite. + else if (aState == "attached") { + this._resumeButton.removeAttribute("checked"); + this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip); + this._toggleButtonsState({ enabled: false }); + } + }, + + _toggleButtonsState: function ({ enabled }) { + const buttons = [ + this._stepOutButton, + this._stepInButton, + this._stepOverButton + ]; + for (let button of buttons) { + button.disabled = !enabled; + } + }, + + /** + * Listener handling the toggle button space and return key event. + */ + _onTogglePanesPressed: function (event) { + if (ViewHelpers.isSpaceOrReturn(event)) { + this._onTogglePanesActivated(); + } + }, + + /** + * Listener handling the toggle button click event. + */ + _onTogglePanesActivated: function() { + DebuggerView.toggleInstrumentsPane({ + visible: DebuggerView.instrumentsPaneHidden, + animated: true, + delayed: true + }); + }, + + /** + * Listener handling the pause/resume button click event. + */ + _onResumePressed: function () { + if (this.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL || + this._resumeButton.disabled) { + return; + } + + if (this.activeThread.paused) { + this.StackFrames.currentFrameDepth = -1; + this.activeThread.resume(this.resumptionWarnFunc); + } else { + this.ThreadState.interruptedByResumeButton = true; + this.toggleResumeButtonState("breakOnNext"); + this.activeThread.breakOnNext(); + } + }, + + /** + * Listener handling the step over button click event. + */ + _onStepOverPressed: function () { + if (this.activeThread.paused && !this._stepOverButton.disabled) { + this.StackFrames.currentFrameDepth = -1; + this.activeThread.stepOver(this.resumptionWarnFunc); + } + }, + + /** + * Listener handling the step in button click event. + */ + _onStepInPressed: function () { + if (this.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL || + this._stepInButton.disabled) { + return; + } + + if (this.activeThread.paused) { + this.StackFrames.currentFrameDepth = -1; + this.activeThread.stepIn(this.resumptionWarnFunc); + } + }, + + /** + * Listener handling the step out button click event. + */ + _onStepOutPressed: function () { + if (this.activeThread.paused && !this._stepOutButton.disabled) { + this.StackFrames.currentFrameDepth = -1; + this.activeThread.stepOut(this.resumptionWarnFunc); + } + }, + + _instrumentsPaneToggleButton: null, + _resumeButton: null, + _stepOverButton: null, + _stepInButton: null, + _stepOutButton: null, + _resumeOrderTooltip: null, + _resumeTooltip: "", + _pauseTooltip: "", + _stepOverTooltip: "", + _stepInTooltip: "", + _stepOutTooltip: "" +}; + +DebuggerView.Toolbar = new ToolbarView(DebuggerController, DebuggerView); diff --git a/devtools/client/debugger/views/variable-bubble-view.js b/devtools/client/debugger/views/variable-bubble-view.js new file mode 100644 index 000000000..3ac2f971e --- /dev/null +++ b/devtools/client/debugger/views/variable-bubble-view.js @@ -0,0 +1,321 @@ +/* -*- 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"; + +const {setTooltipVariableContent} = require("devtools/client/shared/widgets/tooltip/VariableContentHelper"); + +/** + * Functions handling the variables bubble UI. + */ +function VariableBubbleView(DebuggerController, DebuggerView) { + dumpn("VariableBubbleView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.Parser = DebuggerController.Parser; + this.DebuggerView = DebuggerView; + + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onPopupHiding = this._onPopupHiding.bind(this); +} + +VariableBubbleView.prototype = { + /** + * Delay before showing the variables bubble tooltip when hovering a valid + * target. + */ + TOOLTIP_SHOW_DELAY: 750, + + /** + * Tooltip position for the variables bubble tooltip. + */ + TOOLTIP_POSITION: "topcenter bottomleft", + + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the VariableBubbleView"); + + this._toolbox = DebuggerController._toolbox; + this._editorContainer = document.getElementById("editor"); + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.addEventListener("mouseout", this._onMouseOut, false); + + this._tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: this._toolbox, + event: "select" + }, { + emitter: this._editorContainer, + event: "scroll", + useCapture: true + }, { + emitter: document, + event: "keydown" + }] + }); + this._tooltip.defaultPosition = this.TOOLTIP_POSITION; + this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the VariableBubbleView"); + + this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); + this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false); + }, + + /** + * Specifies whether literals can be (redundantly) inspected in a popup. + * This behavior is deprecated, but still tested in a few places. + */ + _ignoreLiterals: true, + + /** + * Searches for an identifier underneath the specified position in the + * source editor, and if found, opens a VariablesView inspection popup. + * + * @param number x, y + * The left/top coordinates where to look for an identifier. + */ + _findIdentifier: function (x, y) { + let editor = this.DebuggerView.editor; + + // Calculate the editor's line and column at the current x and y coords. + let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); + let hoveredOffset = editor.getOffset(hoveredPos); + let hoveredLine = hoveredPos.line; + let hoveredColumn = hoveredPos.ch; + + // A source contains multiple scripts. Find the start index of the script + // containing the specified offset relative to its parent source. + let contents = editor.getText(); + let location = this.DebuggerView.Sources.selectedValue; + let parsedSource = this.Parser.get(contents, location); + let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); + + // If the script length is negative, we're not hovering JS source code. + if (scriptInfo.length == -1) { + return; + } + + // Using the script offset, determine the actual line and column inside the + // script, to use when finding identifiers. + let scriptStart = editor.getPosition(scriptInfo.start); + let scriptLineOffset = scriptStart.line; + let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); + + let scriptLine = hoveredLine - scriptLineOffset; + let scriptColumn = hoveredColumn - scriptColumnOffset; + let identifierInfo = parsedSource.getIdentifierAt({ + line: scriptLine + 1, + column: scriptColumn, + scriptIndex: scriptInfo.index, + ignoreLiterals: this._ignoreLiterals + }); + + // If the info is null, we're not hovering any identifier. + if (!identifierInfo) { + return; + } + + // Transform the line and column relative to the parsed script back + // to the context of the parent source. + let { start: identifierStart, end: identifierEnd } = identifierInfo.location; + let identifierCoords = { + line: identifierStart.line + scriptLineOffset, + column: identifierStart.column + scriptColumnOffset, + length: identifierEnd.column - identifierStart.column + }; + + // Evaluate the identifier in the current stack frame and show the + // results in a VariablesView inspection popup. + this.StackFrames.evaluate(identifierInfo.evalString) + .then(frameFinished => { + if ("return" in frameFinished) { + this.showContents({ + coords: identifierCoords, + evalPrefix: identifierInfo.evalString, + objectActor: frameFinished.return + }); + } else { + let msg = "Evaluation has thrown for: " + identifierInfo.evalString; + console.warn(msg); + dumpn(msg); + } + }) + .then(null, err => { + let msg = "Couldn't evaluate: " + err.message; + console.error(msg); + dumpn(msg); + }); + }, + + /** + * Shows an inspection popup for a specified object actor grip. + * + * @param string object + * An object containing the following properties: + * - coords: the inspected identifier coordinates in the editor, + * containing the { line, column, length } properties. + * - evalPrefix: a prefix for the variables view evaluation macros. + * - objectActor: the value grip for the object actor. + */ + showContents: function ({ coords, evalPrefix, objectActor }) { + let editor = this.DebuggerView.editor; + let { line, column, length } = coords; + + // Highlight the function found at the mouse position. + this._markedText = editor.markText( + { line: line - 1, ch: column }, + { line: line - 1, ch: column + length }); + + // If the grip represents a primitive value, use a more lightweight + // machinery to display it. + if (VariablesView.isPrimitive({ value: objectActor })) { + let className = VariablesView.getClass(objectActor); + let textContent = VariablesView.getString(objectActor); + this._tooltip.setTextContent({ + messages: [textContent], + messagesClass: className, + containerClass: "plain" + }, [{ + label: L10N.getStr("addWatchExpressionButton"), + className: "dbg-expression-button", + command: () => { + this.DebuggerView.VariableBubble.hideContents(); + this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }]); + } else { + setTooltipVariableContent(this._tooltip, objectActor, { + searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), + searchEnabled: Prefs.variablesSearchboxVisible, + eval: (variable, value) => { + let string = variable.evaluationMacro(variable, value); + this.StackFrames.evaluate(string); + this.DebuggerView.VariableBubble.hideContents(); + } + }, { + getEnvironmentClient: aObject => gThreadClient.environment(aObject), + getObjectClient: aObject => gThreadClient.pauseGrip(aObject), + simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), + getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), + overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) + }, { + fetched: (aEvent, aType) => { + if (aType == "properties") { + window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); + } + } + }, [{ + label: L10N.getStr("addWatchExpressionButton"), + className: "dbg-expression-button", + command: () => { + this.DebuggerView.VariableBubble.hideContents(); + this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }], this._toolbox); + } + + this._tooltip.show(this._markedText.anchor); + }, + + /** + * Hides the inspection popup. + */ + hideContents: function () { + clearNamedTimeout("editor-mouse-move"); + this._tooltip.hide(); + }, + + /** + * Checks whether the inspection popup is shown. + * + * @return boolean + * True if the panel is shown or showing, false otherwise. + */ + contentsShown: function () { + return this._tooltip.isShown(); + }, + + /** + * Functions for getting customized variables view evaluation macros. + * + * @param string aPrefix + * See the corresponding VariablesView.* functions. + */ + _getSimpleValueEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.simpleValueEvalMacro(item, string, aPrefix); + }, + _getGetterOrSetterEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); + }, + _getOverrideValueEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.overrideValueEvalMacro(item, string, aPrefix); + }, + + /** + * The mousemove listener for the source editor. + */ + _onMouseMove: function (e) { + // Prevent the variable inspection popup from showing when the thread client + // is not paused, or while a popup is already visible, or when the user tries + // to select text in the editor. + let isResumed = gThreadClient && gThreadClient.state != "paused"; + let isSelecting = this.DebuggerView.editor.somethingSelected() && e.buttons > 0; + let isPopupVisible = !this._tooltip.isHidden(); + if (isResumed || isSelecting || isPopupVisible) { + clearNamedTimeout("editor-mouse-move"); + return; + } + // Allow events to settle down first. If the mouse hovers over + // a certain point in the editor long enough, try showing a variable bubble. + setNamedTimeout("editor-mouse-move", + this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY)); + }, + + /** + * The mouseout listener for the source editor container node. + */ + _onMouseOut: function () { + clearNamedTimeout("editor-mouse-move"); + }, + + /** + * Listener handling the popup hiding event. + */ + _onPopupHiding: function ({ target }) { + if (this._tooltip.panel != target) { + return; + } + if (this._markedText) { + this._markedText.clear(); + this._markedText = null; + } + if (!this._tooltip.isEmpty()) { + this._tooltip.empty(); + } + }, + + _editorContainer: null, + _markedText: null, + _tooltip: null +}; + +DebuggerView.VariableBubble = new VariableBubbleView(DebuggerController, DebuggerView); diff --git a/devtools/client/debugger/views/watch-expressions-view.js b/devtools/client/debugger/views/watch-expressions-view.js new file mode 100644 index 000000000..59d3ad5a0 --- /dev/null +++ b/devtools/client/debugger/views/watch-expressions-view.js @@ -0,0 +1,303 @@ +/* -*- 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 */ +"use strict"; + +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +/** + * Functions handling the watch expressions UI. + */ +function WatchExpressionsView(DebuggerController, DebuggerView) { + dumpn("WatchExpressionsView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.DebuggerView = DebuggerView; + + this.switchExpression = this.switchExpression.bind(this); + this.deleteExpression = this.deleteExpression.bind(this); + this._createItemView = this._createItemView.bind(this); + this._onClick = this._onClick.bind(this); + this._onClose = this._onClose.bind(this); + this._onBlur = this._onBlur.bind(this); + this._onKeyPress = this._onKeyPress.bind(this); +} + +WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the WatchExpressionsView"); + + this.widget = new SimpleListWidget(document.getElementById("expressions")); + this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); + this.widget.addEventListener("click", this._onClick, false); + + this.headerText = L10N.getStr("addWatchExpressionText"); + this._addCommands(); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the WatchExpressionsView"); + + this.widget.removeEventListener("click", this._onClick, false); + }, + + /** + * Add commands that XUL can fire. + */ + _addCommands: function () { + XULUtils.addCommands(document.getElementById("debuggerCommands"), { + addWatchExpressionCommand: () => this._onCmdAddExpression(), + removeAllWatchExpressionsCommand: () => this._onCmdRemoveAllExpressions() + }); + }, + + /** + * Adds a watch expression in this container. + * + * @param string aExpression [optional] + * An optional initial watch expression text. + * @param boolean aSkipUserInput [optional] + * Pass true to avoid waiting for additional user input + * on the watch expression. + */ + addExpression: function (aExpression = "", aSkipUserInput = false) { + // Watch expressions are UI elements which benefit from visible panes. + this.DebuggerView.showInstrumentsPane(); + + // Create the element node for the watch expression item. + let itemView = this._createItemView(aExpression); + + // Append a watch expression item to this container. + let expressionItem = this.push([itemView.container], { + index: 0, /* specifies on which position should the item be appended */ + attachment: { + view: itemView, + initialExpression: aExpression, + currentExpression: "", + } + }); + + // Automatically focus the new watch expression input + // if additional user input is desired. + if (!aSkipUserInput) { + expressionItem.attachment.view.inputNode.select(); + expressionItem.attachment.view.inputNode.focus(); + this.DebuggerView.Variables.parentNode.scrollTop = 0; + } + // Otherwise, add and evaluate the new watch expression immediately. + else { + this.toggleContents(false); + this._onBlur({ target: expressionItem.attachment.view.inputNode }); + } + }, + + /** + * Changes the watch expression corresponding to the specified variable item. + * This function is called whenever a watch expression's code is edited in + * the variables view container. + * + * @param Variable aVar + * The variable representing the watch expression evaluation. + * @param string aExpression + * The new watch expression text. + */ + switchExpression: function (aVar, aExpression) { + let expressionItem = + [...this].filter(i => i.attachment.currentExpression == aVar.name)[0]; + + // Remove the watch expression if it's going to be empty or a duplicate. + if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { + this.deleteExpression(aVar); + return; + } + + // Save the watch expression code string. + expressionItem.attachment.currentExpression = aExpression; + expressionItem.attachment.view.inputNode.value = aExpression; + + // Synchronize with the controller's watch expressions store. + this.StackFrames.syncWatchExpressions(); + }, + + /** + * Removes the watch expression corresponding to the specified variable item. + * This function is called whenever a watch expression's value is edited in + * the variables view container. + * + * @param Variable aVar + * The variable representing the watch expression evaluation. + */ + deleteExpression: function (aVar) { + let expressionItem = + [...this].filter(i => i.attachment.currentExpression == aVar.name)[0]; + + // Remove the watch expression. + this.remove(expressionItem); + + // Synchronize with the controller's watch expressions store. + this.StackFrames.syncWatchExpressions(); + }, + + /** + * Gets the watch expression code string for an item in this container. + * + * @param number aIndex + * The index used to identify the watch expression. + * @return string + * The watch expression code string. + */ + getString: function (aIndex) { + return this.getItemAtIndex(aIndex).attachment.currentExpression; + }, + + /** + * Gets the watch expressions code strings for all items in this container. + * + * @return array + * The watch expressions code strings. + */ + getAllStrings: function () { + return this.items.map(e => e.attachment.currentExpression); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aExpression + * The watch expression string. + */ + _createItemView: function (aExpression) { + let container = document.createElement("hbox"); + container.className = "list-widget-item dbg-expression"; + container.setAttribute("align", "center"); + + let arrowNode = document.createElement("hbox"); + arrowNode.className = "dbg-expression-arrow"; + + let inputNode = document.createElement("textbox"); + inputNode.className = "plain dbg-expression-input devtools-monospace"; + inputNode.setAttribute("value", aExpression); + inputNode.setAttribute("flex", "1"); + + let closeNode = document.createElement("toolbarbutton"); + closeNode.className = "plain variables-view-delete"; + + closeNode.addEventListener("click", this._onClose, false); + inputNode.addEventListener("blur", this._onBlur, false); + inputNode.addEventListener("keypress", this._onKeyPress, false); + + container.appendChild(arrowNode); + container.appendChild(inputNode); + container.appendChild(closeNode); + + return { + container: container, + arrowNode: arrowNode, + inputNode: inputNode, + closeNode: closeNode + }; + }, + + /** + * Called when the add watch expression key sequence was pressed. + */ + _onCmdAddExpression: function (aText) { + // Only add a new expression if there's no pending input. + if (this.getAllStrings().indexOf("") == -1) { + this.addExpression(aText || this.DebuggerView.editor.getSelection()); + } + }, + + /** + * Called when the remove all watch expressions key sequence was pressed. + */ + _onCmdRemoveAllExpressions: function () { + // Empty the view of all the watch expressions and clear the cache. + this.empty(); + + // Synchronize with the controller's watch expressions store. + this.StackFrames.syncWatchExpressions(); + }, + + /** + * The click listener for this container. + */ + _onClick: function (e) { + if (e.button != 0) { + // Only allow left-click to trigger this event. + return; + } + let expressionItem = this.getItemForElement(e.target); + if (!expressionItem) { + // The container is empty or we didn't click on an actual item. + this.addExpression(); + } + }, + + /** + * The click listener for a watch expression's close button. + */ + _onClose: function (e) { + // Remove the watch expression. + this.remove(this.getItemForElement(e.target)); + + // Synchronize with the controller's watch expressions store. + this.StackFrames.syncWatchExpressions(); + + // Prevent clicking the expression element itself. + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * The blur listener for a watch expression's textbox. + */ + _onBlur: function ({ target: textbox }) { + let expressionItem = this.getItemForElement(textbox); + let oldExpression = expressionItem.attachment.currentExpression; + let newExpression = textbox.value.trim(); + + // Remove the watch expression if it's empty. + if (!newExpression) { + this.remove(expressionItem); + } + // Remove the watch expression if it's a duplicate. + else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { + this.remove(expressionItem); + } + // Expression is eligible. + else { + expressionItem.attachment.currentExpression = newExpression; + } + + // Synchronize with the controller's watch expressions store. + this.StackFrames.syncWatchExpressions(); + }, + + /** + * The keypress listener for a watch expression's textbox. + */ + _onKeyPress: function (e) { + switch (e.keyCode) { + case KeyCodes.DOM_VK_RETURN: + case KeyCodes.DOM_VK_ESCAPE: + e.stopPropagation(); + this.DebuggerView.editor.focus(); + } + } +}); + +DebuggerView.WatchExpressions = new WatchExpressionsView(DebuggerController, + DebuggerView); diff --git a/devtools/client/debugger/views/workers-view.js b/devtools/client/debugger/views/workers-view.js new file mode 100644 index 000000000..0dc8dc3a5 --- /dev/null +++ b/devtools/client/debugger/views/workers-view.js @@ -0,0 +1,55 @@ +/* -*- 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 */ +"use strict"; + +function WorkersView() { + this._onWorkerSelect = this._onWorkerSelect.bind(this); +} + +WorkersView.prototype = Heritage.extend(WidgetMethods, { + initialize: function () { + if (!Prefs.workersEnabled) { + return; + } + + document.getElementById("workers-pane").removeAttribute("hidden"); + document.getElementById("workers-splitter").removeAttribute("hidden"); + + this.widget = new SideMenuWidget(document.getElementById("workers"), { + showArrows: true, + }); + this.emptyText = L10N.getStr("noWorkersText"); + this.widget.addEventListener("select", this._onWorkerSelect, false); + }, + + addWorker: function (workerForm) { + let element = document.createElement("label"); + element.className = "plain dbg-worker-item"; + element.setAttribute("value", workerForm.url); + element.setAttribute("flex", "1"); + + this.push([element, workerForm.actor], { + attachment: workerForm + }); + }, + + removeWorker: function (workerForm) { + this.remove(this.getItemByValue(workerForm.actor)); + }, + + _onWorkerSelect: function () { + if (this.selectedItem !== null) { + DebuggerController.Workers._onWorkerSelect(this.selectedItem.attachment); + this.selectedItem = null; + } + } +}); + +DebuggerView.Workers = new WorkersView(); |