mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
925
devtools/client/debugger/views/filter-view.js
Normal file
925
devtools/client/debugger/views/filter-view.js
Normal file
|
|
@ -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);
|
||||
756
devtools/client/debugger/views/global-search-view.js
Normal file
756
devtools/client/debugger/views/global-search-view.js
Normal file
|
|
@ -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;
|
||||
};
|
||||
215
devtools/client/debugger/views/options-view.js
Normal file
215
devtools/client/debugger/views/options-view.js
Normal file
|
|
@ -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);
|
||||
141
devtools/client/debugger/views/stack-frames-classic-view.js
Normal file
141
devtools/client/debugger/views/stack-frames-classic-view.js
Normal file
|
|
@ -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);
|
||||
283
devtools/client/debugger/views/stack-frames-view.js
Normal file
283
devtools/client/debugger/views/stack-frames-view.js
Normal file
|
|
@ -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);
|
||||
287
devtools/client/debugger/views/toolbar-view.js
Normal file
287
devtools/client/debugger/views/toolbar-view.js
Normal file
|
|
@ -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);
|
||||
321
devtools/client/debugger/views/variable-bubble-view.js
Normal file
321
devtools/client/debugger/views/variable-bubble-view.js
Normal file
|
|
@ -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);
|
||||
303
devtools/client/debugger/views/watch-expressions-view.js
Normal file
303
devtools/client/debugger/views/watch-expressions-view.js
Normal file
|
|
@ -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);
|
||||
55
devtools/client/debugger/views/workers-view.js
Normal file
55
devtools/client/debugger/views/workers-view.js
Normal file
|
|
@ -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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue