tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 522997110c22d526113bccc3bd7db6b008be2907
parent b1fe33f12b9abe952bd9f4c48e6f3003be5690d9
Author: Lorenz A <me@lorenzackermann.xyz>
Date:   Mon, 22 Dec 2025 07:49:04 +0000

Bug 2004252 - [devtools] Turn devtools/client/storage/VariablesView.sys.mjs into an ES class. r=devtools-reviewers,nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D277024

Diffstat:
Mdevtools/client/storage/VariablesView.sys.mjs | 2550++++++++++++++++++++++++++++++++++++++++---------------------------------------
1 file changed, 1287 insertions(+), 1263 deletions(-)

diff --git a/devtools/client/storage/VariablesView.sys.mjs b/devtools/client/storage/VariablesView.sys.mjs @@ -20,7 +20,6 @@ const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); const { getSourceNames, } = require("resource://devtools/client/shared/source-utils.js"); -const { extend } = require("resource://devtools/shared/extend.js"); const { ViewHelpers, setNamedTimeout, @@ -49,48 +48,49 @@ XPCOMUtils.defineLazyServiceGetter( * A tree view for inspecting scopes, objects and properties. * Iterable via "for (let [id, scope] of instance) { }". * Requires the devtools common.css and debugger.css skin stylesheets. - * - * @param Node aParentNode - * The parent node to hold this view. - * @param object aFlags [optional] - * An object contaning initialization options for this view. - * e.g. { lazyEmpty: true, searchEnabled: true ... } */ -export function VariablesView(aParentNode, aFlags = {}) { - this._store = []; // Can't use a Map because Scope names needn't be unique. - this._itemsByElement = new WeakMap(); +export class VariablesView extends EventEmitter { + /** + * @param {Node} aParentNode + * The parent node to hold this view. + * @param {object} [aFlags={}] + * An object contaning initialization options for this view. + * e.g. { lazyEmpty: true, searchEnabled: true ... } + */ + constructor(aParentNode, aFlags = {}) { + super(); - // Note: The hierarchy is only used for an assertion in a test at the moment, - // to easily check the tree structure. - this._testOnlyHierarchy = new Map(); + this._store = []; // Can't use a Map because Scope names needn't be unique. + this._itemsByElement = new WeakMap(); - this._parent = aParentNode; - this._parent.classList.add("variables-view-container"); - this._parent.classList.add("theme-body"); - this._appendEmptyNotice(); + // Note: The hierarchy is only used for an assertion in a test at the moment, + // to easily check the tree structure. + this._testOnlyHierarchy = new Map(); - this._onSearchboxInput = this._onSearchboxInput.bind(this); - this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this); - this._onViewKeyDown = this._onViewKeyDown.bind(this); + this._parent = aParentNode; + this._parent.classList.add("variables-view-container"); + this._parent.classList.add("theme-body"); + this._appendEmptyNotice(); - // Create an internal scrollbox container. - this._list = this.document.createXULElement("scrollbox"); - this._list.setAttribute("orient", "vertical"); - this._list.addEventListener("keydown", this._onViewKeyDown); - this._parent.appendChild(this._list); + this._onSearchboxInput = this._onSearchboxInput.bind(this); + this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this); + this._onViewKeyDown = this._onViewKeyDown.bind(this); - for (const name in aFlags) { - this[name] = aFlags[name]; - } + // Create an internal scrollbox container. + this._list = this.document.createXULElement("scrollbox"); + this._list.setAttribute("orient", "vertical"); + this._list.addEventListener("keydown", this._onViewKeyDown); + this._parent.appendChild(this._list); - EventEmitter.decorate(this); -} + for (const name in aFlags) { + this[name] = aFlags[name]; + } + } -VariablesView.prototype = { /** * Helper setter for populating this container with a raw object. * - * @param object aObject + * @param {object} aObject * The raw object to display. You can only provide this object * if you want the variables view to work in sync mode. */ @@ -99,7 +99,7 @@ VariablesView.prototype = { this.addScope() .addItem(undefined, { enumerable: true }) .populate(aObject, { sorted: true }); - }, + } /** * Adds a scope to contain any inspected variables. @@ -107,11 +107,11 @@ VariablesView.prototype = { * This new scope will be considered the parent of any other scope * added afterwards. * - * @param string l10nId + * @param {string} l10nId * The scope localized string id. - * @param string aCustomClass + * @param {string} aCustomClass * An additional class name for the containing element. - * @return Scope + * @return {Scope} * The newly created Scope instance. */ addScope(l10nId = "", aCustomClass = "") { @@ -125,12 +125,12 @@ VariablesView.prototype = { scope.header = !!l10nId; return scope; - }, + } /** * Removes all items from this container. * - * @param number aTimeout [optional] + * @param {number} [aTimeout] * The number of milliseconds to delay the operation if * lazy emptying of this container is enabled. */ @@ -153,7 +153,7 @@ VariablesView.prototype = { this._list.replaceChildren(); this._appendEmptyNotice(); this._toggleSearchVisibility(false); - }, + } /** * Emptying this container and rebuilding it immediately afterwards would @@ -186,26 +186,26 @@ VariablesView.prototype = { this._toggleSearchVisibility(false); } }, aTimeout); - }, + } /** * The amount of time (in milliseconds) it takes to empty this view lazily. */ - lazyEmptyDelay: LAZY_EMPTY_DELAY, + lazyEmptyDelay = LAZY_EMPTY_DELAY; /** * Specifies if this view may be emptied lazily. * * @see VariablesView.prototype.empty */ - lazyEmpty: false, + lazyEmpty = false; /** * The number of elements in this container to jump when Page Up or Page Down * keys are pressed. If falsy, then the page size will be based on the * container height. */ - scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT, + scrollPageSize = SCROLL_PAGE_SIZE_DEFAULT; /** * Specifies the context menu attribute set on variables and properties. @@ -213,7 +213,7 @@ VariablesView.prototype = { * This flag is applied recursively onto each scope in this view and * affects only the child nodes when they're created. */ - contextMenuId: "", + contextMenuId = ""; /** * The separator label between the variables or properties name and value. @@ -221,13 +221,13 @@ VariablesView.prototype = { * This flag is applied recursively onto each scope in this view and * affects only the child nodes when they're created. */ - separatorStr: L10N.getStr("variablesSeparatorLabel"), + separatorStr = L10N.getStr("variablesSeparatorLabel"); /** * Specifies if enumerable properties and variables should be displayed. * These variables and properties are visible by default. * - * @param boolean aFlag + * @param {boolean} aFlag */ set enumVisible(aFlag) { this._enumVisible = aFlag; @@ -235,13 +235,13 @@ VariablesView.prototype = { for (const scope of this._store) { scope._enumVisible = aFlag; } - }, + } /** * Specifies if non-enumerable properties and variables should be displayed. * These variables and properties are visible by default. * - * @param boolean aFlag + * @param {boolean} aFlag */ set nonEnumVisible(aFlag) { this._nonEnumVisible = aFlag; @@ -249,13 +249,13 @@ VariablesView.prototype = { for (const scope of this._store) { scope._nonEnumVisible = aFlag; } - }, + } /** * Specifies if only enumerable properties and variables should be displayed. * Both types of these variables and properties are visible by default. * - * @param boolean aFlag + * @param {boolean} aFlag */ set onlyEnumVisible(aFlag) { if (aFlag) { @@ -265,25 +265,25 @@ VariablesView.prototype = { this.enumVisible = true; this.nonEnumVisible = true; } - }, + } /** * Sets if the variable and property searching is enabled. * - * @param boolean aFlag + * @param {boolean} aFlag */ set searchEnabled(aFlag) { aFlag ? this._enableSearch() : this._disableSearch(); - }, + } /** * Gets if the variable and property searching is enabled. * - * @return boolean + * @return {boolean} */ get searchEnabled() { return !!this._searchboxContainer; - }, + } /** * Enables variable and property searching in this view. @@ -316,7 +316,7 @@ VariablesView.prototype = { container.appendChild(searchbox); ownerNode.insertBefore(container, this._parent); - }, + } /** * Disables variable and property searching in this view. @@ -336,13 +336,13 @@ VariablesView.prototype = { this._searchboxContainer = null; this._searchboxNode = null; - }, + } /** * Sets the variables searchbox container hidden or visible. * It's hidden by default. * - * @param boolean aVisibleFlag + * @param {boolean} aVisibleFlag * Specifies the intended visibility. */ _toggleSearchVisibility(aVisibleFlag) { @@ -351,14 +351,14 @@ VariablesView.prototype = { return; } this._searchboxContainer.hidden = !aVisibleFlag; - }, + } /** * Listener handling the searchbox input event. */ _onSearchboxInput() { this.scheduleSearch(this._searchboxNode.value); - }, + } /** * Listener handling the searchbox keydown event. @@ -372,14 +372,14 @@ VariablesView.prototype = { this._searchboxNode.value = ""; this._onSearchboxInput(); } - }, + } /** * Schedules searching for variables or properties matching the query. * - * @param string aToken + * @param {string} aToken * The variable or property to search for. - * @param number aWait + * @param {number} aWait * The amount of milliseconds to wait until draining. */ scheduleSearch(aToken, aWait) { @@ -389,7 +389,7 @@ VariablesView.prototype = { // Allow requests to settle down first. setNamedTimeout("vview-search", delay, () => this._doSearch(aToken)); - }, + } /** * Performs a case insensitive search for variables or properties matching @@ -399,7 +399,7 @@ VariablesView.prototype = { * while the available variables and properties inside those scopes are * just unhidden. * - * @param string aToken + * @param {string} aToken * The variable or property to search for. */ _doSearch(aToken) { @@ -416,16 +416,16 @@ VariablesView.prototype = { break; } } - }, + } /** * Find the first item in the tree of visible items in this container that * matches the predicate. Searches in visual order (the order seen by the * user). Descends into each scope to check the scope and its children. * - * @param function aPredicate + * @param {Function} aPredicate * A function that returns true when a match is found. - * @return Scope | Variable | Property + * @return {Scope | Variable | Property} * The first visible scope, variable or property, or null if nothing * is found. */ @@ -437,7 +437,7 @@ VariablesView.prototype = { } } return null; - }, + } /** * Find the last item in the tree of visible items in this container that @@ -445,9 +445,9 @@ VariablesView.prototype = { * order seen by the user). Descends into each scope to check the scope and * its children. * - * @param function aPredicate + * @param {Function} aPredicate * A function that returns true when a match is found. - * @return Scope | Variable | Property + * @return {Scope | Variable | Property} * The last visible scope, variable or property, or null if nothing * is found. */ @@ -460,38 +460,38 @@ VariablesView.prototype = { } } return null; - }, + } /** * Gets the scope at the specified index. * - * @param number aIndex + * @param {number} aIndex * The scope's index. - * @return Scope + * @return {Scope} * The scope if found, undefined if not. */ getScopeAtIndex(aIndex) { return this._store[aIndex]; - }, + } /** * Recursively searches this container for the scope, variable or property * displayed by the specified node. * - * @param Node aNode + * @param {Node} aNode * The node to search for. * @return Scope | Variable | Property * The matched scope, variable or property, or null if nothing is found. */ getItemForNode(aNode) { return this._itemsByElement.get(aNode); - }, + } /** * Gets the scope owning a Variable or Property. * - * @param Variable | Property - * The variable or property to retrieven the owner scope for. + * @param {Variable | Property} aItem + * The variable or property to retrieve the owner scope for. * @return Scope * The owner scope. */ @@ -508,13 +508,13 @@ VariablesView.prototype = { return this.getOwnerScopeForVariableOrProperty(aItem.ownerView); } return null; - }, + } /** * Gets the parent scopes for a specified Variable or Property. * The returned list will not include the owner scope. * - * @param Variable | Property + * @param {Variable | Property} aItem * The variable or property for which to find the parent scopes. * @return array * A list of parent Scopes. @@ -522,18 +522,18 @@ VariablesView.prototype = { getParentScopesForVariableOrProperty(aItem) { const scope = this.getOwnerScopeForVariableOrProperty(aItem); return this._store.slice(0, Math.max(this._store.indexOf(scope), 0)); - }, + } /** * Gets the currently focused scope, variable or property in this view. * - * @return Scope | Variable | Property + * @return {Scope | Variable | Property} * The focused scope, variable or property, or null if nothing is found. */ getFocusedItem() { const focused = this.document.commandDispatcher.focusedElement; return this.getItemForNode(focused); - }, + } /** * Focuses the first visible scope, variable, or property in this container. @@ -545,7 +545,7 @@ VariablesView.prototype = { } this._parent.scrollTop = 0; this._parent.scrollLeft = 0; - }, + } /** * Focuses the last visible scope, variable, or property in this container. @@ -559,27 +559,27 @@ VariablesView.prototype = { } this._parent.scrollTop = this._parent.scrollHeight; this._parent.scrollLeft = 0; - }, + } /** * Focuses the next scope, variable or property in this view. */ focusNextItem() { this.focusItemAtDelta(+1); - }, + } /** * Focuses the previous scope, variable or property in this view. */ focusPrevItem() { this.focusItemAtDelta(-1); - }, + } /** * Focuses another scope, variable or property in this view, based on * the index distance from the currently focused item. * - * @param number aDelta + * @param {number} aDelta * A scalar specifying by how many items should the selection change. */ focusItemAtDelta(aDelta) { @@ -590,14 +590,14 @@ VariablesView.prototype = { break; // Out of bounds. } } - }, + } /** * Focuses the next or previous scope, variable or property in this view. * - * @param string aDirection + * @param {string} aDirection * Either "advanceFocus" or "rewindFocus". - * @return boolean + * @return {boolean} * False if the focus went out of bounds and the first or last element * in this view was focused instead. */ @@ -619,16 +619,16 @@ VariablesView.prototype = { // Focus remained within bounds. return true; - }, + } /** * Focuses a scope, variable or property and makes sure it's visible. * - * @param aItem Scope | Variable | Property + * @param {Scope | Variable | Property} aItem * The item to focus. - * @param boolean aCollapseFlag + * @param {boolean} aCollapseFlag * True if the focused item should also be collapsed. - * @return boolean + * @return {boolean} * True if the item was successfully focused. */ _focusItem(aItem, aCollapseFlag) { @@ -641,7 +641,7 @@ VariablesView.prototype = { aItem._target.focus(); aItem._arrow.scrollIntoView({ block: "nearest" }); return true; - }, + } /** * Copy current selection to clipboard. @@ -651,7 +651,7 @@ VariablesView.prototype = { lazy.clipboardHelper.copyString( item._nameString + item.separatorStr + item._valueString ); - }, + } /** * Listener handling a key down event on the view. @@ -739,12 +739,12 @@ VariablesView.prototype = { case KeyCodes.DOM_VK_END: this.focusLastVisibleItem(); } - }, + } /** * Sets the text displayed in this container when there are no available items. * - * @param string aValue + * @param {string} aValue */ set emptyText(aValue) { if (this._emptyTextNode) { @@ -752,7 +752,7 @@ VariablesView.prototype = { } this._emptyTextValue = aValue; this._appendEmptyNotice(); - }, + } /** * Creates and appends a label signaling that this container is empty. @@ -768,7 +768,7 @@ VariablesView.prototype = { this._parent.appendChild(label); this._emptyTextNode = label; - }, + } /** * Removes the label signaling that this container is empty. @@ -780,21 +780,21 @@ VariablesView.prototype = { this._parent.removeChild(this._emptyTextNode); this._emptyTextNode = null; - }, + } /** * Gets if all values should be aligned together. * - * @return boolean + * @return {boolean} */ get alignedValues() { return this._alignedValues; - }, + } /** * Sets if all values should be aligned together. * - * @param boolean aFlag + * @param {boolean} aFlag */ set alignedValues(aFlag) { this._alignedValues = aFlag; @@ -803,23 +803,23 @@ VariablesView.prototype = { } else { this._parent.removeAttribute("aligned-values"); } - }, + } /** * Gets if action buttons (like delete) should be placed at the beginning or * end of a line. * - * @return boolean + * @return {boolean} */ get actionsFirst() { return this._actionsFirst; - }, + } /** * Sets if action buttons (like delete) should be placed at the beginning or * end of a line. * - * @param boolean aFlag + * @param {boolean} aFlag */ set actionsFirst(aFlag) { this._actionsFirst = aFlag; @@ -828,303 +828,1022 @@ VariablesView.prototype = { } else { this._parent.removeAttribute("actions-first"); } - }, + } /** * Gets the parent node holding this view. * - * @return Node + * @return {Node} */ get parentNode() { return this._parent; - }, + } /** * Gets the owner document holding this view. * - * @return HTMLDocument + * @return {HTMLDocument} */ get document() { return this._document || (this._document = this._parent.ownerDocument); - }, + } /** * Gets the default window holding this view. * - * @return nsIDOMWindow + * @return {Window} */ get window() { return this._window || (this._window = this.document.defaultView); - }, - - _document: null, - _window: null, - - _store: null, - _itemsByElement: null, - _testOnlyHierarchy: null, - - _enumVisible: true, - _nonEnumVisible: true, - _alignedValues: false, - _actionsFirst: false, - - _parent: null, - _list: null, - _searchboxNode: null, - _searchboxContainer: null, - _emptyTextNode: null, - _emptyTextValue: "", -}; - -VariablesView.NON_SORTABLE_CLASSES = [ - "Array", - "Int8Array", - "Uint8Array", - "Uint8ClampedArray", - "Int16Array", - "Uint16Array", - "Int32Array", - "Uint32Array", - "Float32Array", - "Float64Array", - "NodeList", -]; - -/** - * Determine whether an object's properties should be sorted based on its class. - * - * @param string aClassName - * The class of the object. - */ -VariablesView.isSortable = function (aClassName) { - return !VariablesView.NON_SORTABLE_CLASSES.includes(aClassName); -}; + } -/** - * A Scope is an object holding Variable instances. - * Iterable via "for (let [name, variable] of instance) { }". - * - * @param VariablesView aView - * The view to contain this scope. - * @param string l10nId - * The scope localized string id. - * @param object aFlags [optional] - * Additional options or flags for this scope. - */ -function Scope(aView, l10nId, aFlags = {}) { - this.ownerView = aView; + _document = null; + _window = null; - this._onClick = this._onClick.bind(this); - this._openEnum = this._openEnum.bind(this); - this._openNonEnum = this._openNonEnum.bind(this); + _store = null; + _itemsByElement = null; + _testOnlyHierarchy = null; - // Inherit properties and flags from the parent view. You can override - // each of these directly onto any scope, variable or property instance. - this.scrollPageSize = aView.scrollPageSize; - this.contextMenuId = aView.contextMenuId; - this.separatorStr = aView.separatorStr; + _enumVisible = true; + _nonEnumVisible = true; + _alignedValues = false; + _actionsFirst = false; - this._init(l10nId, aFlags); -} + _parent = null; + _list = null; + _searchboxNode = null; + _searchboxContainer = null; + _emptyTextNode = null; + _emptyTextValue = ""; -Scope.prototype = { - /** - * Whether this Scope should be prefetched when it is remoted. - */ - shouldPrefetch: true, + *[Symbol.iterator]() { + yield* this._store; + } - /** - * Whether this Scope should paginate its contents. - */ - allowPaginate: false, + static NON_SORTABLE_CLASSES = [ + "Array", + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "NodeList", + ]; /** - * The class name applied to this scope's target element. + * Determine whether an object's properties should be sorted based on its class. + * + * @param {string} aClassName + * The class of the object. */ - targetClassName: "variables-view-scope", + static isSortable(aClassName) { + return !this.NON_SORTABLE_CLASSES.includes(aClassName); + } /** - * Create a new Variable that is a child of this Scope. + * Returns true if the descriptor represents an undefined, null or + * primitive value. * - * @param string aName - * The name of the new Property. - * @param object aDescriptor + * @param {object} aDescriptor * The variable's descriptor. - * @param object aOptions - * Options of the form accepted by addItem. - * @return Variable - * The newly created child Variable. */ - _createChild(aName, aDescriptor, aOptions) { - return new Variable(this, aName, aDescriptor, aOptions); - }, + static isPrimitive(aDescriptor) { + // For accessor property descriptors, the getter and setter need to be + // contained in 'get' and 'set' properties. + const getter = aDescriptor.get; + const setter = aDescriptor.set; + if (getter || setter) { + return false; + } + + // As described in the remote debugger protocol, the value grip + // must be contained in a 'value' property. + const grip = aDescriptor.value; + if (typeof grip != "object") { + return true; + } + + // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long + // strings are considered types. + const type = grip.type; + if ( + type == "undefined" || + type == "null" || + type == "Infinity" || + type == "-Infinity" || + type == "NaN" || + type == "-0" || + type == "symbol" || + type == "longString" + ) { + return true; + } + + return false; + } /** - * Adds a child to contain any inspected properties. + * Returns true if the descriptor represents an undefined value. * - * @param string aName - * The child's name. - * @param object aDescriptor - * Specifies the value and/or type & class of the child, - * or 'get' & 'set' accessor properties. If the type is implicit, - * it will be inferred from the value. If this parameter is omitted, - * a property without a value will be added (useful for branch nodes). - * e.g. - { value: 42 } - * - { value: true } - * - { value: "nasu" } - * - { value: { type: "undefined" } } - * - { value: { type: "null" } } - * - { value: { type: "object", class: "Object" } } - * - { get: { type: "object", class: "Function" }, - * set: { type: "undefined" } } - * @param object aOptions - * Specifies some options affecting the new variable. - * Recognized properties are - * * boolean relaxed true if name duplicates should be allowed. - * You probably shouldn't do it. Use this - * with caution. - * * boolean internalItem true if the item is internally generated. - * This is used for special variables - * like <return> or <exception> and distinguishes - * them from ordinary properties that happen - * to have the same name - * @return Variable - * The newly created Variable instance, null if it already exists. + * @param {object} aDescriptor + * The variable's descriptor. */ - addItem(aName, aDescriptor = {}, aOptions = {}) { - const { relaxed } = aOptions; - if (this._store.has(aName) && !relaxed) { - return this._store.get(aName); + static isUndefined(aDescriptor) { + // For accessor property descriptors, the getter and setter need to be + // contained in 'get' and 'set' properties. + const getter = aDescriptor.get; + const setter = aDescriptor.set; + if ( + typeof getter == "object" && + getter.type == "undefined" && + typeof setter == "object" && + setter.type == "undefined" + ) { + return true; } - const child = this._createChild(aName, aDescriptor, aOptions); - this._store.set(aName, child); - this._variablesView._itemsByElement.set(child._target, child); - this._variablesView._testOnlyHierarchy.set(child.absoluteName, child); - child.header = aName !== undefined; + // As described in the remote debugger protocol, the value grip + // must be contained in a 'value' property. + const grip = aDescriptor.value; + if (typeof grip == "object" && grip.type == "undefined") { + return true; + } - return child; - }, + return false; + } /** - * Adds items for this variable. + * Returns true if the descriptor represents a falsy value. * - * @param object aItems - * An object containing some { name: descriptor } data properties, - * specifying the value and/or type & class of the variable, - * or 'get' & 'set' accessor properties. If the type is implicit, - * it will be inferred from the value. - * 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" } } } - * @param object aOptions [optional] - * Additional options for adding the properties. Supported options: - * - sorted: true to sort all the properties before adding them - * - callback: function invoked after each item is added + * @param {object} aDescriptor + * The variable's descriptor. */ - addItems(aItems, aOptions = {}) { - const names = Object.keys(aItems); + static isFalsy(aDescriptor) { + // As described in the remote debugger protocol, the value grip + // must be contained in a 'value' property. + const grip = aDescriptor.value; + if (typeof grip != "object") { + return !grip; + } - // Sort all of the properties before adding them, if preferred. - if (aOptions.sorted) { - names.sort(this._naturalSort); + // For convenience, undefined, null, NaN, and -0 are all considered types. + const type = grip.type; + if ( + type == "undefined" || + type == "null" || + type == "NaN" || + type == "-0" + ) { + return true; } - // Add the properties to the current scope. - for (const name of names) { - const descriptor = aItems[name]; - const item = this.addItem(name, descriptor); + return false; + } - if (aOptions.callback) { - aOptions.callback(item, descriptor && descriptor.value); - } + /** + * Returns true if the value is an instance of Variable or Property. + * + * @param any aValue + * The value to test. + */ + static isVariable(aValue) { + return aValue instanceof Variable; + } + + /** + * Returns a standard grip for a value. + * + * @param {any} aValue + * The raw value to get a grip for. + * @return {any} + * The value's grip. + */ + static getGrip(aValue) { + switch (typeof aValue) { + case "boolean": + case "string": + return aValue; + case "number": + if (aValue === Infinity) { + return { type: "Infinity" }; + } else if (aValue === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(aValue)) { + return { type: "NaN" }; + } else if (1 / aValue === -Infinity) { + return { type: "-0" }; + } + return aValue; + case "undefined": + // document.all is also "undefined" + if (aValue === undefined) { + return { type: "undefined" }; + } + // fall through + case "object": + if (aValue === null) { + return { type: "null" }; + } + // fall through + case "function": + return { type: "object", class: getObjectClassName(aValue) }; + default: + console.error( + "Failed to provide a grip for value of " + + typeof value + + ": " + + aValue + ); + return null; } - }, + } /** - * Remove this Scope from its parent and remove all children recursively. - */ - remove() { - const view = this._variablesView; - view._store.splice(view._store.indexOf(this), 1); - view._itemsByElement.delete(this._target); - view._testOnlyHierarchy.delete(this._nameString); + * Returns a custom formatted property string for a grip. + * + * @param {any} aGrip + * @see Variable.setGrip + * @param {object} aOptions + * Options: + * - concise: boolean that tells you want a concisely formatted string. + * - noStringQuotes: boolean that tells to not quote strings. + * - noEllipsis: boolean that tells to not add an ellipsis after the + * initial text of a longString. + * @return {string} + * The formatted property string. + */ + static getString(aGrip, aOptions = {}) { + if (aGrip && typeof aGrip == "object") { + switch (aGrip.type) { + case "undefined": + case "null": + case "NaN": + case "Infinity": + case "-Infinity": + case "-0": + return aGrip.type; + default: { + const stringifier = VariablesView.stringifiers.byType[aGrip.type]; + if (stringifier) { + const result = stringifier(aGrip, aOptions); + if (result != null) { + return result; + } + } - this._target.remove(); + if (aGrip.displayString) { + return VariablesView.getString(aGrip.displayString, aOptions); + } - for (const variable of this._store.values()) { - variable.remove(); + if (aGrip.type == "object" && aOptions.concise) { + return aGrip.class; + } + + return "[" + aGrip.type + " " + aGrip.class + "]"; + } + } } - }, - /** - * Gets the variable in this container having the specified name. - * - * @param string aName - * The name of the variable to get. - * @return Variable - * The matched variable, or null if nothing is found. - */ - get(aName) { - return this._store.get(aName); - }, + switch (typeof aGrip) { + case "string": + return VariablesView.stringifiers.byType.string(aGrip, aOptions); + case "boolean": + return aGrip ? "true" : "false"; + case "number": + if (!aGrip && 1 / aGrip === -Infinity) { + return "-0"; + } + // fall through + default: + return aGrip + ""; + } + } /** - * Recursively searches for the variable or property in this container - * displayed by the specified node. + * Returns a custom class style for a grip. * - * @param Node aNode - * The node to search for. - * @return Variable | Property - * The matched variable or property, or null if nothing is found. + * @param {any} aGrip + * @see Variable.setGrip + * @return {string} + * The custom class style. */ - find(aNode) { - for (const [, variable] of this._store) { - let match; - if (variable._target == aNode) { - match = variable; - } else { - match = variable.find(aNode); + static getClass(aGrip) { + if (aGrip && typeof aGrip == "object") { + if (aGrip.preview) { + switch (aGrip.preview.kind) { + case "DOMNode": + return "token-domnode"; + } } - if (match) { - return match; + + switch (aGrip.type) { + case "undefined": + return "token-undefined"; + case "null": + return "token-null"; + case "Infinity": + case "-Infinity": + case "NaN": + case "-0": + return "token-number"; + case "longString": + return "token-string"; } } - return null; - }, - - /** - * Determines if this scope is a direct child of a parent variables view, - * scope, variable or property. - * - * @param VariablesView | Scope | Variable | Property - * The parent to check. - * @return boolean - * True if the specified item is a direct child, false otherwise. - */ - isChildOf(aParent) { - return this.ownerView == aParent; - }, + switch (typeof aGrip) { + case "string": + return "token-string"; + case "boolean": + return "token-boolean"; + case "number": + return "token-number"; + default: + return "token-other"; + } + } /** - * Determines if this scope is a descendant of a parent variables view, - * scope, variable or property. + * The VariablesView stringifiers are used by VariablesView.getString(). These + * are organized by object type, object class and by object actor preview kind. + * Some objects share identical ways for previews, for example Arrays, Sets and + * NodeLists. * - * @param VariablesView | Scope | Variable | Property - * The parent to check. - * @return boolean - * True if the specified item is a descendant, false otherwise. + * Any stringifier function must return a string. If null is returned, * then + * the default stringifier will be used. When invoked, the stringifier is + * given the same two arguments as those given to VariablesView.getString(). */ - isDescendantOf(aParent) { - if (this.isChildOf(aParent)) { - return true; - } + static stringifiers = { + byType: { + string(aGrip, { noStringQuotes }) { + if (noStringQuotes) { + return aGrip; + } + return '"' + aGrip + '"'; + }, + + longString({ initial }, { noStringQuotes, noEllipsis }) { + const ellipsis = noEllipsis ? "" : ELLIPSIS; + if (noStringQuotes) { + return initial + ellipsis; + } + const result = '"' + initial + '"'; + if (!ellipsis) { + return result; + } + return result.substr(0, result.length - 1) + ellipsis + '"'; + }, + + object(aGrip, aOptions) { + const { preview } = aGrip; + let stringifier; + if (aGrip.class) { + stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class]; + } + if (!stringifier && preview && preview.kind) { + stringifier = VariablesView.stringifiers.byObjectKind[preview.kind]; + } + if (stringifier) { + return stringifier(aGrip, aOptions); + } + return null; + }, + + symbol(aGrip) { + const name = aGrip.name || ""; + return "Symbol(" + name + ")"; + }, + + mapEntry(aGrip) { + const { + preview: { key, value }, + } = aGrip; + + const keyString = VariablesView.getString(key, { + concise: true, + noStringQuotes: true, + }); + const valueString = VariablesView.getString(value, { concise: true }); + + return keyString + " \u2192 " + valueString; + }, + }, // VariablesView.stringifiers.byType + byObjectClass: { + Function(aGrip, { concise }) { + // TODO: Bug 948484 - support arrow functions and ES6 generators + + let name = + aGrip.userDisplayName || aGrip.displayName || aGrip.name || ""; + name = VariablesView.getString(name, { noStringQuotes: true }); + + // TODO: Bug 948489 - Support functions with destructured parameters and + // rest parameters + const params = aGrip.parameterNames || ""; + if (!concise) { + return "function " + name + "(" + params + ")"; + } + return (name || "function ") + "(" + params + ")"; + }, + + RegExp({ displayString }) { + return VariablesView.getString(displayString, { noStringQuotes: true }); + }, + + Date({ preview }) { + if (!preview || !("timestamp" in preview)) { + return null; + } + + if (typeof preview.timestamp != "number") { + return new Date(preview.timestamp).toString(); // invalid date + } + + return "Date " + new Date(preview.timestamp).toISOString(); + }, + + Number(aGrip) { + const { preview } = aGrip; + if (preview === undefined) { + return null; + } + return ( + aGrip.class + + " { " + + VariablesView.getString(preview.wrappedValue) + + " }" + ); + }, + Boolean: Number, + }, // VariablesView.stringifiers.byObjectClass + byObjectKind: { + ArrayLike(aGrip, { concise }) { + const { preview } = aGrip; + if (concise) { + return aGrip.class + "[" + preview.length + "]"; + } + + if (!preview.items) { + return null; + } + + let shown = 0, + lastHole = null; + const result = []; + for (const item of preview.items) { + if (item === null) { + if (lastHole !== null) { + result[lastHole] += ","; + } else { + result.push(""); + } + lastHole = result.length - 1; + } else { + lastHole = null; + result.push(VariablesView.getString(item, { concise: true })); + } + shown++; + } + + if (shown < preview.length) { + const n = preview.length - shown; + result.push(VariablesView.stringifiers._getNMoreString(n)); + } else if (lastHole !== null) { + // make sure we have the right number of commas... + result[lastHole] += ","; + } + + const prefix = aGrip.class == "Array" ? "" : aGrip.class + " "; + return prefix + "[" + result.join(", ") + "]"; + }, + + MapLike(aGrip, { concise }) { + const { preview } = aGrip; + if (concise || !preview.entries) { + const size = + typeof preview.size == "number" ? "[" + preview.size + "]" : ""; + return aGrip.class + size; + } + + const entries = []; + for (const [key, value] of preview.entries) { + const keyString = VariablesView.getString(key, { + concise: true, + noStringQuotes: true, + }); + const valueString = VariablesView.getString(value, { concise: true }); + entries.push(keyString + ": " + valueString); + } + + if (typeof preview.size == "number" && preview.size > entries.length) { + const n = preview.size - entries.length; + entries.push(VariablesView.stringifiers._getNMoreString(n)); + } + + return aGrip.class + " {" + entries.join(", ") + "}"; + }, + + ObjectWithText(aGrip, { concise }) { + if (concise) { + return aGrip.class; + } + + return aGrip.class + " " + VariablesView.getString(aGrip.preview.text); + }, + + ObjectWithURL(aGrip, { concise }) { + let result = aGrip.class; + const url = aGrip.preview.url; + if (!VariablesView.isFalsy({ value: url })) { + result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`; + } + return result; + }, + + // Stringifier for any kind of object. + Object(aGrip, { concise }) { + if (concise) { + return aGrip.class; + } + + const { preview } = aGrip; + const props = []; + + if (aGrip.class == "Promise" && aGrip.promiseState) { + const { state, value, reason } = aGrip.promiseState; + props.push("<state>: " + VariablesView.getString(state)); + if (state == "fulfilled") { + props.push( + "<value>: " + VariablesView.getString(value, { concise: true }) + ); + } else if (state == "rejected") { + props.push( + "<reason>: " + VariablesView.getString(reason, { concise: true }) + ); + } + } + + for (const key of Object.keys(preview.ownProperties || {})) { + const value = preview.ownProperties[key]; + let valueString = ""; + if (value.get) { + valueString = "Getter"; + } else if (value.set) { + valueString = "Setter"; + } else { + valueString = VariablesView.getString(value.value, { + concise: true, + }); + } + props.push(key + ": " + valueString); + } + + for (const key of Object.keys(preview.safeGetterValues || {})) { + const value = preview.safeGetterValues[key]; + const valueString = VariablesView.getString(value.getterValue, { + concise: true, + }); + props.push(key + ": " + valueString); + } + + if (!props.length) { + return null; + } + + if (preview.ownPropertiesLength) { + const previewLength = Object.keys(preview.ownProperties).length; + const diff = preview.ownPropertiesLength - previewLength; + if (diff > 0) { + props.push(VariablesView.stringifiers._getNMoreString(diff)); + } + } + + const prefix = aGrip.class != "Object" ? aGrip.class + " " : ""; + return prefix + "{" + props.join(", ") + "}"; + }, // Object + + Error(aGrip, { concise }) { + const { preview } = aGrip; + const name = VariablesView.getString(preview.name, { + noStringQuotes: true, + }); + if (concise) { + return name || aGrip.class; + } + + let msg = + name + + ": " + + VariablesView.getString(preview.message, { noStringQuotes: true }); + + if (!VariablesView.isFalsy({ value: preview.stack })) { + msg += + "\n" + + L10N.getStr("variablesViewErrorStacktrace") + + "\n" + + preview.stack; + } + + return msg; + }, + + DOMException(aGrip, { concise }) { + const { preview } = aGrip; + if (concise) { + return preview.name || aGrip.class; + } + + let msg = + aGrip.class + + " [" + + preview.name + + ": " + + VariablesView.getString(preview.message) + + "\n" + + "code: " + + preview.code + + "\n" + + "nsresult: 0x" + + (+preview.result).toString(16); + + if (preview.filename) { + msg += "\nlocation: " + preview.filename; + if (preview.lineNumber) { + msg += ":" + preview.lineNumber; + } + } + + return msg + "]"; + }, + + DOMEvent(aGrip, { concise }) { + const { preview } = aGrip; + if (!preview.type) { + return null; + } + + if (concise) { + return aGrip.class + " " + preview.type; + } + + let result = preview.type; + + if ( + preview.eventKind == "key" && + preview.modifiers && + preview.modifiers.length + ) { + result += " " + preview.modifiers.join("-"); + } + + const props = []; + if (preview.target) { + const target = VariablesView.getString(preview.target, { + concise: true, + }); + props.push("target: " + target); + } + + for (const prop in preview.properties) { + const value = preview.properties[prop]; + props.push( + prop + ": " + VariablesView.getString(value, { concise: true }) + ); + } + + return result + " {" + props.join(", ") + "}"; + }, // DOMEvent + + DOMNode(aGrip, { concise }) { + const { preview } = aGrip; + + switch (preview.nodeType) { + case nodeConstants.DOCUMENT_NODE: { + let result = aGrip.class; + if (preview.location) { + result += ` \u2192 ${ + getSourceNames(preview.location)[concise ? "short" : "long"] + }`; + } + + return result; + } + + case nodeConstants.ATTRIBUTE_NODE: { + const value = VariablesView.getString(preview.value, { + noStringQuotes: true, + }); + return preview.nodeName + '="' + escapeHTML(value) + '"'; + } + + case nodeConstants.TEXT_NODE: + return ( + preview.nodeName + + " " + + VariablesView.getString(preview.textContent) + ); + + case nodeConstants.COMMENT_NODE: { + const comment = VariablesView.getString(preview.textContent, { + noStringQuotes: true, + }); + return "<!--" + comment + "-->"; + } + + case nodeConstants.DOCUMENT_FRAGMENT_NODE: { + if (concise || !preview.childNodes) { + return aGrip.class + "[" + preview.childNodesLength + "]"; + } + const nodes = []; + for (const node of preview.childNodes) { + nodes.push(VariablesView.getString(node)); + } + if (nodes.length < preview.childNodesLength) { + const n = preview.childNodesLength - nodes.length; + nodes.push(VariablesView.stringifiers._getNMoreString(n)); + } + return aGrip.class + " [" + nodes.join(", ") + "]"; + } + + case nodeConstants.ELEMENT_NODE: { + const attrs = preview.attributes; + if (!concise) { + let n = 0, + result = "<" + preview.nodeName; + for (const name in attrs) { + const value = VariablesView.getString(attrs[name], { + noStringQuotes: true, + }); + result += " " + name + '="' + escapeHTML(value) + '"'; + n++; + } + if (preview.attributesLength > n) { + result += " " + ELLIPSIS; + } + return result + ">"; + } + + let result = "<" + preview.nodeName; + if (attrs.id) { + result += "#" + attrs.id; + } + + if (attrs.class) { + result += "." + attrs.class.trim().replace(/\s+/, "."); + } + return result + ">"; + } + + default: + return null; + } + }, // DOMNode + }, // VariablesView.stringifiers.byObjectKind + + /** + * Get the "N more…" formatted string, given an N. This is used for displaying + * how many elements are not displayed in an object preview (eg. an array). + * + * @private + * @param {number} aNumber + * @return {string} + */ + _getNMoreString(aNumber) { + const str = L10N.getStr("variablesViewMoreObjects"); + return PluralForm.get(aNumber, str).replace("#1", aNumber); + }, + }; +} + +/** + * A Scope is an object holding Variable instances. + * Iterable via "for (let [name, variable] of instance) { }". + */ +class Scope { + /** + * @param {VariablesView} aView + * The view to contain this scope. + * @param {string} l10nId + * The scope localized string id. + * @param {object} [aFlags={}] + * Additional options or flags for this scope. + */ + constructor(aView, l10nId, aFlags = {}) { + this.ownerView = aView; + + this._onClick = this._onClick.bind(this); + this._openEnum = this._openEnum.bind(this); + this._openNonEnum = this._openNonEnum.bind(this); + + // Inherit properties and flags from the parent view. You can override + // each of these directly onto any scope, variable or property instance. + this.scrollPageSize = aView.scrollPageSize; + this.contextMenuId = aView.contextMenuId; + this.separatorStr = aView.separatorStr; + + this._init(l10nId, aFlags); + } + + /** + * Whether this Scope should be prefetched when it is remoted. + */ + shouldPrefetch = true; + + /** + * Whether this Scope should paginate its contents. + */ + allowPaginate = false; + + /** + * The class name applied to this scope's target element. + */ + get targetClassName() { + return "variables-view-scope"; + } + + /** + * Create a new Variable that is a child of this Scope. + * + * @param {string} aName + * The name of the new Property. + * @param {object} aDescriptor + * The variable's descriptor. + * @param {object} aOptions + * Options of the form accepted by addItem. + * @return {Variable} + * The newly created child Variable. + */ + _createChild(aName, aDescriptor, aOptions) { + return new Variable(this, aName, aDescriptor, aOptions); + } + + /** + * Adds a child to contain any inspected properties. + * + * @param {string} aName + * The child's name. + * @param {object} aDescriptor + * Specifies the value and/or type & class of the child, + * or 'get' & 'set' accessor properties. If the type is implicit, + * it will be inferred from the value. If this parameter is omitted, + * a property without a value will be added (useful for branch nodes). + * e.g. - { value: 42 } + * - { value: true } + * - { value: "nasu" } + * - { value: { type: "undefined" } } + * - { value: { type: "null" } } + * - { value: { type: "object", class: "Object" } } + * - { get: { type: "object", class: "Function" }, + * set: { type: "undefined" } } + * @param {object} aOptions + * Specifies some options affecting the new variable. + * Recognized properties are + * * boolean relaxed true if name duplicates should be allowed. + * You probably shouldn't do it. Use this + * with caution. + * * boolean internalItem true if the item is internally generated. + * This is used for special variables + * like <return> or <exception> and distinguishes + * them from ordinary properties that happen + * to have the same name + * @return {Variable} + * The newly created Variable instance, null if it already exists. + */ + addItem(aName, aDescriptor = {}, aOptions = {}) { + const { relaxed } = aOptions; + if (this._store.has(aName) && !relaxed) { + return this._store.get(aName); + } + + const child = this._createChild(aName, aDescriptor, aOptions); + this._store.set(aName, child); + this._variablesView._itemsByElement.set(child._target, child); + this._variablesView._testOnlyHierarchy.set(child.absoluteName, child); + child.header = aName !== undefined; + + return child; + } + + /** + * Adds items for this variable. + * + * @param {object} aItems + * An object containing some { name: descriptor } data properties, + * specifying the value and/or type & class of the variable, + * or 'get' & 'set' accessor properties. If the type is implicit, + * it will be inferred from the value. + * 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" } } } + * @param {object} [aOptions={}] + * Additional options for adding the properties. Supported options: + * - sorted: true to sort all the properties before adding them + * - callback: function invoked after each item is added + */ + addItems(aItems, aOptions = {}) { + const names = Object.keys(aItems); + + // Sort all of the properties before adding them, if preferred. + if (aOptions.sorted) { + names.sort(this._naturalSort); + } + + // Add the properties to the current scope. + for (const name of names) { + const descriptor = aItems[name]; + const item = this.addItem(name, descriptor); + + if (aOptions.callback) { + aOptions.callback(item, descriptor && descriptor.value); + } + } + } + + /** + * Remove this Scope from its parent and remove all children recursively. + */ + remove() { + const view = this._variablesView; + view._store.splice(view._store.indexOf(this), 1); + view._itemsByElement.delete(this._target); + view._testOnlyHierarchy.delete(this._nameString); + + this._target.remove(); + + for (const variable of this._store.values()) { + variable.remove(); + } + } + + /** + * Gets the variable in this container having the specified name. + * + * @param {string} aName + * The name of the variable to get. + * @return {Variable | null} + * The matched variable, or null if nothing is found. + */ + get(aName) { + return this._store.get(aName); + } + + /** + * Recursively searches for the variable or property in this container + * displayed by the specified node. + * + * @param {Node} aNode + * The node to search for. + * @return {Variable | Property | null} + * The matched variable or property, or null if nothing is found. + */ + find(aNode) { + for (const [, variable] of this._store) { + let match; + if (variable._target == aNode) { + match = variable; + } else { + match = variable.find(aNode); + } + if (match) { + return match; + } + } + return null; + } + + /** + * Determines if this scope is a direct child of a parent variables view, + * scope, variable or property. + * + * @param {VariablesView | Scope | Variable | Property} aParent + * The parent to check. + * @return {boolean} + * True if the specified item is a direct child, false otherwise. + */ + isChildOf(aParent) { + return this.ownerView == aParent; + } + + /** + * Determines if this scope is a descendant of a parent variables view, + * scope, variable or property. + * + * @param {VariablesView | Scope | Variable | Property} aParent + * The parent to check. + * @return {boolean} + * True if the specified item is a descendant, false otherwise. + */ + isDescendantOf(aParent) { + if (this.isChildOf(aParent)) { + return true; + } // Recurse to parent if it is a Scope, Variable, or Property. if (this.ownerView instanceof Scope) { @@ -1132,7 +1851,7 @@ Scope.prototype = { } return false; - }, + } /** * Shows the scope. @@ -1144,7 +1863,7 @@ Scope.prototype = { if (this.onshow) { this.onshow(this); } - }, + } /** * Hides the scope. @@ -1156,7 +1875,7 @@ Scope.prototype = { if (this.onhide) { this.onhide(this); } - }, + } /** * Expands the scope, showing all the added details. @@ -1180,7 +1899,7 @@ Scope.prototype = { // and attributes are available. (Mostly used for tests) await this.onexpand(this); } - }, + } /** * Collapses the scope, hiding all the added details. @@ -1197,7 +1916,7 @@ Scope.prototype = { if (this.oncollapse) { this.oncollapse(this); } - }, + } /** * Toggles between the scope's collapsed and expanded state. @@ -1217,7 +1936,7 @@ Scope.prototype = { if (this.ontoggle) { this.ontoggle(this); } - }, + } /** * Shows the scope's title header. @@ -1228,7 +1947,7 @@ Scope.prototype = { } this._target.removeAttribute("untitled"); this._isHeaderVisible = true; - }, + } /** * Hides the scope's title header. @@ -1241,16 +1960,16 @@ Scope.prototype = { this.expand(); this._target.setAttribute("untitled", ""); this._isHeaderVisible = false; - }, + } /** * Sort in ascending order * This only needs to compare non-numbers since it is dealing with an array * which numeric-based indices are placed in order. * - * @param string a - * @param string b - * @return number + * @param {string} a + * @param {string} b + * @return {number} * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0 */ _naturalSort(a, b) { @@ -1258,7 +1977,7 @@ Scope.prototype = { return a < b ? -1 : 1; } return 0; - }, + } /** * Shows the scope's expand/collapse arrow. @@ -1269,7 +1988,7 @@ Scope.prototype = { } this._arrow.removeAttribute("invisible"); this._isArrowVisible = true; - }, + } /** * Hides the scope's expand/collapse arrow. @@ -1280,83 +1999,83 @@ Scope.prototype = { } this._arrow.setAttribute("invisible", ""); this._isArrowVisible = false; - }, + } /** * Gets the visibility state. * - * @return boolean + * @return {boolean} */ get visible() { return this._isContentVisible; - }, + } /** * Gets the expanded state. * - * @return boolean + * @return {boolean} */ get expanded() { return this._isExpanded; - }, + } /** * Gets the header visibility state. * - * @return boolean + * @return {boolean} */ get header() { return this._isHeaderVisible; - }, + } /** * Gets the twisty visibility state. * - * @return boolean + * @return {boolean} */ get twisty() { return this._isArrowVisible; - }, + } /** * Sets the visibility state. * - * @param boolean aFlag + * @param {boolean} aFlag */ set visible(aFlag) { aFlag ? this.show() : this.hide(); - }, + } /** * Sets the expanded state. * - * @param boolean aFlag + * @param {boolean} aFlag */ set expanded(aFlag) { aFlag ? this.expand() : this.collapse(); - }, + } /** * Sets the header visibility state. * - * @param boolean aFlag + * @param {boolean} aFlag */ set header(aFlag) { aFlag ? this.showHeader() : this.hideHeader(); - }, + } /** * Sets the twisty visibility state. * - * @param boolean aFlag + * @param {boolean} aFlag */ set twisty(aFlag) { aFlag ? this.showArrow() : this.hideArrow(); - }, + } /** * Specifies if this target node may be focused. * - * @return boolean + * @return {boolean} */ get focusable() { // Check if this target node is actually visibile. @@ -1378,88 +2097,88 @@ Scope.prototype = { } } return true; - }, + } /** * Focus this scope. */ focus() { this._variablesView._focusItem(this); - }, + } /** * Adds an event listener for a certain event on this scope's title. * - * @param string aName - * @param function aCallback - * @param boolean aCapture + * @param {string} aName + * @param {Function} aCallback + * @param {boolean} aCapture */ addEventListener(aName, aCallback, aCapture) { this._title.addEventListener(aName, aCallback, aCapture); - }, + } /** * Removes an event listener for a certain event on this scope's title. * - * @param string aName - * @param function aCallback - * @param boolean aCapture + * @param {string} aName + * @param {Function} aCallback + * @param {boolean} aCapture */ removeEventListener(aName, aCallback, aCapture) { this._title.removeEventListener(aName, aCallback, aCapture); - }, + } /** * Gets the id associated with this item. * - * @return string + * @return {string} */ get id() { return this._idString; - }, + } /** * Gets the name associated with this item. * - * @return string + * @return {string} */ get name() { return this._nameString; - }, + } /** * Gets the displayed value for this item. * - * @return string + * @return {string} */ get displayValue() { return this._valueString; - }, + } /** * Gets the class names used for the displayed value. * - * @return string + * @return {string} */ get displayValueClassName() { return this._valueClassName; - }, + } /** * Gets the element associated with this item. * - * @return Node + * @return {Node} */ get target() { return this._target; - }, + } /** * Initializes this scope's id, view and binds event listeners. * - * @param string l10nId + * @param {string} l10nId * The scope localized string id. - * @param object aFlags [optional] + * @param {object} [aFlags] * Additional options or flags for this scope. */ _init(l10nId, aFlags) { @@ -1471,19 +2190,19 @@ Scope.prototype = { }); this._addEventListeners(); this.parentNode.appendChild(this._target); - }, + } /** * Creates the necessary nodes for this scope. * - * @param Object options - * @param string options.l10nId [optional] + * @param {object} options + * @param {string} [options.l10nId] * The scope localized string id. - * @param string options.value [optional] + * @param {string} [options.value] * The scope's name. Either this or l10nId need to be passed - * @param string options.targetClassName + * @param {string} options.targetClassName * A custom class name for this scope's target element. - * @param string options.titleClassName [optional] + * @param {string} [options.titleClassName] * A custom class name for this scope's title element. */ _displayScope({ l10nId, value, targetClassName, titleClassName = "" }) { @@ -1520,14 +2239,14 @@ Scope.prototype = { element.appendChild(title); element.appendChild(enumerable); element.appendChild(nonenum); - }, + } /** * Adds the necessary event listeners for this scope. */ _addEventListeners() { this._title.addEventListener("mousedown", this._onClick); - }, + } /** * The click listener for this scope's title. @@ -1538,7 +2257,7 @@ Scope.prototype = { } this.toggle(); this.focus(); - }, + } /** * Opens the enumerable items container. @@ -1546,19 +2265,19 @@ Scope.prototype = { _openEnum() { this._arrow.setAttribute("open", ""); this._enum.setAttribute("open", ""); - }, + } /** * Opens the non-enumerable items container. */ _openNonEnum() { this._nonenum.setAttribute("open", ""); - }, + } /** * Specifies if enumerable properties and variables should be displayed. * - * @param boolean aFlag + * @param {boolean} aFlag */ set _enumVisible(aFlag) { for (const [, variable] of this._store) { @@ -1573,12 +2292,12 @@ Scope.prototype = { this._enum.removeAttribute("open"); } } - }, + } /** * Specifies if non-enumerable properties and variables should be displayed. * - * @param boolean aFlag + * @param {boolean} aFlag */ set _nonEnumVisible(aFlag) { for (const [, variable] of this._store) { @@ -1593,13 +2312,13 @@ Scope.prototype = { this._nonenum.removeAttribute("open"); } } - }, + } /** * Performs a case insensitive search for variables or properties matching * the query, and hides non-matched items. * - * @param string aLowerCaseQuery + * @param {string} aLowerCaseQuery * The lowercased name of the variable or property to search for. */ _performSearch(aLowerCaseQuery) { @@ -1643,12 +2362,12 @@ Scope.prototype = { currentObject._performSearch(aLowerCaseQuery); } } - }, + } /** * Sets if this object instance is a matched or non-matched item. * - * @param boolean aStatus + * @param {boolean} aStatus */ set _matched(aStatus) { if (this._isMatch == aStatus) { @@ -1661,7 +2380,7 @@ Scope.prototype = { this._isMatch = false; this.target.setAttribute("unmatched", ""); } - }, + } /** * Find the first item in the tree of visible items in this item that matches @@ -1669,9 +2388,9 @@ Scope.prototype = { * Tests itself, then descends into first the enumerable children and then * the non-enumerable children (since they are presented in separate groups). * - * @param function aPredicate + * @param {Function} aPredicate * A function that returns true when a match is found. - * @return Scope | Variable | Property + * @return {Scope | Variable | Property} * The first visible scope, variable or property, or null if nothing * is found. */ @@ -1701,7 +2420,7 @@ Scope.prototype = { } return null; - }, + } /** * Find the last item in the tree of visible items in this item that matches @@ -1710,9 +2429,9 @@ Scope.prototype = { * the enumerable children (since they are presented in separate groups), and * finally tests itself. * - * @param function aPredicate + * @param {Function} aPredicate * A function that returns true when a match is found. - * @return Scope | Variable | Property + * @return {Scope | Variable | Property} * The last visible scope, variable or property, or null if nothing * is found. */ @@ -1744,12 +2463,12 @@ Scope.prototype = { } return null; - }, + } /** * Gets top level variables view instance. * - * @return VariablesView + * @return {VariablesView} */ get _variablesView() { return ( @@ -1764,61 +2483,62 @@ Scope.prototype = { return parentView; })()) ); - }, + } /** * Gets the parent node holding this scope. * - * @return Node + * @return {Node} */ get parentNode() { return this.ownerView._list; - }, + } /** * Gets the owner document holding this scope. * - * @return HTMLDocument + * @return {HTMLDocument} */ get document() { return this._document || (this._document = this.ownerView.document); - }, + } /** * Gets the default window holding this scope. * - * @return nsIDOMWindow + * @return {Window} */ get window() { return this._window || (this._window = this.ownerView.window); - }, - - _topView: null, - _document: null, - _window: null, - - ownerView: null, - contextMenuId: "", - separatorStr: "", - - _store: null, - _enumItems: null, - _nonEnumItems: null, - _fetched: false, - _isExpanded: false, - _isContentVisible: true, - _isHeaderVisible: true, - _isArrowVisible: true, - _isMatch: true, - _idString: "", - _nameString: "", - _target: null, - _arrow: null, - _name: null, - _title: null, - _enum: null, - _nonenum: null, -}; + } + + _topView = null; + _document = null; + _window = null; + + ownerView = null; + contextMenuId = ""; + separatorStr = ""; + + _fetched = false; + _isExpanded = false; + _isContentVisible = true; + _isHeaderVisible = true; + _isArrowVisible = true; + _isMatch = true; + _idString = ""; + _nameString = ""; + _target = null; + _arrow = null; + _name = null; + _title = null; + _enum = null; + _nonenum = null; + + *[Symbol.iterator]() { + yield* this._store; + } +} // Creating maps and arrays thousands of times for variables or properties // with a large number of children fills up a lot of memory. Make sure @@ -1838,65 +2558,70 @@ DevToolsUtils.defineLazyPrototypeGetter( /** * A Variable is a Scope holding Property instances. * Iterable via "for (let [name, property] of instance) { }". - * - * @param Scope aScope - * The scope to contain this variable. - * @param string aName - * The variable's name. - * @param object aDescriptor - * The variable's descriptor. - * @param object aOptions - * Options of the form accepted by Scope.addItem */ -function Variable(aScope, aName, aDescriptor, aOptions) { - this._internalItem = aOptions.internalItem; +class Variable extends Scope { + /** + * @param {Scope} aScope + * The scope to contain this variable. + * @param {string} aName + * The variable's name. + * @param {object} aDescriptor + * The variable's descriptor. + * @param {object} aOptions + * Options of the form accepted by Scope.addItem + */ + constructor(aScope, aName, aDescriptor, aOptions) { + // Treat safe getter descriptors as descriptors with a value. + if ("getterValue" in aDescriptor) { + aDescriptor.value = aDescriptor.getterValue; + delete aDescriptor.get; + delete aDescriptor.set; + } - // Treat safe getter descriptors as descriptors with a value. - if ("getterValue" in aDescriptor) { - aDescriptor.value = aDescriptor.getterValue; - delete aDescriptor.get; - delete aDescriptor.set; - } + super(aScope, aName, { + _internalItem: aOptions.internalItem, + _initialDescriptor: aDescriptor, + }); - Scope.call(this, aScope, aName, (this._initialDescriptor = aDescriptor)); - this.setGrip(aDescriptor.value); -} + this.setGrip(aDescriptor.value); + } -Variable.prototype = extend(Scope.prototype, { /** * Whether this Variable should be prefetched when it is remoted. */ get shouldPrefetch() { return this.name == "window" || this.name == "this"; - }, + } /** * Whether this Variable should paginate its contents. */ get allowPaginate() { return this.name != "window" && this.name != "this"; - }, + } /** * The class name applied to this variable's target element. */ - targetClassName: "variables-view-variable variable-or-property", + get targetClassName() { + return "variables-view-variable variable-or-property"; + } /** * Create a new Property that is a child of Variable. * - * @param string aName + * @param {string} aName * The name of the new Property. - * @param object aDescriptor + * @param {object} aDescriptor * The property's descriptor. - * @param object aOptions + * @param {object} aOptions * Options of the form accepted by Scope.addItem - * @return Property + * @return {Property} * The newly created child Property. */ _createChild(aName, aDescriptor, aOptions) { return new Property(this, aName, aDescriptor, aOptions); - }, + } /** * Remove this Variable from its parent and remove all children recursively. @@ -1911,14 +2636,14 @@ Variable.prototype = extend(Scope.prototype, { for (const property of this._store.values()) { property.remove(); } - }, + } /** * Populates this variable to contain all the properties of an object. * - * @param object aObject + * @param {object} aObject * The raw object you want to display. - * @param object aOptions [optional] + * @param {object} [aOptions={}] * Additional options for adding the properties. Supported options: * - sorted: true to sort all the properties before adding them * - expanded: true to expand all the properties after adding them @@ -1957,33 +2682,33 @@ Variable.prototype = extend(Scope.prototype, { if (prototype) { this._addRawValueProperty("__proto__", {}, prototype); } - }, + } /** * Populates a specific variable or property instance to contain all the * properties of an object * - * @param Variable | Property aVar + * @param {Variable | Property} aVar * The target variable to populate. - * @param object aObject [optional] + * @param {object} [aObject=aVar._sourceValue] * The raw object you want to display. If unspecified, the object is * assumed to be defined in a _sourceValue property on the target. */ _populateTarget(aVar, aObject = aVar._sourceValue) { aVar.populate(aObject); - }, + } /** * Adds a property for this variable based on a raw value descriptor. * - * @param string aName + * @param {string} aName * The property's name. - * @param object aDescriptor + * @param {object} aDescriptor * Specifies the exact property descriptor as returned by a call to * Object.getOwnPropertyDescriptor. - * @param object aValue + * @param {object} aValue * The raw property value you want to display. - * @return Property + * @return {Property} * The newly added property instance. */ _addRawValueProperty(aName, aDescriptor, aValue) { @@ -1999,17 +2724,17 @@ Variable.prototype = extend(Scope.prototype, { propertyItem.onexpand = this._populateTarget; } return propertyItem; - }, + } /** * Adds a property for this variable based on a getter/setter descriptor. * - * @param string aName + * @param {string} aName * The property's name. - * @param object aDescriptor + * @param {object} aDescriptor * Specifies the exact property descriptor as returned by a call to * Object.getOwnPropertyDescriptor. - * @return Property + * @return {Property} * The newly added property instance. */ _addRawNonValueProperty(aName, aDescriptor) { @@ -2018,23 +2743,23 @@ Variable.prototype = extend(Scope.prototype, { descriptor.set = VariablesView.getGrip(aDescriptor.set); return this.addItem(aName, descriptor); - }, + } /** * Gets this variable's path to the topmost scope in the form of a string * meant for use via eval() or a similar approach. * For example, a symbolic name may look like "arguments['0']['foo']['bar']". * - * @return string + * @return {string} */ get symbolicName() { return this._nameString || ""; - }, + } /** * Gets full path to this variable, including name of the scope. * - * @return string + * @return {string} */ get absoluteName() { if (this._absoluteName) { @@ -2044,12 +2769,12 @@ Variable.prototype = extend(Scope.prototype, { this._absoluteName = this.ownerView._nameString + "[" + escapeString(this._nameString) + "]"; return this._absoluteName; - }, + } /** * Gets this variable's symbolic path to the topmost scope. * - * @return array + * @return {Array} * @see Variable._buildSymbolicPath */ get symbolicPath() { @@ -2058,14 +2783,14 @@ Variable.prototype = extend(Scope.prototype, { } this._symbolicPath = this._buildSymbolicPath(); return this._symbolicPath; - }, + } /** * Build this variable's path to the topmost scope in form of an array of * strings, one for each segment of the path. * For example, a symbolic path may look like ["0", "foo", "bar"]. * - * @return array + * @return {Array} */ _buildSymbolicPath(path = []) { if (this.name) { @@ -2075,34 +2800,34 @@ Variable.prototype = extend(Scope.prototype, { } } return path; - }, + } /** * Returns this variable's value from the descriptor if available. * - * @return any + * @return {any} */ get value() { return this._initialDescriptor.value; - }, + } /** * Returns this variable's getter from the descriptor if available. * - * @return object + * @return {object} */ get getter() { return this._initialDescriptor.get; - }, + } /** * Returns this variable's getter from the descriptor if available. * - * @return object + * @return {object} */ get setter() { return this._initialDescriptor.set; - }, + } /** * Sets the specific grip for this variable (applies the text content and @@ -2112,7 +2837,7 @@ Variable.prototype = extend(Scope.prototype, { * remote debugger protocol. For convenience, undefined and null are * both considered types. * - * @param any aGrip + * @param {any} aGrip * Specifies the value and/or type & class of the variable. * e.g. - 42 * - true @@ -2167,15 +2892,22 @@ Variable.prototype = extend(Scope.prototype, { this._valueLabel.classList.add(this._valueClassName); this._valueLabel.setAttribute("value", this._valueString); this._separatorLabel.hidden = false; - }, + } /** * Initializes this variable's id, view and binds event listeners. * - * @param string aName + * @override + * @param {string} aName * The variable's name. + * @param {object} options + * @param {object} options._internalItem + * @param {object} options._initialDescriptor */ - _init(aName) { + _init(aName, { _internalItem, _initialDescriptor }) { + this._internalItem = _internalItem; + this._initialDescriptor = _initialDescriptor; + this._idString = generateId((this._nameString = aName)); this._displayScope({ value: aName, targetClassName: this.targetClassName }); this._displayVariable(); @@ -2197,7 +2929,7 @@ Variable.prototype = extend(Scope.prototype, { this.ownerView._nonenum.appendChild(this._target); this.ownerView._nonEnumItems.push(this); } - }, + } /** * Creates the necessary nodes for this variable. @@ -2241,55 +2973,56 @@ Variable.prototype = extend(Scope.prototype, { setter.hideArrow(); this.expand(); } - }, + } /** * Adds the necessary event listeners for this variable. */ _addEventListeners() { this._title.addEventListener("mousedown", this._onClick); - }, - - _symbolicName: null, - _symbolicPath: null, - _absoluteName: null, - _initialDescriptor: null, - _separatorLabel: null, - _valueLabel: null, - _spacer: null, - _valueGrip: null, - _valueString: "", - _valueClassName: "", - _prevExpandable: false, - _prevExpanded: false, -}); + } + + _symbolicName = null; + _symbolicPath = null; + _absoluteName = null; + + _spacer = null; + _valueGrip = null; + _valueString = ""; + _valueClassName = ""; + _prevExpandable = false; + _prevExpanded = false; +} /** * A Property is a Variable holding additional child Property instances. * Iterable via "for (let [name, property] of instance) { }". - * - * @param Variable aVar - * The variable to contain this property. - * @param string aName - * The property's name. - * @param object aDescriptor - * The property's descriptor. - * @param object aOptions - * Options of the form accepted by Scope.addItem */ -function Property(aVar, aName, aDescriptor, aOptions) { - Variable.call(this, aVar, aName, aDescriptor, aOptions); -} +class Property extends Variable { + /** + * @param {Variable} aVar + * The variable to contain this property. + * @param {string} aName + * The property's name. + * @param {object} aDescriptor + * The property's descriptor. + * @param {object} aOptions + * Options of the form accepted by Scope.addItem + */ + constructor(aVar, aName, aDescriptor, aOptions) { + super(aVar, aName, aDescriptor, aOptions); + } -Property.prototype = extend(Variable.prototype, { /** * The class name applied to this property's target element. */ - targetClassName: "variables-view-property variable-or-property", + get targetClassName() { + return "variables-view-property variable-or-property"; + } /** * @see Variable.symbolicName - * @return string + * @return {string} */ get symbolicName() { if (this._symbolicName) { @@ -2299,11 +3032,11 @@ Property.prototype = extend(Variable.prototype, { this._symbolicName = this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]"; return this._symbolicName; - }, + } /** * @see Variable.absoluteName - * @return string + * @return {string} */ get absoluteName() { if (this._absoluteName) { @@ -2313,169 +3046,8 @@ Property.prototype = extend(Variable.prototype, { this._absoluteName = this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]"; return this._absoluteName; - }, -}); - -/** - * A generator-iterator over the VariablesView, Scopes, Variables and Properties. - */ -VariablesView.prototype[Symbol.iterator] = - Scope.prototype[Symbol.iterator] = - Variable.prototype[Symbol.iterator] = - Property.prototype[Symbol.iterator] = - function* () { - yield* this._store; - }; - -/** - * Returns true if the descriptor represents an undefined, null or - * primitive value. - * - * @param object aDescriptor - * The variable's descriptor. - */ -VariablesView.isPrimitive = function (aDescriptor) { - // For accessor property descriptors, the getter and setter need to be - // contained in 'get' and 'set' properties. - const getter = aDescriptor.get; - const setter = aDescriptor.set; - if (getter || setter) { - return false; - } - - // As described in the remote debugger protocol, the value grip - // must be contained in a 'value' property. - const grip = aDescriptor.value; - if (typeof grip != "object") { - return true; - } - - // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long - // strings are considered types. - const type = grip.type; - if ( - type == "undefined" || - type == "null" || - type == "Infinity" || - type == "-Infinity" || - type == "NaN" || - type == "-0" || - type == "symbol" || - type == "longString" - ) { - return true; - } - - return false; -}; - -/** - * Returns true if the descriptor represents an undefined value. - * - * @param object aDescriptor - * The variable's descriptor. - */ -VariablesView.isUndefined = function (aDescriptor) { - // For accessor property descriptors, the getter and setter need to be - // contained in 'get' and 'set' properties. - const getter = aDescriptor.get; - const setter = aDescriptor.set; - if ( - typeof getter == "object" && - getter.type == "undefined" && - typeof setter == "object" && - setter.type == "undefined" - ) { - return true; - } - - // As described in the remote debugger protocol, the value grip - // must be contained in a 'value' property. - const grip = aDescriptor.value; - if (typeof grip == "object" && grip.type == "undefined") { - return true; - } - - return false; -}; - -/** - * Returns true if the descriptor represents a falsy value. - * - * @param object aDescriptor - * The variable's descriptor. - */ -VariablesView.isFalsy = function (aDescriptor) { - // As described in the remote debugger protocol, the value grip - // must be contained in a 'value' property. - const grip = aDescriptor.value; - if (typeof grip != "object") { - return !grip; - } - - // For convenience, undefined, null, NaN, and -0 are all considered types. - const type = grip.type; - if (type == "undefined" || type == "null" || type == "NaN" || type == "-0") { - return true; - } - - return false; -}; - -/** - * Returns true if the value is an instance of Variable or Property. - * - * @param any aValue - * The value to test. - */ -VariablesView.isVariable = function (aValue) { - return aValue instanceof Variable; -}; - -/** - * Returns a standard grip for a value. - * - * @param any aValue - * The raw value to get a grip for. - * @return any - * The value's grip. - */ -VariablesView.getGrip = function (aValue) { - switch (typeof aValue) { - case "boolean": - case "string": - return aValue; - case "number": - if (aValue === Infinity) { - return { type: "Infinity" }; - } else if (aValue === -Infinity) { - return { type: "-Infinity" }; - } else if (Number.isNaN(aValue)) { - return { type: "NaN" }; - } else if (1 / aValue === -Infinity) { - return { type: "-0" }; - } - return aValue; - case "undefined": - // document.all is also "undefined" - if (aValue === undefined) { - return { type: "undefined" }; - } - // fall through - case "object": - if (aValue === null) { - return { type: "null" }; - } - // fall through - case "function": - return { type: "object", class: getObjectClassName(aValue) }; - default: - console.error( - "Failed to provide a grip for value of " + typeof value + ": " + aValue - ); - return null; } -}; +} // Match the function name from the result of toString() or toSource(). // @@ -2488,9 +3060,9 @@ const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; /** * Helper function to deduce the name of the provided function. * - * @param function function + * @param {Function} function * The function whose name will be returned. - * @return string + * @return {string} * Function name. */ function getFunctionName(func) { @@ -2523,9 +3095,9 @@ function getFunctionName(func) { * Get the object class name. For example, the |window| object has the Window * class name (based on [object Window]). * - * @param object object + * @param {object} object * The object you want to get the class name for. - * @return string + * @return {string} * The object class name. */ function getObjectClassName(object) { @@ -2561,554 +3133,6 @@ function getObjectClassName(object) { } /** - * Returns a custom formatted property string for a grip. - * - * @param any aGrip - * @see Variable.setGrip - * @param object aOptions - * Options: - * - concise: boolean that tells you want a concisely formatted string. - * - noStringQuotes: boolean that tells to not quote strings. - * - noEllipsis: boolean that tells to not add an ellipsis after the - * initial text of a longString. - * @return string - * The formatted property string. - */ -VariablesView.getString = function (aGrip, aOptions = {}) { - if (aGrip && typeof aGrip == "object") { - switch (aGrip.type) { - case "undefined": - case "null": - case "NaN": - case "Infinity": - case "-Infinity": - case "-0": - return aGrip.type; - default: { - const stringifier = VariablesView.stringifiers.byType[aGrip.type]; - if (stringifier) { - const result = stringifier(aGrip, aOptions); - if (result != null) { - return result; - } - } - - if (aGrip.displayString) { - return VariablesView.getString(aGrip.displayString, aOptions); - } - - if (aGrip.type == "object" && aOptions.concise) { - return aGrip.class; - } - - return "[" + aGrip.type + " " + aGrip.class + "]"; - } - } - } - - switch (typeof aGrip) { - case "string": - return VariablesView.stringifiers.byType.string(aGrip, aOptions); - case "boolean": - return aGrip ? "true" : "false"; - case "number": - if (!aGrip && 1 / aGrip === -Infinity) { - return "-0"; - } - // fall through - default: - return aGrip + ""; - } -}; - -/** - * The VariablesView stringifiers are used by VariablesView.getString(). These - * are organized by object type, object class and by object actor preview kind. - * Some objects share identical ways for previews, for example Arrays, Sets and - * NodeLists. - * - * Any stringifier function must return a string. If null is returned, * then - * the default stringifier will be used. When invoked, the stringifier is - * given the same two arguments as those given to VariablesView.getString(). - */ -VariablesView.stringifiers = {}; - -VariablesView.stringifiers.byType = { - string(aGrip, { noStringQuotes }) { - if (noStringQuotes) { - return aGrip; - } - return '"' + aGrip + '"'; - }, - - longString({ initial }, { noStringQuotes, noEllipsis }) { - const ellipsis = noEllipsis ? "" : ELLIPSIS; - if (noStringQuotes) { - return initial + ellipsis; - } - const result = '"' + initial + '"'; - if (!ellipsis) { - return result; - } - return result.substr(0, result.length - 1) + ellipsis + '"'; - }, - - object(aGrip, aOptions) { - const { preview } = aGrip; - let stringifier; - if (aGrip.class) { - stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class]; - } - if (!stringifier && preview && preview.kind) { - stringifier = VariablesView.stringifiers.byObjectKind[preview.kind]; - } - if (stringifier) { - return stringifier(aGrip, aOptions); - } - return null; - }, - - symbol(aGrip) { - const name = aGrip.name || ""; - return "Symbol(" + name + ")"; - }, - - mapEntry(aGrip) { - const { - preview: { key, value }, - } = aGrip; - - const keyString = VariablesView.getString(key, { - concise: true, - noStringQuotes: true, - }); - const valueString = VariablesView.getString(value, { concise: true }); - - return keyString + " \u2192 " + valueString; - }, -}; // VariablesView.stringifiers.byType - -VariablesView.stringifiers.byObjectClass = { - Function(aGrip, { concise }) { - // TODO: Bug 948484 - support arrow functions and ES6 generators - - let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || ""; - name = VariablesView.getString(name, { noStringQuotes: true }); - - // TODO: Bug 948489 - Support functions with destructured parameters and - // rest parameters - const params = aGrip.parameterNames || ""; - if (!concise) { - return "function " + name + "(" + params + ")"; - } - return (name || "function ") + "(" + params + ")"; - }, - - RegExp({ displayString }) { - return VariablesView.getString(displayString, { noStringQuotes: true }); - }, - - Date({ preview }) { - if (!preview || !("timestamp" in preview)) { - return null; - } - - if (typeof preview.timestamp != "number") { - return new Date(preview.timestamp).toString(); // invalid date - } - - return "Date " + new Date(preview.timestamp).toISOString(); - }, - - Number(aGrip) { - const { preview } = aGrip; - if (preview === undefined) { - return null; - } - return ( - aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) + " }" - ); - }, -}; // VariablesView.stringifiers.byObjectClass - -VariablesView.stringifiers.byObjectClass.Boolean = - VariablesView.stringifiers.byObjectClass.Number; - -VariablesView.stringifiers.byObjectKind = { - ArrayLike(aGrip, { concise }) { - const { preview } = aGrip; - if (concise) { - return aGrip.class + "[" + preview.length + "]"; - } - - if (!preview.items) { - return null; - } - - let shown = 0, - lastHole = null; - const result = []; - for (const item of preview.items) { - if (item === null) { - if (lastHole !== null) { - result[lastHole] += ","; - } else { - result.push(""); - } - lastHole = result.length - 1; - } else { - lastHole = null; - result.push(VariablesView.getString(item, { concise: true })); - } - shown++; - } - - if (shown < preview.length) { - const n = preview.length - shown; - result.push(VariablesView.stringifiers._getNMoreString(n)); - } else if (lastHole !== null) { - // make sure we have the right number of commas... - result[lastHole] += ","; - } - - const prefix = aGrip.class == "Array" ? "" : aGrip.class + " "; - return prefix + "[" + result.join(", ") + "]"; - }, - - MapLike(aGrip, { concise }) { - const { preview } = aGrip; - if (concise || !preview.entries) { - const size = - typeof preview.size == "number" ? "[" + preview.size + "]" : ""; - return aGrip.class + size; - } - - const entries = []; - for (const [key, value] of preview.entries) { - const keyString = VariablesView.getString(key, { - concise: true, - noStringQuotes: true, - }); - const valueString = VariablesView.getString(value, { concise: true }); - entries.push(keyString + ": " + valueString); - } - - if (typeof preview.size == "number" && preview.size > entries.length) { - const n = preview.size - entries.length; - entries.push(VariablesView.stringifiers._getNMoreString(n)); - } - - return aGrip.class + " {" + entries.join(", ") + "}"; - }, - - ObjectWithText(aGrip, { concise }) { - if (concise) { - return aGrip.class; - } - - return aGrip.class + " " + VariablesView.getString(aGrip.preview.text); - }, - - ObjectWithURL(aGrip, { concise }) { - let result = aGrip.class; - const url = aGrip.preview.url; - if (!VariablesView.isFalsy({ value: url })) { - result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`; - } - return result; - }, - - // Stringifier for any kind of object. - Object(aGrip, { concise }) { - if (concise) { - return aGrip.class; - } - - const { preview } = aGrip; - const props = []; - - if (aGrip.class == "Promise" && aGrip.promiseState) { - const { state, value, reason } = aGrip.promiseState; - props.push("<state>: " + VariablesView.getString(state)); - if (state == "fulfilled") { - props.push( - "<value>: " + VariablesView.getString(value, { concise: true }) - ); - } else if (state == "rejected") { - props.push( - "<reason>: " + VariablesView.getString(reason, { concise: true }) - ); - } - } - - for (const key of Object.keys(preview.ownProperties || {})) { - const value = preview.ownProperties[key]; - let valueString = ""; - if (value.get) { - valueString = "Getter"; - } else if (value.set) { - valueString = "Setter"; - } else { - valueString = VariablesView.getString(value.value, { concise: true }); - } - props.push(key + ": " + valueString); - } - - for (const key of Object.keys(preview.safeGetterValues || {})) { - const value = preview.safeGetterValues[key]; - const valueString = VariablesView.getString(value.getterValue, { - concise: true, - }); - props.push(key + ": " + valueString); - } - - if (!props.length) { - return null; - } - - if (preview.ownPropertiesLength) { - const previewLength = Object.keys(preview.ownProperties).length; - const diff = preview.ownPropertiesLength - previewLength; - if (diff > 0) { - props.push(VariablesView.stringifiers._getNMoreString(diff)); - } - } - - const prefix = aGrip.class != "Object" ? aGrip.class + " " : ""; - return prefix + "{" + props.join(", ") + "}"; - }, // Object - - Error(aGrip, { concise }) { - const { preview } = aGrip; - const name = VariablesView.getString(preview.name, { - noStringQuotes: true, - }); - if (concise) { - return name || aGrip.class; - } - - let msg = - name + - ": " + - VariablesView.getString(preview.message, { noStringQuotes: true }); - - if (!VariablesView.isFalsy({ value: preview.stack })) { - msg += - "\n" + - L10N.getStr("variablesViewErrorStacktrace") + - "\n" + - preview.stack; - } - - return msg; - }, - - DOMException(aGrip, { concise }) { - const { preview } = aGrip; - if (concise) { - return preview.name || aGrip.class; - } - - let msg = - aGrip.class + - " [" + - preview.name + - ": " + - VariablesView.getString(preview.message) + - "\n" + - "code: " + - preview.code + - "\n" + - "nsresult: 0x" + - (+preview.result).toString(16); - - if (preview.filename) { - msg += "\nlocation: " + preview.filename; - if (preview.lineNumber) { - msg += ":" + preview.lineNumber; - } - } - - return msg + "]"; - }, - - DOMEvent(aGrip, { concise }) { - const { preview } = aGrip; - if (!preview.type) { - return null; - } - - if (concise) { - return aGrip.class + " " + preview.type; - } - - let result = preview.type; - - if ( - preview.eventKind == "key" && - preview.modifiers && - preview.modifiers.length - ) { - result += " " + preview.modifiers.join("-"); - } - - const props = []; - if (preview.target) { - const target = VariablesView.getString(preview.target, { concise: true }); - props.push("target: " + target); - } - - for (const prop in preview.properties) { - const value = preview.properties[prop]; - props.push( - prop + ": " + VariablesView.getString(value, { concise: true }) - ); - } - - return result + " {" + props.join(", ") + "}"; - }, // DOMEvent - - DOMNode(aGrip, { concise }) { - const { preview } = aGrip; - - switch (preview.nodeType) { - case nodeConstants.DOCUMENT_NODE: { - let result = aGrip.class; - if (preview.location) { - result += ` \u2192 ${ - getSourceNames(preview.location)[concise ? "short" : "long"] - }`; - } - - return result; - } - - case nodeConstants.ATTRIBUTE_NODE: { - const value = VariablesView.getString(preview.value, { - noStringQuotes: true, - }); - return preview.nodeName + '="' + escapeHTML(value) + '"'; - } - - case nodeConstants.TEXT_NODE: - return ( - preview.nodeName + " " + VariablesView.getString(preview.textContent) - ); - - case nodeConstants.COMMENT_NODE: { - const comment = VariablesView.getString(preview.textContent, { - noStringQuotes: true, - }); - return "<!--" + comment + "-->"; - } - - case nodeConstants.DOCUMENT_FRAGMENT_NODE: { - if (concise || !preview.childNodes) { - return aGrip.class + "[" + preview.childNodesLength + "]"; - } - const nodes = []; - for (const node of preview.childNodes) { - nodes.push(VariablesView.getString(node)); - } - if (nodes.length < preview.childNodesLength) { - const n = preview.childNodesLength - nodes.length; - nodes.push(VariablesView.stringifiers._getNMoreString(n)); - } - return aGrip.class + " [" + nodes.join(", ") + "]"; - } - - case nodeConstants.ELEMENT_NODE: { - const attrs = preview.attributes; - if (!concise) { - let n = 0, - result = "<" + preview.nodeName; - for (const name in attrs) { - const value = VariablesView.getString(attrs[name], { - noStringQuotes: true, - }); - result += " " + name + '="' + escapeHTML(value) + '"'; - n++; - } - if (preview.attributesLength > n) { - result += " " + ELLIPSIS; - } - return result + ">"; - } - - let result = "<" + preview.nodeName; - if (attrs.id) { - result += "#" + attrs.id; - } - - if (attrs.class) { - result += "." + attrs.class.trim().replace(/\s+/, "."); - } - return result + ">"; - } - - default: - return null; - } - }, // DOMNode -}; // VariablesView.stringifiers.byObjectKind - -/** - * Get the "N more…" formatted string, given an N. This is used for displaying - * how many elements are not displayed in an object preview (eg. an array). - * - * @private - * @param number aNumber - * @return string - */ -VariablesView.stringifiers._getNMoreString = function (aNumber) { - const str = L10N.getStr("variablesViewMoreObjects"); - return PluralForm.get(aNumber, str).replace("#1", aNumber); -}; - -/** - * Returns a custom class style for a grip. - * - * @param any aGrip - * @see Variable.setGrip - * @return string - * The custom class style. - */ -VariablesView.getClass = function (aGrip) { - if (aGrip && typeof aGrip == "object") { - if (aGrip.preview) { - switch (aGrip.preview.kind) { - case "DOMNode": - return "token-domnode"; - } - } - - switch (aGrip.type) { - case "undefined": - return "token-undefined"; - case "null": - return "token-null"; - case "Infinity": - case "-Infinity": - case "NaN": - case "-0": - return "token-number"; - case "longString": - return "token-string"; - } - } - switch (typeof aGrip) { - case "string": - return "token-string"; - case "boolean": - return "token-boolean"; - case "number": - return "token-number"; - default: - return "token-other"; - } -}; - -/** * A monotonically-increasing counter, that guarantees the uniqueness of scope, * variables and properties ids. *