summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/views
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/views')
-rw-r--r--devtools/client/debugger/views/filter-view.js925
-rw-r--r--devtools/client/debugger/views/global-search-view.js756
-rw-r--r--devtools/client/debugger/views/options-view.js215
-rw-r--r--devtools/client/debugger/views/stack-frames-classic-view.js141
-rw-r--r--devtools/client/debugger/views/stack-frames-view.js283
-rw-r--r--devtools/client/debugger/views/toolbar-view.js287
-rw-r--r--devtools/client/debugger/views/variable-bubble-view.js321
-rw-r--r--devtools/client/debugger/views/watch-expressions-view.js303
-rw-r--r--devtools/client/debugger/views/workers-view.js55
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();