mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
2575 lines
77 KiB
JavaScript
2575 lines
77 KiB
JavaScript
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
const PROPERTY_VIEW_FLASH_DURATION = 400; // ms
|
|
const BREAKPOINT_LINE_TOOLTIP_MAX_SIZE = 1000;
|
|
|
|
/**
|
|
* Object mediating visual changes and event listeners between the debugger and
|
|
* the html view.
|
|
*/
|
|
let DebuggerView = {
|
|
|
|
/**
|
|
* An instance of SourceEditor.
|
|
*/
|
|
editor: null,
|
|
|
|
/**
|
|
* Initializes UI properties for all the displayed panes.
|
|
*/
|
|
initializePanes: function DV_initializePanes() {
|
|
let stackframes = document.getElementById("stackframes+breakpoints");
|
|
stackframes.setAttribute("width", Prefs.stackframesWidth);
|
|
|
|
let variables = document.getElementById("variables");
|
|
variables.setAttribute("width", Prefs.variablesWidth);
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
let stackframes = document.getElementById("stackframes+breakpoints");
|
|
Prefs.stackframesWidth = stackframes.getAttribute("width");
|
|
|
|
let variables = document.getElementById("variables");
|
|
Prefs.variablesWidth = variables.getAttribute("width");
|
|
|
|
let bkps = document.getElementById("breakpoints");
|
|
let frames = document.getElementById("stackframes");
|
|
bkps.parentNode.removeChild(bkps);
|
|
frames.parentNode.removeChild(frames);
|
|
|
|
stackframes.parentNode.removeChild(stackframes);
|
|
variables.parentNode.removeChild(variables);
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
},
|
|
|
|
/**
|
|
* Sets the close button hidden or visible. It's hidden by default.
|
|
* @param boolean aVisibleFlag
|
|
*/
|
|
showCloseButton: function DV_showCloseButton(aVisibleFlag) {
|
|
document.getElementById("close").setAttribute("hidden", !aVisibleFlag);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 scripts UI.
|
|
*/
|
|
function ScriptsView() {
|
|
this._onScriptsChange = this._onScriptsChange.bind(this);
|
|
this._onScriptsSearch = this._onScriptsSearch.bind(this);
|
|
this._onScriptsKeyUp = this._onScriptsKeyUp.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,
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
_getSearchboxInfo: function DVS__getSearchboxInfo() {
|
|
let rawValue = this._searchbox.value.toLowerCase();
|
|
|
|
let rawLength = rawValue.length;
|
|
let lastColon = rawValue.lastIndexOf(":");
|
|
let lastAt = rawValue.lastIndexOf("#");
|
|
|
|
let fileEnd = lastColon != -1 ? lastColon : lastAt != -1 ? lastAt : rawLength;
|
|
let lineEnd = lastAt != -1 ? lastAt : rawLength;
|
|
|
|
let file = rawValue.slice(0, fileEnd);
|
|
let line = window.parseInt(rawValue.slice(fileEnd + 1, lineEnd)) || -1;
|
|
let token = rawValue.slice(lineEnd + 1);
|
|
|
|
return [file, line, token];
|
|
},
|
|
|
|
/**
|
|
* 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"));
|
|
},
|
|
|
|
/**
|
|
* The search listener for the scripts search box.
|
|
*/
|
|
_onScriptsSearch: function DVS__onScriptsSearch(e) {
|
|
let editor = DebuggerView.editor;
|
|
let scripts = this._scripts;
|
|
let [file, line, token] = this._getSearchboxInfo();
|
|
|
|
// If the webpage has no scripts, searching is redundant.
|
|
if (!scripts.itemCount) {
|
|
return;
|
|
}
|
|
|
|
// 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 (!file) {
|
|
for (let i = 0, l = scripts.itemCount; i < l; i++) {
|
|
scripts.getItemAtIndex(i).hidden = false;
|
|
}
|
|
} else if (this._prevSearchedFile !== file) {
|
|
let found = false;
|
|
|
|
for (let i = 0, l = scripts.itemCount; i < l; i++) {
|
|
let item = scripts.getItemAtIndex(i);
|
|
let target = item.label.toLowerCase();
|
|
|
|
// Search is not case sensitive, and is tied to the label not the url.
|
|
if (target.match(file)) {
|
|
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;
|
|
}
|
|
}
|
|
if (!found) {
|
|
scripts.setAttribute("label", L10N.getStr("noMatchingScriptsText"));
|
|
scripts.removeAttribute("tooltiptext");
|
|
}
|
|
}
|
|
if (this._prevSearchedLine !== line && line > -1) {
|
|
editor.setCaretPosition(line - 1);
|
|
}
|
|
if (this._prevSearchedToken !== token && token.length > 0) {
|
|
let offset = editor.find(token, { ignoreCase: true });
|
|
if (offset > -1) {
|
|
editor.setSelection(offset, offset + token.length)
|
|
}
|
|
}
|
|
this._prevSearchedFile = file;
|
|
this._prevSearchedLine = line;
|
|
this._prevSearchedToken = token;
|
|
},
|
|
|
|
/**
|
|
* The keyup listener for the scripts search box.
|
|
*/
|
|
_onScriptsKeyUp: function DVS__onScriptsKeyUp(e) {
|
|
if (e.keyCode === e.DOM_VK_ESCAPE) {
|
|
DebuggerView.editor.focus();
|
|
return;
|
|
}
|
|
|
|
if (e.keyCode === e.DOM_VK_RETURN || e.keyCode === e.DOM_VK_ENTER) {
|
|
let token = this._getSearchboxInfo()[2];
|
|
if (!token.length) {
|
|
return;
|
|
}
|
|
|
|
let editor = DebuggerView.editor;
|
|
let offset = editor.findNext(true);
|
|
if (offset > -1) {
|
|
editor.setSelection(offset, offset + token.length)
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when the scripts filter key sequence was pressed.
|
|
*/
|
|
_onSearch: function DVS__onSearch() {
|
|
this._searchbox.focus();
|
|
this._searchbox.value = "";
|
|
},
|
|
|
|
/**
|
|
* Called when the scripts token filter key sequence was pressed.
|
|
*/
|
|
_onTokenSearch: function DVS__onTokenSearch() {
|
|
this._searchbox.focus();
|
|
this._searchbox.value = "#";
|
|
},
|
|
|
|
/**
|
|
* The cached scripts container and search box.
|
|
*/
|
|
_scripts: null,
|
|
_searchbox: null,
|
|
|
|
/**
|
|
* Initialization function, called when the debugger is initialized.
|
|
*/
|
|
initialize: function DVS_initialize() {
|
|
this._scripts = document.getElementById("scripts");
|
|
this._searchbox = document.getElementById("scripts-search");
|
|
this._scripts.addEventListener("select", this._onScriptsChange, false);
|
|
this._searchbox.addEventListener("select", this._onScriptsSearch, false);
|
|
this._searchbox.addEventListener("input", this._onScriptsSearch, false);
|
|
this._searchbox.addEventListener("keyup", this._onScriptsKeyUp, 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("select", this._onScriptsSearch, false);
|
|
this._searchbox.removeEventListener("input", this._onScriptsSearch, false);
|
|
this._searchbox.removeEventListener("keyup", this._onScriptsKeyUp, false);
|
|
|
|
this.empty();
|
|
this._scripts = null;
|
|
this._searchbox = 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 = document.getElementById("resume");
|
|
|
|
// If we're paused, show a pause label and a resume label on the button.
|
|
if (aState == "paused") {
|
|
resume.setAttribute("tooltiptext", L10N.getStr("resumeTooltip"));
|
|
resume.setAttribute("checked", true);
|
|
}
|
|
// If we're attached, do the opposite.
|
|
else if (aState == "attached") {
|
|
resume.setAttribute("tooltiptext", L10N.getStr("pauseTooltip"));
|
|
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;
|
|
}
|
|
|
|
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 = document.getElementById("resume");
|
|
let stepOver = document.getElementById("step-over");
|
|
let stepIn = document.getElementById("step-in");
|
|
let stepOut = document.getElementById("step-out");
|
|
let frames = document.getElementById("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 = document.getElementById("resume");
|
|
let stepOver = document.getElementById("step-over");
|
|
let stepIn = document.getElementById("step-in");
|
|
let stepOut = document.getElementById("step-out");
|
|
let frames = this._frames;
|
|
|
|
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 = document.getElementById("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");
|
|
|
|
// Contains generic nodes and functionality.
|
|
let element = this._createPropertyElement(aName, aId, "variable",
|
|
aScope.getElementsByClassName("details")[0]);
|
|
|
|
// 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");
|
|
|
|
// Contains generic nodes and functionality.
|
|
let element = this._createPropertyElement(aName, aId, "property",
|
|
aVar.getElementsByClassName("details")[0]);
|
|
|
|
// 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");
|
|
|
|
// 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";
|
|
|
|
// 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);
|
|
|
|
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 (!aSkipAnimationFlag) {
|
|
details.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");
|
|
|
|
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() {
|
|
if (element._forceShowArrow || details.childNodes.length) {
|
|
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);
|
|
}
|
|
|
|
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.getElementsByClassName("details")[0].childNodes.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,
|
|
|
|
/**
|
|
* Initialization function, called when the debugger is initialized.
|
|
*/
|
|
initialize: function DVP_initialize() {
|
|
this._vars = document.getElementById("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.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; }
|
|
});
|