gecko/browser/devtools/debugger/debugger-view.js

3647 lines
112 KiB
JavaScript
Raw Normal View History

/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
2012-05-21 04:12:37 -07:00
/* 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/. */
"use strict";
const BREAKPOINT_LINE_TOOLTIP_MAX_SIZE = 1000; // chars
const PANES_APPEARANCE_DELAY = 50; // ms
const PROPERTY_VIEW_FLASH_DURATION = 400; // ms
const GLOBAL_SEARCH_MATCH_FLASH_DURATION = 100; // ms
const GLOBAL_SEARCH_URL_MAX_SIZE = 100; // chars
const GLOBAL_SEARCH_LINE_MAX_SIZE = 300; // chars
const GLOBAL_SEARCH_ACTION_DELAY = 150; // ms
const SEARCH_GLOBAL_FLAG = "!";
const SEARCH_LINE_FLAG = ":";
const SEARCH_TOKEN_FLAG = "#";
/**
* Object mediating visual changes and event listeners between the debugger and
* the html view.
*/
let DebuggerView = {
/**
* An instance of SourceEditor.
*/
editor: null,
/**
* Caches frequently used global view elements.
*/
cacheView: function DV_cacheView() {
this._onTogglePanesButtonPressed = this._onTogglePanesButtonPressed.bind(this);
// Panes and view containers
this._togglePanesButton = document.getElementById("toggle-panes");
this._stackframesAndBreakpoints = document.getElementById("stackframes+breakpoints");
this._stackframes = document.getElementById("stackframes");
this._breakpoints = document.getElementById("breakpoints");
this._variables = document.getElementById("variables");
this._scripts = document.getElementById("scripts");
this._globalSearch = document.getElementById("globalsearch");
this._globalSearchSplitter = document.getElementById("globalsearch-splitter");
// Keys
this._fileSearchKey = document.getElementById("fileSearchKey");
this._lineSearchKey = document.getElementById("lineSearchKey");
this._tokenSearchKey = document.getElementById("tokenSearchKey");
this._globalSearchKey = document.getElementById("globalSearchKey");
this._resumeKey = document.getElementById("resumeKey");
this._stepOverKey = document.getElementById("stepOverKey");
this._stepInKey = document.getElementById("stepInKey");
this._stepOutKey = document.getElementById("stepOutKey");
// Buttons, textboxes etc.
this._resumeButton = document.getElementById("resume");
this._stepOverButton = document.getElementById("step-over");
this._stepInButton = document.getElementById("step-in");
this._stepOutButton = document.getElementById("step-out");
this._scriptsSearchbox = document.getElementById("scripts-search");
this._globalOperatorLabel = document.getElementById("global-operator-label");
this._globalOperatorButton = document.getElementById("global-operator-button");
this._tokenOperatorLabel = document.getElementById("token-operator-label");
this._tokenOperatorButton = document.getElementById("token-operator-button");
this._lineOperatorLabel = document.getElementById("line-operator-label");
this._lineOperatorButton = document.getElementById("line-operator-button");
},
/**
* Applies the correct key labels and tooltips across global view elements.
*/
initializeKeys: function DV_initializeKeys() {
this._resumeButton.setAttribute("tooltiptext",
L10N.getFormatStr("pauseButtonTooltip", [LayoutHelpers.prettyKey(this._resumeKey)]));
this._stepOverButton.setAttribute("tooltiptext",
L10N.getFormatStr("stepOverTooltip", [LayoutHelpers.prettyKey(this._stepOverKey)]));
this._stepInButton.setAttribute("tooltiptext",
L10N.getFormatStr("stepInTooltip", [LayoutHelpers.prettyKey(this._stepInKey)]));
this._stepOutButton.setAttribute("tooltiptext",
L10N.getFormatStr("stepOutTooltip", [LayoutHelpers.prettyKey(this._stepOutKey)]));
this._scriptsSearchbox.setAttribute("placeholder",
L10N.getFormatStr("emptyFilterText", [LayoutHelpers.prettyKey(this._fileSearchKey)]));
this._globalOperatorLabel.setAttribute("value",
L10N.getFormatStr("searchPanelGlobal", [LayoutHelpers.prettyKey(this._globalSearchKey)]));
this._tokenOperatorLabel.setAttribute("value",
L10N.getFormatStr("searchPanelToken", [LayoutHelpers.prettyKey(this._tokenSearchKey)]));
this._lineOperatorLabel.setAttribute("value",
L10N.getFormatStr("searchPanelLine", [LayoutHelpers.prettyKey(this._lineSearchKey)]));
this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG);
this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG);
this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG);
},
/**
* Initializes UI properties for all the displayed panes.
*/
initializePanes: function DV_initializePanes() {
this._togglePanesButton.addEventListener("click", this._onTogglePanesButtonPressed);
this._stackframesAndBreakpoints.setAttribute("width", Prefs.stackframesWidth);
this._variables.setAttribute("width", Prefs.variablesWidth);
this.toggleStackframesAndBreakpointsPane({ silent: true });
this.toggleVariablesPane({ silent: true });
},
/**
* Initializes the SourceEditor instance.
*
* @param function aCallback
* Called after the editor finishes initializing.
*/
initializeEditor: function DV_initializeEditor(aCallback) {
let placeholder = document.getElementById("editor");
let config = {
mode: SourceEditor.MODES.JAVASCRIPT,
showLineNumbers: true,
readOnly: true,
showAnnotationRuler: true,
showOverviewRuler: true,
};
this.editor = new SourceEditor();
this.editor.init(placeholder, config, function() {
this._onEditorLoad();
aCallback();
}.bind(this));
},
/**
* Removes the displayed panes and saves any necessary state.
*/
destroyPanes: function DV_destroyPanes() {
this._togglePanesButton.removeEventListener("click", this._onTogglePanesButtonPressed);
Prefs.stackframesWidth = this._stackframesAndBreakpoints.getAttribute("width");
Prefs.variablesWidth = this._variables.getAttribute("width");
this._breakpoints.parentNode.removeChild(this._breakpoints);
this._stackframes.parentNode.removeChild(this._stackframes);
this._stackframesAndBreakpoints.parentNode.removeChild(this._stackframesAndBreakpoints);
this._variables.parentNode.removeChild(this._variables);
this._globalSearch.parentNode.removeChild(this._globalSearch);
// Delete all the cached global view elements.
for (let i in this) {
if (!(this[i] instanceof Function)) delete this[i];
}
},
/**
* Removes the SourceEditor instance and added breakpoints.
*/
destroyEditor: function DV_destroyEditor() {
DebuggerController.Breakpoints.destroy();
this.editor = null;
},
/**
* The load event handler for the source editor. This method does post-load
* editor initialization.
*/
_onEditorLoad: function DV__onEditorLoad() {
DebuggerController.Breakpoints.initialize();
this.editor.focus();
},
/**
* Called when the panes toggle button is clicked.
*/
_onTogglePanesButtonPressed: function DV__onTogglePanesButtonPressed() {
this.toggleStackframesAndBreakpointsPane({
visible: !!this._togglePanesButton.getAttribute("stackframesAndBreakpointsHidden"),
animated: true
});
this.toggleVariablesPane({
visible: !!this._togglePanesButton.getAttribute("variablesHidden"),
animated: true
});
this._onPanesToggle();
},
/**
* Sets the close button hidden or visible. It's hidden by default.
* @param boolean aVisibleFlag
*/
toggleCloseButton: function DV_toggleCloseButton(aVisibleFlag) {
document.getElementById("close").setAttribute("hidden", !aVisibleFlag);
},
/**
* Sets the stackframes and breakpoints pane hidden or visible.
*
* @param object aFlags [optional]
* An object containing some of the following booleans:
* - visible: true if the pane should be shown, false for hidden
* - animated: true to display an animation on toggle
* - silent: true to not update any designated prefs
*/
toggleStackframesAndBreakpointsPane:
function DV_toggleStackframesAndBreakpointsPane(aFlags = {}) {
if (aFlags.animated) {
this._stackframesAndBreakpoints.setAttribute("animated", "");
} else {
this._stackframesAndBreakpoints.removeAttribute("animated");
}
if (aFlags.visible) {
this._stackframesAndBreakpoints.style.marginLeft = "0";
this._togglePanesButton.removeAttribute("stackframesAndBreakpointsHidden");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes"));
} else {
let margin = parseInt(this._stackframesAndBreakpoints.getAttribute("width")) + 1;
this._stackframesAndBreakpoints.style.marginLeft = -margin + "px";
this._togglePanesButton.setAttribute("stackframesAndBreakpointsHidden", "true");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes"));
}
if (!aFlags.silent) {
Prefs.stackframesPaneVisible = !!aFlags.visible;
}
},
/**
* Sets the variable spane hidden or visible.
*
* @param object aFlags [optional]
* An object containing some of the following booleans:
* - visible: true if the pane should be shown, false for hidden
* - animated: true to display an animation on toggle
* - silent: true to not update any designated prefs
*/
toggleVariablesPane:
function DV_toggleVariablesPane(aFlags = {}) {
if (aFlags.animated) {
this._variables.setAttribute("animated", "");
} else {
this._variables.removeAttribute("animated");
}
if (aFlags.visible) {
this._variables.style.marginRight = "0";
this._togglePanesButton.removeAttribute("variablesHidden");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("collapsePanes"));
} else {
let margin = parseInt(this._variables.getAttribute("width")) + 1;
this._variables.style.marginRight = -margin + "px";
this._togglePanesButton.setAttribute("variablesHidden", "true");
this._togglePanesButton.setAttribute("tooltiptext", L10N.getStr("expandPanes"));
}
if (!aFlags.silent) {
Prefs.variablesPaneVisible = !!aFlags.visible;
}
},
/**
* Shows the stackframes, breakpoints and variable panes if currently hidden
* and the preferences dictate otherwise.
*/
showPanesIfAllowed: function DV_showPanesIfAllowed() {
// Try to keep animations as smooth as possible, so wait a few cycles.
window.setTimeout(function() {
let shown;
if (Prefs.stackframesPaneVisible &&
this._togglePanesButton.getAttribute("stackframesAndBreakpointsHidden")) {
this.toggleStackframesAndBreakpointsPane({
visible: true,
animated: true,
silent: true
});
shown = true;
}
if (Prefs.variablesPaneVisible &&
this._togglePanesButton.getAttribute("variablesHidden")) {
this.toggleVariablesPane({
visible: true,
animated: true,
silent: true
});
shown = true;
}
if (shown) {
this._onPanesToggle();
}
}.bind(this), PANES_APPEARANCE_DELAY);
},
/**
* Displaying the panes may have the effect of triggering scrollbars to
* appear in the source editor, which would render the currently highlighted
* line to appear behind them in some cases.
*/
_onPanesToggle: function DV__onPanesToggle() {
document.addEventListener("transitionend", function onEvent() {
document.removeEventListener("transitionend", onEvent);
DebuggerController.StackFrames.updateEditorLocation();
});
},
/**
* The cached global view elements.
*/
_togglePanesButton: null,
_stackframesAndBreakpoints: null,
_stackframes: null,
_breakpoints: null,
_variables: null,
_scripts: null,
_globalSearch: null,
_globalSearchSplitter: null,
_fileSearchKey: null,
_lineSearchKey: null,
_tokenSearchKey: null,
_globalSearchKey: null,
_resumeKey: null,
_stepOverKey: null,
_stepInKey: null,
_stepOutKey: null,
_resumeButton: null,
_stepOverButton: null,
_stepInButton: null,
_stepOutButton: null,
_scriptsSearchbox: null,
_globalOperatorLabel: null,
_globalOperatorButton: null,
_tokenOperatorLabel: null,
_tokenOperatorButton: null,
_lineOperatorLabel: null,
_lineOperatorButton: null
};
/**
* A simple way of displaying a "Connect to..." prompt.
*/
function RemoteDebuggerPrompt() {
/**
* The remote host and port the user wants to connect to.
*/
this.remote = {};
}
RemoteDebuggerPrompt.prototype = {
/**
* Shows the prompt and sets the uri using the user input.
*
* @param boolean aIsReconnectingFlag
* True to show the reconnect message instead.
*/
show: function RDP_show(aIsReconnectingFlag) {
let check = { value: Prefs.remoteAutoConnect };
let input = { value: Prefs.remoteHost + ":" + Prefs.remotePort };
let parts;
while (true) {
let result = Services.prompt.prompt(null,
L10N.getStr("remoteDebuggerPromptTitle"),
L10N.getStr(aIsReconnectingFlag
? "remoteDebuggerReconnectMessage"
: "remoteDebuggerPromptMessage"), input,
L10N.getStr("remoteDebuggerPromptCheck"), check);
Prefs.remoteAutoConnect = check.value;
if (!result) {
return false;
}
if ((parts = input.value.split(":")).length === 2) {
let [host, port] = parts;
if (host.length && port.length) {
this.remote = { host: host, port: port };
return true;
}
}
}
}
};
/**
* Functions handling the global search UI.
*/
function GlobalSearchView() {
this._onFetchScriptFinished = this._onFetchScriptFinished.bind(this);
this._onFetchScriptsFinished = this._onFetchScriptsFinished.bind(this);
this._onLineClick = this._onLineClick.bind(this);
this._onMatchClick = this._onMatchClick.bind(this);
this._onResultsScroll = this._onResultsScroll.bind(this);
this._onFocusLost = this._onFocusLost.bind(this);
this._startSearch = this._startSearch.bind(this);
}
GlobalSearchView.prototype = {
/**
* Hides or shows the search results container.
* @param boolean value
*/
set hidden(value) {
this._pane.hidden = value;
this._splitter.hidden = value;
},
/**
* True if the search results container is hidden.
* @return boolean
*/
get hidden() this._pane.hidden,
/**
* Removes all elements from the search results container, leaving it empty.
*/
empty: function DVGS_empty() {
while (this._pane.firstChild) {
this._pane.removeChild(this._pane.firstChild);
}
this._pane.scrollTop = 0;
this._pane.scrollLeft = 0;
this._currentlyFocusedMatch = -1;
},
/**
* Hides and empties the search results container.
*/
hideAndEmpty: function DVGS_hideAndEmpty() {
this.hidden = true;
this.empty();
DebuggerController.dispatchEvent("Debugger:GlobalSearch:ViewCleared");
},
/**
* Clears all the fetched scripts from the cache.
*/
clearCache: function DVGS_clearCache() {
this._scriptSources = new Map();
DebuggerController.dispatchEvent("Debugger:GlobalSearch:CacheCleared");
},
/**
* Starts fetching all the script sources, silently.
*
* @param function aFetchCallback [optional]
* Called after each script is fetched.
* @param function aFetchedCallback [optional]
* Called if all the scripts were already fetched.
* @param array aUrls [optional]
* The urls for the scripts to fetch. If undefined, it defaults to
* all the currently known scripts.
*/
fetchScripts:
function DVGS_fetchScripts(aFetchCallback = null,
aFetchedCallback = null,
aUrls = DebuggerView.Scripts.scriptLocations) {
// If all the scripts sources were already fetched, then don't do anything.
if (this._scriptSources.size() === aUrls.length) {
aFetchedCallback && aFetchedCallback();
return;
}
// Fetch each new script's source.
for (let url of aUrls) {
if (this._scriptSources.has(url)) {
continue;
}
DebuggerController.dispatchEvent("Debugger:LoadSource", {
script: DebuggerView.Scripts.getScriptByLocation(url).getUserData("sourceScript"),
options: {
silent: true,
callback: aFetchCallback
}
});
}
},
/**
* Schedules searching for a token in all the scripts.
*/
scheduleSearch: function DVGS_scheduleSearch() {
window.clearTimeout(this._searchTimeout);
this._searchTimeout = window.setTimeout(this._startSearch, GLOBAL_SEARCH_ACTION_DELAY);
},
/**
* Starts searching for a token in all the scripts.
*/
_startSearch: function DVGS__startSearch() {
let scriptLocations = DebuggerView.Scripts.scriptLocations;
this._scriptCount = scriptLocations.length;
this.fetchScripts(
this._onFetchScriptFinished, this._onFetchScriptsFinished, scriptLocations);
},
/**
* Called when a script's source has been fetched.
*
* @param string aScriptUrl
* The URL of the source script.
* @param string aSourceText
* The text of the source script.
*/
_onFetchScriptFinished: function DVGS__onFetchScriptFinished(aScriptUrl, aSourceText) {
this._scriptSources.set(aScriptUrl, aSourceText);
if (this._scriptSources.size() === this._scriptCount) {
this._onFetchScriptsFinished();
}
},
/**
* Called when all the script's sources have been fetched.
*/
_onFetchScriptsFinished: function DVGS__onFetchScriptsFinished() {
this.empty();
let token = DebuggerView.Scripts.searchToken;
let lowerCaseToken = token.toLowerCase();
// Make sure we're actually searching for something.
if (!token) {
DebuggerController.dispatchEvent("Debugger:GlobalSearch:TokenEmpty");
this.hidden = true;
return;
}
// Prepare the results map, containing search details for each script/line.
let globalResults = new Map();
for (let [url, text] of this._scriptSources) {
// Check if the search token is not found anywhere in the script source.
if (!text.toLowerCase().contains(lowerCaseToken)) {
continue;
}
let lines = text.split("\n");
let scriptResults = {
lineResults: [],
matchCount: 0
};
for (let i = 0, len = lines.length; i < len; i++) {
let line = lines[i];
let lowerCaseLine = line.toLowerCase();
// Search is not case sensitive, and is tied to each line in the source.
if (!lowerCaseLine.contains(lowerCaseToken)) {
continue;
}
let lineNumber = i;
let lineContents = [];
lowerCaseLine.split(lowerCaseToken).reduce(function(prev, curr, index, {length}) {
let unmatched = line.substr(prev.length, curr.length);
lineContents.push({ string: unmatched });
if (index !== length - 1) {
let matched = line.substr(prev.length + curr.length, token.length);
let range = {
start: prev.length + curr.length,
length: matched.length
};
lineContents.push({
string: matched,
range: range,
match: true
});
scriptResults.matchCount++;
}
return prev + token + curr;
}, "");
scriptResults.lineResults.push({
lineNumber: lineNumber,
lineContents: lineContents
});
}
if (scriptResults.matchCount) {
globalResults.set(url, scriptResults);
}
}
if (globalResults.size()) {
this._createGlobalResultsUI(globalResults);
this.hidden = false;
DebuggerController.dispatchEvent("Debugger:GlobalSearch:MatchFound");
} else {
this.hidden = true;
DebuggerController.dispatchEvent("Debugger:GlobalSearch:MatchNotFound");
}
},
/**
* Creates global search results elements and adds them to the results container.
*
* @param Map aGlobalResults
* A map containing the search results, grouped by script url.
*/
_createGlobalResultsUI:
function DVGS__createGlobalResultsUI(aGlobalResults) {
let i = 0;
for (let [scriptUrl, scriptResults] of aGlobalResults) {
if (i++ === 0) {
this._createScriptResultsUI(scriptUrl, scriptResults, true);
} 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._createScriptResultsUI.bind(this, scriptUrl, scriptResults) }, 0);
}
}
},
/**
* Creates script search results elements and adds them to the results container.
*
* @param string aScriptUrl
* The URL of the source script.
* @param array aScriptResults
* An array containing the search results for a single script url.
* @param boolean aExpandFlag
* True to expand the script results container.
*/
_createScriptResultsUI:
function DVGS__createScriptResultsUI(aScriptUrl, aScriptResults, aExpandFlag) {
let { lineResults, matchCount } = aScriptResults;
let element;
for (let lineResult of lineResults) {
element = this._createLineSearchResultsUI({
scriptUrl: aScriptUrl,
matchCount: matchCount,
lineNumber: lineResult.lineNumber + 1,
lineContents: lineResult.lineContents
});
}
if (aExpandFlag) {
element.expand(true);
}
},
/**
* Creates per-line search results elements and adds them to the results container.
*
* @param object aLineResults
* An object containing the search results for each line in a script.
* @return object
* The newly created html node representing the added search results.
*/
_createLineSearchResultsUI:
function DVGS__createLineSearchresultsUI(aLineResults) {
let scriptResultsId = "search-results-" + aLineResults.scriptUrl;
let scriptResults = document.getElementById(scriptResultsId);
// Create the script results container if not available yet.
if (!scriptResults) {
let trimFunc = DebuggerController.SourceScripts.trimUrlLength;
let urlLabel = trimFunc(aLineResults.scriptUrl, GLOBAL_SEARCH_URL_MAX_SIZE);
let resultsUrl = document.createElement("label");
resultsUrl.className = "plain script-url";
resultsUrl.setAttribute("value", urlLabel);
let resultsCount = document.createElement("label");
resultsCount.className = "plain match-count";
resultsCount.setAttribute("value", "(" + aLineResults.matchCount + ")");
let arrow = document.createElement("box");
arrow.className = "arrow";
let resultsHeader = document.createElement("hbox");
resultsHeader.className = "dbg-results-header";
resultsHeader.setAttribute("align", "center")
resultsHeader.appendChild(arrow);
resultsHeader.appendChild(resultsUrl);
resultsHeader.appendChild(resultsCount);
let resultsContainer = document.createElement("vbox");
resultsContainer.className = "dbg-results-container";
scriptResults = document.createElement("vbox");
scriptResults.id = scriptResultsId;
scriptResults.className = "dbg-script-results";
scriptResults.header = resultsHeader;
scriptResults.container = resultsContainer;
scriptResults.appendChild(resultsHeader);
scriptResults.appendChild(resultsContainer);
this._pane.appendChild(scriptResults);
/**
* Expands the element, showing all the added details.
*
* @param boolean aSkipAnimationFlag
* Pass true to not show an opening animation.
* @return object
* The same element.
*/
scriptResults.expand = function DVGS_element_expand(aSkipAnimationFlag) {
resultsContainer.setAttribute("open", "");
arrow.setAttribute("open", "");
if (!aSkipAnimationFlag) {
resultsContainer.setAttribute("animated", "");
}
return scriptResults;
};
/**
* Collapses the element, hiding all the added details.
* @return object
* The same element.
*/
scriptResults.collapse = function DVGS_element_collapse() {
resultsContainer.removeAttribute("animated");
resultsContainer.removeAttribute("open");
arrow.removeAttribute("open");
return scriptResults;
};
/**
* Toggles between the element collapse/expand state.
* @return object
* The same element.
*/
scriptResults.toggle = function DVGS_element_toggle(e) {
if (e instanceof Event) {
scriptResults._userToggle = true;
}
scriptResults.expanded = !scriptResults.expanded;
return scriptResults;
};
/**
* Returns if the element is expanded.
* @return boolean
* True if the element is expanded.
*/
Object.defineProperty(scriptResults, "expanded", {
get: function DVP_element_getExpanded() {
return arrow.hasAttribute("open");
},
set: function DVP_element_setExpanded(value) {
if (value) {
scriptResults.expand();
} else {
scriptResults.collapse();
}
}
});
/**
* Called when a header in the search results container is clicked.
*/
resultsHeader.addEventListener("click", scriptResults.toggle, false);
}
let lineNumber = document.createElement("label");
lineNumber.className = "plain line-number";
lineNumber.setAttribute("value", aLineResults.lineNumber);
let lineContents = document.createElement("hbox");
lineContents.setAttribute("flex", "1");
lineContents.className = "line-contents";
lineContents.addEventListener("click", this._onLineClick, false);
let lineContent;
let totalLength = 0;
let ellipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString);
for (lineContent of aLineResults.lineContents) {
let string = lineContent.string;
let match = lineContent.match;
string = string.substr(0, GLOBAL_SEARCH_LINE_MAX_SIZE - totalLength);
totalLength += string.length;
let label = document.createElement("label");
label.className = "plain string";
label.setAttribute("value", string);
label.setAttribute("match", match || false);
lineContents.appendChild(label);
if (match) {
label.addEventListener("click", this._onMatchClick, false);
label.setUserData("lineResults", aLineResults, null);
label.setUserData("lineContentRange", lineContent.range, null);
label.container = scriptResults;
}
if (totalLength >= GLOBAL_SEARCH_LINE_MAX_SIZE) {
label = document.createElement("label");
label.className = "plain string";
label.setAttribute("value", ellipsis.data);
lineContents.appendChild(label);
break;
}
}
let searchResult = document.createElement("hbox");
searchResult.className = "dbg-search-result";
searchResult.appendChild(lineNumber);
searchResult.appendChild(lineContents);
let resultsContainer = scriptResults.container;
resultsContainer.appendChild(searchResult);
// Return the element for later use if necessary.
return scriptResults;
},
/**
* Focuses the next found match in the source editor.
*/
focusNextMatch: function DVGS_focusNextMatch() {
let matches = this._pane.querySelectorAll(".string[match=true]");
if (!matches.length) {
return;
}
if (++this._currentlyFocusedMatch >= matches.length) {
this._currentlyFocusedMatch = 0;
}
this._onMatchClick({ target: matches[this._currentlyFocusedMatch] });
},
/**
* Focuses the previously found match in the source editor.
*/
focusPrevMatch: function DVGS_focusPrevMatch() {
let matches = this._pane.querySelectorAll(".string[match=true]");
if (!matches.length) {
return;
}
if (--this._currentlyFocusedMatch < 0) {
this._currentlyFocusedMatch = matches.length - 1;
}
this._onMatchClick({ target: matches[this._currentlyFocusedMatch] });
},
/**
* Called when a line in the search results container is clicked.
*/
_onLineClick: function DVGS__onLineClick(e) {
let firstMatch = e.target.parentNode.querySelector(".string[match=true]");
this._onMatchClick({ target: firstMatch });
},
/**
* Called when a match in the search results container is clicked.
*/
_onMatchClick: function DVGLS__onMatchClick(e) {
if (e instanceof Event) {
e.preventDefault();
e.stopPropagation();
}
let match = e.target;
match.container.expand(true);
this._scrollMatchIntoViewIfNeeded(match);
this._animateMatchBounce(match);
let results = match.getUserData("lineResults");
let range = match.getUserData("lineContentRange");
let stackframes = DebuggerController.StackFrames;
stackframes.updateEditorToLocation(results.scriptUrl, results.lineNumber, 0, 0, 1);
let editor = DebuggerView.editor;
let offset = editor.getCaretOffset();
editor.setSelection(offset + range.start, offset + range.start + range.length);
},
/**
* Listener handling the searchbox blur event.
*/
_onFocusLost: function DVGS__onFocusLost(e) {
this.hideAndEmpty();
},
/**
* Listener handling the global search container scroll event.
*/
_onResultsScroll: function DVGS__onResultsScroll(e) {
this._expandAllVisibleResults();
},
/**
* Expands all the script results that are currently visible.
*/
_expandAllVisibleResults: function DVGS__expandAllVisibleResults() {
let collapsed = this._pane.querySelectorAll(".dbg-results-container:not([open])");
for (let i = 0, l = collapsed.length; i < l; i++) {
this._expandResultsIfNeeded(collapsed[i].parentNode);
}
},
/**
* Expands the script results it they are currently visible.
* @param nsIDOMElement aTarget
*/
_expandResultsIfNeeded: function DVGS__expandResultsIfNeeded(aTarget) {
if (aTarget.expanded || aTarget._userToggle) {
return;
}
let { clientHeight } = this._pane;
let { top, height } = aTarget.getBoundingClientRect();
if (top - height <= clientHeight || this._forceExpandResults) {
aTarget.expand(true);
}
},
/**
* Scrolls a match into view.
* @param nsIDOMElement aTarget
*/
_scrollMatchIntoViewIfNeeded: function DVGS__scrollMatchIntoViewIfNeeded(aTarget) {
let { clientHeight } = this._pane;
let { top, height } = aTarget.getBoundingClientRect();
let style = window.getComputedStyle(aTarget);
let topBorderSize = window.parseInt(style.getPropertyValue("border-top-width"));
let bottomBorderSize = window.parseInt(style.getPropertyValue("border-bottom-width"));
let marginY = top - (height + topBorderSize + bottomBorderSize) * 2;
if (marginY <= 0) {
this._pane.scrollTop += marginY;
}
if (marginY + height > clientHeight) {
this._pane.scrollTop += height - (clientHeight - marginY);
}
},
/**
* Starts a bounce animation for a match.
* @param nsIDOMElement aTarget
*/
_animateMatchBounce: function DVGS__animateMatchBounce(aTarget) {
aTarget.setAttribute("focused", "");
window.setTimeout(function() {
aTarget.removeAttribute("focused");
}, GLOBAL_SEARCH_MATCH_FLASH_DURATION);
},
/**
* Map containing the sources for all the currently known scripts.
*/
_scriptSources: new Map(),
/**
* The currently focused match from the search results container.
*/
_currentlyFocusedMatch: -1,
/**
* The cached global search results container.
*/
_pane: null,
_splitter: null,
_searchbox: null,
/**
* Initialization function, called when the debugger is initialized.
*/
initialize: function DVGS_initialize() {
this._pane = DebuggerView._globalSearch;
this._splitter = DebuggerView._globalSearchSplitter;
this._searchbox = DebuggerView._scriptsSearchbox;
this._pane.addEventListener("scroll", this._onResultsScroll, false);
this._searchbox.addEventListener("blur", this._onFocusLost, false);
},
/**
* Destruction function, called when the debugger is shut down.
*/
destroy: function DVS_destroy() {
this._pane.removeEventListener("scroll", this._onResultsScroll, false);
this._searchbox.removeEventListener("blur", this._onFocusLost, false);
this.hideAndEmpty();
this._pane = null;
this._splitter = null;
this._searchbox = null;
this._scriptSources = null;
}
};
/**
* Functions handling the scripts UI.
*/
function ScriptsView() {
this._onScriptsChange = this._onScriptsChange.bind(this);
this._onScriptsSearchClick = this._onScriptsSearchClick.bind(this);
this._onScriptsSearchBlur = this._onScriptsSearchBlur.bind(this);
this._onScriptsSearch = this._onScriptsSearch.bind(this);
this._onScriptsKeyPress = this._onScriptsKeyPress.bind(this);
}
ScriptsView.prototype = {
/**
* Removes all elements from the scripts container, leaving it empty.
*/
empty: function DVS_empty() {
this._scripts.selectedIndex = -1;
this._scripts.setAttribute("label", L10N.getStr("noScriptsText"));
this._scripts.removeAttribute("tooltiptext");
while (this._scripts.firstChild) {
this._scripts.removeChild(this._scripts.firstChild);
}
},
/**
* Removes the input in the searchbox and unhides all the scripts.
*/
clearSearch: function DVS_clearSearch() {
this._searchbox.value = "";
this._onScriptsSearch({});
},
/**
* Checks whether the script with the specified URL is among the scripts
* known to the debugger (ignoring the query & reference).
*
* @param string aUrl
* The script URL.
* @return boolean
*/
containsIgnoringQuery: function DVS_containsIgnoringQuery(aUrl) {
let sourceScripts = DebuggerController.SourceScripts;
aUrl = sourceScripts.trimUrlQuery(aUrl);
if (this._tmpScripts.some(function(element) {
return sourceScripts.trimUrlQuery(element.script.url) == aUrl;
})) {
return true;
}
if (this.scriptLocations.some(function(url) {
return sourceScripts.trimUrlQuery(url) == aUrl;
})) {
return true;
}
return false;
},
/**
* Checks whether the script with the specified URL is among the scripts
* known to the debugger and shown in the list.
*
* @param string aUrl
* The script URL.
* @return boolean
*/
contains: function DVS_contains(aUrl) {
if (this._tmpScripts.some(function(element) {
return element.script.url == aUrl;
})) {
return true;
}
if (this._scripts.getElementsByAttribute("value", aUrl).length > 0) {
return true;
}
return false;
},
/**
* Checks whether the script with the specified label is among the scripts
* known to the debugger and shown in the list.
*
* @param string aLabel
* The script label.
* @return boolean
*/
containsLabel: function DVS_containsLabel(aLabel) {
if (this._tmpScripts.some(function(element) {
return element.label == aLabel;
})) {
return true;
}
if (this._scripts.getElementsByAttribute("label", aLabel).length > 0) {
return true;
}
return false;
},
/**
* Selects the script with the specified index from the list.
*
* @param number aIndex
* The script index.
*/
selectIndex: function DVS_selectIndex(aIndex) {
this._scripts.selectedIndex = aIndex;
},
/**
* Selects the script with the specified URL from the list.
*
* @param string aUrl
* The script URL.
*/
selectScript: function DVS_selectScript(aUrl) {
for (let i = 0, l = this._scripts.itemCount; i < l; i++) {
if (this._scripts.getItemAtIndex(i).value == aUrl) {
this._scripts.selectedIndex = i;
return;
}
}
},
/**
* Checks whether the script with the specified URL is selected in the list.
*
* @param string aUrl
* The script URL.
*/
isSelected: function DVS_isSelected(aUrl) {
if (this._scripts.selectedItem &&
this._scripts.selectedItem.value == aUrl) {
return true;
}
return false;
},
/**
* Retrieve the URL of the selected script.
* @return string | null
*/
get selected() {
return this._scripts.selectedItem ?
this._scripts.selectedItem.value : null;
},
/**
* Gets the most recently selected script url.
* @return string | null
*/
get preferredScriptUrl()
this._preferredScriptUrl ? this._preferredScriptUrl : null,
/**
* Sets the most recently selected script url.
* @param string
*/
set preferredScriptUrl(value) this._preferredScriptUrl = value,
/**
* Gets the script in the container having the specified label.
*
* @param string aLabel
* The label used to identify the script.
* @return element | null
* The matched element, or null if nothing is found.
*/
getScriptByLabel: function DVS_getScriptByLabel(aLabel) {
return this._scripts.getElementsByAttribute("label", aLabel)[0];
},
/**
* Returns the list of labels in the scripts container.
* @return array
*/
get scriptLabels() {
let labels = [];
for (let i = 0, l = this._scripts.itemCount; i < l; i++) {
labels.push(this._scripts.getItemAtIndex(i).label);
}
return labels;
},
/**
* Gets the script in the container having the specified label.
*
* @param string aUrl
* The url used to identify the script.
* @return element | null
* The matched element, or null if nothing is found.
*/
getScriptByLocation: function DVS_getScriptByLocation(aUrl) {
return this._scripts.getElementsByAttribute("value", aUrl)[0];
},
/**
* Returns the list of URIs for scripts in the page.
* @return array
*/
get scriptLocations() {
let locations = [];
for (let i = 0, l = this._scripts.itemCount; i < l; i++) {
locations.push(this._scripts.getItemAtIndex(i).value);
}
return locations;
},
/**
* Gets the number of visible (hidden=false) scripts in the container.
* @return number
*/
get visibleItemsCount() {
let count = 0;
for (let i = 0, l = this._scripts.itemCount; i < l; i++) {
count += this._scripts.getItemAtIndex(i).hidden ? 0 : 1;
}
return count;
},
/**
* Prepares a script to be added to the scripts container. This allows
* for a large number of scripts to be batched up before being
* alphabetically sorted and added in the container.
* @see ScriptsView.commitScripts
*
* If aForceFlag is true, the script will be immediately inserted at the
* necessary position in the container so that all the scripts remain sorted.
* This can be much slower than batching up multiple scripts.
*
* @param string aLabel
* The simplified script location to be shown.
* @param string aScript
* The source script.
* @param boolean aForceFlag
* True to force the script to be immediately added.
*/
addScript: function DVS_addScript(aLabel, aScript, aForceFlag) {
// Batch the script to be added later.
if (!aForceFlag) {
this._tmpScripts.push({ label: aLabel, script: aScript });
return;
}
// Find the target position in the menulist and insert the script there.
for (let i = 0, l = this._scripts.itemCount; i < l; i++) {
if (this._scripts.getItemAtIndex(i).label > aLabel) {
this._createScriptElement(aLabel, aScript, i);
return;
}
}
// The script is alphabetically the last one.
this._createScriptElement(aLabel, aScript, -1);
},
/**
* Adds all the prepared scripts to the scripts container.
* If a script already exists (was previously added), nothing happens.
*/
commitScripts: function DVS_commitScripts() {
let newScripts = this._tmpScripts;
this._tmpScripts = [];
if (!newScripts || !newScripts.length) {
return;
}
newScripts.sort(function(a, b) {
return a.label.toLowerCase() > b.label.toLowerCase();
});
for (let i = 0, l = newScripts.length; i < l; i++) {
let item = newScripts[i];
this._createScriptElement(item.label, item.script, -1);
}
},
/**
* Creates a custom script element and adds it to the scripts container.
* If the script with the specified label already exists, nothing happens.
*
* @param string aLabel
* The simplified script location to be shown.
* @param string aScript
* The source script.
* @param number aIndex
* The index where to insert to new script in the container.
* Pass -1 to append the script at the end.
*/
_createScriptElement: function DVS__createScriptElement(aLabel, aScript, aIndex)
{
// Make sure we don't duplicate anything.
if (aLabel == "null" || this.containsLabel(aLabel) || this.contains(aScript.url)) {
return;
}
let scriptItem =
aIndex == -1 ? this._scripts.appendItem(aLabel, aScript.url)
: this._scripts.insertItemAt(aIndex, aLabel, aScript.url);
scriptItem.setAttribute("tooltiptext", aScript.url);
scriptItem.setUserData("sourceScript", aScript, null);
},
/**
* Gets the entered file, line and token entered in the searchbox.
*
* @return array
* A [file, line, token] array.
*/
get searchboxInfo() {
let file, line, token, isGlobal;
let rawValue = this._searchbox.value;
let rawLength = rawValue.length;
let lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG);
let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG);
let globalFlagIndex = rawValue.lastIndexOf(SEARCH_GLOBAL_FLAG);
if (globalFlagIndex !== 0) {
let fileEnd = lineFlagIndex !== -1 ? lineFlagIndex : tokenFlagIndex !== -1 ? tokenFlagIndex : rawLength;
let lineEnd = tokenFlagIndex !== -1 ? tokenFlagIndex : rawLength;
file = rawValue.slice(0, fileEnd);
line = window.parseInt(rawValue.slice(fileEnd + 1, lineEnd)) || -1;
token = rawValue.slice(lineEnd + 1);
isGlobal = false;
} else {
file = "";
line = -1;
token = rawValue.slice(1);
isGlobal = true;
}
return [file, line, token, isGlobal];
},
/**
* Returns the current search token.
*/
get searchToken() this.searchboxInfo[2],
/**
* The click listener for the scripts container.
*/
_onScriptsChange: function DVS__onScriptsChange() {
let selectedItem = this._scripts.selectedItem;
if (!selectedItem) {
return;
}
this._preferredScript = selectedItem;
this._preferredScriptUrl = selectedItem.value;
this._scripts.setAttribute("tooltiptext", selectedItem.value);
DebuggerController.SourceScripts.showScript(selectedItem.getUserData("sourceScript"));
},
_prevSearchedFile: "",
_prevSearchedLine: 0,
_prevSearchedToken: "",
/**
* Performs a file search if necessary.
*
* @param string aFile
* The script filename to search for.
*/
_performFileSearch: function DVS__performFileSearch(aFile) {
let scripts = this._scripts;
// Presume we won't find anything.
scripts.selectedItem = this._preferredScript;
scripts.setAttribute("label", this._preferredScript.label);
scripts.setAttribute("tooltiptext", this._preferredScript.value);
// If we're not searching for a file anymore, unhide all the scripts.
if (!aFile && this._someScriptsHidden) {
this._someScriptsHidden = false;
for (let i = 0, l = scripts.itemCount; i < l; i++) {
scripts.getItemAtIndex(i).hidden = false;
}
} else if (this._prevSearchedFile !== aFile) {
let lowerCaseFile = aFile.toLowerCase();
let found = false;
for (let i = 0, l = scripts.itemCount; i < l; i++) {
let item = scripts.getItemAtIndex(i);
let lowerCaseLabel = item.label.toLowerCase();
// Search is not case sensitive, and is tied to the label not the url.
if (lowerCaseLabel.match(aFile)) {
item.hidden = false;
if (!found) {
found = true;
scripts.selectedItem = item;
scripts.setAttribute("label", item.label);
scripts.setAttribute("tooltiptext", item.value);
}
}
// Hide what doesn't match our search.
else {
item.hidden = true;
this._someScriptsHidden = true;
}
}
if (!found) {
scripts.setAttribute("label", L10N.getStr("noMatchingScriptsText"));
scripts.removeAttribute("tooltiptext");
}
}
this._prevSearchedFile = aFile;
},
/**
* Performs a line search if necessary.
*
* @param number aLine
* The script line number to jump to.
*/
_performLineSearch: function DVS__performLineSearch(aLine) {
// Jump to lines in the currently visible source.
if (this._prevSearchedLine !== aLine && aLine > -1) {
DebuggerView.editor.setCaretPosition(aLine - 1);
}
this._prevSearchedLine = aLine;
},
/**
* Performs a token search if necessary.
*
* @param string aToken
* The script token to find.
*/
_performTokenSearch: function DVS__performTokenSearch(aToken) {
// Search for tokens in the currently visible source.
if (this._prevSearchedToken !== aToken && aToken.length > 0) {
let editor = DebuggerView.editor;
let offset = editor.find(aToken, { ignoreCase: true });
if (offset > -1) {
editor.setSelection(offset, offset + aToken.length)
}
}
this._prevSearchedToken = aToken;
},
/**
* The focus listener for the scripts search box.
*/
_onScriptsSearchClick: function DVS__onScriptsSearchClick() {
this._searchboxPanel.openPopup(this._searchbox);
},
/**
* The blur listener for the scripts search box.
*/
_onScriptsSearchBlur: function DVS__onScriptsSearchBlur() {
this._searchboxPanel.hidePopup();
},
/**
* The search listener for the scripts search box.
*/
_onScriptsSearch: function DVS__onScriptsSearch() {
// If the webpage has no scripts, searching is redundant.
if (!this._scripts.itemCount) {
return;
}
this._searchboxPanel.hidePopup();
let [file, line, token, isGlobal] = this.searchboxInfo;
// If this is a global script search, schedule a search in all the sources,
// or hide the pane otherwise.
if (isGlobal) {
DebuggerView.GlobalSearch.scheduleSearch();
} else {
DebuggerView.GlobalSearch.hideAndEmpty();
this._performFileSearch(file);
this._performLineSearch(line);
this._performTokenSearch(token);
}
},
/**
* The keypress listener for the scripts search box.
*/
_onScriptsKeyPress: function DVS__onScriptsKeyPress(e) {
if (e.keyCode === e.DOM_VK_ESCAPE) {
DebuggerView.editor.focus();
return;
}
var action;
if (e.keyCode === e.DOM_VK_DOWN ||
e.keyCode === e.DOM_VK_RETURN ||
e.keyCode === e.DOM_VK_ENTER) {
action = 1;
} else if (e.keyCode === e.DOM_VK_UP) {
action = 2;
}
if (action) {
let [file, line, token, isGlobal] = this.searchboxInfo;
if (token.length) {
e.preventDefault();
e.stopPropagation();
} else {
return;
}
if (isGlobal) {
if (DebuggerView.GlobalSearch.hidden) {
DebuggerView.GlobalSearch.scheduleSearch();
} else {
DebuggerView.GlobalSearch[action === 1
? "focusNextMatch"
: "focusPrevMatch"]();
}
return;
}
let editor = DebuggerView.editor;
let offset = editor[action === 1 ? "findNext" : "findPrevious"](true);
if (offset > -1) {
editor.setSelection(offset, offset + token.length)
}
}
},
/**
* Called when the scripts filter key sequence was pressed.
*/
_onSearch: function DVS__onSearch(aValue = "") {
this._searchbox.focus();
this._searchbox.value = aValue;
DebuggerView.GlobalSearch.hideAndEmpty();
},
/**
* Called when the scripts path filter key sequence was pressed.
*/
_onFileSearch: function DVS__onFileSearch() {
this._onSearch();
this._searchboxPanel.openPopup(this._searchbox);
},
/**
* Called when the scripts token filter key sequence was pressed.
*/
_onLineSearch: function DVS__onLineSearch() {
this._onSearch(SEARCH_LINE_FLAG);
this._searchboxPanel.hidePopup();
},
/**
* Called when the scripts token filter key sequence was pressed.
*/
_onTokenSearch: function DVS__onTokenSearch() {
this._onSearch(SEARCH_TOKEN_FLAG);
this._searchboxPanel.hidePopup();
},
/**
* Called when the scripts token filter key sequence was pressed.
*/
_onGlobalSearch: function DVS__onGlobalSearch() {
this._onSearch(SEARCH_GLOBAL_FLAG);
this._searchboxPanel.hidePopup();
},
/**
* The cached scripts container and search box.
*/
_scripts: null,
_searchbox: null,
_searchboxPanel: null,
/**
* Initialization function, called when the debugger is initialized.
*/
initialize: function DVS_initialize() {
this._scripts = DebuggerView._scripts;
this._searchbox = document.getElementById("scripts-search");
this._searchboxPanel = document.getElementById("scripts-search-panel");
this._scripts.addEventListener("select", this._onScriptsChange, false);
this._searchbox.addEventListener("click", this._onScriptsSearchClick, false);
this._searchbox.addEventListener("blur", this._onScriptsSearchBlur, false);
this._searchbox.addEventListener("select", this._onScriptsSearch, false);
this._searchbox.addEventListener("input", this._onScriptsSearch, false);
this._searchbox.addEventListener("keypress", this._onScriptsKeyPress, false);
this.commitScripts();
},
/**
* Destruction function, called when the debugger is shut down.
*/
destroy: function DVS_destroy() {
this._scripts.removeEventListener("select", this._onScriptsChange, false);
this._searchbox.removeEventListener("click", this._onScriptsSearchClick, false);
this._searchbox.removeEventListener("blur", this._onScriptsSearchBlur, false);
this._searchbox.removeEventListener("select", this._onScriptsSearch, false);
this._searchbox.removeEventListener("input", this._onScriptsSearch, false);
this._searchbox.removeEventListener("keypress", this._onScriptsKeyPress, false);
this.empty();
this._scripts = null;
this._searchbox = null;
this._searchboxPanel = null;
}
};
/**
* Functions handling the html stackframes UI.
*/
function StackFramesView() {
this._onFramesScroll = this._onFramesScroll.bind(this);
this._onPauseExceptionsClick = this._onPauseExceptionsClick.bind(this);
this._onCloseButtonClick = this._onCloseButtonClick.bind(this);
this._onResume = this._onResume.bind(this);
this._onStepOver = this._onStepOver.bind(this);
this._onStepIn = this._onStepIn.bind(this);
this._onStepOut = this._onStepOut.bind(this);
}
StackFramesView.prototype = {
/**
* Sets the current frames state based on the debugger active thread state.
*
* @param string aState
* Either "paused" or "attached".
*/
updateState: function DVF_updateState(aState) {
let resume = DebuggerView._resumeButton;
let resumeKey = LayoutHelpers.prettyKey(DebuggerView._resumeKey);
// If we're paused, show a pause label and a resume label on the button.
if (aState == "paused") {
resume.setAttribute("tooltiptext", L10N.getFormatStr("resumeButtonTooltip", [resumeKey]));
resume.setAttribute("checked", true);
}
// If we're attached, do the opposite.
else if (aState == "attached") {
resume.setAttribute("tooltiptext", L10N.getFormatStr("pauseButtonTooltip", [resumeKey]));
resume.removeAttribute("checked");
}
},
/**
* Removes all elements from the stackframes container, leaving it empty.
*/
empty: function DVF_empty() {
while (this._frames.firstChild) {
this._frames.removeChild(this._frames.firstChild);
}
},
/**
* Removes all elements from the stackframes container, and adds a child node
* with an empty text note attached.
*/
emptyText: function DVF_emptyText() {
// Make sure the container is empty first.
this.empty();
let item = document.createElement("label");
// The empty node should look grayed out to avoid confusion.
item.className = "list-item empty";
item.setAttribute("value", L10N.getStr("emptyStackText"));
this._frames.appendChild(item);
},
/**
* Adds a frame to the stackframes container.
* If the frame already exists (was previously added), null is returned.
* Otherwise, the newly created element is returned.
*
* @param number aDepth
* The frame depth specified by the debugger.
* @param string aFrameNameText
* The name to be displayed in the list.
* @param string aFrameDetailsText
* The details to be displayed in the list.
* @return object
* The newly created html node representing the added frame.
*/
addFrame: function DVF_addFrame(aDepth, aFrameNameText, aFrameDetailsText) {
// Make sure we don't duplicate anything.
if (document.getElementById("stackframe-" + aDepth)) {
return null;
}
// Stackframes are UI elements which benefit from visible panes.
DebuggerView.showPanesIfAllowed();
let frame = document.createElement("box");
let frameName = document.createElement("label");
let frameDetails = document.createElement("label");
// Create a list item to be added to the stackframes container.
frame.id = "stackframe-" + aDepth;
frame.className = "dbg-stackframe list-item";
// This list should display the name and details for the frame.
frameName.className = "dbg-stackframe-name plain";
frameDetails.className = "dbg-stackframe-details plain";
frameName.setAttribute("value", aFrameNameText);
frameDetails.setAttribute("value", aFrameDetailsText);
let spacer = document.createElement("spacer");
spacer.setAttribute("flex", "1");
frame.appendChild(frameName);
frame.appendChild(spacer);
frame.appendChild(frameDetails);
this._frames.appendChild(frame);
// Return the element for later use if necessary.
return frame;
},
/**
* Highlights a frame from the stackframe container as selected/deselected.
*
* @param number aDepth
* The frame depth specified by the debugger.
* @param boolean aFlag
* True if the frame should be deselected, false otherwise.
*/
highlightFrame: function DVF_highlightFrame(aDepth, aFlag) {
let frame = document.getElementById("stackframe-" + aDepth);
// The list item wasn't found in the stackframe container.
if (!frame) {
return;
}
// Add the 'selected' css class if the frame isn't already selected.
if (!aFlag && !frame.classList.contains("selected")) {
frame.classList.add("selected");
}
// Remove the 'selected' css class if the frame is already selected.
else if (aFlag && frame.classList.contains("selected")) {
frame.classList.remove("selected");
}
},
/**
* Deselects a frame from the stackframe container.
*
* @param number aDepth
* The frame depth specified by the debugger.
*/
unhighlightFrame: function DVF_unhighlightFrame(aDepth) {
this.highlightFrame(aDepth, true);
},
/**
* Gets the current dirty state.
*
* @return boolean value
* True if should load more frames.
*/
get dirty() {
return this._dirty;
},
/**
* Sets if the active thread has more frames that need to be loaded.
*
* @param boolean aValue
* True if should load more frames.
*/
set dirty(aValue) {
this._dirty = aValue;
},
/**
* Listener handling the stackframes container click event.
*/
_onFramesClick: function DVF__onFramesClick(aEvent) {
let target = aEvent.target;
while (target) {
if (target.debuggerFrame) {
DebuggerController.StackFrames.selectFrame(target.debuggerFrame.depth);
return;
}
target = target.parentNode;
}
},
/**
* Listener handling the stackframes container scroll event.
*/
_onFramesScroll: function DVF__onFramesScroll(aEvent) {
// Update the stackframes container only if we have to.
if (this._dirty) {
let clientHeight = this._frames.clientHeight;
let scrollTop = this._frames.scrollTop;
let scrollHeight = this._frames.scrollHeight;
// If the stackframes container was scrolled past 95% of the height,
// load more content.
if (scrollTop >= (scrollHeight - clientHeight) * 0.95) {
this._dirty = false;
DebuggerController.StackFrames.addMoreFrames();
}
}
},
/**
* Listener handling the close button click event.
*/
_onCloseButtonClick: function DVF__onCloseButtonClick() {
DebuggerController.dispatchEvent("Debugger:Close");
},
/**
* Listener handling the pause-on-exceptions click event.
*/
_onPauseExceptionsClick: function DVF__onPauseExceptionsClick() {
let option = document.getElementById("pause-exceptions");
DebuggerController.StackFrames.updatePauseOnExceptions(option.checked);
},
/**
* Listener handling the pause/resume button click event.
*/
_onResume: function DVF__onResume(e) {
if (DebuggerController.activeThread.paused) {
DebuggerController.activeThread.resume();
} else {
DebuggerController.activeThread.interrupt();
}
},
/**
* Listener handling the step over button click event.
*/
_onStepOver: function DVF__onStepOver(e) {
if (DebuggerController.activeThread.paused) {
DebuggerController.activeThread.stepOver();
}
},
/**
* Listener handling the step in button click event.
*/
_onStepIn: function DVF__onStepIn(e) {
if (DebuggerController.activeThread.paused) {
DebuggerController.activeThread.stepIn();
}
},
/**
* Listener handling the step out button click event.
*/
_onStepOut: function DVF__onStepOut(e) {
if (DebuggerController.activeThread.paused) {
DebuggerController.activeThread.stepOut();
}
},
/**
* Specifies if the active thread has more frames which need to be loaded.
*/
_dirty: false,
/**
* The cached stackframes container.
*/
_frames: null,
/**
* Initialization function, called when the debugger is initialized.
*/
initialize: function DVF_initialize() {
let close = document.getElementById("close");
let pauseOnExceptions = document.getElementById("pause-exceptions");
let resume = DebuggerView._resumeButton;
let stepOver = DebuggerView._stepOverButton;
let stepIn = DebuggerView._stepInButton;
let stepOut = DebuggerView._stepOutButton;
let frames = DebuggerView._stackframes;
close.addEventListener("click", this._onCloseButtonClick, false);
pauseOnExceptions.checked = DebuggerController.StackFrames.pauseOnExceptions;
pauseOnExceptions.addEventListener("click", this._onPauseExceptionsClick, false);
resume.addEventListener("click", this._onResume, false);
stepOver.addEventListener("click", this._onStepOver, false);
stepIn.addEventListener("click", this._onStepIn, false);
stepOut.addEventListener("click", this._onStepOut, false);
frames.addEventListener("click", this._onFramesClick, false);
frames.addEventListener("scroll", this._onFramesScroll, false);
window.addEventListener("resize", this._onFramesScroll, false);
this._frames = frames;
this.emptyText();
},
/**
* Destruction function, called when the debugger is shut down.
*/
destroy: function DVF_destroy() {
let close = document.getElementById("close");
let pauseOnExceptions = document.getElementById("pause-exceptions");
let resume = DebuggerView._resumeButton;
let stepOver = DebuggerView._stepOverButton;
let stepIn = DebuggerView._stepInButton;
let stepOut = DebuggerView._stepOutButton;
let frames = DebuggerView._stackframes;
close.removeEventListener("click", this._onCloseButtonClick, false);
pauseOnExceptions.removeEventListener("click", this._onPauseExceptionsClick, false);
resume.removeEventListener("click", this._onResume, false);
stepOver.removeEventListener("click", this._onStepOver, false);
stepIn.removeEventListener("click", this._onStepIn, false);
stepOut.removeEventListener("click", this._onStepOut, false);
frames.removeEventListener("click", this._onFramesClick, false);
frames.removeEventListener("scroll", this._onFramesScroll, false);
window.removeEventListener("resize", this._onFramesScroll, false);
this.empty();
this._frames = null;
}
};
/**
* Functions handling the breakpoints view.
*/
function BreakpointsView() {
this._onBreakpointClick = this._onBreakpointClick.bind(this);
this._onBreakpointCheckboxChange = this._onBreakpointCheckboxChange.bind(this);
}
BreakpointsView.prototype = {
/**
* Removes all elements from the breakpoints container, leaving it empty.
*/
empty: function DVB_empty() {
let firstChild;
while (firstChild = this._breakpoints.firstChild) {
this._destroyContextMenu(firstChild);
this._breakpoints.removeChild(firstChild);
}
},
/**
* Removes all elements from the breakpoints container, and adds a child node
* with an empty text note attached.
*/
emptyText: function DVB_emptyText() {
// Make sure the container is empty first.
this.empty();
let item = document.createElement("label");
// The empty node should look grayed out to avoid confusion.
item.className = "list-item empty";
item.setAttribute("value", L10N.getStr("emptyBreakpointsText"));
this._breakpoints.appendChild(item);
},
/**
* Checks whether the breakpoint with the specified script URL and line is
* among the breakpoints known to the debugger and shown in the list, and
* returns the matched result or null if nothing is found.
*
* @param string aUrl
* The original breakpoint script url.
* @param number aLine
* The original breakpoint script line.
* @return object | null
* The queried breakpoint
*/
getBreakpoint: function DVB_getBreakpoint(aUrl, aLine) {
return this._breakpoints.getElementsByAttribute("location", aUrl + ":" + aLine)[0];
},
/**
* Removes a breakpoint only from the breakpoints container.
* This doesn't remove the breakpoint from the DebuggerController!
*
* @param string aId
* A breakpoint identifier specified by the debugger.
*/
removeBreakpoint: function DVB_removeBreakpoint(aId) {
let breakpoint = document.getElementById("breakpoint-" + aId);
// Make sure we have something to remove.
if (!breakpoint) {
return;
}
this._destroyContextMenu(breakpoint);
this._breakpoints.removeChild(breakpoint);
if (!this.count) {
this.emptyText();
}
},
/**
* Adds a breakpoint to the breakpoints container.
* If the breakpoint already exists (was previously added), null is returned.
* If it's already added but disabled, it will be enabled and null is returned.
* Otherwise, the newly created element is returned.
*
* @param string aId
* A breakpoint identifier specified by the debugger.
* @param string aLineInfo
* The script line information to be displayed in the list.
* @param string aLineText
* The script line text to be displayed in the list.
* @param string aUrl
* The original breakpoint script url.
* @param number aLine
* The original breakpoint script line.
* @return object
* The newly created html node representing the added breakpoint.
*/
addBreakpoint: function DVB_addBreakpoint(aId, aLineInfo, aLineText, aUrl, aLine) {
// Make sure we don't duplicate anything.
if (document.getElementById("breakpoint-" + aId)) {
return null;
}
// Remove the empty list text if it was there.
if (!this.count) {
this.empty();
}
// If the breakpoint was already added but disabled, enable it now.
let breakpoint = this.getBreakpoint(aUrl, aLine);
if (breakpoint) {
breakpoint.id = "breakpoint-" + aId;
breakpoint.breakpointActor = aId;
breakpoint.getElementsByTagName("checkbox")[0].setAttribute("checked", "true");
return;
}
breakpoint = document.createElement("box");
let bkpCheckbox = document.createElement("checkbox");
let bkpLineInfo = document.createElement("label");
let bkpLineText = document.createElement("label");
// Create a list item to be added to the stackframes container.
breakpoint.id = "breakpoint-" + aId;
breakpoint.className = "dbg-breakpoint list-item";
breakpoint.setAttribute("location", aUrl + ":" + aLine);
breakpoint.breakpointUrl = aUrl;
breakpoint.breakpointLine = aLine;
breakpoint.breakpointActor = aId;
aLineInfo = aLineInfo.trim();
aLineText = aLineText.trim();
// A checkbox specifies if the breakpoint is enabled or not.
bkpCheckbox.setAttribute("checked", "true");
bkpCheckbox.addEventListener("click", this._onBreakpointCheckboxChange, false);
// This list should display the line info and text for the breakpoint.
bkpLineInfo.className = "dbg-breakpoint-info plain";
bkpLineText.className = "dbg-breakpoint-text plain";
bkpLineInfo.setAttribute("value", aLineInfo);
bkpLineText.setAttribute("value", aLineText);
bkpLineInfo.setAttribute("crop", "end");
bkpLineText.setAttribute("crop", "end");
bkpLineText.setAttribute("tooltiptext", aLineText.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_SIZE));
// Create a context menu for the breakpoint.
let menupopupId = this._createContextMenu(breakpoint);
breakpoint.setAttribute("contextmenu", menupopupId);
let state = document.createElement("vbox");
state.className = "state";
state.appendChild(bkpCheckbox);
let content = document.createElement("vbox");
content.className = "content";
content.setAttribute("flex", "1");
content.appendChild(bkpLineInfo);
content.appendChild(bkpLineText);
breakpoint.appendChild(state);
breakpoint.appendChild(content);
this._breakpoints.appendChild(breakpoint);
// Return the element for later use if necessary.
return breakpoint;
},
/**
* Enables a breakpoint.
*
* @param object aBreakpoint
* An element representing a breakpoint.
* @param function aCallback
* Optional function to invoke once the breakpoint is enabled.
* @param boolean aNoCheckboxUpdate
* Pass true to not update the checkbox checked state.
* This is usually necessary when the checked state will be updated
* automatically (e.g: on a checkbox click).
*/
enableBreakpoint:
function DVB_enableBreakpoint(aTarget, aCallback, aNoCheckboxUpdate) {
let { breakpointUrl: url, breakpointLine: line } = aTarget;
let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line);
if (!breakpoint) {
if (!aNoCheckboxUpdate) {
aTarget.getElementsByTagName("checkbox")[0].setAttribute("checked", "true");
}
DebuggerController.Breakpoints.
addBreakpoint({ url: url, line: line }, aCallback);
return true;
}
return false;
},
/**
* Disables a breakpoint.
*
* @param object aTarget
* An element representing a breakpoint.
* @param function aCallback
* Optional function to invoke once the breakpoint is disabled.
* @param boolean aNoCheckboxUpdate
* Pass true to not update the checkbox checked state.
* This is usually necessary when the checked state will be updated
* automatically (e.g: on a checkbox click).
*/
disableBreakpoint:
function DVB_disableBreakpoint(aTarget, aCallback, aNoCheckboxUpdate) {
let { breakpointUrl: url, breakpointLine: line } = aTarget;
let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line)
if (breakpoint) {
if (!aNoCheckboxUpdate) {
aTarget.getElementsByTagName("checkbox")[0].removeAttribute("checked");
}
DebuggerController.Breakpoints.
removeBreakpoint(breakpoint, aCallback, false, true);
return true;
}
return false;
},
/**
* Gets the current number of added breakpoints.
*/
get count() {
return this._breakpoints.getElementsByClassName("dbg-breakpoint").length;
},
/**
* Iterates through all the added breakpoints.
*
* @param function aCallback
* Function called for each element.
*/
_iterate: function DVB_iterate(aCallback) {
Array.forEach(Array.slice(this._breakpoints.childNodes), aCallback);
},
/**
* Gets the real breakpoint target when an event is handled.
* @return object
*/
_getBreakpointTarget: function DVB__getBreakpointTarget(aEvent) {
let target = aEvent.target;
while (target) {
if (target.breakpointActor) {
return target;
}
target = target.parentNode;
}
},
/**
* Listener handling the breakpoint click event.
*/
_onBreakpointClick: function DVB__onBreakpointClick(aEvent) {
let target = this._getBreakpointTarget(aEvent);
let { breakpointUrl: url, breakpointLine: line } = target;
DebuggerController.StackFrames.updateEditorToLocation(url, line, 0, 0, 1);
},
/**
* Listener handling the breakpoint checkbox change event.
*/
_onBreakpointCheckboxChange: function DVB__onBreakpointCheckboxChange(aEvent) {
aEvent.stopPropagation();
let target = this._getBreakpointTarget(aEvent);
let { breakpointUrl: url, breakpointLine: line } = target;
if (aEvent.target.getAttribute("checked") === "true") {
this.disableBreakpoint(target, null, true);
} else {
this.enableBreakpoint(target, null, true);
}
},
/**
* Listener handling the "enableSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onEnableSelf: function DVB__onEnableSelf(aTarget) {
if (!aTarget) {
return;
}
if (this.enableBreakpoint(aTarget)) {
aTarget.enableSelf.menuitem.setAttribute("hidden", "true");
aTarget.disableSelf.menuitem.removeAttribute("hidden");
}
},
/**
* Listener handling the "disableSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDisableSelf: function DVB__onDisableSelf(aTarget) {
if (!aTarget) {
return;
}
if (this.disableBreakpoint(aTarget)) {
aTarget.enableSelf.menuitem.removeAttribute("hidden");
aTarget.disableSelf.menuitem.setAttribute("hidden", "true");
}
},
/**
* Listener handling the "deleteSelf" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDeleteSelf: function DVB__onDeleteSelf(aTarget) {
let { breakpointUrl: url, breakpointLine: line } = aTarget;
let breakpoint = DebuggerController.Breakpoints.getBreakpoint(url, line)
if (aTarget) {
this.removeBreakpoint(aTarget.breakpointActor);
}
if (breakpoint) {
DebuggerController.Breakpoints.removeBreakpoint(breakpoint);
}
},
/**
* Listener handling the "enableOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onEnableOthers: function DVB__onEnableOthers(aTarget) {
this._iterate(function(element) {
if (element !== aTarget) {
this._onEnableSelf(element);
}
}.bind(this));
},
/**
* Listener handling the "disableOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDisableOthers: function DVB__onDisableOthers(aTarget) {
this._iterate(function(element) {
if (element !== aTarget) {
this._onDisableSelf(element);
}
}.bind(this));
},
/**
* Listener handling the "deleteOthers" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDeleteOthers: function DVB__onDeleteOthers(aTarget) {
this._iterate(function(element) {
if (element !== aTarget) {
this._onDeleteSelf(element);
}
}.bind(this));
},
/**
* Listener handling the "disableAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onEnableAll: function DVB__onEnableAll(aTarget) {
this._onEnableOthers(aTarget);
this._onEnableSelf(aTarget);
},
/**
* Listener handling the "disableAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDisableAll: function DVB__onDisableAll(aTarget) {
this._onDisableOthers(aTarget);
this._onDisableSelf(aTarget);
},
/**
* Listener handling the "deleteAll" menuitem command.
*
* @param object aTarget
* The corresponding breakpoint element.
*/
_onDeleteAll: function DVB__onDeleteAll(aTarget) {
this._onDeleteOthers(aTarget);
this._onDeleteSelf(aTarget);
},
/**
* The cached breakpoints container.
*/
_breakpoints: null,
/**
* Creates a breakpoint context menu.
*
* @param object aBreakpoint
* An element representing a breakpoint.
* @return string
* The popup id.
*/
_createContextMenu: function DVB_createContextMenu(aBreakpoint) {
let commandsetId = "breakpointMenuCommands-" + aBreakpoint.id;
let menupopupId = "breakpointContextMenu-" + aBreakpoint.id;
let commandset = document.createElement("commandset");
commandset.setAttribute("id", commandsetId);
let menupopup = document.createElement("menupopup");
menupopup.setAttribute("id", menupopupId);
/**
* Creates a menu item specified by a name with the appropriate attributes
* (label and command handler).
*
* @param string aName
* A global identifier for the menu item.
* @param boolean aHiddenFlag
* True if this menuitem should be hidden.
*/
function createMenuItem(aName, aHiddenFlag) {
let menuitem = document.createElement("menuitem");
let command = document.createElement("command");
let func = this["_on" + aName.charAt(0).toUpperCase() + aName.slice(1)];
let label = L10N.getStr("breakpointMenuItem." + aName);
let prefix = "bp-cMenu-";
let commandId = prefix + aName + "-" + aBreakpoint.id + "-command";
let menuitemId = prefix + aName + "-" + aBreakpoint.id + "-menuitem";
command.setAttribute("id", commandId);
command.setAttribute("label", label);
command.addEventListener("command", func.bind(this, aBreakpoint), true);
menuitem.setAttribute("id", menuitemId);
menuitem.setAttribute("command", commandId);
menuitem.setAttribute("hidden", aHiddenFlag);
commandset.appendChild(command);
menupopup.appendChild(menuitem);
aBreakpoint[aName] = {
menuitem: menuitem,
command: command
};
}
/**
* Creates a simple menu separator element and appends it to the current
* menupopup hierarchy.
*/
function createMenuSeparator() {
let menuseparator = document.createElement("menuseparator");
menupopup.appendChild(menuseparator);
}
createMenuItem.call(this, "enableSelf", true);
createMenuItem.call(this, "disableSelf");
createMenuItem.call(this, "deleteSelf");
createMenuSeparator();
createMenuItem.call(this, "enableOthers");
createMenuItem.call(this, "disableOthers");
createMenuItem.call(this, "deleteOthers");
createMenuSeparator();
createMenuItem.call(this, "enableAll");
createMenuItem.call(this, "disableAll");
createMenuSeparator();
createMenuItem.call(this, "deleteAll");
let popupset = document.getElementById("debugger-popups");
popupset.appendChild(menupopup);
document.documentElement.appendChild(commandset);
aBreakpoint.commandsetId = commandsetId;
aBreakpoint.menupopupId = menupopupId;
return menupopupId;
},
/**
* Destroys a breakpoint context menu.
*
* @param object aBreakpoint
* An element representing a breakpoint.
*/
_destroyContextMenu: function DVB__destroyContextMenu(aBreakpoint) {
if (!aBreakpoint.commandsetId || !aBreakpoint.menupopupId) {
return;
}
let commandset = document.getElementById(aBreakpoint.commandsetId);
let menupopup = document.getElementById(aBreakpoint.menupopupId);
commandset.parentNode.removeChild(commandset);
menupopup.parentNode.removeChild(menupopup);
},
/**
* Initialization function, called when the debugger is initialized.
*/
initialize: function DVB_initialize() {
let breakpoints = DebuggerView._breakpoints;
breakpoints.addEventListener("click", this._onBreakpointClick, false);
this._breakpoints = breakpoints;
this.emptyText();
},
/**
* Destruction function, called when the debugger is shut down.
*/
destroy: function DVB_destroy() {
let breakpoints = this._breakpoints;
breakpoints.removeEventListener("click", this._onBreakpointClick, false);
this.empty();
this._breakpoints = null;
}
};
/**
* Functions handling the properties view.
*/
function PropertiesView() {
this.addScope = this._addScope.bind(this);
this._addVar = this._addVar.bind(this);
this._addProperties = this._addProperties.bind(this);
}
PropertiesView.prototype = {
/**
* A monotonically-increasing counter, that guarantees the uniqueness of scope
* IDs.
*/
_idCount: 1,
/**
* Adds a scope to contain any inspected variables.
* If the optional id is not specified, the scope html node will have a
* default id set as aName-scope.
*
* @param string aName
* The scope name (e.g. "Local", "Global" or "With block").
* @param string aId
* Optional, an id for the scope html node.
* @return object
* The newly created html node representing the added scope or null
* if a node was not created.
*/
_addScope: function DVP__addScope(aName, aId) {
// Make sure the parent container exists.
if (!this._vars) {
return null;
}
// Generate a unique id for the element, if not specified.
aId = aId || aName.toLowerCase().trim().replace(/\s+/g, "-") + this._idCount++;
// Contains generic nodes and functionality.
let element = this._createPropertyElement(aName, aId, "scope", this._vars);
// Make sure the element was created successfully.
if (!element) {
return null;
}
element._identifier = aName;
/**
* @see DebuggerView.Properties._addVar
*/
element.addVar = this._addVar.bind(this, element);
/**
* @see DebuggerView.Properties.addScopeToHierarchy
*/
element.addToHierarchy = this.addScopeToHierarchy.bind(this, element);
// Setup the additional elements specific for a scope node.
element.refresh(function() {
let title = element.getElementsByClassName("title")[0];
title.classList.add("devtools-toolbar");
}.bind(this));
// Return the element for later use if necessary.
return element;
},
/**
* Removes all added scopes in the property container tree.
*/
empty: function DVP_empty() {
while (this._vars.firstChild) {
this._vars.removeChild(this._vars.firstChild);
}
},
/**
* Removes all elements from the variables container, and adds a child node
* with an empty text note attached.
*/
emptyText: function DVP_emptyText() {
// Make sure the container is empty first.
this.empty();
let item = document.createElement("label");
// The empty node should look grayed out to avoid confusion.
item.className = "list-item empty";
item.setAttribute("value", L10N.getStr("emptyVariablesText"));
this._vars.appendChild(item);
},
/**
* Adds a variable to a specified scope.
* If the optional id is not specified, the variable html node will have a
* default id set as aScope.id->aName-variable.
*
* @param object aScope
* The parent scope element.
* @param string aName
* The variable name.
* @param object aFlags
* Optional, contains configurable, enumerable or writable flags.
* @param string aId
* Optional, an id for the variable html node.
* @return object
* The newly created html node representing the added var.
*/
_addVar: function DVP__addVar(aScope, aName, aFlags, aId) {
// Make sure the scope container exists.
if (!aScope) {
return null;
}
// Compute the id of the element if not specified.
aId = aId || (aScope.id + "->" + aName + "-variable");
let parent;
if (aFlags && !aFlags.enumerable) {
parent = aScope.childNodes[2];
}
else {
parent = aScope.childNodes[1];
}
// Contains generic nodes and functionality.
let element = this._createPropertyElement(aName, aId, "variable", parent);
// Make sure the element was created successfully.
if (!element) {
return null;
}
element._identifier = aName;
/**
* @see DebuggerView.Properties._setGrip
*/
element.setGrip = this._setGrip.bind(this, element);
/**
* @see DebuggerView.Properties._addProperties
*/
element.addProperties = this._addProperties.bind(this, element);
// Setup the additional elements specific for a variable node.
element.refresh(function() {
let separatorLabel = document.createElement("label");
let valueLabel = document.createElement("label");
let title = element.getElementsByClassName("title")[0];
// Use attribute flags to specify the element type and tooltip text.
this._setAttributes(element, aName, aFlags);
// Separator between the variable name and its value.
separatorLabel.className = "plain";
separatorLabel.setAttribute("value", ":");
// The variable information (type, class and/or value).
valueLabel.className = "value plain";
// Handle the click event when pressing the element value label.
valueLabel.addEventListener("click", this._activateElementInputMode.bind({
scope: this,
element: element,
valueLabel: valueLabel
}));
// Maintain the symbolic name of the variable.
Object.defineProperty(element, "token", {
value: aName,
writable: false,
enumerable: true,
configurable: true
});
title.appendChild(separatorLabel);
title.appendChild(valueLabel);
// Remember a simple hierarchy between the parent and the element.
this._saveHierarchy({
parent: aScope,
element: element,
valueLabel: valueLabel
});
}.bind(this));
// Return the element for later use if necessary.
return element;
},
/**
* Sets a variable's configurable, enumerable or writable attributes.
*
* @param object aVar
* The object to set the attributes on.
* @param object aName
* The varialbe name.
* @param object aFlags
* Contains configurable, enumerable or writable flags.
*/
_setAttributes: function DVP_setAttributes(aVar, aName, aFlags) {
if (aFlags) {
if (!aFlags.configurable) {
aVar.setAttribute("non-configurable", "");
}
if (!aFlags.enumerable) {
aVar.setAttribute("non-enumerable", "");
}
if (!aFlags.writable) {
aVar.setAttribute("non-writable", "");
}
}
if (aName === "this") {
aVar.setAttribute("self", "");
}
if (aName === "__proto__ ") {
aVar.setAttribute("proto", "");
}
},
/**
* Sets the specific grip for a variable.
* The grip should contain the value or the type & class, as defined in the
* remote debugger protocol. For convenience, undefined and null are
* both considered types.
*
* @param object aVar
* The parent variable element.
* @param object aGrip
* The primitive or object defining the grip, specifying
* the value and/or type & class of the variable (if the type
* is not specified, it will be inferred from the value).
* e.g. 42
* true
* "nasu"
* { type: "undefined" }
* { type: "null" }
* { type: "object", class: "Object" }
* @return object
* The same variable.
*/
_setGrip: function DVP__setGrip(aVar, aGrip) {
// Make sure the variable container exists.
if (!aVar) {
return null;
}
if (aGrip === undefined) {
aGrip = { type: "undefined" };
}
if (aGrip === null) {
aGrip = { type: "null" };
}
let valueLabel = aVar.getElementsByClassName("value")[0];
// Make sure the value node exists.
if (!valueLabel) {
return null;
}
this._applyGrip(valueLabel, aGrip);
return aVar;
},
/**
* Applies the necessary text content and class name to a value node based
* on a grip.
*
* @param object aValueLabel
* The value node to apply the changes to.
* @param object aGrip
* @see DebuggerView.Properties._setGrip
*/
_applyGrip: function DVP__applyGrip(aValueLabel, aGrip) {
let prevGrip = aValueLabel.currentGrip;
if (prevGrip) {
aValueLabel.classList.remove(this._propertyColor(prevGrip));
}
aValueLabel.setAttribute("value", this._propertyString(aGrip));
aValueLabel.classList.add(this._propertyColor(aGrip));
aValueLabel.currentGrip = aGrip;
},
/**
* Adds multiple properties to a specified variable.
* This function handles two types of properties: data properties and
* accessor properties, as defined in the remote debugger protocol spec.
*
* @param object aVar
* The parent variable element.
* @param object aProperties
* An object containing the key: descriptor data properties,
* specifying the value and/or type & class of the variable,
* or 'get' & 'set' accessor properties.
* e.g. { "someProp0": { value: 42 },
* "someProp1": { value: true },
* "someProp2": { value: "nasu" },
* "someProp3": { value: { type: "undefined" } },
* "someProp4": { value: { type: "null" } },
* "someProp5": { value: { type: "object", class: "Object" } },
* "someProp6": { get: { type: "object", class: "Function" },
* set: { type: "undefined" } }
* @return object
* The same variable.
*/
_addProperties: function DVP__addProperties(aVar, aProperties) {
// For each property, add it using the passed object key/grip.
for (let i in aProperties) {
// Can't use aProperties.hasOwnProperty(i), because it may be overridden.
if (Object.getOwnPropertyDescriptor(aProperties, i)) {
// Get the specified descriptor for current property.
let desc = aProperties[i];
// As described in the remote debugger protocol, the value grip must be
// contained in a 'value' property.
let value = desc["value"];
// For accessor property descriptors, the two grips need to be
// contained in 'get' and 'set' properties.
let getter = desc["get"];
let setter = desc["set"];
// Handle data property and accessor property descriptors.
if (value !== undefined) {
this._addProperty(aVar, [i, value], desc);
}
if (getter !== undefined || setter !== undefined) {
let prop = this._addProperty(aVar, [i]).expand();
prop.getter = this._addProperty(prop, ["get", getter], desc);
prop.setter = this._addProperty(prop, ["set", setter], desc);
}
}
}
return aVar;
},
/**
* Adds a property to a specified variable.
* If the optional id is not specified, the property html node will have a
* default id set as aVar.id->aKey-property.
*
* @param object aVar
* The parent variable element.
* @param array aProperty
* An array containing the key and grip properties, specifying
* the value and/or type & class of the variable (if the type
* is not specified, it will be inferred from the value).
* e.g. ["someProp0", 42]
* ["someProp1", true]
* ["someProp2", "nasu"]
* ["someProp3", { type: "undefined" }]
* ["someProp4", { type: "null" }]
* ["someProp5", { type: "object", class: "Object" }]
* @param object aFlags
* Contains configurable, enumerable or writable flags.
* @param string aName
* Optional, the property name.
* @paarm string aId
* Optional, an id for the property html node.
* @return object
* The newly created html node representing the added prop.
*/
_addProperty: function DVP__addProperty(aVar, aProperty, aFlags, aName, aId) {
// Make sure the variable container exists.
if (!aVar) {
return null;
}
// Compute the id of the element if not specified.
aId = aId || (aVar.id + "->" + aProperty[0] + "-property");
let parent;
if (aFlags && !aFlags.enumerable) {
parent = aVar.childNodes[2];
}
else {
parent = aVar.childNodes[1];
}
// Contains generic nodes and functionality.
let element = this._createPropertyElement(aName, aId, "property", parent);
// Make sure the element was created successfully.
if (!element) {
return null;
}
element._identifier = aName;
/**
* @see DebuggerView.Properties._setGrip
*/
element.setGrip = this._setGrip.bind(this, element);
/**
* @see DebuggerView.Properties._addProperties
*/
element.addProperties = this._addProperties.bind(this, element);
// Setup the additional elements specific for a variable node.
element.refresh(function(pKey, pGrip) {
let title = element.getElementsByClassName("title")[0];
let nameLabel = title.getElementsByClassName("name")[0];
let separatorLabel = document.createElement("label");
let valueLabel = document.createElement("label");
// Use attribute flags to specify the element type and tooltip text.
this._setAttributes(element, pKey, aFlags);
if ("undefined" !== typeof pKey) {
// Use a key element to specify the property name.
nameLabel.className = "key plain";
nameLabel.setAttribute("value", pKey.trim());
title.appendChild(nameLabel);
}
if ("undefined" !== typeof pGrip) {
// Separator between the variable name and its value.
separatorLabel.className = "plain";
separatorLabel.setAttribute("value", ":");
// Use a value element to specify the property value.
valueLabel.className = "value plain";
this._applyGrip(valueLabel, pGrip);
title.appendChild(separatorLabel);
title.appendChild(valueLabel);
}
// Handle the click event when pressing the element value label.
valueLabel.addEventListener("click", this._activateElementInputMode.bind({
scope: this,
element: element,
valueLabel: valueLabel
}));
// Maintain the symbolic name of the property.
Object.defineProperty(element, "token", {
value: aVar.token + "['" + pKey + "']",
writable: false,
enumerable: true,
configurable: true
});
// Remember a simple hierarchy between the parent and the element.
this._saveHierarchy({
parent: aVar,
element: element,
valueLabel: valueLabel
});
// Save the property to the variable for easier access.
Object.defineProperty(aVar, pKey, { value: element,
writable: false,
enumerable: true,
configurable: true });
}.bind(this), aProperty);
// Return the element for later use if necessary.
return element;
},
/**
* Makes an element's (variable or priperty) value editable.
* Make sure 'this' is bound to an object containing the properties:
* {
* "scope": the original scope to be used, probably DebuggerView.Properties,
* "element": the element whose value should be made editable,
* "valueLabel": the label displaying the value
* }
*
* @param event aEvent [optional]
* The event requesting this action.
*/
_activateElementInputMode: function DVP__activateElementInputMode(aEvent) {
if (aEvent) {
aEvent.stopPropagation();
}
let self = this.scope;
let element = this.element;
let valueLabel = this.valueLabel;
let titleNode = valueLabel.parentNode;
let initialValue = valueLabel.getAttribute("value");
// When editing an object we need to collapse it first, in order to avoid
// displaying an inconsistent state while the user is editing.
element._previouslyExpanded = element.expanded;
element._preventExpand = true;
element.collapse();
element.forceHideArrow();
// Create a texbox input element which will be shown in the current
// element's value location.
let textbox = document.createElement("textbox");
textbox.setAttribute("value", initialValue);
textbox.className = "element-input";
textbox.width = valueLabel.clientWidth + 1;
// Save the new value when the texbox looses focus or ENTER is pressed.
function DVP_element_textbox_blur(aTextboxEvent) {
DVP_element_textbox_save();
}
function DVP_element_textbox_keyup(aTextboxEvent) {
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_LEFT ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_RIGHT ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_UP ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_DOWN) {
return;
}
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_RETURN ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_ENTER) {
DVP_element_textbox_save();
return;
}
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_ESCAPE) {
valueLabel.setAttribute("value", initialValue);
DVP_element_textbox_clear();
return;
}
}
// The actual save mechanism for the new variable/property value.
function DVP_element_textbox_save() {
if (textbox.value !== valueLabel.getAttribute("value")) {
valueLabel.setAttribute("value", textbox.value);
let expr = "(" + element.token + "=" + textbox.value + ")";
DebuggerController.StackFrames.evaluate(expr);
}
DVP_element_textbox_clear();
}
// Removes the event listeners and appends the value node again.
function DVP_element_textbox_clear() {
element._preventExpand = false;
if (element._previouslyExpanded) {
element._previouslyExpanded = false;
element.expand();
}
element.showArrow();
textbox.removeEventListener("blur", DVP_element_textbox_blur, false);
textbox.removeEventListener("keyup", DVP_element_textbox_keyup, false);
titleNode.removeChild(textbox);
titleNode.appendChild(valueLabel);
}
textbox.addEventListener("blur", DVP_element_textbox_blur, false);
textbox.addEventListener("keyup", DVP_element_textbox_keyup, false);
titleNode.removeChild(valueLabel);
titleNode.appendChild(textbox);
textbox.select();
// When the value is a string (displayed as "value"), then we probably want
// to change it to another string in the textbox, so to avoid typing the ""
// again, tackle with the selection bounds just a bit.
if (valueLabel.getAttribute("value").match(/^"[^"]*"$/)) {
textbox.selectionEnd--;
textbox.selectionStart++;
}
},
/**
* Returns a custom formatted property string for a type and a value.
*
* @param string | object aGrip
* The variable grip.
* @return string
* The formatted property string.
*/
_propertyString: function DVP__propertyString(aGrip) {
if (aGrip && "object" === typeof aGrip) {
switch (aGrip.type) {
case "undefined":
return "undefined";
case "null":
return "null";
default:
return "[" + aGrip.type + " " + aGrip.class + "]";
}
} else {
switch (typeof aGrip) {
case "string":
return "\"" + aGrip + "\"";
case "boolean":
return aGrip ? "true" : "false";
default:
return aGrip + "";
}
}
return aGrip + "";
},
/**
* Returns a custom class style for a type and a value.
*
* @param string | object aGrip
* The variable grip.
*
* @return string
* The css class style.
*/
_propertyColor: function DVP__propertyColor(aGrip) {
if (aGrip && "object" === typeof aGrip) {
switch (aGrip.type) {
case "undefined":
return "token-undefined";
case "null":
return "token-null";
}
} else {
switch (typeof aGrip) {
case "string":
return "token-string";
case "boolean":
return "token-boolean";
case "number":
return "token-number";
}
}
return "token-other";
},
/**
* Creates an element which contains generic nodes and functionality used by
* any scope, variable or property added to the tree.
* If the variable or property already exists, null is returned.
* Otherwise, the newly created element is returned.
*
* @param string aName
* A generic name used in a title strip.
* @param string aId
* id used by the created element node.
* @param string aClass
* Recommended style class used by the created element node.
* @param object aParent
* The parent node which will contain the element.
* @return object
* The newly created html node representing the generic elem.
*/
_createPropertyElement: function DVP__createPropertyElement(aName, aId, aClass, aParent) {
// Make sure we don't duplicate anything and the parent exists.
if (document.getElementById(aId)) {
return null;
}
if (!aParent) {
return null;
}
let element = document.createElement("vbox");
let arrow = document.createElement("box");
let name = document.createElement("label");
let title = document.createElement("box");
let details = document.createElement("vbox");
let nonEnum = document.createElement("vbox");
// Create a scope node to contain all the elements.
element.id = aId;
element.className = aClass;
// The expand/collapse arrow.
arrow.className = "arrow";
arrow.style.visibility = "hidden";
// The name element.
name.className = "name plain";
name.setAttribute("value", aName || "");
// The title element, containing the arrow and the name.
title.className = "title";
title.setAttribute("align", "center")
// The node element which will contain any added scope variables.
details.className = "details";
nonEnum.className = "details nonenum";
// Add the click event handler for the title, or arrow and name.
if (aClass === "scope") {
title.addEventListener("click", function() element.toggle(), false);
} else {
arrow.addEventListener("click", function() element.toggle(), false);
name.addEventListener("click", function() element.toggle(), false);
name.addEventListener("mouseover", function() element.updateTooltip(name), false);
}
title.appendChild(arrow);
title.appendChild(name);
element.appendChild(title);
element.appendChild(details);
element.appendChild(nonEnum);
aParent.appendChild(element);
/**
* Shows the element, setting the display style to "block".
* @return object
* The same element.
*/
element.show = function DVP_element_show() {
element.style.display = "-moz-box";
if ("function" === typeof element.onshow) {
element.onshow(element);
}
return element;
};
/**
* Hides the element, setting the display style to "none".
* @return object
* The same element.
*/
element.hide = function DVP_element_hide() {
element.style.display = "none";
if ("function" === typeof element.onhide) {
element.onhide(element);
}
return element;
};
/**
* Expands the element, showing all the added details.
*
* @param boolean aSkipAnimationFlag
* Pass true to not show an opening animation.
* @return object
* The same element.
*/
element.expand = function DVP_element_expand(aSkipAnimationFlag) {
if (element._preventExpand) {
return;
}
arrow.setAttribute("open", "");
details.setAttribute("open", "");
if (Prefs.nonEnumVisible) {
nonEnum.setAttribute("open", "");
}
if (!aSkipAnimationFlag) {
details.setAttribute("animated", "");
nonEnum.setAttribute("animated", "");
}
if ("function" === typeof element.onexpand) {
element.onexpand(element);
}
return element;
};
/**
* Collapses the element, hiding all the added details.
* @return object
* The same element.
*/
element.collapse = function DVP_element_collapse() {
if (element._preventCollapse) {
return;
}
arrow.removeAttribute("open");
details.removeAttribute("open");
details.removeAttribute("animated");
nonEnum.removeAttribute("open");
nonEnum.removeAttribute("animated");
if ("function" === typeof element.oncollapse) {
element.oncollapse(element);
}
return element;
};
/**
* Toggles between the element collapse/expand state.
* @return object
* The same element.
*/
element.toggle = function DVP_element_toggle() {
element.expanded = !element.expanded;
if ("function" === typeof element.ontoggle) {
element.ontoggle(element);
}
return element;
};
/**
* Shows the element expand/collapse arrow (only if necessary!).
* @return object
* The same element.
*/
element.showArrow = function DVP_element_showArrow() {
let len = details.childNodes.length + nonEnum.childNodes.length;
if (element._forceShowArrow || len) {
arrow.style.visibility = "visible";
}
return element;
};
/**
* Hides the element expand/collapse arrow.
* @return object
* The same element.
*/
element.hideArrow = function DVP_element_hideArrow() {
if (!element._forceShowArrow) {
arrow.style.visibility = "hidden";
}
return element;
};
/**
* Forces the element expand/collapse arrow to be visible, even if there
* are no child elements.
*
* @return object
* The same element.
*/
element.forceShowArrow = function DVP_element_forceShowArrow() {
element._forceShowArrow = true;
arrow.style.visibility = "visible";
return element;
};
/**
* Forces the element expand/collapse arrow to be hidden, even if there
* are some child elements.
*
* @return object
* The same element.
*/
element.forceHideArrow = function DVP_element_forceHideArrow() {
arrow.style.visibility = "hidden";
return element;
};
/**
* Returns if the element is visible.
* @return boolean
* True if the element is visible.
*/
Object.defineProperty(element, "visible", {
get: function DVP_element_getVisible() {
return element.style.display !== "none";
},
set: function DVP_element_setVisible(value) {
if (value) {
element.show();
} else {
element.hide();
}
}
});
/**
* Returns if the element is expanded.
* @return boolean
* True if the element is expanded.
*/
Object.defineProperty(element, "expanded", {
get: function DVP_element_getExpanded() {
return arrow.hasAttribute("open");
},
set: function DVP_element_setExpanded(value) {
if (value) {
element.expand();
} else {
element.collapse();
}
}
});
/**
* Removes all added children in the details container tree.
* @return object
* The same element.
*/
element.empty = function DVP_element_empty() {
// This details node won't have any elements, so hide the arrow.
arrow.style.visibility = "hidden";
while (details.firstChild) {
details.removeChild(details.firstChild);
}
while (nonEnum.firstChild) {
nonEnum.removeChild(nonEnum.firstChild);
}
if ("function" === typeof element.onempty) {
element.onempty(element);
}
return element;
};
/**
* Removes the element from the parent node details container tree.
* @return object
* The same element.
*/
element.remove = function DVP_element_remove() {
element.parentNode.removeChild(element);
if ("function" === typeof element.onremove) {
element.onremove(element);
}
return element;
};
/**
* Returns if the element expander (arrow) is visible.
* @return boolean
* True if the arrow is visible.
*/
Object.defineProperty(element, "arrowVisible", {
get: function DVP_element_getArrowVisible() {
return arrow.style.visibility !== "hidden";
},
set: function DVP_element_setExpanded(value) {
if (value) {
element.showArrow();
} else {
element.hideArrow();
}
}
});
/**
* Creates a tooltip for the element displaying certain attributes.
*
* @param object aAnchor
* The element which will anchor the tooltip.
*/
element.updateTooltip = function DVP_element_updateTooltip(aAnchor) {
let tooltip = document.getElementById("element-tooltip");
if (tooltip) {
document.documentElement.removeChild(tooltip);
}
tooltip = document.createElement("tooltip");
tooltip.id = "element-tooltip";
let configurableLabel = document.createElement("label");
configurableLabel.id = "configurableLabel";
configurableLabel.setAttribute("value", "configurable");
let enumerableLabel = document.createElement("label");
enumerableLabel.id = "enumerableLabel";
enumerableLabel.setAttribute("value", "enumerable");
let writableLabel = document.createElement("label");
writableLabel.id = "writableLabel";
writableLabel.setAttribute("value", "writable");
tooltip.setAttribute("orient", "horizontal")
tooltip.appendChild(configurableLabel);
tooltip.appendChild(enumerableLabel);
tooltip.appendChild(writableLabel);
if (element.hasAttribute("non-configurable")) {
configurableLabel.setAttribute("non-configurable", "");
}
if (element.hasAttribute("non-enumerable")) {
enumerableLabel.setAttribute("non-enumerable", "");
}
if (element.hasAttribute("non-writable")) {
writableLabel.setAttribute("non-writable", "");
}
document.documentElement.appendChild(tooltip);
aAnchor.setAttribute("tooltip", tooltip.id);
};
/**
* Generic function refreshing the internal state of the element when
* it's modified (e.g. a child detail, variable, property is added).
*
* @param function aFunction
* The function logic used to modify the internal state.
* @param array aArguments
* Optional arguments array to be applied to aFunction.
*/
element.refresh = function DVP_element_refresh(aFunction, aArguments) {
if ("function" === typeof aFunction) {
aFunction.apply(this, aArguments);
}
let node = aParent.parentNode;
let arrow = node.getElementsByClassName("arrow")[0];
let children = node.querySelectorAll(".details > vbox").length;
// If the parent details node has at least one element, set the
// expand/collapse arrow visible.
if (children) {
arrow.style.visibility = "visible";
} else {
arrow.style.visibility = "hidden";
}
}.bind(this);
// Return the element for later use and customization.
return element;
},
/**
* Remember a simple hierarchy of parent->element->children.
*
* @param object aProperties
* Container for the parent, element and the associated value node.
*/
_saveHierarchy: function DVP__saveHierarchy(aProperties) {
let parent = aProperties.parent;
let element = aProperties.element;
let valueLabel = aProperties.valueLabel;
let store = aProperties.store || parent._children;
// Make sure we have a valid element and a children storage object.
if (!element || !store) {
return;
}
let relation = {
root: parent ? (parent._root || parent) : null,
parent: parent || null,
element: element,
valueLabel: valueLabel,
children: {}
};
store[element._identifier] = relation;
element._root = relation.root;
element._children = relation.children;
},
/**
* Creates an object to store a hierarchy of scopes, variables and properties
* and saves the previous store.
*/
createHierarchyStore: function DVP_createHierarchyStore() {
this._prevHierarchy = this._currHierarchy;
this._currHierarchy = {};
},
/**
* Creates a hierarchy holder for a scope.
*
* @param object aScope
* The designated scope to track.
*/
addScopeToHierarchy: function DVP_addScopeToHierarchy(aScope) {
this._saveHierarchy({ element: aScope, store: this._currHierarchy });
},
/**
* Briefly flash the variables that changed between pauses.
*/
commitHierarchy: function DVS_commitHierarchy() {
for (let i in this._currHierarchy) {
let currScope = this._currHierarchy[i];
let prevScope = this._prevHierarchy[i];
if (!prevScope) {
continue;
}
for (let v in currScope.children) {
let currVar = currScope.children[v];
let prevVar = prevScope.children[v];
let action = "";
if (prevVar) {
let prevValue = prevVar.valueLabel.getAttribute("value");
let currValue = currVar.valueLabel.getAttribute("value");
if (currValue != prevValue) {
action = "changed";
} else {
action = "unchanged";
}
} else {
action = "added";
}
if (action) {
currVar.element.setAttribute(action, "");
window.setTimeout(function() {
currVar.element.removeAttribute(action);
}, PROPERTY_VIEW_FLASH_DURATION);
}
}
}
},
/**
* A simple model representation of all the scopes, variables and properties,
* with parent-child relations.
*/
_currHierarchy: null,
_prevHierarchy: null,
/**
* The cached variable properties container.
*/
_vars: null,
_onShowNonEnums: function DVP__onShowNonEnums() {
let option = document.getElementById("show-nonenum");
Prefs.nonEnumVisible = option.checked;
let els = document.getElementsByClassName("nonenum").iterator();
for (let el of els) {
if (el.parentNode.expanded) {
if (Prefs.nonEnumVisible) {
el.setAttribute("open", "");
} else {
el.removeAttribute("open");
el.removeAttribute("animated");
}
}
}
},
/**
* Initialization function, called when the debugger is initialized.
*/
initialize: function DVP_initialize() {
let showNonEnums = document.getElementById("show-nonenum");
showNonEnums.addEventListener("click", this._onShowNonEnums, false);
showNonEnums.checked = Prefs.nonEnumVisible;
this._vars = DebuggerView._variables;
this.emptyText();
this.createHierarchyStore();
},
/**
* Destruction function, called when the debugger is shut down.
*/
destroy: function DVP_destroy() {
this.empty();
this._currHierarchy = null;
this._prevHierarchy = null;
this._vars = null;
}
};
/**
* Preliminary setup for the DebuggerView object.
*/
DebuggerView.GlobalSearch = new GlobalSearchView();
DebuggerView.Scripts = new ScriptsView();
DebuggerView.StackFrames = new StackFramesView();
DebuggerView.Breakpoints = new BreakpointsView();
DebuggerView.Properties = new PropertiesView();
/**
* Export the source editor to the global scope for easier access in tests.
*/
Object.defineProperty(window, "editor", {
get: function() { return DebuggerView.editor; }
});