Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  VariablesView.sys.mjs   Sprache: unbekannt

 
Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-disable mozilla/no-aArgs */

const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
const LAZY_EMPTY_DELAY = 150; // ms
const SCROLL_PAGE_SIZE_DEFAULT = 0;
const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
const PAGE_SIZE_MAX_JUMPS = 30;
const SEARCH_ACTION_MAX_DELAY = 300; // ms
const ITEM_FLASH_DURATION = 300; // ms

import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const EventEmitter = require("resource://devtools/shared/event-emitter.js");
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,
} = require("resource://devtools/client/shared/widgets/view-helpers.js");
const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
const { PluralForm } = require("resource://devtools/shared/plural-form.js");
const {
  LocalizationHelper,
  ELLIPSIS,
} = require("resource://devtools/shared/l10n.js");

const L10N = new LocalizationHelper(DBG_STRINGS_URI);
const HTML_NS = "http://www.w3.org/1999/xhtml";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "clipboardHelper",
  "@mozilla.org/widget/clipboardhelper;1",
  "nsIClipboardHelper"
);

/**
 * 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.
 *
 * To allow replacing variable or property values in this view, provide an
 * "eval" function property. To allow replacing variable or property names,
 * provide a "switch" function. To handle deleting variables or properties,
 * provide a "delete" function.
 *
 * @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();
  this._prevHierarchy = new Map();
  this._currHierarchy = new Map();

  this._parent = aParentNode;
  this._parent.classList.add("variables-view-container");
  this._parent.classList.add("theme-body");
  this._appendEmptyNotice();

  this._onSearchboxInput = this._onSearchboxInput.bind(this);
  this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this);
  this._onViewKeyDown = this._onViewKeyDown.bind(this);

  // 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);

  for (const name in aFlags) {
    this[name] = aFlags[name];
  }

  EventEmitter.decorate(this);
}

VariablesView.prototype = {
  /**
   * Helper setter for populating this container with a raw object.
   *
   * @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.
   */
  set rawObject(aObject) {
    this.empty();
    this.addScope()
      .addItem(undefined, { enumerable: true })
      .populate(aObject, { sorted: true });
  },

  /**
   * Adds a scope to contain any inspected variables.
   *
   * This new scope will be considered the parent of any other scope
   * added afterwards.
   *
   * @param string l10nId
   *        The scope localized string id.
   * @param string aCustomClass
   *        An additional class name for the containing element.
   * @return Scope
   *         The newly created Scope instance.
   */
  addScope(l10nId = "", aCustomClass = "") {
    this._removeEmptyNotice();
    this._toggleSearchVisibility(true);

    const scope = new Scope(this, l10nId, { customClass: aCustomClass });
    this._store.push(scope);
    this._itemsByElement.set(scope._target, scope);
    this._currHierarchy.set(l10nId, scope);
    scope.header = !!l10nId;

    return scope;
  },

  /**
   * Removes all items from this container.
   *
   * @param number aTimeout [optional]
   *        The number of milliseconds to delay the operation if
   *        lazy emptying of this container is enabled.
   */
  empty(aTimeout = this.lazyEmptyDelay) {
    // If there are no items in this container, emptying is useless.
    if (!this._store.length) {
      return;
    }

    this._store.length = 0;
    this._itemsByElement = new WeakMap();
    this._prevHierarchy = this._currHierarchy;
    this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.

    // Check if this empty operation may be executed lazily.
    if (this.lazyEmpty && aTimeout > 0) {
      this._emptySoon(aTimeout);
      return;
    }

    while (this._list.hasChildNodes()) {
      this._list.firstChild.remove();
    }

    this._appendEmptyNotice();
    this._toggleSearchVisibility(false);
  },

  /**
   * Emptying this container and rebuilding it immediately afterwards would
   * result in a brief redraw flicker, because the previously expanded nodes
   * may get asynchronously re-expanded, after fetching the prototype and
   * properties from a server.
   *
   * To avoid such behaviour, a normal container list is rebuild, but not
   * immediately attached to the parent container. The old container list
   * is kept around for a short period of time, hopefully accounting for the
   * data fetching delay. In the meantime, any operations can be executed
   * normally.
   *
   * @see VariablesView.empty
   * @see VariablesView.commitHierarchy
   */
  _emptySoon(aTimeout) {
    const prevList = this._list;
    const currList = (this._list = this.document.createXULElement("scrollbox"));

    this.window.setTimeout(() => {
      prevList.removeEventListener("keydown", this._onViewKeyDown);
      currList.addEventListener("keydown", this._onViewKeyDown);
      currList.setAttribute("orient", "vertical");

      this._parent.removeChild(prevList);
      this._parent.appendChild(currList);

      if (!this._store.length) {
        this._appendEmptyNotice();
        this._toggleSearchVisibility(false);
      }
    }, aTimeout);
  },

  /**
   * Optional DevTools toolbox containing this VariablesView. Used to
   * communicate with the inspector and highlighter.
   */
  toolbox: null,

  /**
   * The controller for this VariablesView, if it has one.
   */
  controller: null,

  /**
   * The amount of time (in milliseconds) it takes to empty this view lazily.
   */
  lazyEmptyDelay: LAZY_EMPTY_DELAY,

  /**
   * Specifies if this view may be emptied lazily.
   * @see VariablesView.prototype.empty
   */
  lazyEmpty: false,

  /**
   * Specifies if nodes in this view may be searched lazily.
   */
  lazySearch: true,

  /**
   * 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,

  /**
   * Function called each time a variable or property's value is changed via
   * user interaction. If null, then value changes are disabled.
   *
   * This property is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  eval: null,

  /**
   * Function called each time a variable or property's name is changed via
   * user interaction. If null, then name changes are disabled.
   *
   * This property is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  switch: null,

  /**
   * Function called each time a variable or property is deleted via
   * user interaction. If null, then deletions are disabled.
   *
   * This property is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  delete: null,

  /**
   * Function called each time a property is added via user interaction. If
   * null, then property additions are disabled.
   *
   * This property is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  new: null,

  /**
   * Specifies if after an eval or switch operation, the variable or property
   * which has been edited should be disabled.
   */
  preventDisableOnChange: false,

  /**
   * Specifies if, whenever a variable or property descriptor is available,
   * configurable, enumerable, writable, frozen, sealed and extensible
   * attributes should not affect presentation.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  preventDescriptorModifiers: false,

  /**
   * The tooltip text shown on a variable or property's value if an |eval|
   * function is provided, in order to change the variable or property's value.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),

  /**
   * The tooltip text shown on a variable or property's name if a |switch|
   * function is provided, in order to change the variable or property's name.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),

  /**
   * The tooltip text shown on a variable or property's edit button if an
   * |eval| function is provided and a getter/setter descriptor is present,
   * in order to change the variable or property to a plain value.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),

  /**
   * The tooltip text shown on a variable or property's value if that value is
   * a DOMNode that can be highlighted and selected in the inspector.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),

  /**
   * The tooltip text shown on a variable or property's delete button if a
   * |delete| function is provided, in order to delete the variable or property.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),

  /**
   * Specifies the context menu attribute set on variables and properties.
   *
   * This flag is applied recursively onto each scope in this view and
   * affects only the child nodes when they're created.
   */
  contextMenuId: "",

  /**
   * The separator label between the variables or properties name and value.
   *
   * 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"),

  /**
   * Specifies if enumerable properties and variables should be displayed.
   * These variables and properties are visible by default.
   * @param boolean aFlag
   */
  set enumVisible(aFlag) {
    this._enumVisible = aFlag;

    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
   */
  set nonEnumVisible(aFlag) {
    this._nonEnumVisible = aFlag;

    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
   */
  set onlyEnumVisible(aFlag) {
    if (aFlag) {
      this.enumVisible = true;
      this.nonEnumVisible = false;
    } else {
      this.enumVisible = true;
      this.nonEnumVisible = true;
    }
  },

  /**
   * Sets if the variable and property searching is enabled.
   * @param boolean aFlag
   */
  set searchEnabled(aFlag) {
    aFlag ? this._enableSearch() : this._disableSearch();
  },

  /**
   * Gets if the variable and property searching is enabled.
   * @return boolean
   */
  get searchEnabled() {
    return !!this._searchboxContainer;
  },

  /**
   * Enables variable and property searching in this view.
   * Use the "searchEnabled" setter to enable searching.
   */
  _enableSearch() {
    // If searching was already enabled, no need to re-enable it again.
    if (this._searchboxContainer) {
      return;
    }
    const document = this.document;
    const ownerNode = this._parent.parentNode;

    const container = (this._searchboxContainer =
      document.createXULElement("hbox"));
    container.className = "devtools-toolbar devtools-input-toolbar";

    // Hide the variables searchbox container if there are no variables or
    // properties to display.
    container.hidden = !this._store.length;

    const searchbox = (this._searchboxNode = document.createElementNS(
      HTML_NS,
      "input"
    ));
    searchbox.className = "variables-view-searchinput devtools-filterinput";
    document.l10n.setAttributes(searchbox, "storage-variable-view-search-box");
    searchbox.addEventListener("input", this._onSearchboxInput);
    searchbox.addEventListener("keydown", this._onSearchboxKeyDown);

    container.appendChild(searchbox);
    ownerNode.insertBefore(container, this._parent);
  },

  /**
   * Disables variable and property searching in this view.
   * Use the "searchEnabled" setter to disable searching.
   */
  _disableSearch() {
    // If searching was already disabled, no need to re-disable it again.
    if (!this._searchboxContainer) {
      return;
    }
    this._searchboxContainer.remove();
    this._searchboxNode.removeEventListener("input", this._onSearchboxInput);
    this._searchboxNode.removeEventListener(
      "keydown",
      this._onSearchboxKeyDown
    );

    this._searchboxContainer = null;
    this._searchboxNode = null;
  },

  /**
   * Sets the variables searchbox container hidden or visible.
   * It's hidden by default.
   *
   * @param boolean aVisibleFlag
   *        Specifies the intended visibility.
   */
  _toggleSearchVisibility(aVisibleFlag) {
    // If searching was already disabled, there's no need to hide it.
    if (!this._searchboxContainer) {
      return;
    }
    this._searchboxContainer.hidden = !aVisibleFlag;
  },

  /**
   * Listener handling the searchbox input event.
   */
  _onSearchboxInput() {
    this.scheduleSearch(this._searchboxNode.value);
  },

  /**
   * Listener handling the searchbox keydown event.
   */
  _onSearchboxKeyDown(e) {
    switch (e.keyCode) {
      case KeyCodes.DOM_VK_RETURN:
        this._onSearchboxInput();
        return;
      case KeyCodes.DOM_VK_ESCAPE:
        this._searchboxNode.value = "";
        this._onSearchboxInput();
    }
  },

  /**
   * Schedules searching for variables or properties matching the query.
   *
   * @param string aToken
   *        The variable or property to search for.
   * @param number aWait
   *        The amount of milliseconds to wait until draining.
   */
  scheduleSearch(aToken, aWait) {
    // Check if this search operation may not be executed lazily.
    if (!this.lazySearch) {
      this._doSearch(aToken);
      return;
    }

    // The amount of time to wait for the requests to settle.
    const maxDelay = SEARCH_ACTION_MAX_DELAY;
    const delay = aWait === undefined ? maxDelay / aToken.length : aWait;

    // Allow requests to settle down first.
    setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
  },

  /**
   * Performs a case insensitive search for variables or properties matching
   * the query, and hides non-matched items.
   *
   * If aToken is falsy, then all the scopes are unhidden and expanded,
   * while the available variables and properties inside those scopes are
   * just unhidden.
   *
   * @param string aToken
   *        The variable or property to search for.
   */
  _doSearch(aToken) {
    if (this.controller && this.controller.supportsSearch()) {
      // Retrieve the main Scope in which we add attributes
      const scope = this._store[0]._store.get(undefined);
      if (!aToken) {
        // Prune the view from old previous content
        // so that we delete the intermediate search results
        // we created in previous searches
        for (const property of scope._store.values()) {
          property.remove();
        }
      }
      // Retrieve new attributes eventually hidden in splits
      this.controller.performSearch(scope, aToken);
      // Filter already displayed attributes
      if (aToken) {
        scope._performSearch(aToken.toLowerCase());
      }
      return;
    }
    for (const scope of this._store) {
      switch (aToken) {
        case "":
        case null:
        case undefined:
          scope.expand();
          scope._performSearch("");
          break;
        default:
          scope._performSearch(aToken.toLowerCase());
          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
   *        A function that returns true when a match is found.
   * @return Scope | Variable | Property
   *         The first visible scope, variable or property, or null if nothing
   *         is found.
   */
  _findInVisibleItems(aPredicate) {
    for (const scope of this._store) {
      const result = scope._findInVisibleItems(aPredicate);
      if (result) {
        return result;
      }
    }
    return null;
  },

  /**
   * Find the last item in the tree of visible items in this container that
   * matches the predicate. Searches in reverse visual order (opposite of the
   * order seen by the user). Descends into each scope to check the scope and
   * its children.
   *
   * @param function aPredicate
   *        A function that returns true when a match is found.
   * @return Scope | Variable | Property
   *         The last visible scope, variable or property, or null if nothing
   *         is found.
   */
  _findInVisibleItemsReverse(aPredicate) {
    for (let i = this._store.length - 1; i >= 0; i--) {
      const scope = this._store[i];
      const result = scope._findInVisibleItemsReverse(aPredicate);
      if (result) {
        return result;
      }
    }
    return null;
  },

  /**
   * Gets the scope at the specified index.
   *
   * @param number aIndex
   *        The scope's index.
   * @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
   *        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.
   * @return Scope
   *         The owner scope.
   */
  getOwnerScopeForVariableOrProperty(aItem) {
    if (!aItem) {
      return null;
    }
    // If this is a Scope, return it.
    if (!(aItem instanceof Variable)) {
      return aItem;
    }
    // If this is a Variable or Property, find its owner scope.
    if (aItem instanceof Variable && aItem.ownerView) {
      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
   *        The variable or property for which to find the parent scopes.
   * @return array
   *         A list of parent Scopes.
   */
  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
   *         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.
   */
  focusFirstVisibleItem() {
    const focusableItem = this._findInVisibleItems(item => item.focusable);
    if (focusableItem) {
      this._focusItem(focusableItem);
    }
    this._parent.scrollTop = 0;
    this._parent.scrollLeft = 0;
  },

  /**
   * Focuses the last visible scope, variable, or property in this container.
   */
  focusLastVisibleItem() {
    const focusableItem = this._findInVisibleItemsReverse(
      item => item.focusable
    );
    if (focusableItem) {
      this._focusItem(focusableItem);
    }
    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
   *        A scalar specifying by how many items should the selection change.
   */
  focusItemAtDelta(aDelta) {
    const direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
    let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
    while (distance--) {
      if (!this._focusChange(direction)) {
        break; // Out of bounds.
      }
    }
  },

  /**
   * Focuses the next or previous scope, variable or property in this view.
   *
   * @param string aDirection
   *        Either "advanceFocus" or "rewindFocus".
   * @return boolean
   *         False if the focus went out of bounds and the first or last element
   *         in this view was focused instead.
   */
  _focusChange(aDirection) {
    const commandDispatcher = this.document.commandDispatcher;
    const prevFocusedElement = commandDispatcher.focusedElement;
    let currFocusedItem = null;

    do {
      commandDispatcher[aDirection]();

      // Make sure the newly focused item is a part of this view.
      // If the focus goes out of bounds, revert the previously focused item.
      if (!(currFocusedItem = this.getFocusedItem())) {
        prevFocusedElement.focus();
        return false;
      }
    } while (!currFocusedItem.focusable);

    // Focus remained within bounds.
    return true;
  },

  /**
   * Focuses a scope, variable or property and makes sure it's visible.
   *
   * @param aItem Scope | Variable | Property
   *        The item to focus.
   * @param boolean aCollapseFlag
   *        True if the focused item should also be collapsed.
   * @return boolean
   *         True if the item was successfully focused.
   */
  _focusItem(aItem, aCollapseFlag) {
    if (!aItem.focusable) {
      return false;
    }
    if (aCollapseFlag) {
      aItem.collapse();
    }
    aItem._target.focus();
    aItem._arrow.scrollIntoView({ block: "nearest" });
    return true;
  },

  /**
   * Copy current selection to clipboard.
   */
  _copyItem() {
    const item = this.getFocusedItem();
    lazy.clipboardHelper.copyString(
      item._nameString + item.separatorStr + item._valueString
    );
  },

  /**
   * Listener handling a key down event on the view.
   */
  // eslint-disable-next-line complexity
  _onViewKeyDown(e) {
    const item = this.getFocusedItem();

    // Prevent scrolling when pressing navigation keys.
    ViewHelpers.preventScrolling(e);

    switch (e.keyCode) {
      case KeyCodes.DOM_VK_C:
        if (e.ctrlKey || e.metaKey) {
          this._copyItem();
        }
        return;

      case KeyCodes.DOM_VK_UP:
        // Always rewind focus.
        this.focusPrevItem(true);
        return;

      case KeyCodes.DOM_VK_DOWN:
        // Always advance focus.
        this.focusNextItem(true);
        return;

      case KeyCodes.DOM_VK_LEFT:
        // Collapse scopes, variables and properties before rewinding focus.
        if (item._isExpanded && item._isArrowVisible) {
          item.collapse();
        } else {
          this._focusItem(item.ownerView);
        }
        return;

      case KeyCodes.DOM_VK_RIGHT:
        // Nothing to do here if this item never expands.
        if (!item._isArrowVisible) {
          return;
        }
        // Expand scopes, variables and properties before advancing focus.
        if (!item._isExpanded) {
          item.expand();
        } else {
          this.focusNextItem(true);
        }
        return;

      case KeyCodes.DOM_VK_PAGE_UP:
        // Rewind a certain number of elements based on the container height.
        this.focusItemAtDelta(
          -(
            this.scrollPageSize ||
            Math.min(
              Math.floor(
                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
              ),
              PAGE_SIZE_MAX_JUMPS
            )
          )
        );
        return;

      case KeyCodes.DOM_VK_PAGE_DOWN:
        // Advance a certain number of elements based on the container height.
        this.focusItemAtDelta(
          +(
            this.scrollPageSize ||
            Math.min(
              Math.floor(
                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
              ),
              PAGE_SIZE_MAX_JUMPS
            )
          )
        );
        return;

      case KeyCodes.DOM_VK_HOME:
        this.focusFirstVisibleItem();
        return;

      case KeyCodes.DOM_VK_END:
        this.focusLastVisibleItem();
        return;

      case KeyCodes.DOM_VK_RETURN:
        // Start editing the value or name of the Variable or Property.
        if (item instanceof Variable) {
          if (e.metaKey || e.altKey || e.shiftKey) {
            item._activateNameInput();
          } else {
            item._activateValueInput();
          }
        }
        return;

      case KeyCodes.DOM_VK_DELETE:
      case KeyCodes.DOM_VK_BACK_SPACE:
        // Delete the Variable or Property if allowed.
        if (item instanceof Variable) {
          item._onDelete(e);
        }
        return;

      case KeyCodes.DOM_VK_INSERT:
        item._onAddProperty(e);
    }
  },

  /**
   * Sets the text displayed in this container when there are no available items.
   * @param string aValue
   */
  set emptyText(aValue) {
    if (this._emptyTextNode) {
      this._emptyTextNode.setAttribute("value", aValue);
    }
    this._emptyTextValue = aValue;
    this._appendEmptyNotice();
  },

  /**
   * Creates and appends a label signaling that this container is empty.
   */
  _appendEmptyNotice() {
    if (this._emptyTextNode || !this._emptyTextValue) {
      return;
    }

    const label = this.document.createXULElement("label");
    label.className = "variables-view-empty-notice";
    label.setAttribute("value", this._emptyTextValue);

    this._parent.appendChild(label);
    this._emptyTextNode = label;
  },

  /**
   * Removes the label signaling that this container is empty.
   */
  _removeEmptyNotice() {
    if (!this._emptyTextNode) {
      return;
    }

    this._parent.removeChild(this._emptyTextNode);
    this._emptyTextNode = null;
  },

  /**
   * Gets if all values should be aligned together.
   * @return boolean
   */
  get alignedValues() {
    return this._alignedValues;
  },

  /**
   * Sets if all values should be aligned together.
   * @param boolean aFlag
   */
  set alignedValues(aFlag) {
    this._alignedValues = aFlag;
    if (aFlag) {
      this._parent.setAttribute("aligned-values", "");
    } 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
   */
  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
   */
  set actionsFirst(aFlag) {
    this._actionsFirst = aFlag;
    if (aFlag) {
      this._parent.setAttribute("actions-first", "");
    } else {
      this._parent.removeAttribute("actions-first");
    }
  },

  /**
   * Gets the parent node holding this view.
   * @return Node
   */
  get parentNode() {
    return this._parent;
  },

  /**
   * Gets the owner document holding this view.
   * @return HTMLDocument
   */
  get document() {
    return this._document || (this._document = this._parent.ownerDocument);
  },

  /**
   * Gets the default window holding this view.
   * @return nsIDOMWindow
   */
  get window() {
    return this._window || (this._window = this.document.defaultView);
  },

  _document: null,
  _window: null,

  _store: null,
  _itemsByElement: null,
  _prevHierarchy: null,
  _currHierarchy: 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);
};

/**
 * Generates the string evaluated when performing simple value changes.
 *
 * @param Variable | Property aItem
 *        The current variable or property.
 * @param string aCurrentString
 *        The trimmed user inputted string.
 * @param string aPrefix [optional]
 *        Prefix for the symbolic name.
 * @return string
 *         The string to be evaluated.
 */
VariablesView.simpleValueEvalMacro = function (
  aItem,
  aCurrentString,
  aPrefix = ""
) {
  return aPrefix + aItem.symbolicName + "=" + aCurrentString;
};

/**
 * Generates the string evaluated when overriding getters and setters with
 * plain values.
 *
 * @param Property aItem
 *        The current getter or setter property.
 * @param string aCurrentString
 *        The trimmed user inputted string.
 * @param string aPrefix [optional]
 *        Prefix for the symbolic name.
 * @return string
 *         The string to be evaluated.
 */
VariablesView.overrideValueEvalMacro = function (
  aItem,
  aCurrentString,
  aPrefix = ""
) {
  const property = escapeString(aItem._nameString);
  const parent = aPrefix + aItem.ownerView.symbolicName || "this";

  return (
    "Object.defineProperty(" +
    parent +
    "," +
    property +
    "," +
    "{ value: " +
    aCurrentString +
    ", enumerable: " +
    parent +
    ".propertyIsEnumerable(" +
    property +
    ")" +
    ", configurable: true" +
    ", writable: true" +
    "})"
  );
};

/**
 * Generates the string evaluated when performing getters and setters changes.
 *
 * @param Property aItem
 *        The current getter or setter property.
 * @param string aCurrentString
 *        The trimmed user inputted string.
 * @param string aPrefix [optional]
 *        Prefix for the symbolic name.
 * @return string
 *         The string to be evaluated.
 */
VariablesView.getterOrSetterEvalMacro = function (
  aItem,
  aCurrentString,
  aPrefix = ""
) {
  const type = aItem._nameString;
  const propertyObject = aItem.ownerView;
  const parentObject = propertyObject.ownerView;
  const property = escapeString(propertyObject._nameString);
  const parent = aPrefix + parentObject.symbolicName || "this";

  switch (aCurrentString) {
    case "":
    case "null":
    case "undefined":
      const mirrorType = type == "get" ? "set" : "get";
      const mirrorLookup =
        type == "get" ? "__lookupSetter__" : "__lookupGetter__";

      // If the parent object will end up without any getter or setter,
      // morph it into a plain value.
      if (
        (type == "set" && propertyObject.getter.type == "undefined") ||
        (type == "get" && propertyObject.setter.type == "undefined")
      ) {
        // Make sure the right getter/setter to value override macro is applied
        // to the target object.
        return propertyObject.evaluationMacro(
          propertyObject,
          "undefined",
          aPrefix
        );
      }

      // Construct and return the getter/setter removal evaluation string.
      // e.g: Object.defineProperty(foo, "bar", {
      //   get: foo.__lookupGetter__("bar"),
      //   set: undefined,
      //   enumerable: true,
      //   configurable: true
      // })
      return (
        "Object.defineProperty(" +
        parent +
        "," +
        property +
        "," +
        "{" +
        mirrorType +
        ":" +
        parent +
        "." +
        mirrorLookup +
        "(" +
        property +
        ")" +
        "," +
        type +
        ":" +
        undefined +
        ", enumerable: " +
        parent +
        ".propertyIsEnumerable(" +
        property +
        ")" +
        ", configurable: true" +
        "})"
      );

    default:
      // Wrap statements inside a function declaration if not already wrapped.
      if (!aCurrentString.startsWith("function")) {
        const header = "function(" + (type == "set" ? "value" : "") + ")";
        let body = "";
        // If there's a return statement explicitly written, always use the
        // standard function definition syntax
        if (aCurrentString.includes("return ")) {
          body = "{" + aCurrentString + "}";
        } else if (aCurrentString.startsWith("{")) {
          // If block syntax is used, use the whole string as the function body.
          body = aCurrentString;
        } else {
          // Prefer an expression closure.
          body = "(" + aCurrentString + ")";
        }
        aCurrentString = header + body;
      }

      // Determine if a new getter or setter should be defined.
      const defineType =
        type == "get" ? "__defineGetter__" : "__defineSetter__";

      // Make sure all quotes are escaped in the expression's syntax,
      const defineFunc =
        'eval("(' + aCurrentString.replace(/"/g, "\\$&") + ')")';

      // Construct and return the getter/setter evaluation string.
      // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
      return (
        parent + "." + defineType + "(" + property + "," + defineFunc + ")"
      );
  }
};

/**
 * Function invoked when a getter or setter is deleted.
 *
 * @param Property aItem
 *        The current getter or setter property.
 */
VariablesView.getterOrSetterDeleteCallback = function (aItem) {
  aItem._disable();

  // Make sure the right getter/setter to value override macro is applied
  // to the target object.
  aItem.ownerView.eval(aItem, "");

  return true; // Don't hide the element.
};

/**
 * 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;

  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.eval = aView.eval;
  this.switch = aView.switch;
  this.delete = aView.delete;
  this.new = aView.new;
  this.preventDisableOnChange = aView.preventDisableOnChange;
  this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
  this.editableNameTooltip = aView.editableNameTooltip;
  this.editableValueTooltip = aView.editableValueTooltip;
  this.editButtonTooltip = aView.editButtonTooltip;
  this.deleteButtonTooltip = aView.deleteButtonTooltip;
  this.domNodeValueTooltip = aView.domNodeValueTooltip;
  this.contextMenuId = aView.contextMenuId;
  this.separatorStr = aView.separatorStr;

  this._init(l10nId, aFlags);
}

Scope.prototype = {
  /**
   * 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.
   */
  targetClassName: "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._currHierarchy.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 [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
   */
  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._currHierarchy.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
   *         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
   *         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
   *        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
   *        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) {
      return this.ownerView.isDescendantOf(aParent);
    }

    return false;
  },

  /**
   * Shows the scope.
   */
  show() {
    this._target.hidden = false;
    this._isContentVisible = true;

    if (this.onshow) {
      this.onshow(this);
    }
  },

  /**
   * Hides the scope.
   */
  hide() {
    this._target.hidden = true;
    this._isContentVisible = false;

    if (this.onhide) {
      this.onhide(this);
    }
  },

  /**
   * Expands the scope, showing all the added details.
   */
  async expand() {
    if (this._isExpanded || this._isLocked) {
      return;
    }
    if (this._variablesView._enumVisible) {
      this._openEnum();
    }
    if (this._variablesView._nonEnumVisible) {
      Services.tm.dispatchToMainThread({ run: this._openNonEnum });
    }
    this._isExpanded = true;

    if (this.onexpand) {
      // We return onexpand as it sometimes returns a promise
      // (up to the user of VariableView to do it)
      // that can indicate when the view is done expanding
      // and attributes are available. (Mostly used for tests)
      await this.onexpand(this);
    }
  },

  /**
   * Collapses the scope, hiding all the added details.
   */
  collapse() {
    if (!this._isExpanded || this._isLocked) {
      return;
    }
    this._arrow.removeAttribute("open");
    this._enum.removeAttribute("open");
    this._nonenum.removeAttribute("open");
    this._isExpanded = false;

    if (this.oncollapse) {
      this.oncollapse(this);
    }
  },

  /**
   * Toggles between the scope's collapsed and expanded state.
   */
  toggle(e) {
    if (e && e.button != 0) {
      // Only allow left-click to trigger this event.
      return;
    }
    this.expanded ^= 1;

    // Make sure the scope and its contents are visibile.
    for (const [, variable] of this._store) {
      variable.header = true;
      variable._matched = true;
    }
    if (this.ontoggle) {
      this.ontoggle(this);
    }
  },

  /**
   * Shows the scope's title header.
   */
  showHeader() {
    if (this._isHeaderVisible || !this._nameString) {
      return;
    }
    this._target.removeAttribute("untitled");
    this._isHeaderVisible = true;
  },

  /**
   * Hides the scope's title header.
   * This action will automatically expand the scope.
   */
  hideHeader() {
    if (!this._isHeaderVisible) {
      return;
    }
    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
   *         -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
   */
  _naturalSort(a, b) {
    if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
      return a < b ? -1 : 1;
    }
    return 0;
  },

  /**
   * Shows the scope's expand/collapse arrow.
   */
  showArrow() {
    if (this._isArrowVisible) {
      return;
    }
    this._arrow.removeAttribute("invisible");
    this._isArrowVisible = true;
  },

  /**
   * Hides the scope's expand/collapse arrow.
   */
  hideArrow() {
    if (!this._isArrowVisible) {
      return;
    }
    this._arrow.setAttribute("invisible", "");
    this._isArrowVisible = false;
  },

  /**
   * Gets the visibility state.
   * @return boolean
   */
  get visible() {
    return this._isContentVisible;
  },

  /**
   * Gets the expanded state.
   * @return boolean
   */
  get expanded() {
    return this._isExpanded;
  },

  /**
   * Gets the header visibility state.
   * @return boolean
   */
  get header() {
    return this._isHeaderVisible;
  },

  /**
   * Gets the twisty visibility state.
   * @return boolean
   */
  get twisty() {
    return this._isArrowVisible;
  },

  /**
   * Gets the expand lock state.
   * @return boolean
   */
  get locked() {
    return this._isLocked;
  },

  /**
   * Sets the visibility state.
   * @param boolean aFlag
   */
  set visible(aFlag) {
    aFlag ? this.show() : this.hide();
  },

  /**
   * Sets the expanded state.
   * @param boolean aFlag
   */
  set expanded(aFlag) {
    aFlag ? this.expand() : this.collapse();
  },

  /**
   * Sets the header visibility state.
   * @param boolean aFlag
   */
  set header(aFlag) {
    aFlag ? this.showHeader() : this.hideHeader();
  },

  /**
   * Sets the twisty visibility state.
   * @param boolean aFlag
   */
  set twisty(aFlag) {
    aFlag ? this.showArrow() : this.hideArrow();
  },

  /**
   * Sets the expand lock state.
   * @param boolean aFlag
   */
  set locked(aFlag) {
    this._isLocked = aFlag;
  },

  /**
   * Specifies if this target node may be focused.
   * @return boolean
   */
  get focusable() {
    // Check if this target node is actually visibile.
    if (
      !this._nameString ||
      !this._isContentVisible ||
      !this._isHeaderVisible ||
      !this._isMatch
    ) {
      return false;
    }
    // Check if all parent objects are expanded.
    let item = this;

    // Recurse while parent is a Scope, Variable, or Property
    while ((item = item.ownerView) && item instanceof Scope) {
      if (!item._isExpanded) {
        return false;
      }
    }
    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
   */
  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
   */
  removeEventListener(aName, aCallback, aCapture) {
    this._title.removeEventListener(aName, aCallback, aCapture);
  },

  /**
   * Gets the id associated with this item.
   * @return string
   */
  get id() {
    return this._idString;
  },

  /**
   * Gets the name associated with this item.
   * @return string
   */
  get name() {
    return this._nameString;
  },

  /**
   * Gets the displayed value for this item.
   * @return string
   */
  get displayValue() {
    return this._valueString;
  },

  /**
   * Gets the class names used for the displayed value.
   * @return string
   */
  get displayValueClassName() {
    return this._valueClassName;
  },

  /**
   * Gets the element associated with this item.
   * @return Node
   */
  get target() {
    return this._target;
  },

  /**
   * Initializes this scope's id, view and binds event listeners.
   *
   * @param string l10nId
   *        The scope localized string id.
   * @param object aFlags [optional]
   *        Additional options or flags for this scope.
   */
  _init(l10nId, aFlags) {
    this._idString = generateId((this._nameString = l10nId));
    this._displayScope({
      l10nId,
      targetClassName: `${this.targetClassName} ${aFlags.customClass}`,
      titleClassName: "devtools-toolbar",
    });
    this._addEventListeners();
    this.parentNode.appendChild(this._target);
  },

  /**
   * Creates the necessary nodes for this scope.
   *
   * @param Object options
   * @param string options.l10nId [optional]
   *        The scope localized string id.
   * @param string options.value [optional]
   *        The scope's name. Either this or l10nId need to be passed
   * @param string options.targetClassName
   *        A custom class name for this scope's target element.
   * @param string options.titleClassName [optional]
   *        A custom class name for this scope's title element.
   */
  _displayScope({ l10nId, value, targetClassName, titleClassName = "" }) {
    const document = this.document;

    const element = (this._target = document.createXULElement("vbox"));
    element.id = this._idString;
    element.className = targetClassName;

    const arrow = (this._arrow = document.createXULElement("hbox"));
    arrow.className = "arrow theme-twisty";

    const name = (this._name = document.createXULElement("label"));
    name.className = "name";
    if (l10nId) {
      document.l10n.setAttributes(name, l10nId);
    } else {
      name.setAttribute("value", value);
    }
    name.setAttribute("crop", "end");

    const title = (this._title = document.createXULElement("hbox"));
    title.className = "title " + titleClassName;
    title.setAttribute("align", "center");

    const enumerable = (this._enum = document.createXULElement("vbox"));
    const nonenum = (this._nonenum = document.createXULElement("vbox"));
    enumerable.className = "variables-view-element-details enum";
    nonenum.className = "variables-view-element-details nonenum";

    title.appendChild(arrow);
    title.appendChild(name);

    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.
   */
  _onClick(e) {
    if (
      this.editing ||
      e.button != 0 ||
      e.target == this._editNode ||
      e.target == this._deleteNode ||
      e.target == this._addPropertyNode
    ) {
      return;
    }
    this.toggle();
    this.focus();
  },

  /**
   * Opens the enumerable items container.
   */
  _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
   */
  set _enumVisible(aFlag) {
    for (const [, variable] of this._store) {
      variable._enumVisible = aFlag;

      if (!this._isExpanded) {
        continue;
      }
      if (aFlag) {
        this._enum.setAttribute("open", "");
      } else {
        this._enum.removeAttribute("open");
      }
    }
  },

  /**
   * Specifies if non-enumerable properties and variables should be displayed.
   * @param boolean aFlag
   */
  set _nonEnumVisible(aFlag) {
    for (const [, variable] of this._store) {
      variable._nonEnumVisible = aFlag;

      if (!this._isExpanded) {
        continue;
      }
      if (aFlag) {
        this._nonenum.setAttribute("open", "");
      } else {
        this._nonenum.removeAttribute("open");
      }
    }
  },

  /**
   * Performs a case insensitive search for variables or properties matching
   * the query, and hides non-matched items.
   *
   * @param string aLowerCaseQuery
   *        The lowercased name of the variable or property to search for.
   */
  _performSearch(aLowerCaseQuery) {
    for (let [, variable] of this._store) {
      const currentObject = variable;
      const lowerCaseName = variable._nameString.toLowerCase();
      const lowerCaseValue = variable._valueString.toLowerCase();

      // Non-matched variables or properties require a corresponding attribute.
      if (
        !lowerCaseName.includes(aLowerCaseQuery) &&
        !lowerCaseValue.includes(aLowerCaseQuery)
      ) {
        variable._matched = false;
      } else {
        // Variable or property is matched.
        variable._matched = true;

        // If the variable was ever expanded, there's a possibility it may
        // contain some matched properties, so make sure they're visible
        // ("expand downwards").
        if (variable._store.size) {
          variable.expand();
        }

        // If the variable is contained in another Scope, Variable, or Property,
        // the parent may not be a match, thus hidden. It should be visible
        // ("expand upwards").
        while ((variable = variable.ownerView) && variable instanceof Scope) {
          variable._matched = true;
          variable.expand();
        }
      }

      // Proceed with the search recursively inside this variable or property.
      if (
        currentObject._store.size ||
        currentObject.getter ||
        currentObject.setter
      ) {
        currentObject._performSearch(aLowerCaseQuery);
      }
    }
  },

  /**
   * Sets if this object instance is a matched or non-matched item.
   * @param boolean aStatus
   */
  set _matched(aStatus) {
    if (this._isMatch == aStatus) {
      return;
    }
    if (aStatus) {
      this._isMatch = true;
      this.target.removeAttribute("unmatched");
    } else {
      this._isMatch = false;
      this.target.setAttribute("unmatched", "");
    }
  },

  /**
   * Find the first item in the tree of visible items in this item that matches
   * the predicate. Searches in visual order (the order seen by the user).
   * 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
   *        A function that returns true when a match is found.
   * @return Scope | Variable | Property
   *         The first visible scope, variable or property, or null if nothing
   *         is found.
   */
  _findInVisibleItems(aPredicate) {
    if (aPredicate(this)) {
      return this;
    }

    if (this._isExpanded) {
      if (this._variablesView._enumVisible) {
        for (const item of this._enumItems) {
          const result = item._findInVisibleItems(aPredicate);
          if (result) {
            return result;
          }
        }
      }

      if (this._variablesView._nonEnumVisible) {
        for (const item of this._nonEnumItems) {
          const result = item._findInVisibleItems(aPredicate);
          if (result) {
            return result;
          }
        }
      }
    }

    return null;
  },

  /**
   * Find the last item in the tree of visible items in this item that matches
   * the predicate. Searches in reverse visual order (opposite of the order
   * seen by the user). Descends into first the non-enumerable children, then
   * the enumerable children (since they are presented in separate groups), and
   * finally tests itself.
   *
   * @param function aPredicate
   *        A function that returns true when a match is found.
   * @return Scope | Variable | Property
   *         The last visible scope, variable or property, or null if nothing
   *         is found.
   */
  _findInVisibleItemsReverse(aPredicate) {
    if (this._isExpanded) {
      if (this._variablesView._nonEnumVisible) {
        for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
          const item = this._nonEnumItems[i];
          const result = item._findInVisibleItemsReverse(aPredicate);
          if (result) {
            return result;
          }
        }
      }

      if (this._variablesView._enumVisible) {
        for (let i = this._enumItems.length - 1; i >= 0; i--) {
          const item = this._enumItems[i];
          const result = item._findInVisibleItemsReverse(aPredicate);
          if (result) {
            return result;
          }
        }
      }
    }

    if (aPredicate(this)) {
      return this;
    }

    return null;
  },

  /**
   * Gets top level variables view instance.
   * @return VariablesView
   */
  get _variablesView() {
    return (
      this._topView ||
      (this._topView = (() => {
        let parentView = this.ownerView;
        let topView;

        while ((topView = parentView.ownerView)) {
          parentView = topView;
        }
        return parentView;
      })())
    );
  },

  /**
   * Gets the parent node holding this scope.
   * @return Node
   */
  get parentNode() {
    return this.ownerView._list;
  },

  /**
   * Gets the owner document holding this scope.
   * @return HTMLDocument
   */
  get document() {
    return this._document || (this._document = this.ownerView.document);
  },

  /**
   * Gets the default window holding this scope.
   * @return nsIDOMWindow
   */
  get window() {
    return this._window || (this._window = this.ownerView.window);
  },

  _topView: null,
  _document: null,
  _window: null,

  ownerView: null,
  eval: null,
  switch: null,
  delete: null,
  new: null,
  preventDisableOnChange: false,
  preventDescriptorModifiers: false,
  editing: false,
  editableNameTooltip: "",
  editableValueTooltip: "",
  editButtonTooltip: "",
  deleteButtonTooltip: "",
  domNodeValueTooltip: "",
  contextMenuId: "",
  separatorStr: "",

  _store: null,
  _enumItems: null,
  _nonEnumItems: null,
  _fetched: false,
  _committed: false,
  _isLocked: 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,
};

// 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
// these are instantiated only if needed.
DevToolsUtils.defineLazyPrototypeGetter(
  Scope.prototype,
  "_store",
  () => new Map()
);
DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
DevToolsUtils.defineLazyPrototypeGetter(
  Scope.prototype,
  "_nonEnumItems",
  Array
);

/**
 * 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._setTooltips = this._setTooltips.bind(this);
  this._activateNameInput = this._activateNameInput.bind(this);
  this._activateValueInput = this._activateValueInput.bind(this);
  this.openNodeInInspector = this.openNodeInInspector.bind(this);
  this.highlightDomNode = this.highlightDomNode.bind(this);
  this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
  this._internalItem = aOptions.internalItem;

  // Treat safe getter descriptors as descriptors with a value.
  if ("getterValue" in aDescriptor) {
    aDescriptor.value = aDescriptor.getterValue;
    delete aDescriptor.get;
    delete aDescriptor.set;
  }

  Scope.call(this, aScope, aName, (this._initialDescriptor = aDescriptor));
  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",

  /**
   * Create a new Property that is a child of Variable.
   *
   * @param string aName
   *        The name of the new Property.
   * @param object aDescriptor
   *        The property's descriptor.
   * @param object aOptions
   *        Options of the form accepted by Scope.addItem
   * @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.
   */
  remove() {
    if (this._linkedToInspector) {
      this.unhighlightDomNode();
      this._valueLabel.removeEventListener("mouseover", this.highlightDomNode);
      this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode);
      this._openInspectorNode.removeEventListener(
        "mousedown",
        this.openNodeInInspector
      );
    }

    this.ownerView._store.delete(this._nameString);
    this._variablesView._itemsByElement.delete(this._target);
    this._variablesView._currHierarchy.delete(this.absoluteName);

    this._target.remove();

    for (const property of this._store.values()) {
      property.remove();
    }
  },

  /**
   * Populates this variable to contain all the properties of an object.
   *
   * @param object aObject
   *        The raw object you want to display.
   * @param object aOptions [optional]
   *        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
   */
  populate(aObject, aOptions = {}) {
    // Retrieve the properties only once.
    if (this._fetched) {
      return;
    }
    this._fetched = true;

    const propertyNames = Object.getOwnPropertyNames(aObject);
    const prototype = Object.getPrototypeOf(aObject);

    // Sort all of the properties before adding them, if preferred.
    if (aOptions.sorted) {
      propertyNames.sort(this._naturalSort);
    }

    // Add all the variable properties.
    for (const name of propertyNames) {
      const descriptor = Object.getOwnPropertyDescriptor(aObject, name);
      if (descriptor.get || descriptor.set) {
        const prop = this._addRawNonValueProperty(name, descriptor);
        if (aOptions.expanded) {
          prop.expanded = true;
        }
      } else {
        const prop = this._addRawValueProperty(name, descriptor, aObject[name]);
        if (aOptions.expanded) {
          prop.expanded = true;
        }
      }
    }
    // Add the variable's __proto__.
    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
   *        The target variable to populate.
   * @param object aObject [optional]
   *        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
   *        The property's name.
   * @param object aDescriptor
   *        Specifies the exact property descriptor as returned by a call to
   *        Object.getOwnPropertyDescriptor.
   * @param object aValue
   *        The raw property value you want to display.
   * @return Property
   *         The newly added property instance.
   */
  _addRawValueProperty(aName, aDescriptor, aValue) {
    const descriptor = Object.create(aDescriptor);
    descriptor.value = VariablesView.getGrip(aValue);

    const propertyItem = this.addItem(aName, descriptor);
    propertyItem._sourceValue = aValue;

    // Add an 'onexpand' callback for the property, lazily handling
    // the addition of new child properties.
    if (!VariablesView.isPrimitive(descriptor)) {
      propertyItem.onexpand = this._populateTarget;
    }
    return propertyItem;
  },

  /**
   * Adds a property for this variable based on a getter/setter descriptor.
   *
   * @param string aName
   *        The property's name.
   * @param object aDescriptor
   *        Specifies the exact property descriptor as returned by a call to
   *        Object.getOwnPropertyDescriptor.
   * @return Property
   *         The newly added property instance.
   */
  _addRawNonValueProperty(aName, aDescriptor) {
    const descriptor = Object.create(aDescriptor);
    descriptor.get = VariablesView.getGrip(aDescriptor.get);
    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
   */
  get symbolicName() {
    return this._nameString || "";
  },

  /**
   * Gets full path to this variable, including name of the scope.
   * @return string
   */
  get absoluteName() {
    if (this._absoluteName) {
      return this._absoluteName;
    }

    this._absoluteName =
      this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
    return this._absoluteName;
  },

  /**
   * Gets this variable's symbolic path to the topmost scope.
   * @return array
   * @see Variable._buildSymbolicPath
   */
  get symbolicPath() {
    if (this._symbolicPath) {
      return this._symbolicPath;
    }
    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
   */
  _buildSymbolicPath(path = []) {
    if (this.name) {
      path.unshift(this.name);
      if (this.ownerView instanceof Variable) {
        return this.ownerView._buildSymbolicPath(path);
      }
    }
    return path;
  },

  /**
   * Returns this variable's value from the descriptor if available.
   * @return any
   */
  get value() {
    return this._initialDescriptor.value;
  },

  /**
   * Returns this variable's getter from the descriptor if available.
   * @return object
   */
  get getter() {
    return this._initialDescriptor.get;
  },

  /**
   * Returns this variable's getter from the descriptor if available.
   * @return object
   */
  get setter() {
    return this._initialDescriptor.set;
  },

  /**
   * Sets the specific grip for this variable (applies the text content and
   * class name to the value label).
   *
   * 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 any aGrip
   *        Specifies the value and/or type & class of the variable.
   *        e.g. - 42
   *             - true
   *             - "nasu"
   *             - { type: "undefined" }
   *             - { type: "null" }
   *             - { type: "object", class: "Object" }
   */
  setGrip(aGrip) {
    // Don't allow displaying grip information if there's no name available
    // or the grip is malformed.
    if (
      this._nameString === undefined ||
      aGrip === undefined ||
      aGrip === null
    ) {
      return;
    }
    // Getters and setters should display grip information in sub-properties.
    if (this.getter || this.setter) {
      return;
    }

    const prevGrip = this._valueGrip;
    if (prevGrip) {
      this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
    }
    this._valueGrip = aGrip;

    if (
      aGrip &&
      (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)
    ) {
      if (aGrip.optimizedOut) {
        this._valueString = L10N.getStr("variablesViewOptimizedOut");
      } else if (aGrip.uninitialized) {
        this._valueString = L10N.getStr("variablesViewUninitialized");
      } else if (aGrip.missingArguments) {
        this._valueString = L10N.getStr("variablesViewMissingArgs");
      }
      this.eval = null;
    } else {
      this._valueString = VariablesView.getString(aGrip, {
        concise: true,
        noEllipsis: true,
      });
      this.eval = this.ownerView.eval;
    }

    this._valueClassName = VariablesView.getClass(aGrip);

    this._valueLabel.classList.add(this._valueClassName);
    this._valueLabel.setAttribute("value", this._valueString);
    this._separatorLabel.hidden = false;

    // DOMNodes get special treatment since they can be linked to the inspector
    if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
      this._linkToInspector();
    }
  },

  /**
   * Marks this variable as overridden.
   *
   * @param boolean aFlag
   *        Whether this variable is overridden or not.
   */
  setOverridden(aFlag) {
    if (aFlag) {
      this._target.setAttribute("overridden", "");
    } else {
      this._target.removeAttribute("overridden");
    }
  },

  /**
   * Briefly flashes this variable.
   *
   * @param number aDuration [optional]
   *        An optional flash animation duration.
   */
  flash(aDuration = ITEM_FLASH_DURATION) {
    const fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
    const fadeOutDelay = fadeInDelay + aDuration;

    setNamedTimeout("vview-flash-in" + this.absoluteName, fadeInDelay, () =>
      this._target.setAttribute("changed", "")
    );

    setNamedTimeout("vview-flash-out" + this.absoluteName, fadeOutDelay, () =>
      this._target.removeAttribute("changed")
    );
  },

  /**
   * Initializes this variable's id, view and binds event listeners.
   *
   * @param string aName
   *        The variable's name.
   */
  _init(aName) {
    this._idString = generateId((this._nameString = aName));
    this._displayScope({ value: aName, targetClassName: this.targetClassName });
    this._displayVariable();
    this._customizeVariable();
    this._prepareTooltips();
    this._setAttributes();
    this._addEventListeners();

    if (
      this._initialDescriptor.enumerable ||
      this._nameString == "this" ||
      this._internalItem
    ) {
      this.ownerView._enum.appendChild(this._target);
      this.ownerView._enumItems.push(this);
    } else {
      this.ownerView._nonenum.appendChild(this._target);
      this.ownerView._nonEnumItems.push(this);
    }
  },

  /**
   * Creates the necessary nodes for this variable.
   */
  _displayVariable() {
    const document = this.document;
    const descriptor = this._initialDescriptor;

    const separatorLabel = (this._separatorLabel =
      document.createXULElement("label"));
    separatorLabel.className = "separator";
    separatorLabel.setAttribute("value", this.separatorStr + " ");

    const valueLabel = (this._valueLabel = document.createXULElement("label"));
    valueLabel.className = "value";
    valueLabel.setAttribute("flex", "1");
    valueLabel.setAttribute("crop", "center");

    this._title.appendChild(separatorLabel);
    this._title.appendChild(valueLabel);

    if (VariablesView.isPrimitive(descriptor)) {
      this.hideArrow();
    }

    // If no value will be displayed, we don't need the separator.
    if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
      separatorLabel.hidden = true;
    }

    // If this is a getter/setter property, create two child pseudo-properties
    // called "get" and "set" that display the corresponding functions.
    if (descriptor.get || descriptor.set) {
      separatorLabel.hidden = true;
      valueLabel.hidden = true;

      // Changing getter/setter names is never allowed.
      this.switch = null;

      // Getter/setter properties require special handling when it comes to
      // evaluation and deletion.
      if (this.ownerView.eval) {
        this.delete = VariablesView.getterOrSetterDeleteCallback;
        this.evaluationMacro = VariablesView.overrideValueEvalMacro;
      } else {
        // Deleting getters and setters individually is not allowed if no
        // evaluation method is provided.
        this.delete = null;
        this.evaluationMacro = null;
      }

      const getter = this.addItem("get", { value: descriptor.get });
      const setter = this.addItem("set", { value: descriptor.set });
      getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
      setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;

      getter.hideArrow();
      setter.hideArrow();
      this.expand();
    }
  },

  /**
   * Adds specific nodes for this variable based on custom flags.
   */
  _customizeVariable() {
    const ownerView = this.ownerView;
    const descriptor = this._initialDescriptor;

    if ((ownerView.eval && this.getter) || this.setter) {
      const editNode = (this._editNode =
        this.document.createXULElement("toolbarbutton"));
      editNode.className = "variables-view-edit";
      editNode.addEventListener("mousedown", this._onEdit.bind(this));
      this._title.insertBefore(editNode, this._spacer);
    }

    if (ownerView.delete) {
      const deleteNode = (this._deleteNode =
        this.document.createXULElement("toolbarbutton"));
      deleteNode.className = "variables-view-delete";
      deleteNode.addEventListener("click", this._onDelete.bind(this));
      this._title.appendChild(deleteNode);
    }

    if (ownerView.new) {
      const addPropertyNode = (this._addPropertyNode =
        this.document.createXULElement("toolbarbutton"));
      addPropertyNode.className = "variables-view-add-property";
      addPropertyNode.addEventListener(
        "mousedown",
        this._onAddProperty.bind(this)
      );
      this._title.appendChild(addPropertyNode);

      // Can't add properties to primitive values, hide the node in those cases.
      if (VariablesView.isPrimitive(descriptor)) {
        addPropertyNode.setAttribute("invisible", "");
      }
    }

    if (ownerView.contextMenuId) {
      this._title.setAttribute("context", ownerView.contextMenuId);
    }

    if (ownerView.preventDescriptorModifiers) {
      return;
    }

    if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
      const nonWritableIcon = this.document.createXULElement("hbox");
      nonWritableIcon.className = "variable-or-property-non-writable-icon";
      nonWritableIcon.setAttribute("optional-visibility", "");
      this._title.appendChild(nonWritableIcon);
    }
    if (descriptor.value && typeof descriptor.value == "object") {
      if (descriptor.value.frozen) {
        const frozenLabel = this.document.createXULElement("label");
        frozenLabel.className = "variable-or-property-frozen-label";
        frozenLabel.setAttribute("optional-visibility", "");
        frozenLabel.setAttribute("value", "F");
        this._title.appendChild(frozenLabel);
      }
      if (descriptor.value.sealed) {
        const sealedLabel = this.document.createXULElement("label");
        sealedLabel.className = "variable-or-property-sealed-label";
        sealedLabel.setAttribute("optional-visibility", "");
        sealedLabel.setAttribute("value", "S");
        this._title.appendChild(sealedLabel);
      }
      if (!descriptor.value.extensible) {
        const nonExtensibleLabel = this.document.createXULElement("label");
        nonExtensibleLabel.className =
          "variable-or-property-non-extensible-label";
        nonExtensibleLabel.setAttribute("optional-visibility", "");
        nonExtensibleLabel.setAttribute("value", "N");
        this._title.appendChild(nonExtensibleLabel);
      }
    }
  },

  /**
   * Prepares all tooltips for this variable.
   */
  _prepareTooltips() {
    this._target.addEventListener("mouseover", this._setTooltips);
  },

  /**
   * Sets all tooltips for this variable.
   */
  _setTooltips() {
    this._target.removeEventListener("mouseover", this._setTooltips);

    const ownerView = this.ownerView;
    if (ownerView.preventDescriptorModifiers) {
      return;
    }

    const tooltip = this.document.createXULElement("tooltip");
    tooltip.id = "tooltip-" + this._idString;
    tooltip.setAttribute("orient", "horizontal");

    const labels = [
      "configurable",
      "enumerable",
      "writable",
      "frozen",
      "sealed",
      "extensible",
      "overridden",
      "WebIDL",
    ];

    for (const type of labels) {
      const labelElement = this.document.createXULElement("label");
      labelElement.className = type;
      labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
      tooltip.appendChild(labelElement);
    }

    this._target.appendChild(tooltip);
    this._target.setAttribute("tooltip", tooltip.id);

    if (this._editNode && ownerView.eval) {
      this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
    }
    if (this._openInspectorNode && this._linkedToInspector) {
      this._openInspectorNode.setAttribute(
        "tooltiptext",
        this.ownerView.domNodeValueTooltip
      );
    }
    if (this._valueLabel && ownerView.eval) {
      this._valueLabel.setAttribute(
        "tooltiptext",
        ownerView.editableValueTooltip
      );
    }
    if (this._name && ownerView.switch) {
      this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
    }
    if (this._deleteNode && ownerView.delete) {
      this._deleteNode.setAttribute(
        "tooltiptext",
        ownerView.deleteButtonTooltip
      );
    }
  },

  /**
   * Get the parent variablesview toolbox, if any.
   */
  get toolbox() {
    return this._variablesView.toolbox;
  },

  /**
   * Checks if this variable is a DOMNode and is part of a variablesview that
   * has been linked to the toolbox, so that highlighting and jumping to the
   * inspector can be done.
   */
  _isLinkableToInspector() {
    const isDomNode =
      this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
    const hasBeenLinked = this._linkedToInspector;
    const hasToolbox = !!this.toolbox;

    return isDomNode && !hasBeenLinked && hasToolbox;
  },

  /**
   * If the variable is a DOMNode, and if a toolbox is set, then link it to the
   * inspector (highlight on hover, and jump to markup-view on click)
   */
  _linkToInspector() {
    if (!this._isLinkableToInspector()) {
      return;
    }

    // Listen to value mouseover/click events to highlight and jump
    this._valueLabel.addEventListener("mouseover", this.highlightDomNode);
    this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode);

    // Add a button to open the node in the inspector
    this._openInspectorNode = this.document.createXULElement("toolbarbutton");
    this._openInspectorNode.className = "variables-view-open-inspector";
    this._openInspectorNode.addEventListener(
      "mousedown",
      this.openNodeInInspector
    );
    this._title.appendChild(this._openInspectorNode);

    this._linkedToInspector = true;
  },

  /**
   * In case this variable is a DOMNode and part of a variablesview that has been
   * linked to the toolbox's inspector, then select the corresponding node in
   * the inspector, and switch the inspector tool in the toolbox
   * @return a promise that resolves when the node is selected and the inspector
   * has been switched to and is ready
   */
  openNodeInInspector(event) {
    if (!this.toolbox) {
      return Promise.reject(new Error("Toolbox not available"));
    }

    event && event.stopPropagation();

    return async function () {
      let nodeFront = this._nodeFront;
      if (!nodeFront) {
        const inspectorFront = await this.toolbox.target.getFront("inspector");
        nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
          this._valueGrip
        );
      }

      if (nodeFront) {
        await this.toolbox.selectTool("inspector");

        const inspectorReady = new Promise(resolve => {
          this.toolbox.getPanel("inspector").once("inspector-updated", resolve);
        });

        await this.toolbox.selection.setNodeFront(nodeFront, {
          reason: "variables-view",
        });
        await inspectorReady;
      }
    }.bind(this)();
  },

  /**
   * In case this variable is a DOMNode and part of a variablesview that has been
   * linked to the toolbox's inspector, then highlight the corresponding node
   */
  async highlightDomNode() {
    if (!this.toolbox) {
      return;
    }

    if (!this._nodeFront) {
      const inspectorFront = await this.toolbox.target.getFront("inspector");
      this._nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
        this._valueGrip
      );
    }

    await this.toolbox.getHighlighter().highlight(this._nodeFront);
  },

  /**
   * Unhighlight a previously highlit node
   * @see highlightDomNode
   */
  unhighlightDomNode() {
    if (!this.toolbox) {
      return;
    }

    this.toolbox.getHighlighter().unhighlight();
  },

  /**
   * Sets a variable's configurable, enumerable and writable attributes,
   * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
   * reference.
   */
  // eslint-disable-next-line complexity
  _setAttributes() {
    const ownerView = this.ownerView;
    if (ownerView.preventDescriptorModifiers) {
      return;
    }

    const descriptor = this._initialDescriptor;
    const target = this._target;
    const name = this._nameString;

    if (ownerView.eval) {
      target.setAttribute("editable", "");
    }

    if (!descriptor.configurable) {
      target.setAttribute("non-configurable", "");
    }
    if (!descriptor.enumerable) {
      target.setAttribute("non-enumerable", "");
    }
    if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
      target.setAttribute("non-writable", "");
    }

    if (descriptor.value && typeof descriptor.value == "object") {
      if (descriptor.value.frozen) {
        target.setAttribute("frozen", "");
      }
      if (descriptor.value.sealed) {
        target.setAttribute("sealed", "");
      }
      if (!descriptor.value.extensible) {
        target.setAttribute("non-extensible", "");
      }
    }

    if (descriptor && "getterValue" in descriptor) {
      target.setAttribute("safe-getter", "");
    }

    if (name == "this") {
      target.setAttribute("self", "");
    } else if (this._internalItem && name == "<exception>") {
      target.setAttribute("exception", "");
      target.setAttribute("pseudo-item", "");
    } else if (this._internalItem && name == "<return>") {
      target.setAttribute("return", "");
      target.setAttribute("pseudo-item", "");
    } else if (name == "__proto__") {
      target.setAttribute("proto", "");
      target.setAttribute("pseudo-item", "");
    }

    if (!Object.keys(descriptor).length) {
      target.setAttribute("pseudo-item", "");
    }
  },

  /**
   * Adds the necessary event listeners for this variable.
   */
  _addEventListeners() {
    this._name.addEventListener("dblclick", this._activateNameInput);
    this._valueLabel.addEventListener("mousedown", this._activateValueInput);
    this._title.addEventListener("mousedown", this._onClick);
  },

  /**
   * Makes this variable's name editable.
   */
  _activateNameInput(e) {
    if (!this._variablesView.alignedValues) {
      this._separatorLabel.hidden = true;
      this._valueLabel.hidden = true;
    }

    EditableName.create(
      this,
      {
        onSave: aKey => {
          if (!this._variablesView.preventDisableOnChange) {
            this._disable();
          }
          this.ownerView.switch(this, aKey);
        },
        onCleanup: () => {
          if (!this._variablesView.alignedValues) {
            this._separatorLabel.hidden = false;
            this._valueLabel.hidden = false;
          }
        },
      },
      e
    );
  },

  /**
   * Makes this variable's value editable.
   */
  _activateValueInput(e) {
    EditableValue.create(
      this,
      {
        onSave: aString => {
          if (this._linkedToInspector) {
            this.unhighlightDomNode();
          }
          if (!this._variablesView.preventDisableOnChange) {
            this._disable();
          }
          this.ownerView.eval(this, aString);
        },
      },
      e
    );
  },

  /**
   * Disables this variable prior to a new name switch or value evaluation.
   */
  _disable() {
    // Prevent the variable from being collapsed or expanded.
    this.hideArrow();

    // Hide any nodes that may offer information about the variable.
    for (const node of this._title.childNodes) {
      node.hidden = node != this._arrow && node != this._name;
    }
    this._enum.hidden = true;
    this._nonenum.hidden = true;
  },

  /**
   * The current macro used to generate the string evaluated when performing
   * a variable or property value change.
   */
  evaluationMacro: VariablesView.simpleValueEvalMacro,

  /**
   * The click listener for the edit button.
   */
  _onEdit(e) {
    if (e.button != 0) {
      return;
    }

    e.preventDefault();
    e.stopPropagation();
    this._activateValueInput();
  },

  /**
   * The click listener for the delete button.
   */
  _onDelete(e) {
    if ("button" in e && e.button != 0) {
      return;
    }

    e.preventDefault();
    e.stopPropagation();

    if (this.ownerView.delete) {
      if (!this.ownerView.delete(this)) {
        this.hide();
      }
    }
  },

  /**
   * The click listener for the add property button.
   */
  _onAddProperty(e) {
    if ("button" in e && e.button != 0) {
      return;
    }

    e.preventDefault();
    e.stopPropagation();

    this.expanded = true;

    const item = this.addItem(
      " ",
      {
        value: undefined,
        configurable: true,
        enumerable: true,
        writable: true,
      },
      { relaxed: true }
    );

    // Force showing the separator.
    item._separatorLabel.hidden = false;

    EditableNameAndValue.create(
      item,
      {
        onSave: ([aKey, aValue]) => {
          if (!this._variablesView.preventDisableOnChange) {
            this._disable();
          }
          this.ownerView.new(this, aKey, aValue);
        },
      },
      e
    );
  },

  _symbolicName: null,
  _symbolicPath: null,
  _absoluteName: null,
  _initialDescriptor: null,
  _separatorLabel: null,
  _valueLabel: null,
  _spacer: null,
  _editNode: null,
  _deleteNode: null,
  _addPropertyNode: null,
  _tooltip: 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);
}

