зеркало из https://github.com/mozilla/pjs.git
1314 строки
38 KiB
JavaScript
1314 строки
38 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: */
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is mozilla.org code.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla Foundation
|
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Victor Porof <vporof@mozilla.com> (original author)
|
|
* Mihai Sucan <mihai.sucan@gmail.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
"use strict";
|
|
|
|
/**
|
|
* Object mediating visual changes and event listeners between the debugger and
|
|
* the html view.
|
|
*/
|
|
let DebuggerView = {
|
|
|
|
/**
|
|
* An instance of SourceEditor.
|
|
*/
|
|
editor: null,
|
|
|
|
/**
|
|
* Initializes the SourceEditor instance.
|
|
*/
|
|
initializeEditor: function DV_initializeEditor() {
|
|
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, this._onEditorLoad.bind(this));
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Functions handling the scripts UI.
|
|
*/
|
|
function ScriptsView() {
|
|
this._onScriptsChange = this._onScriptsChange.bind(this);
|
|
}
|
|
|
|
ScriptsView.prototype = {
|
|
|
|
/**
|
|
* Removes all elements from the scripts container, leaving it empty.
|
|
*/
|
|
empty: function DVS_empty() {
|
|
while (this._scripts.firstChild) {
|
|
this._scripts.removeChild(this._scripts.firstChild);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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._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._scripts.getElementsByAttribute("label", aLabel).length > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* Adds a script to the scripts container.
|
|
* If the script already exists (was previously added), null is returned.
|
|
* Otherwise, the newly created element is returned.
|
|
*
|
|
* @param string aLabel
|
|
* The simplified script location to be shown.
|
|
* @param string aScript
|
|
* The source script.
|
|
* @return object
|
|
* The newly created html node representing the added script.
|
|
*/
|
|
addScript: function DVS_addScript(aLabel, aScript) {
|
|
// Make sure we don't duplicate anything.
|
|
if (this.containsLabel(aLabel)) {
|
|
return null;
|
|
}
|
|
|
|
let script = this._scripts.appendItem(aLabel, aScript.url);
|
|
script.setAttribute("tooltiptext", aScript.url);
|
|
script.setUserData("sourceScript", aScript, null);
|
|
|
|
this._scripts.selectedItem = script;
|
|
return script;
|
|
},
|
|
|
|
/**
|
|
* The cached click listener for the scripts container.
|
|
*/
|
|
_onScriptsChange: function DVS__onScriptsChange() {
|
|
let script = this._scripts.selectedItem.getUserData("sourceScript");
|
|
DebuggerController.SourceScripts.showScript(script);
|
|
},
|
|
|
|
/**
|
|
* The cached scripts container.
|
|
*/
|
|
_scripts: null,
|
|
|
|
/**
|
|
* Initialization function, called when the debugger is initialized.
|
|
*/
|
|
initialize: function DVS_initialize() {
|
|
this._scripts = document.getElementById("scripts");
|
|
this._scripts.addEventListener("select", this._onScriptsChange, false);
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the debugger is shut down.
|
|
*/
|
|
destroy: function DVS_destroy() {
|
|
this._scripts.removeEventListener("select", this._onScriptsChange, false);
|
|
this._scripts = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Functions handling the html stackframes UI.
|
|
*/
|
|
function StackFramesView() {
|
|
this._onFramesScroll = this._onFramesScroll.bind(this);
|
|
this._onCloseButtonClick = this._onCloseButtonClick.bind(this);
|
|
this._onResumeButtonClick = this._onResumeButtonClick.bind(this);
|
|
this._onStepOverClick = this._onStepOverClick.bind(this);
|
|
this._onStepInClick = this._onStepInClick.bind(this);
|
|
this._onStepOutClick = this._onStepOutClick.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");
|
|
let status = document.getElementById("status");
|
|
|
|
// If we're paused, show a pause label and a resume label on the button.
|
|
if (aState == "paused") {
|
|
status.textContent = L10N.getStr("pausedState");
|
|
resume.label = L10N.getStr("resumeLabel");
|
|
}
|
|
// If we're attached, do the opposite.
|
|
else if (aState == "attached") {
|
|
status.textContent = L10N.getStr("runningState");
|
|
resume.label = L10N.getStr("pauseLabel");
|
|
}
|
|
// No valid state parameter.
|
|
else {
|
|
status.textContent = "";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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("div");
|
|
|
|
// The empty node should look grayed out to avoid confusion.
|
|
item.className = "empty list-item";
|
|
item.appendChild(document.createTextNode(L10N.getStr("emptyText")));
|
|
|
|
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("div");
|
|
let frameName = document.createElement("span");
|
|
let frameDetails = document.createElement("span");
|
|
|
|
// 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";
|
|
frameDetails.className = "dbg-stackframe-details";
|
|
frameName.appendChild(document.createTextNode(aFrameNameText));
|
|
frameDetails.appendChild(document.createTextNode(aFrameDetailsText));
|
|
|
|
frame.appendChild(frameName);
|
|
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/resume button click event.
|
|
*/
|
|
_onResumeButtonClick: function DVF__onResumeButtonClick() {
|
|
if (DebuggerController.activeThread.paused) {
|
|
DebuggerController.activeThread.resume();
|
|
} else {
|
|
DebuggerController.activeThread.interrupt();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Listener handling the step over button click event.
|
|
*/
|
|
_onStepOverClick: function DVF__onStepOverClick() {
|
|
DebuggerController.activeThread.stepOver();
|
|
},
|
|
|
|
/**
|
|
* Listener handling the step in button click event.
|
|
*/
|
|
_onStepInClick: function DVF__onStepInClick() {
|
|
DebuggerController.activeThread.stepIn();
|
|
},
|
|
|
|
/**
|
|
* Listener handling the step out button click event.
|
|
*/
|
|
_onStepOutClick: function DVF__onStepOutClick() {
|
|
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 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);
|
|
resume.addEventListener("click", this._onResumeButtonClick, false);
|
|
stepOver.addEventListener("click", this._onStepOverClick, false);
|
|
stepIn.addEventListener("click", this._onStepInClick, false);
|
|
stepOut.addEventListener("click", this._onStepOutClick, false);
|
|
frames.addEventListener("click", this._onFramesClick, false);
|
|
frames.addEventListener("scroll", this._onFramesScroll, false);
|
|
window.addEventListener("resize", this._onFramesScroll, false);
|
|
|
|
this._frames = frames;
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the debugger is shut down.
|
|
*/
|
|
destroy: function DVF_destroy() {
|
|
let close = document.getElementById("close");
|
|
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);
|
|
resume.removeEventListener("click", this._onResumeButtonClick, false);
|
|
stepOver.removeEventListener("click", this._onStepOverClick, false);
|
|
stepIn.removeEventListener("click", this._onStepInClick, false);
|
|
stepOut.removeEventListener("click", this._onStepOutClick, false);
|
|
frames.removeEventListener("click", this._onFramesClick, false);
|
|
frames.removeEventListener("scroll", this._onFramesScroll, false);
|
|
window.removeEventListener("resize", this._onFramesScroll, false);
|
|
|
|
this._frames = 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 = {
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
// Compute the id of the element if not specified.
|
|
aId = aId || (aName.toLowerCase().trim().replace(" ", "-") + "-scope");
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* @see DebuggerView.Properties._addVar
|
|
*/
|
|
element.addVar = this._addVar.bind(this, element);
|
|
|
|
// Return the element for later use if necessary.
|
|
return element;
|
|
},
|
|
|
|
/**
|
|
* 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 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, 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.querySelector(".details"));
|
|
|
|
// Make sure the element was created successfully.
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @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 separator = document.createElement("span");
|
|
let info = document.createElement("span");
|
|
let title = element.querySelector(".title");
|
|
let arrow = element.querySelector(".arrow");
|
|
|
|
// Separator shouldn't be selectable.
|
|
separator.className = "unselectable";
|
|
separator.appendChild(document.createTextNode(": "));
|
|
|
|
// The variable information (type, class and/or value).
|
|
info.className = "info";
|
|
|
|
title.appendChild(separator);
|
|
title.appendChild(info);
|
|
|
|
}.bind(this));
|
|
|
|
// Return the element for later use if necessary.
|
|
return element;
|
|
},
|
|
|
|
/**
|
|
* 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 info = aVar.querySelector(".info") || aVar.target.info;
|
|
|
|
// Make sure the info node exists.
|
|
if (!info) {
|
|
return null;
|
|
}
|
|
|
|
info.textContent = this._propertyString(aGrip);
|
|
info.classList.add(this._propertyColor(aGrip));
|
|
|
|
return aVar;
|
|
},
|
|
|
|
/**
|
|
* 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]);
|
|
}
|
|
if (getter !== undefined || setter !== undefined) {
|
|
let prop = this._addProperty(aVar, [i]).expand();
|
|
prop.getter = this._addProperty(prop, ["get", getter]);
|
|
prop.setter = this._addProperty(prop, ["set", setter]);
|
|
}
|
|
}
|
|
}
|
|
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 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, 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.querySelector(".details"));
|
|
|
|
// Make sure the element was created successfully.
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @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 propertyString = this._propertyString(pGrip);
|
|
let propertyColor = this._propertyColor(pGrip);
|
|
let key = document.createElement("div");
|
|
let value = document.createElement("div");
|
|
let separator = document.createElement("span");
|
|
let title = element.querySelector(".title");
|
|
let arrow = element.querySelector(".arrow");
|
|
|
|
// Use a key element to specify the property name.
|
|
key.className = "key";
|
|
key.appendChild(document.createTextNode(pKey));
|
|
|
|
// Use a value element to specify the property value.
|
|
value.className = "value";
|
|
value.appendChild(document.createTextNode(propertyString));
|
|
value.classList.add(propertyColor);
|
|
|
|
// Separator shouldn't be selected.
|
|
separator.className = "unselectable";
|
|
separator.appendChild(document.createTextNode(": "));
|
|
|
|
if ("undefined" !== typeof pKey) {
|
|
title.appendChild(key);
|
|
}
|
|
if ("undefined" !== typeof pGrip) {
|
|
title.appendChild(separator);
|
|
title.appendChild(value);
|
|
}
|
|
|
|
// Make the property also behave as a variable, to allow
|
|
// recursively adding properties to properties.
|
|
element.target = {
|
|
info: value
|
|
};
|
|
|
|
// 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;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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("div");
|
|
let arrow = document.createElement("span");
|
|
let name = document.createElement("span");
|
|
let title = document.createElement("div");
|
|
let details = document.createElement("div");
|
|
|
|
// 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 unselectable";
|
|
name.appendChild(document.createTextNode(aName || ""));
|
|
|
|
// The title element, containing the arrow and the name.
|
|
title.className = "title";
|
|
title.addEventListener("click", function() { element.toggle(); }, true);
|
|
|
|
// The node element which will contain any added scope variables.
|
|
details.className = "details";
|
|
|
|
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.
|
|
* @return object
|
|
* The same element.
|
|
*/
|
|
element.expand = function DVP_element_expand() {
|
|
arrow.setAttribute("open", "");
|
|
details.setAttribute("open", "");
|
|
|
|
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() {
|
|
arrow.removeAttribute("open");
|
|
details.removeAttribute("open");
|
|
|
|
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 (details.childNodes.length) {
|
|
arrow.style.visibility = "visible";
|
|
}
|
|
return element;
|
|
};
|
|
|
|
/**
|
|
* Forces the element expand/collapse arrow to be visible, even if there
|
|
* are no child elements.
|
|
*
|
|
* @param boolean aPreventHideFlag
|
|
* Prevents the arrow to be hidden when requested.
|
|
* @return object
|
|
* The same element.
|
|
*/
|
|
element.forceShowArrow = function DVP_element_forceShowArrow(aPreventHideFlag) {
|
|
element._preventHide = aPreventHideFlag;
|
|
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._preventHide) {
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* 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.querySelector(".arrow");
|
|
let children = node.querySelector(".details").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;
|
|
},
|
|
|
|
/**
|
|
* Returns the global scope container.
|
|
*/
|
|
get globalScope() {
|
|
return this._globalScope;
|
|
},
|
|
|
|
/**
|
|
* Sets the display mode for the global scope container.
|
|
*
|
|
* @param boolean aFlag
|
|
* False to hide the container, true to show.
|
|
*/
|
|
set globalScope(aFlag) {
|
|
if (aFlag) {
|
|
this._globalScope.show();
|
|
} else {
|
|
this._globalScope.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the local scope container.
|
|
*/
|
|
get localScope() {
|
|
return this._localScope;
|
|
},
|
|
|
|
/**
|
|
* Sets the display mode for the local scope container.
|
|
*
|
|
* @param boolean aFlag
|
|
* False to hide the container, true to show.
|
|
*/
|
|
set localScope(aFlag) {
|
|
if (aFlag) {
|
|
this._localScope.show();
|
|
} else {
|
|
this._localScope.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the with block scope container.
|
|
*/
|
|
get withScope() {
|
|
return this._withScope;
|
|
},
|
|
|
|
/**
|
|
* Sets the display mode for the with block scope container.
|
|
*
|
|
* @param boolean aFlag
|
|
* False to hide the container, true to show.
|
|
*/
|
|
set withScope(aFlag) {
|
|
if (aFlag) {
|
|
this._withScope.show();
|
|
} else {
|
|
this._withScope.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the closure scope container.
|
|
*/
|
|
get closureScope() {
|
|
return this._closureScope;
|
|
},
|
|
|
|
/**
|
|
* Sets the display mode for the with block scope container.
|
|
*
|
|
* @param boolean aFlag
|
|
* False to hide the container, true to show.
|
|
*/
|
|
set closureScope(aFlag) {
|
|
if (aFlag) {
|
|
this._closureScope.show();
|
|
} else {
|
|
this._closureScope.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The cached variable properties container.
|
|
*/
|
|
_vars: null,
|
|
|
|
/**
|
|
* Auto-created global, local, with block and closure scopes containing vars.
|
|
*/
|
|
_globalScope: null,
|
|
_localScope: null,
|
|
_withScope: null,
|
|
_closureScope: null,
|
|
|
|
/**
|
|
* Initialization function, called when the debugger is initialized.
|
|
*/
|
|
initialize: function DVP_initialize() {
|
|
this._vars = document.getElementById("variables");
|
|
this._localScope = this._addScope(L10N.getStr("localScope")).expand();
|
|
this._withScope = this._addScope(L10N.getStr("withScope")).hide();
|
|
this._closureScope = this._addScope(L10N.getStr("closureScope")).hide();
|
|
this._globalScope = this._addScope(L10N.getStr("globalScope"));
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the debugger is shut down.
|
|
*/
|
|
destroy: function DVP_destroy() {
|
|
this._vars = null;
|
|
this._globalScope = null;
|
|
this._localScope = null;
|
|
this._withScope = null;
|
|
this._closureScope = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Preliminary setup for the DebuggerView object.
|
|
*/
|
|
DebuggerView.Scripts = new ScriptsView();
|
|
DebuggerView.StackFrames = new StackFramesView();
|
|
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; }
|
|
});
|