Property.prototype = extend(Variable.prototype, {
  /**
   * The class name applied to this property's target element.
   */
  targetClassName: "variables-view-property variable-or-property",

  /**
   * @see Variable.symbolicName
   * @return string
   */
  get symbolicName() {
    if (this._symbolicName) {
      return this._symbolicName;
    }

    this._symbolicName =
      this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
    return this._symbolicName;
  },

  /**
   * @see Variable.absoluteName
   * @return string
   */
  get absoluteName() {
    if (this._absoluteName) {
      return this._absoluteName;
    }

    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;
    };

/**
 * Forget everything recorded about added scopes, variables or properties.
 * @see VariablesView.commitHierarchy
 */
VariablesView.prototype.clearHierarchy = function () {
  this._prevHierarchy.clear();
  this._currHierarchy.clear();
};

/**
 * Perform operations on all the VariablesView Scopes, Variables and Properties
 * after you've added all the items you wanted.
 *
 * Calling this method is optional, and does the following:
 *   - styles the items overridden by other items in parent scopes
 *   - reopens the items which were previously expanded
 *   - flashes the items whose values changed
 */
VariablesView.prototype.commitHierarchy = function () {
  for (const [, currItem] of this._currHierarchy) {
    // Avoid performing expensive operations.
    if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
      continue;
    }
    const overridden = this.isOverridden(currItem);
    if (overridden) {
      currItem.setOverridden(true);
    }
    const expanded = !currItem._committed && this.wasExpanded(currItem);
    if (expanded) {
      currItem.expand();
    }
    const changed = !currItem._committed && this.hasChanged(currItem);
    if (changed) {
      currItem.flash();
    }
    currItem._committed = true;
  }
  if (this.oncommit) {
    this.oncommit(this);
  }
};

// Some variables are likely to contain a very large number of properties.
// It would be a bad idea to re-expand them or perform expensive operations.
VariablesView.prototype.commitHierarchyIgnoredItems = extend(null, {
  window: true,
  this: true,
});

/**
 * Checks if the an item was previously expanded, if it existed in a
 * previous hierarchy.
 *
 * @param Scope | Variable | Property aItem
 *        The item to verify.
 * @return boolean
 *         Whether the item was expanded.
 */
VariablesView.prototype.wasExpanded = function (aItem) {
  if (!(aItem instanceof Scope)) {
    return false;
  }
  const prevItem = this._prevHierarchy.get(
    aItem.absoluteName || aItem._nameString
  );
  return prevItem ? prevItem._isExpanded : false;
};

/**
 * Checks if the an item's displayed value (a representation of the grip)
 * has changed, if it existed in a previous hierarchy.
 *
 * @param Variable | Property aItem
 *        The item to verify.
 * @return boolean
 *         Whether the item has changed.
 */
VariablesView.prototype.hasChanged = function (aItem) {
  // Only analyze Variables and Properties for displayed value changes.
  // Scopes are just collections of Variables and Properties and
  // don't have a "value", so they can't change.
  if (!(aItem instanceof Variable)) {
    return false;
  }
  const prevItem = this._prevHierarchy.get(aItem.absoluteName);
  return prevItem ? prevItem._valueString != aItem._valueString : false;
};

/**
 * Checks if the an item was previously expanded, if it existed in a
 * previous hierarchy.
 *
 * @param Scope | Variable | Property aItem
 *        The item to verify.
 * @return boolean
 *         Whether the item was expanded.
 */
VariablesView.prototype.isOverridden = function (aItem) {
  // Only analyze Variables for being overridden in different Scopes.
  if (!(aItem instanceof Variable) || aItem instanceof Property) {
    return false;
  }
  const currVariableName = aItem._nameString;
  const parentScopes = this.getParentScopesForVariableOrProperty(aItem);

  for (const otherScope of parentScopes) {
    for (const [otherVariableName] of otherScope) {
      if (otherVariableName == currVariableName) {
        return true;
      }
    }
  }
  return false;
};

/**
 * 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().
//
// Examples:
// (function foobar(a, b) { ...
// function foobar2(a) { ...
// function() { ...
const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;

/**
 * Helper function to deduce the name of the provided function.
 *
 * @param function function
 *        The function whose name will be returned.
 * @return string
 *         Function name.
 */
function getFunctionName(func) {
  let name = null;
  if (func.name) {
    name = func.name;
  } else {
    let desc;
    try {
      desc = func.getOwnPropertyDescriptor("displayName");
    } catch (ex) {
      // Ignore.
    }
    if (desc && typeof desc.value == "string") {
      name = desc.value;
    }
  }
  if (!name) {
    try {
      const str = (func.toString() || func.toSource()) + "";
      name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
    } catch (ex) {
      // Ignore.
    }
  }
  return name;
}

/**
 * Get the object class name. For example, the |window| object has the Window
 * class name (based on [object Window]).
 *
 * @param object object
 *        The object you want to get the class name for.
 * @return string
 *         The object class name.
 */
function getObjectClassName(object) {
  if (object === null) {
    return "null";
  }
  if (object === undefined) {
    return "undefined";
  }

  const type = typeof object;
  if (type != "object") {
    // Grip class names should start with an uppercase letter.
    return type.charAt(0).toUpperCase() + type.substr(1);
  }

  let className;

  try {
    className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
    if (!className) {
      className = ((object.constructor + "").match(/^\[object (\S+)\]$/) ||
        [])[1];
    }
    if (!className && typeof object.constructor == "function") {
      className = getFunctionName(object.constructor);
    }
  } catch (ex) {
    // Ignore.
  }

  return className;
}

/**
 * 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.
 *
 * @param string aName
 *        An optional string to prefix the id with.
 * @return number
 *         A unique id.
 */
var generateId = (function () {
  let count = 0;
  return function (aName = "") {
    return aName.toLowerCase().trim().replace(/\s+/g, "-") + ++count;
  };
})();

/**
 * Quote and escape a string. The result will be another string containing an
 * ECMAScript StringLiteral which will produce the original one when evaluated
 * by `eval` or similar.
 *
 * @param string aString
 *       An optional string to be escaped. If no string is passed, the function
 *       returns an empty string.
 * @return string
 */
function escapeString(aString) {
  if (typeof aString !== "string") {
    return "";
  }
  // U+2028 and U+2029 are allowed in JSON but not in ECMAScript string literals.
  return JSON.stringify(aString)
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029");
}

/**
 * Escape some HTML special characters. We do not need full HTML serialization
 * here, we just want to make strings safe to display in HTML attributes, for
 * the stringifiers.
 *
 * @param string aString
 * @return string
 */
export function escapeHTML(aString) {
  return aString
    .replace(/&/g, "&")
    .replace(/"/g, """)
    .replace(/</g, "<")
    .replace(/>/g, ">");
}

/**
 * An Editable encapsulates the UI of an edit box that overlays a label,
 * allowing the user to edit the value.
 *
 * @param Variable aVariable
 *        The Variable or Property to make editable.
 * @param object aOptions
 *        - onSave
 *          The callback to call with the value when editing is complete.
 *        - onCleanup
 *          The callback to call when the editable is removed for any reason.
 */
function Editable(aVariable, aOptions) {
  this._variable = aVariable;
  this._onSave = aOptions.onSave;
  this._onCleanup = aOptions.onCleanup;
}

Editable.create = function (aVariable, aOptions, aEvent) {
  const editable = new this(aVariable, aOptions);
  editable.activate(aEvent);
  return editable;
};

Editable.prototype = {
  /**
   * The class name for targeting this Editable type's label element. Overridden
   * by inheriting classes.
   */
  className: null,

  /**
   * Boolean indicating whether this Editable should activate. Overridden by
   * inheriting classes.
   */
  shouldActivate: null,

  /**
   * The label element for this Editable. Overridden by inheriting classes.
   */
  label: null,

  /**
   * Activate this editable by replacing the input box it overlays and
   * initialize the handlers.
   *
   * @param Event e [optional]
   *        Optionally, the Event object that was used to activate the Editable.
   */
  activate(e) {
    if (!this.shouldActivate) {
      this._onCleanup && this._onCleanup();
      return;
    }

    const { label } = this;
    const initialString = label.getAttribute("value");

    if (e) {
      e.preventDefault();
      e.stopPropagation();
    }

    // Create a texbox input element which will be shown in the current
    // element's specified label location.
    const input = (this._input = this._variable.document.createElementNS(
      HTML_NS,
      "input"
    ));
    input.className = this.className;
    input.setAttribute("value", initialString);

    // Replace the specified label with a textbox input element.
    label.parentNode.replaceChild(input, label);
    input.scrollIntoView({ block: "nearest" });
    input.select();

    // When the value is a string (displayed as "value"), then we probably want
    // to change it to another string in the textbox, so to avoid typing the ""
    // again, tackle with the selection bounds just a bit.
    if (initialString.match(/^".+"$/)) {
      input.selectionEnd--;
      input.selectionStart++;
    }

    this._onKeydown = this._onKeydown.bind(this);
    this._onBlur = this._onBlur.bind(this);
    input.addEventListener("keydown", this._onKeydown);
    input.addEventListener("blur", this._onBlur);

    this._prevExpandable = this._variable.twisty;
    this._prevExpanded = this._variable.expanded;
    this._variable.collapse();
    this._variable.hideArrow();
    this._variable.locked = true;
    this._variable.editing = true;
  },

  /**
   * Remove the input box and restore the Variable or Property to its previous
   * state.
   */
  deactivate() {
    this._input.removeEventListener("keydown", this._onKeydown);
    this._input.removeEventListener("blur", this.deactivate);
    this._input.parentNode.replaceChild(this.label, this._input);
    this._input = null;

    const scrollbox = this._variable._variablesView._list;
    scrollbox.scrollBy(-this._variable._target, 0);
    this._variable.locked = false;
    this._variable.twisty = this._prevExpandable;
    this._variable.expanded = this._prevExpanded;
    this._variable.editing = false;
    this._onCleanup && this._onCleanup();
  },

  /**
   * Save the current value and deactivate the Editable.
   */
  _save() {
    const initial = this.label.getAttribute("value");
    const current = this._input.value.trim();
    this.deactivate();
    if (initial != current) {
      this._onSave(current);
    }
  },

  /**
   * Called when tab is pressed, allowing subclasses to link different
   * behavior to tabbing if desired.
   */
  _next() {
    this._save();
  },

  /**
   * Called when escape is pressed, indicating a cancelling of editing without
   * saving.
   */
  _reset() {
    this.deactivate();
    this._variable.focus();
  },

  /**
   * Event handler for when the input loses focus.
   */
  _onBlur() {
    this.deactivate();
  },

  /**
   * Event handler for when the input receives a key press.
   */
  _onKeydown(e) {
    e.stopPropagation();

    switch (e.keyCode) {
      case KeyCodes.DOM_VK_TAB:
        this._next();
        break;
      case KeyCodes.DOM_VK_RETURN:
        this._save();
        break;
      case KeyCodes.DOM_VK_ESCAPE:
        this._reset();
        break;
    }
  },
};

/**
 * An Editable specific to editing the name of a Variable or Property.
 */
function EditableName(aVariable, aOptions) {
  Editable.call(this, aVariable, aOptions);
}

EditableName.create = Editable.create;

EditableName.prototype = extend(Editable.prototype, {
  className: "element-name-input",

  get label() {
    return this._variable._name;
  },

  get shouldActivate() {
    return !!this._variable.ownerView.switch;
  },
});

/**
 * An Editable specific to editing the value of a Variable or Property.
 */
function EditableValue(aVariable, aOptions) {
  Editable.call(this, aVariable, aOptions);
}

EditableValue.create = Editable.create;

EditableValue.prototype = extend(Editable.prototype, {
  className: "element-value-input",

  get label() {
    return this._variable._valueLabel;
  },

  get shouldActivate() {
    return !!this._variable.ownerView.eval;
  },
});

/**
 * An Editable specific to editing the key and value of a new property.
 */
function EditableNameAndValue(aVariable, aOptions) {
  EditableName.call(this, aVariable, aOptions);
}

EditableNameAndValue.create = Editable.create;

EditableNameAndValue.prototype = extend(EditableName.prototype, {
  _reset() {
    // Hide the Variable or Property if the user presses escape.
    this._variable.remove();
    this.deactivate();
  },

  _next() {
    // Override _next so as to set both key and value at the same time.
    const key = this._input.value;
    this.label.setAttribute("value", key);

    const valueEditable = EditableValue.create(this._variable, {
      onSave: aValue => {
        this._onSave([key, aValue]);
      },
    });
    valueEditable._reset = () => {
      this._variable.remove();
      valueEditable.deactivate();
    };
  },

  _save(e) {
    // Both _save and _next activate the value edit box.
    this._next(e);
  },
});

[zur Elbe Produktseite wechseln0.82QuellennavigatorsAnalyse erneut starten2026-05-08]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge