tor-browser

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

VariablesView.sys.mjs (87658B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* eslint-disable mozilla/no-aArgs */
      6 
      7 const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
      8 const LAZY_EMPTY_DELAY = 150; // ms
      9 const SCROLL_PAGE_SIZE_DEFAULT = 0;
     10 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
     11 const PAGE_SIZE_MAX_JUMPS = 30;
     12 const SEARCH_ACTION_MAX_DELAY = 300; // ms
     13 
     14 import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
     15 
     16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     17 
     18 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     19 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     20 const {
     21  getSourceNames,
     22 } = require("resource://devtools/client/shared/source-utils.js");
     23 const {
     24  ViewHelpers,
     25  setNamedTimeout,
     26 } = require("resource://devtools/client/shared/widgets/view-helpers.js");
     27 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
     28 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
     29 const { PluralForm } = require("resource://devtools/shared/plural-form.js");
     30 const {
     31  LocalizationHelper,
     32  ELLIPSIS,
     33 } = require("resource://devtools/shared/l10n.js");
     34 
     35 const L10N = new LocalizationHelper(DBG_STRINGS_URI);
     36 const HTML_NS = "http://www.w3.org/1999/xhtml";
     37 
     38 const lazy = {};
     39 
     40 XPCOMUtils.defineLazyServiceGetter(
     41  lazy,
     42  "clipboardHelper",
     43  "@mozilla.org/widget/clipboardhelper;1",
     44  Ci.nsIClipboardHelper
     45 );
     46 
     47 /**
     48 * A tree view for inspecting scopes, objects and properties.
     49 * Iterable via "for (let [id, scope] of instance) { }".
     50 * Requires the devtools common.css and debugger.css skin stylesheets.
     51 */
     52 export class VariablesView extends EventEmitter {
     53  /**
     54   * @param {Node} aParentNode
     55   *        The parent node to hold this view.
     56   * @param {object} [aFlags={}]
     57   *        An object contaning initialization options for this view.
     58   *        e.g. { lazyEmpty: true, searchEnabled: true ... }
     59   */
     60  constructor(aParentNode, aFlags = {}) {
     61    super();
     62 
     63    this._store = []; // Can't use a Map because Scope names needn't be unique.
     64    this._itemsByElement = new WeakMap();
     65 
     66    // Note: The hierarchy is only used for an assertion in a test at the moment,
     67    // to easily check the tree structure.
     68    this._testOnlyHierarchy = new Map();
     69 
     70    this._parent = aParentNode;
     71    this._parent.classList.add("variables-view-container");
     72    this._parent.classList.add("theme-body");
     73    this._appendEmptyNotice();
     74 
     75    this._onSearchboxInput = this._onSearchboxInput.bind(this);
     76    this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this);
     77    this._onViewKeyDown = this._onViewKeyDown.bind(this);
     78 
     79    // Create an internal scrollbox container.
     80    this._list = this.document.createXULElement("scrollbox");
     81    this._list.setAttribute("orient", "vertical");
     82    this._list.addEventListener("keydown", this._onViewKeyDown);
     83    this._parent.appendChild(this._list);
     84 
     85    for (const name in aFlags) {
     86      this[name] = aFlags[name];
     87    }
     88  }
     89 
     90  /**
     91   * Helper setter for populating this container with a raw object.
     92   *
     93   * @param {object} aObject
     94   *        The raw object to display. You can only provide this object
     95   *        if you want the variables view to work in sync mode.
     96   */
     97  set rawObject(aObject) {
     98    this.empty();
     99    this.addScope()
    100      .addItem(undefined, { enumerable: true })
    101      .populate(aObject, { sorted: true });
    102  }
    103 
    104  /**
    105   * Adds a scope to contain any inspected variables.
    106   *
    107   * This new scope will be considered the parent of any other scope
    108   * added afterwards.
    109   *
    110   * @param {string} l10nId
    111   *        The scope localized string id.
    112   * @param {string} aCustomClass
    113   *        An additional class name for the containing element.
    114   * @return {Scope}
    115   *         The newly created Scope instance.
    116   */
    117  addScope(l10nId = "", aCustomClass = "") {
    118    this._removeEmptyNotice();
    119    this._toggleSearchVisibility(true);
    120 
    121    const scope = new Scope(this, l10nId, { customClass: aCustomClass });
    122    this._store.push(scope);
    123    this._itemsByElement.set(scope._target, scope);
    124    this._testOnlyHierarchy.set(l10nId, scope);
    125    scope.header = !!l10nId;
    126 
    127    return scope;
    128  }
    129 
    130  /**
    131   * Removes all items from this container.
    132   *
    133   * @param {number} [aTimeout]
    134   *        The number of milliseconds to delay the operation if
    135   *        lazy emptying of this container is enabled.
    136   */
    137  empty(aTimeout = this.lazyEmptyDelay) {
    138    // If there are no items in this container, emptying is useless.
    139    if (!this._store.length) {
    140      return;
    141    }
    142 
    143    this._store.length = 0;
    144    this._itemsByElement = new WeakMap();
    145    this._testOnlyHierarchy = new Map();
    146 
    147    // Check if this empty operation may be executed lazily.
    148    if (this.lazyEmpty && aTimeout > 0) {
    149      this._emptySoon(aTimeout);
    150      return;
    151    }
    152 
    153    this._list.replaceChildren();
    154    this._appendEmptyNotice();
    155    this._toggleSearchVisibility(false);
    156  }
    157 
    158  /**
    159   * Emptying this container and rebuilding it immediately afterwards would
    160   * result in a brief redraw flicker, because the previously expanded nodes
    161   * may get asynchronously re-expanded, after fetching the prototype and
    162   * properties from a server.
    163   *
    164   * To avoid such behaviour, a normal container list is rebuild, but not
    165   * immediately attached to the parent container. The old container list
    166   * is kept around for a short period of time, hopefully accounting for the
    167   * data fetching delay. In the meantime, any operations can be executed
    168   * normally.
    169   *
    170   * @see VariablesView.empty
    171   */
    172  _emptySoon(aTimeout) {
    173    const prevList = this._list;
    174    const currList = (this._list = this.document.createXULElement("scrollbox"));
    175 
    176    this.window.setTimeout(() => {
    177      prevList.removeEventListener("keydown", this._onViewKeyDown);
    178      currList.addEventListener("keydown", this._onViewKeyDown);
    179      currList.setAttribute("orient", "vertical");
    180 
    181      this._parent.removeChild(prevList);
    182      this._parent.appendChild(currList);
    183 
    184      if (!this._store.length) {
    185        this._appendEmptyNotice();
    186        this._toggleSearchVisibility(false);
    187      }
    188    }, aTimeout);
    189  }
    190 
    191  /**
    192   * The amount of time (in milliseconds) it takes to empty this view lazily.
    193   */
    194  lazyEmptyDelay = LAZY_EMPTY_DELAY;
    195 
    196  /**
    197   * Specifies if this view may be emptied lazily.
    198   *
    199   * @see VariablesView.prototype.empty
    200   */
    201  lazyEmpty = false;
    202 
    203  /**
    204   * The number of elements in this container to jump when Page Up or Page Down
    205   * keys are pressed. If falsy, then the page size will be based on the
    206   * container height.
    207   */
    208  scrollPageSize = SCROLL_PAGE_SIZE_DEFAULT;
    209 
    210  /**
    211   * Specifies the context menu attribute set on variables and properties.
    212   *
    213   * This flag is applied recursively onto each scope in this view and
    214   * affects only the child nodes when they're created.
    215   */
    216  contextMenuId = "";
    217 
    218  /**
    219   * The separator label between the variables or properties name and value.
    220   *
    221   * This flag is applied recursively onto each scope in this view and
    222   * affects only the child nodes when they're created.
    223   */
    224  separatorStr = L10N.getStr("variablesSeparatorLabel");
    225 
    226  /**
    227   * Specifies if enumerable properties and variables should be displayed.
    228   * These variables and properties are visible by default.
    229   *
    230   * @param {boolean} aFlag
    231   */
    232  set enumVisible(aFlag) {
    233    this._enumVisible = aFlag;
    234 
    235    for (const scope of this._store) {
    236      scope._enumVisible = aFlag;
    237    }
    238  }
    239 
    240  /**
    241   * Specifies if non-enumerable properties and variables should be displayed.
    242   * These variables and properties are visible by default.
    243   *
    244   * @param {boolean} aFlag
    245   */
    246  set nonEnumVisible(aFlag) {
    247    this._nonEnumVisible = aFlag;
    248 
    249    for (const scope of this._store) {
    250      scope._nonEnumVisible = aFlag;
    251    }
    252  }
    253 
    254  /**
    255   * Specifies if only enumerable properties and variables should be displayed.
    256   * Both types of these variables and properties are visible by default.
    257   *
    258   * @param {boolean} aFlag
    259   */
    260  set onlyEnumVisible(aFlag) {
    261    if (aFlag) {
    262      this.enumVisible = true;
    263      this.nonEnumVisible = false;
    264    } else {
    265      this.enumVisible = true;
    266      this.nonEnumVisible = true;
    267    }
    268  }
    269 
    270  /**
    271   * Sets if the variable and property searching is enabled.
    272   *
    273   * @param {boolean} aFlag
    274   */
    275  set searchEnabled(aFlag) {
    276    aFlag ? this._enableSearch() : this._disableSearch();
    277  }
    278 
    279  /**
    280   * Gets if the variable and property searching is enabled.
    281   *
    282   * @return {boolean}
    283   */
    284  get searchEnabled() {
    285    return !!this._searchboxContainer;
    286  }
    287 
    288  /**
    289   * Enables variable and property searching in this view.
    290   * Use the "searchEnabled" setter to enable searching.
    291   */
    292  _enableSearch() {
    293    // If searching was already enabled, no need to re-enable it again.
    294    if (this._searchboxContainer) {
    295      return;
    296    }
    297    const document = this.document;
    298    const ownerNode = this._parent.parentNode;
    299 
    300    const container = (this._searchboxContainer =
    301      document.createXULElement("hbox"));
    302    container.className = "devtools-toolbar devtools-input-toolbar";
    303 
    304    // Hide the variables searchbox container if there are no variables or
    305    // properties to display.
    306    container.hidden = !this._store.length;
    307 
    308    const searchbox = (this._searchboxNode = document.createElementNS(
    309      HTML_NS,
    310      "input"
    311    ));
    312    searchbox.className = "variables-view-searchinput devtools-filterinput";
    313    document.l10n.setAttributes(searchbox, "storage-variable-view-search-box");
    314    searchbox.addEventListener("input", this._onSearchboxInput);
    315    searchbox.addEventListener("keydown", this._onSearchboxKeyDown);
    316 
    317    container.appendChild(searchbox);
    318    ownerNode.insertBefore(container, this._parent);
    319  }
    320 
    321  /**
    322   * Disables variable and property searching in this view.
    323   * Use the "searchEnabled" setter to disable searching.
    324   */
    325  _disableSearch() {
    326    // If searching was already disabled, no need to re-disable it again.
    327    if (!this._searchboxContainer) {
    328      return;
    329    }
    330    this._searchboxContainer.remove();
    331    this._searchboxNode.removeEventListener("input", this._onSearchboxInput);
    332    this._searchboxNode.removeEventListener(
    333      "keydown",
    334      this._onSearchboxKeyDown
    335    );
    336 
    337    this._searchboxContainer = null;
    338    this._searchboxNode = null;
    339  }
    340 
    341  /**
    342   * Sets the variables searchbox container hidden or visible.
    343   * It's hidden by default.
    344   *
    345   * @param {boolean} aVisibleFlag
    346   *        Specifies the intended visibility.
    347   */
    348  _toggleSearchVisibility(aVisibleFlag) {
    349    // If searching was already disabled, there's no need to hide it.
    350    if (!this._searchboxContainer) {
    351      return;
    352    }
    353    this._searchboxContainer.hidden = !aVisibleFlag;
    354  }
    355 
    356  /**
    357   * Listener handling the searchbox input event.
    358   */
    359  _onSearchboxInput() {
    360    this.scheduleSearch(this._searchboxNode.value);
    361  }
    362 
    363  /**
    364   * Listener handling the searchbox keydown event.
    365   */
    366  _onSearchboxKeyDown(e) {
    367    switch (e.keyCode) {
    368      case KeyCodes.DOM_VK_RETURN:
    369        this._onSearchboxInput();
    370        return;
    371      case KeyCodes.DOM_VK_ESCAPE:
    372        this._searchboxNode.value = "";
    373        this._onSearchboxInput();
    374    }
    375  }
    376 
    377  /**
    378   * Schedules searching for variables or properties matching the query.
    379   *
    380   * @param {string} aToken
    381   *        The variable or property to search for.
    382   * @param {number} aWait
    383   *        The amount of milliseconds to wait until draining.
    384   */
    385  scheduleSearch(aToken, aWait) {
    386    // The amount of time to wait for the requests to settle.
    387    const maxDelay = SEARCH_ACTION_MAX_DELAY;
    388    const delay = aWait === undefined ? maxDelay / aToken.length : aWait;
    389 
    390    // Allow requests to settle down first.
    391    setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
    392  }
    393 
    394  /**
    395   * Performs a case insensitive search for variables or properties matching
    396   * the query, and hides non-matched items.
    397   *
    398   * If aToken is falsy, then all the scopes are unhidden and expanded,
    399   * while the available variables and properties inside those scopes are
    400   * just unhidden.
    401   *
    402   * @param {string} aToken
    403   *        The variable or property to search for.
    404   */
    405  _doSearch(aToken) {
    406    for (const scope of this._store) {
    407      switch (aToken) {
    408        case "":
    409        case null:
    410        case undefined:
    411          scope.expand();
    412          scope._performSearch("");
    413          break;
    414        default:
    415          scope._performSearch(aToken.toLowerCase());
    416          break;
    417      }
    418    }
    419  }
    420 
    421  /**
    422   * Find the first item in the tree of visible items in this container that
    423   * matches the predicate. Searches in visual order (the order seen by the
    424   * user). Descends into each scope to check the scope and its children.
    425   *
    426   * @param {Function} aPredicate
    427   *        A function that returns true when a match is found.
    428   * @return {Scope | Variable | Property}
    429   *         The first visible scope, variable or property, or null if nothing
    430   *         is found.
    431   */
    432  _findInVisibleItems(aPredicate) {
    433    for (const scope of this._store) {
    434      const result = scope._findInVisibleItems(aPredicate);
    435      if (result) {
    436        return result;
    437      }
    438    }
    439    return null;
    440  }
    441 
    442  /**
    443   * Find the last item in the tree of visible items in this container that
    444   * matches the predicate. Searches in reverse visual order (opposite of the
    445   * order seen by the user). Descends into each scope to check the scope and
    446   * its children.
    447   *
    448   * @param {Function} aPredicate
    449   *        A function that returns true when a match is found.
    450   * @return {Scope | Variable | Property}
    451   *         The last visible scope, variable or property, or null if nothing
    452   *         is found.
    453   */
    454  _findInVisibleItemsReverse(aPredicate) {
    455    for (let i = this._store.length - 1; i >= 0; i--) {
    456      const scope = this._store[i];
    457      const result = scope._findInVisibleItemsReverse(aPredicate);
    458      if (result) {
    459        return result;
    460      }
    461    }
    462    return null;
    463  }
    464 
    465  /**
    466   * Gets the scope at the specified index.
    467   *
    468   * @param {number} aIndex
    469   *        The scope's index.
    470   * @return {Scope}
    471   *         The scope if found, undefined if not.
    472   */
    473  getScopeAtIndex(aIndex) {
    474    return this._store[aIndex];
    475  }
    476 
    477  /**
    478   * Recursively searches this container for the scope, variable or property
    479   * displayed by the specified node.
    480   *
    481   * @param {Node} aNode
    482   *        The node to search for.
    483   * @return Scope | Variable | Property
    484   *         The matched scope, variable or property, or null if nothing is found.
    485   */
    486  getItemForNode(aNode) {
    487    return this._itemsByElement.get(aNode);
    488  }
    489 
    490  /**
    491   * Gets the scope owning a Variable or Property.
    492   *
    493   * @param {Variable | Property} aItem
    494   *        The variable or property to retrieve the owner scope for.
    495   * @return Scope
    496   *         The owner scope.
    497   */
    498  getOwnerScopeForVariableOrProperty(aItem) {
    499    if (!aItem) {
    500      return null;
    501    }
    502    // If this is a Scope, return it.
    503    if (!(aItem instanceof Variable)) {
    504      return aItem;
    505    }
    506    // If this is a Variable or Property, find its owner scope.
    507    if (aItem instanceof Variable && aItem.ownerView) {
    508      return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
    509    }
    510    return null;
    511  }
    512 
    513  /**
    514   * Gets the parent scopes for a specified Variable or Property.
    515   * The returned list will not include the owner scope.
    516   *
    517   * @param {Variable | Property} aItem
    518   *        The variable or property for which to find the parent scopes.
    519   * @return array
    520   *         A list of parent Scopes.
    521   */
    522  getParentScopesForVariableOrProperty(aItem) {
    523    const scope = this.getOwnerScopeForVariableOrProperty(aItem);
    524    return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
    525  }
    526 
    527  /**
    528   * Gets the currently focused scope, variable or property in this view.
    529   *
    530   * @return {Scope | Variable | Property}
    531   *         The focused scope, variable or property, or null if nothing is found.
    532   */
    533  getFocusedItem() {
    534    const focused = this.document.commandDispatcher.focusedElement;
    535    return this.getItemForNode(focused);
    536  }
    537 
    538  /**
    539   * Focuses the first visible scope, variable, or property in this container.
    540   */
    541  focusFirstVisibleItem() {
    542    const focusableItem = this._findInVisibleItems(item => item.focusable);
    543    if (focusableItem) {
    544      this._focusItem(focusableItem);
    545    }
    546    this._parent.scrollTop = 0;
    547    this._parent.scrollLeft = 0;
    548  }
    549 
    550  /**
    551   * Focuses the last visible scope, variable, or property in this container.
    552   */
    553  focusLastVisibleItem() {
    554    const focusableItem = this._findInVisibleItemsReverse(
    555      item => item.focusable
    556    );
    557    if (focusableItem) {
    558      this._focusItem(focusableItem);
    559    }
    560    this._parent.scrollTop = this._parent.scrollHeight;
    561    this._parent.scrollLeft = 0;
    562  }
    563 
    564  /**
    565   * Focuses the next scope, variable or property in this view.
    566   */
    567  focusNextItem() {
    568    this.focusItemAtDelta(+1);
    569  }
    570 
    571  /**
    572   * Focuses the previous scope, variable or property in this view.
    573   */
    574  focusPrevItem() {
    575    this.focusItemAtDelta(-1);
    576  }
    577 
    578  /**
    579   * Focuses another scope, variable or property in this view, based on
    580   * the index distance from the currently focused item.
    581   *
    582   * @param {number} aDelta
    583   *        A scalar specifying by how many items should the selection change.
    584   */
    585  focusItemAtDelta(aDelta) {
    586    const direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
    587    let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
    588    while (distance--) {
    589      if (!this._focusChange(direction)) {
    590        break; // Out of bounds.
    591      }
    592    }
    593  }
    594 
    595  /**
    596   * Focuses the next or previous scope, variable or property in this view.
    597   *
    598   * @param {string} aDirection
    599   *        Either "advanceFocus" or "rewindFocus".
    600   * @return {boolean}
    601   *         False if the focus went out of bounds and the first or last element
    602   *         in this view was focused instead.
    603   */
    604  _focusChange(aDirection) {
    605    const commandDispatcher = this.document.commandDispatcher;
    606    const prevFocusedElement = commandDispatcher.focusedElement;
    607    let currFocusedItem = null;
    608 
    609    do {
    610      commandDispatcher[aDirection]();
    611 
    612      // Make sure the newly focused item is a part of this view.
    613      // If the focus goes out of bounds, revert the previously focused item.
    614      if (!(currFocusedItem = this.getFocusedItem())) {
    615        prevFocusedElement.focus();
    616        return false;
    617      }
    618    } while (!currFocusedItem.focusable);
    619 
    620    // Focus remained within bounds.
    621    return true;
    622  }
    623 
    624  /**
    625   * Focuses a scope, variable or property and makes sure it's visible.
    626   *
    627   * @param {Scope | Variable | Property} aItem
    628   *        The item to focus.
    629   * @param {boolean} aCollapseFlag
    630   *        True if the focused item should also be collapsed.
    631   * @return {boolean}
    632   *         True if the item was successfully focused.
    633   */
    634  _focusItem(aItem, aCollapseFlag) {
    635    if (!aItem.focusable) {
    636      return false;
    637    }
    638    if (aCollapseFlag) {
    639      aItem.collapse();
    640    }
    641    aItem._target.focus();
    642    aItem._arrow.scrollIntoView({ block: "nearest" });
    643    return true;
    644  }
    645 
    646  /**
    647   * Copy current selection to clipboard.
    648   */
    649  _copyItem() {
    650    const item = this.getFocusedItem();
    651    lazy.clipboardHelper.copyString(
    652      item._nameString + item.separatorStr + item._valueString
    653    );
    654  }
    655 
    656  /**
    657   * Listener handling a key down event on the view.
    658   */
    659  // eslint-disable-next-line complexity
    660  _onViewKeyDown(e) {
    661    const item = this.getFocusedItem();
    662 
    663    // Prevent scrolling when pressing navigation keys.
    664    ViewHelpers.preventScrolling(e);
    665 
    666    switch (e.keyCode) {
    667      case KeyCodes.DOM_VK_C:
    668        if (e.ctrlKey || e.metaKey) {
    669          this._copyItem();
    670        }
    671        return;
    672 
    673      case KeyCodes.DOM_VK_UP:
    674        // Always rewind focus.
    675        this.focusPrevItem(true);
    676        return;
    677 
    678      case KeyCodes.DOM_VK_DOWN:
    679        // Always advance focus.
    680        this.focusNextItem(true);
    681        return;
    682 
    683      case KeyCodes.DOM_VK_LEFT:
    684        // Collapse scopes, variables and properties before rewinding focus.
    685        if (item._isExpanded && item._isArrowVisible) {
    686          item.collapse();
    687        } else {
    688          this._focusItem(item.ownerView);
    689        }
    690        return;
    691 
    692      case KeyCodes.DOM_VK_RIGHT:
    693        // Nothing to do here if this item never expands.
    694        if (!item._isArrowVisible) {
    695          return;
    696        }
    697        // Expand scopes, variables and properties before advancing focus.
    698        if (!item._isExpanded) {
    699          item.expand();
    700        } else {
    701          this.focusNextItem(true);
    702        }
    703        return;
    704 
    705      case KeyCodes.DOM_VK_PAGE_UP:
    706        // Rewind a certain number of elements based on the container height.
    707        this.focusItemAtDelta(
    708          -(
    709            this.scrollPageSize ||
    710            Math.min(
    711              Math.floor(
    712                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
    713              ),
    714              PAGE_SIZE_MAX_JUMPS
    715            )
    716          )
    717        );
    718        return;
    719 
    720      case KeyCodes.DOM_VK_PAGE_DOWN:
    721        // Advance a certain number of elements based on the container height.
    722        this.focusItemAtDelta(
    723          +(
    724            this.scrollPageSize ||
    725            Math.min(
    726              Math.floor(
    727                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
    728              ),
    729              PAGE_SIZE_MAX_JUMPS
    730            )
    731          )
    732        );
    733        return;
    734 
    735      case KeyCodes.DOM_VK_HOME:
    736        this.focusFirstVisibleItem();
    737        return;
    738 
    739      case KeyCodes.DOM_VK_END:
    740        this.focusLastVisibleItem();
    741    }
    742  }
    743 
    744  /**
    745   * Sets the text displayed in this container when there are no available items.
    746   *
    747   * @param {string} aValue
    748   */
    749  set emptyText(aValue) {
    750    if (this._emptyTextNode) {
    751      this._emptyTextNode.setAttribute("value", aValue);
    752    }
    753    this._emptyTextValue = aValue;
    754    this._appendEmptyNotice();
    755  }
    756 
    757  /**
    758   * Creates and appends a label signaling that this container is empty.
    759   */
    760  _appendEmptyNotice() {
    761    if (this._emptyTextNode || !this._emptyTextValue) {
    762      return;
    763    }
    764 
    765    const label = this.document.createXULElement("label");
    766    label.className = "variables-view-empty-notice";
    767    label.setAttribute("value", this._emptyTextValue);
    768 
    769    this._parent.appendChild(label);
    770    this._emptyTextNode = label;
    771  }
    772 
    773  /**
    774   * Removes the label signaling that this container is empty.
    775   */
    776  _removeEmptyNotice() {
    777    if (!this._emptyTextNode) {
    778      return;
    779    }
    780 
    781    this._parent.removeChild(this._emptyTextNode);
    782    this._emptyTextNode = null;
    783  }
    784 
    785  /**
    786   * Gets if all values should be aligned together.
    787   *
    788   * @return {boolean}
    789   */
    790  get alignedValues() {
    791    return this._alignedValues;
    792  }
    793 
    794  /**
    795   * Sets if all values should be aligned together.
    796   *
    797   * @param {boolean} aFlag
    798   */
    799  set alignedValues(aFlag) {
    800    this._alignedValues = aFlag;
    801    if (aFlag) {
    802      this._parent.setAttribute("aligned-values", "");
    803    } else {
    804      this._parent.removeAttribute("aligned-values");
    805    }
    806  }
    807 
    808  /**
    809   * Gets if action buttons (like delete) should be placed at the beginning or
    810   * end of a line.
    811   *
    812   * @return {boolean}
    813   */
    814  get actionsFirst() {
    815    return this._actionsFirst;
    816  }
    817 
    818  /**
    819   * Sets if action buttons (like delete) should be placed at the beginning or
    820   * end of a line.
    821   *
    822   * @param {boolean} aFlag
    823   */
    824  set actionsFirst(aFlag) {
    825    this._actionsFirst = aFlag;
    826    if (aFlag) {
    827      this._parent.setAttribute("actions-first", "");
    828    } else {
    829      this._parent.removeAttribute("actions-first");
    830    }
    831  }
    832 
    833  /**
    834   * Gets the parent node holding this view.
    835   *
    836   * @return {Node}
    837   */
    838  get parentNode() {
    839    return this._parent;
    840  }
    841 
    842  /**
    843   * Gets the owner document holding this view.
    844   *
    845   * @return {HTMLDocument}
    846   */
    847  get document() {
    848    return this._document || (this._document = this._parent.ownerDocument);
    849  }
    850 
    851  /**
    852   * Gets the default window holding this view.
    853   *
    854   * @return {Window}
    855   */
    856  get window() {
    857    return this._window || (this._window = this.document.defaultView);
    858  }
    859 
    860  _document = null;
    861  _window = null;
    862 
    863  _store = null;
    864  _itemsByElement = null;
    865  _testOnlyHierarchy = null;
    866 
    867  _enumVisible = true;
    868  _nonEnumVisible = true;
    869  _alignedValues = false;
    870  _actionsFirst = false;
    871 
    872  _parent = null;
    873  _list = null;
    874  _searchboxNode = null;
    875  _searchboxContainer = null;
    876  _emptyTextNode = null;
    877  _emptyTextValue = "";
    878 
    879  *[Symbol.iterator]() {
    880    yield* this._store;
    881  }
    882 
    883  static NON_SORTABLE_CLASSES = [
    884    "Array",
    885    "Int8Array",
    886    "Uint8Array",
    887    "Uint8ClampedArray",
    888    "Int16Array",
    889    "Uint16Array",
    890    "Int32Array",
    891    "Uint32Array",
    892    "Float32Array",
    893    "Float64Array",
    894    "NodeList",
    895  ];
    896 
    897  /**
    898   * Determine whether an object's properties should be sorted based on its class.
    899   *
    900   * @param {string} aClassName
    901   *        The class of the object.
    902   */
    903  static isSortable(aClassName) {
    904    return !this.NON_SORTABLE_CLASSES.includes(aClassName);
    905  }
    906 
    907  /**
    908   * Returns true if the descriptor represents an undefined, null or
    909   * primitive value.
    910   *
    911   * @param {object} aDescriptor
    912   *        The variable's descriptor.
    913   */
    914  static isPrimitive(aDescriptor) {
    915    // For accessor property descriptors, the getter and setter need to be
    916    // contained in 'get' and 'set' properties.
    917    const getter = aDescriptor.get;
    918    const setter = aDescriptor.set;
    919    if (getter || setter) {
    920      return false;
    921    }
    922 
    923    // As described in the remote debugger protocol, the value grip
    924    // must be contained in a 'value' property.
    925    const grip = aDescriptor.value;
    926    if (typeof grip != "object") {
    927      return true;
    928    }
    929 
    930    // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
    931    // strings are considered types.
    932    const type = grip.type;
    933    if (
    934      type == "undefined" ||
    935      type == "null" ||
    936      type == "Infinity" ||
    937      type == "-Infinity" ||
    938      type == "NaN" ||
    939      type == "-0" ||
    940      type == "symbol" ||
    941      type == "longString"
    942    ) {
    943      return true;
    944    }
    945 
    946    return false;
    947  }
    948 
    949  /**
    950   * Returns true if the descriptor represents an undefined value.
    951   *
    952   * @param {object} aDescriptor
    953   *        The variable's descriptor.
    954   */
    955  static isUndefined(aDescriptor) {
    956    // For accessor property descriptors, the getter and setter need to be
    957    // contained in 'get' and 'set' properties.
    958    const getter = aDescriptor.get;
    959    const setter = aDescriptor.set;
    960    if (
    961      typeof getter == "object" &&
    962      getter.type == "undefined" &&
    963      typeof setter == "object" &&
    964      setter.type == "undefined"
    965    ) {
    966      return true;
    967    }
    968 
    969    // As described in the remote debugger protocol, the value grip
    970    // must be contained in a 'value' property.
    971    const grip = aDescriptor.value;
    972    if (typeof grip == "object" && grip.type == "undefined") {
    973      return true;
    974    }
    975 
    976    return false;
    977  }
    978 
    979  /**
    980   * Returns true if the descriptor represents a falsy value.
    981   *
    982   * @param {object} aDescriptor
    983   *        The variable's descriptor.
    984   */
    985  static isFalsy(aDescriptor) {
    986    // As described in the remote debugger protocol, the value grip
    987    // must be contained in a 'value' property.
    988    const grip = aDescriptor.value;
    989    if (typeof grip != "object") {
    990      return !grip;
    991    }
    992 
    993    // For convenience, undefined, null, NaN, and -0 are all considered types.
    994    const type = grip.type;
    995    if (
    996      type == "undefined" ||
    997      type == "null" ||
    998      type == "NaN" ||
    999      type == "-0"
   1000    ) {
   1001      return true;
   1002    }
   1003 
   1004    return false;
   1005  }
   1006 
   1007  /**
   1008   * Returns true if the value is an instance of Variable or Property.
   1009   *
   1010   * @param any aValue
   1011   *        The value to test.
   1012   */
   1013  static isVariable(aValue) {
   1014    return aValue instanceof Variable;
   1015  }
   1016 
   1017  /**
   1018   * Returns a standard grip for a value.
   1019   *
   1020   * @param {any} aValue
   1021   *        The raw value to get a grip for.
   1022   * @return {any}
   1023   *         The value's grip.
   1024   */
   1025  static getGrip(aValue) {
   1026    switch (typeof aValue) {
   1027      case "boolean":
   1028      case "string":
   1029        return aValue;
   1030      case "number":
   1031        if (aValue === Infinity) {
   1032          return { type: "Infinity" };
   1033        } else if (aValue === -Infinity) {
   1034          return { type: "-Infinity" };
   1035        } else if (Number.isNaN(aValue)) {
   1036          return { type: "NaN" };
   1037        } else if (1 / aValue === -Infinity) {
   1038          return { type: "-0" };
   1039        }
   1040        return aValue;
   1041      case "undefined":
   1042        // document.all is also "undefined"
   1043        if (aValue === undefined) {
   1044          return { type: "undefined" };
   1045        }
   1046      // fall through
   1047      case "object":
   1048        if (aValue === null) {
   1049          return { type: "null" };
   1050        }
   1051      // fall through
   1052      case "function":
   1053        return { type: "object", class: getObjectClassName(aValue) };
   1054      default:
   1055        console.error(
   1056          "Failed to provide a grip for value of " +
   1057            typeof value +
   1058            ": " +
   1059            aValue
   1060        );
   1061        return null;
   1062    }
   1063  }
   1064 
   1065  /**
   1066   * Returns a custom formatted property string for a grip.
   1067   *
   1068   * @param {any} aGrip
   1069   *        @see Variable.setGrip
   1070   * @param {object} aOptions
   1071   *        Options:
   1072   *        - concise: boolean that tells you want a concisely formatted string.
   1073   *        - noStringQuotes: boolean that tells to not quote strings.
   1074   *        - noEllipsis: boolean that tells to not add an ellipsis after the
   1075   *        initial text of a longString.
   1076   * @return {string}
   1077   *         The formatted property string.
   1078   */
   1079  static getString(aGrip, aOptions = {}) {
   1080    if (aGrip && typeof aGrip == "object") {
   1081      switch (aGrip.type) {
   1082        case "undefined":
   1083        case "null":
   1084        case "NaN":
   1085        case "Infinity":
   1086        case "-Infinity":
   1087        case "-0":
   1088          return aGrip.type;
   1089        default: {
   1090          const stringifier = VariablesView.stringifiers.byType[aGrip.type];
   1091          if (stringifier) {
   1092            const result = stringifier(aGrip, aOptions);
   1093            if (result != null) {
   1094              return result;
   1095            }
   1096          }
   1097 
   1098          if (aGrip.displayString) {
   1099            return VariablesView.getString(aGrip.displayString, aOptions);
   1100          }
   1101 
   1102          if (aGrip.type == "object" && aOptions.concise) {
   1103            return aGrip.class;
   1104          }
   1105 
   1106          return "[" + aGrip.type + " " + aGrip.class + "]";
   1107        }
   1108      }
   1109    }
   1110 
   1111    switch (typeof aGrip) {
   1112      case "string":
   1113        return VariablesView.stringifiers.byType.string(aGrip, aOptions);
   1114      case "boolean":
   1115        return aGrip ? "true" : "false";
   1116      case "number":
   1117        if (!aGrip && 1 / aGrip === -Infinity) {
   1118          return "-0";
   1119        }
   1120      // fall through
   1121      default:
   1122        return aGrip + "";
   1123    }
   1124  }
   1125 
   1126  /**
   1127   * Returns a custom class style for a grip.
   1128   *
   1129   * @param {any} aGrip
   1130   *        @see Variable.setGrip
   1131   * @return {string}
   1132   *         The custom class style.
   1133   */
   1134  static getClass(aGrip) {
   1135    if (aGrip && typeof aGrip == "object") {
   1136      if (aGrip.preview) {
   1137        switch (aGrip.preview.kind) {
   1138          case "DOMNode":
   1139            return "token-domnode";
   1140        }
   1141      }
   1142 
   1143      switch (aGrip.type) {
   1144        case "undefined":
   1145          return "token-undefined";
   1146        case "null":
   1147          return "token-null";
   1148        case "Infinity":
   1149        case "-Infinity":
   1150        case "NaN":
   1151        case "-0":
   1152          return "token-number";
   1153        case "longString":
   1154          return "token-string";
   1155      }
   1156    }
   1157    switch (typeof aGrip) {
   1158      case "string":
   1159        return "token-string";
   1160      case "boolean":
   1161        return "token-boolean";
   1162      case "number":
   1163        return "token-number";
   1164      default:
   1165        return "token-other";
   1166    }
   1167  }
   1168 
   1169  /**
   1170   * The VariablesView stringifiers are used by VariablesView.getString(). These
   1171   * are organized by object type, object class and by object actor preview kind.
   1172   * Some objects share identical ways for previews, for example Arrays, Sets and
   1173   * NodeLists.
   1174   *
   1175   * Any stringifier function must return a string. If null is returned, * then
   1176   * the default stringifier will be used. When invoked, the stringifier is
   1177   * given the same two arguments as those given to VariablesView.getString().
   1178   */
   1179  static stringifiers = {
   1180    byType: {
   1181      string(aGrip, { noStringQuotes }) {
   1182        if (noStringQuotes) {
   1183          return aGrip;
   1184        }
   1185        return '"' + aGrip + '"';
   1186      },
   1187 
   1188      longString({ initial }, { noStringQuotes, noEllipsis }) {
   1189        const ellipsis = noEllipsis ? "" : ELLIPSIS;
   1190        if (noStringQuotes) {
   1191          return initial + ellipsis;
   1192        }
   1193        const result = '"' + initial + '"';
   1194        if (!ellipsis) {
   1195          return result;
   1196        }
   1197        return result.substr(0, result.length - 1) + ellipsis + '"';
   1198      },
   1199 
   1200      object(aGrip, aOptions) {
   1201        const { preview } = aGrip;
   1202        let stringifier;
   1203        if (aGrip.class) {
   1204          stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
   1205        }
   1206        if (!stringifier && preview && preview.kind) {
   1207          stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
   1208        }
   1209        if (stringifier) {
   1210          return stringifier(aGrip, aOptions);
   1211        }
   1212        return null;
   1213      },
   1214 
   1215      symbol(aGrip) {
   1216        const name = aGrip.name || "";
   1217        return "Symbol(" + name + ")";
   1218      },
   1219 
   1220      mapEntry(aGrip) {
   1221        const {
   1222          preview: { key, value },
   1223        } = aGrip;
   1224 
   1225        const keyString = VariablesView.getString(key, {
   1226          concise: true,
   1227          noStringQuotes: true,
   1228        });
   1229        const valueString = VariablesView.getString(value, { concise: true });
   1230 
   1231        return keyString + " \u2192 " + valueString;
   1232      },
   1233    }, // VariablesView.stringifiers.byType
   1234    byObjectClass: {
   1235      Function(aGrip, { concise }) {
   1236        // TODO: Bug 948484 - support arrow functions and ES6 generators
   1237 
   1238        let name =
   1239          aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
   1240        name = VariablesView.getString(name, { noStringQuotes: true });
   1241 
   1242        // TODO: Bug 948489 - Support functions with destructured parameters and
   1243        // rest parameters
   1244        const params = aGrip.parameterNames || "";
   1245        if (!concise) {
   1246          return "function " + name + "(" + params + ")";
   1247        }
   1248        return (name || "function ") + "(" + params + ")";
   1249      },
   1250 
   1251      RegExp({ displayString }) {
   1252        return VariablesView.getString(displayString, { noStringQuotes: true });
   1253      },
   1254 
   1255      Date({ preview }) {
   1256        if (!preview || !("timestamp" in preview)) {
   1257          return null;
   1258        }
   1259 
   1260        if (typeof preview.timestamp != "number") {
   1261          return new Date(preview.timestamp).toString(); // invalid date
   1262        }
   1263 
   1264        return "Date " + new Date(preview.timestamp).toISOString();
   1265      },
   1266 
   1267      Number(aGrip) {
   1268        const { preview } = aGrip;
   1269        if (preview === undefined) {
   1270          return null;
   1271        }
   1272        return (
   1273          aGrip.class +
   1274          " { " +
   1275          VariablesView.getString(preview.wrappedValue) +
   1276          " }"
   1277        );
   1278      },
   1279      Boolean: Number,
   1280    }, // VariablesView.stringifiers.byObjectClass
   1281    byObjectKind: {
   1282      ArrayLike(aGrip, { concise }) {
   1283        const { preview } = aGrip;
   1284        if (concise) {
   1285          return aGrip.class + "[" + preview.length + "]";
   1286        }
   1287 
   1288        if (!preview.items) {
   1289          return null;
   1290        }
   1291 
   1292        let shown = 0,
   1293          lastHole = null;
   1294        const result = [];
   1295        for (const item of preview.items) {
   1296          if (item === null) {
   1297            if (lastHole !== null) {
   1298              result[lastHole] += ",";
   1299            } else {
   1300              result.push("");
   1301            }
   1302            lastHole = result.length - 1;
   1303          } else {
   1304            lastHole = null;
   1305            result.push(VariablesView.getString(item, { concise: true }));
   1306          }
   1307          shown++;
   1308        }
   1309 
   1310        if (shown < preview.length) {
   1311          const n = preview.length - shown;
   1312          result.push(VariablesView.stringifiers._getNMoreString(n));
   1313        } else if (lastHole !== null) {
   1314          // make sure we have the right number of commas...
   1315          result[lastHole] += ",";
   1316        }
   1317 
   1318        const prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
   1319        return prefix + "[" + result.join(", ") + "]";
   1320      },
   1321 
   1322      MapLike(aGrip, { concise }) {
   1323        const { preview } = aGrip;
   1324        if (concise || !preview.entries) {
   1325          const size =
   1326            typeof preview.size == "number" ? "[" + preview.size + "]" : "";
   1327          return aGrip.class + size;
   1328        }
   1329 
   1330        const entries = [];
   1331        for (const [key, value] of preview.entries) {
   1332          const keyString = VariablesView.getString(key, {
   1333            concise: true,
   1334            noStringQuotes: true,
   1335          });
   1336          const valueString = VariablesView.getString(value, { concise: true });
   1337          entries.push(keyString + ": " + valueString);
   1338        }
   1339 
   1340        if (typeof preview.size == "number" && preview.size > entries.length) {
   1341          const n = preview.size - entries.length;
   1342          entries.push(VariablesView.stringifiers._getNMoreString(n));
   1343        }
   1344 
   1345        return aGrip.class + " {" + entries.join(", ") + "}";
   1346      },
   1347 
   1348      ObjectWithText(aGrip, { concise }) {
   1349        if (concise) {
   1350          return aGrip.class;
   1351        }
   1352 
   1353        return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
   1354      },
   1355 
   1356      ObjectWithURL(aGrip, { concise }) {
   1357        let result = aGrip.class;
   1358        const url = aGrip.preview.url;
   1359        if (!VariablesView.isFalsy({ value: url })) {
   1360          result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
   1361        }
   1362        return result;
   1363      },
   1364 
   1365      // Stringifier for any kind of object.
   1366      Object(aGrip, { concise }) {
   1367        if (concise) {
   1368          return aGrip.class;
   1369        }
   1370 
   1371        const { preview } = aGrip;
   1372        const props = [];
   1373 
   1374        if (aGrip.class == "Promise" && aGrip.promiseState) {
   1375          const { state, value, reason } = aGrip.promiseState;
   1376          props.push("<state>: " + VariablesView.getString(state));
   1377          if (state == "fulfilled") {
   1378            props.push(
   1379              "<value>: " + VariablesView.getString(value, { concise: true })
   1380            );
   1381          } else if (state == "rejected") {
   1382            props.push(
   1383              "<reason>: " + VariablesView.getString(reason, { concise: true })
   1384            );
   1385          }
   1386        }
   1387 
   1388        for (const key of Object.keys(preview.ownProperties || {})) {
   1389          const value = preview.ownProperties[key];
   1390          let valueString = "";
   1391          if (value.get) {
   1392            valueString = "Getter";
   1393          } else if (value.set) {
   1394            valueString = "Setter";
   1395          } else {
   1396            valueString = VariablesView.getString(value.value, {
   1397              concise: true,
   1398            });
   1399          }
   1400          props.push(key + ": " + valueString);
   1401        }
   1402 
   1403        for (const key of Object.keys(preview.safeGetterValues || {})) {
   1404          const value = preview.safeGetterValues[key];
   1405          const valueString = VariablesView.getString(value.getterValue, {
   1406            concise: true,
   1407          });
   1408          props.push(key + ": " + valueString);
   1409        }
   1410 
   1411        if (!props.length) {
   1412          return null;
   1413        }
   1414 
   1415        if (preview.ownPropertiesLength) {
   1416          const previewLength = Object.keys(preview.ownProperties).length;
   1417          const diff = preview.ownPropertiesLength - previewLength;
   1418          if (diff > 0) {
   1419            props.push(VariablesView.stringifiers._getNMoreString(diff));
   1420          }
   1421        }
   1422 
   1423        const prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
   1424        return prefix + "{" + props.join(", ") + "}";
   1425      }, // Object
   1426 
   1427      Error(aGrip, { concise }) {
   1428        const { preview } = aGrip;
   1429        const name = VariablesView.getString(preview.name, {
   1430          noStringQuotes: true,
   1431        });
   1432        if (concise) {
   1433          return name || aGrip.class;
   1434        }
   1435 
   1436        let msg =
   1437          name +
   1438          ": " +
   1439          VariablesView.getString(preview.message, { noStringQuotes: true });
   1440 
   1441        if (!VariablesView.isFalsy({ value: preview.stack })) {
   1442          msg +=
   1443            "\n" +
   1444            L10N.getStr("variablesViewErrorStacktrace") +
   1445            "\n" +
   1446            preview.stack;
   1447        }
   1448 
   1449        return msg;
   1450      },
   1451 
   1452      DOMException(aGrip, { concise }) {
   1453        const { preview } = aGrip;
   1454        if (concise) {
   1455          return preview.name || aGrip.class;
   1456        }
   1457 
   1458        let msg =
   1459          aGrip.class +
   1460          " [" +
   1461          preview.name +
   1462          ": " +
   1463          VariablesView.getString(preview.message) +
   1464          "\n" +
   1465          "code: " +
   1466          preview.code +
   1467          "\n" +
   1468          "nsresult: 0x" +
   1469          (+preview.result).toString(16);
   1470 
   1471        if (preview.filename) {
   1472          msg += "\nlocation: " + preview.filename;
   1473          if (preview.lineNumber) {
   1474            msg += ":" + preview.lineNumber;
   1475          }
   1476        }
   1477 
   1478        return msg + "]";
   1479      },
   1480 
   1481      DOMEvent(aGrip, { concise }) {
   1482        const { preview } = aGrip;
   1483        if (!preview.type) {
   1484          return null;
   1485        }
   1486 
   1487        if (concise) {
   1488          return aGrip.class + " " + preview.type;
   1489        }
   1490 
   1491        let result = preview.type;
   1492 
   1493        if (
   1494          preview.eventKind == "key" &&
   1495          preview.modifiers &&
   1496          preview.modifiers.length
   1497        ) {
   1498          result += " " + preview.modifiers.join("-");
   1499        }
   1500 
   1501        const props = [];
   1502        if (preview.target) {
   1503          const target = VariablesView.getString(preview.target, {
   1504            concise: true,
   1505          });
   1506          props.push("target: " + target);
   1507        }
   1508 
   1509        for (const prop in preview.properties) {
   1510          const value = preview.properties[prop];
   1511          props.push(
   1512            prop + ": " + VariablesView.getString(value, { concise: true })
   1513          );
   1514        }
   1515 
   1516        return result + " {" + props.join(", ") + "}";
   1517      }, // DOMEvent
   1518 
   1519      DOMNode(aGrip, { concise }) {
   1520        const { preview } = aGrip;
   1521 
   1522        switch (preview.nodeType) {
   1523          case nodeConstants.DOCUMENT_NODE: {
   1524            let result = aGrip.class;
   1525            if (preview.location) {
   1526              result += ` \u2192 ${
   1527                getSourceNames(preview.location)[concise ? "short" : "long"]
   1528              }`;
   1529            }
   1530 
   1531            return result;
   1532          }
   1533 
   1534          case nodeConstants.ATTRIBUTE_NODE: {
   1535            const value = VariablesView.getString(preview.value, {
   1536              noStringQuotes: true,
   1537            });
   1538            return preview.nodeName + '="' + escapeHTML(value) + '"';
   1539          }
   1540 
   1541          case nodeConstants.TEXT_NODE:
   1542            return (
   1543              preview.nodeName +
   1544              " " +
   1545              VariablesView.getString(preview.textContent)
   1546            );
   1547 
   1548          case nodeConstants.COMMENT_NODE: {
   1549            const comment = VariablesView.getString(preview.textContent, {
   1550              noStringQuotes: true,
   1551            });
   1552            return "<!--" + comment + "-->";
   1553          }
   1554 
   1555          case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
   1556            if (concise || !preview.childNodes) {
   1557              return aGrip.class + "[" + preview.childNodesLength + "]";
   1558            }
   1559            const nodes = [];
   1560            for (const node of preview.childNodes) {
   1561              nodes.push(VariablesView.getString(node));
   1562            }
   1563            if (nodes.length < preview.childNodesLength) {
   1564              const n = preview.childNodesLength - nodes.length;
   1565              nodes.push(VariablesView.stringifiers._getNMoreString(n));
   1566            }
   1567            return aGrip.class + " [" + nodes.join(", ") + "]";
   1568          }
   1569 
   1570          case nodeConstants.ELEMENT_NODE: {
   1571            const attrs = preview.attributes;
   1572            if (!concise) {
   1573              let n = 0,
   1574                result = "<" + preview.nodeName;
   1575              for (const name in attrs) {
   1576                const value = VariablesView.getString(attrs[name], {
   1577                  noStringQuotes: true,
   1578                });
   1579                result += " " + name + '="' + escapeHTML(value) + '"';
   1580                n++;
   1581              }
   1582              if (preview.attributesLength > n) {
   1583                result += " " + ELLIPSIS;
   1584              }
   1585              return result + ">";
   1586            }
   1587 
   1588            let result = "<" + preview.nodeName;
   1589            if (attrs.id) {
   1590              result += "#" + attrs.id;
   1591            }
   1592 
   1593            if (attrs.class) {
   1594              result += "." + attrs.class.trim().replace(/\s+/, ".");
   1595            }
   1596            return result + ">";
   1597          }
   1598 
   1599          default:
   1600            return null;
   1601        }
   1602      }, // DOMNode
   1603    }, // VariablesView.stringifiers.byObjectKind
   1604 
   1605    /**
   1606     * Get the "N more…" formatted string, given an N. This is used for displaying
   1607     * how many elements are not displayed in an object preview (eg. an array).
   1608     *
   1609     * @private
   1610     * @param {number} aNumber
   1611     * @return {string}
   1612     */
   1613    _getNMoreString(aNumber) {
   1614      const str = L10N.getStr("variablesViewMoreObjects");
   1615      return PluralForm.get(aNumber, str).replace("#1", aNumber);
   1616    },
   1617  };
   1618 }
   1619 
   1620 /**
   1621 * A Scope is an object holding Variable instances.
   1622 * Iterable via "for (let [name, variable] of instance) { }".
   1623 */
   1624 class Scope {
   1625  /**
   1626   * @param {VariablesView} aView
   1627   *        The view to contain this scope.
   1628   * @param {string} l10nId
   1629   *        The scope localized string id.
   1630   * @param {object} [aFlags={}]
   1631   *        Additional options or flags for this scope.
   1632   */
   1633  constructor(aView, l10nId, aFlags = {}) {
   1634    this.ownerView = aView;
   1635 
   1636    this._onClick = this._onClick.bind(this);
   1637    this._openEnum = this._openEnum.bind(this);
   1638    this._openNonEnum = this._openNonEnum.bind(this);
   1639 
   1640    // Inherit properties and flags from the parent view. You can override
   1641    // each of these directly onto any scope, variable or property instance.
   1642    this.scrollPageSize = aView.scrollPageSize;
   1643    this.contextMenuId = aView.contextMenuId;
   1644    this.separatorStr = aView.separatorStr;
   1645 
   1646    this._init(l10nId, aFlags);
   1647  }
   1648 
   1649  /**
   1650   * Whether this Scope should be prefetched when it is remoted.
   1651   */
   1652  shouldPrefetch = true;
   1653 
   1654  /**
   1655   * Whether this Scope should paginate its contents.
   1656   */
   1657  allowPaginate = false;
   1658 
   1659  /**
   1660   * The class name applied to this scope's target element.
   1661   */
   1662  get targetClassName() {
   1663    return "variables-view-scope";
   1664  }
   1665 
   1666  /**
   1667   * Create a new Variable that is a child of this Scope.
   1668   *
   1669   * @param {string} aName
   1670   *        The name of the new Property.
   1671   * @param {object} aDescriptor
   1672   *        The variable's descriptor.
   1673   * @param {object} aOptions
   1674   *        Options of the form accepted by addItem.
   1675   * @return {Variable}
   1676   *         The newly created child Variable.
   1677   */
   1678  _createChild(aName, aDescriptor, aOptions) {
   1679    return new Variable(this, aName, aDescriptor, aOptions);
   1680  }
   1681 
   1682  /**
   1683   * Adds a child to contain any inspected properties.
   1684   *
   1685   * @param {string} aName
   1686   *        The child's name.
   1687   * @param {object} aDescriptor
   1688   *        Specifies the value and/or type & class of the child,
   1689   *        or 'get' & 'set' accessor properties. If the type is implicit,
   1690   *        it will be inferred from the value. If this parameter is omitted,
   1691   *        a property without a value will be added (useful for branch nodes).
   1692   *        e.g. - { value: 42 }
   1693   *             - { value: true }
   1694   *             - { value: "nasu" }
   1695   *             - { value: { type: "undefined" } }
   1696   *             - { value: { type: "null" } }
   1697   *             - { value: { type: "object", class: "Object" } }
   1698   *             - { get: { type: "object", class: "Function" },
   1699   *                 set: { type: "undefined" } }
   1700   * @param {object} aOptions
   1701   *        Specifies some options affecting the new variable.
   1702   *        Recognized properties are
   1703   *        * boolean relaxed  true if name duplicates should be allowed.
   1704   *                           You probably shouldn't do it. Use this
   1705   *                           with caution.
   1706   *        * boolean internalItem  true if the item is internally generated.
   1707   *                           This is used for special variables
   1708   *                           like <return> or <exception> and distinguishes
   1709   *                           them from ordinary properties that happen
   1710   *                           to have the same name
   1711   * @return {Variable}
   1712   *         The newly created Variable instance, null if it already exists.
   1713   */
   1714  addItem(aName, aDescriptor = {}, aOptions = {}) {
   1715    const { relaxed } = aOptions;
   1716    if (this._store.has(aName) && !relaxed) {
   1717      return this._store.get(aName);
   1718    }
   1719 
   1720    const child = this._createChild(aName, aDescriptor, aOptions);
   1721    this._store.set(aName, child);
   1722    this._variablesView._itemsByElement.set(child._target, child);
   1723    this._variablesView._testOnlyHierarchy.set(child.absoluteName, child);
   1724    child.header = aName !== undefined;
   1725 
   1726    return child;
   1727  }
   1728 
   1729  /**
   1730   * Adds items for this variable.
   1731   *
   1732   * @param {object} aItems
   1733   *        An object containing some { name: descriptor } data properties,
   1734   *        specifying the value and/or type & class of the variable,
   1735   *        or 'get' & 'set' accessor properties. If the type is implicit,
   1736   *        it will be inferred from the value.
   1737   *        e.g. - { someProp0: { value: 42 },
   1738   *                 someProp1: { value: true },
   1739   *                 someProp2: { value: "nasu" },
   1740   *                 someProp3: { value: { type: "undefined" } },
   1741   *                 someProp4: { value: { type: "null" } },
   1742   *                 someProp5: { value: { type: "object", class: "Object" } },
   1743   *                 someProp6: { get: { type: "object", class: "Function" },
   1744   *                              set: { type: "undefined" } } }
   1745   * @param {object} [aOptions={}]
   1746   *        Additional options for adding the properties. Supported options:
   1747   *        - sorted: true to sort all the properties before adding them
   1748   *        - callback: function invoked after each item is added
   1749   */
   1750  addItems(aItems, aOptions = {}) {
   1751    const names = Object.keys(aItems);
   1752 
   1753    // Sort all of the properties before adding them, if preferred.
   1754    if (aOptions.sorted) {
   1755      names.sort(this._naturalSort);
   1756    }
   1757 
   1758    // Add the properties to the current scope.
   1759    for (const name of names) {
   1760      const descriptor = aItems[name];
   1761      const item = this.addItem(name, descriptor);
   1762 
   1763      if (aOptions.callback) {
   1764        aOptions.callback(item, descriptor && descriptor.value);
   1765      }
   1766    }
   1767  }
   1768 
   1769  /**
   1770   * Remove this Scope from its parent and remove all children recursively.
   1771   */
   1772  remove() {
   1773    const view = this._variablesView;
   1774    view._store.splice(view._store.indexOf(this), 1);
   1775    view._itemsByElement.delete(this._target);
   1776    view._testOnlyHierarchy.delete(this._nameString);
   1777 
   1778    this._target.remove();
   1779 
   1780    for (const variable of this._store.values()) {
   1781      variable.remove();
   1782    }
   1783  }
   1784 
   1785  /**
   1786   * Gets the variable in this container having the specified name.
   1787   *
   1788   * @param {string} aName
   1789   *        The name of the variable to get.
   1790   * @return {Variable | null}
   1791   *         The matched variable, or null if nothing is found.
   1792   */
   1793  get(aName) {
   1794    return this._store.get(aName);
   1795  }
   1796 
   1797  /**
   1798   * Recursively searches for the variable or property in this container
   1799   * displayed by the specified node.
   1800   *
   1801   * @param {Node} aNode
   1802   *        The node to search for.
   1803   * @return {Variable | Property | null}
   1804   *         The matched variable or property, or null if nothing is found.
   1805   */
   1806  find(aNode) {
   1807    for (const [, variable] of this._store) {
   1808      let match;
   1809      if (variable._target == aNode) {
   1810        match = variable;
   1811      } else {
   1812        match = variable.find(aNode);
   1813      }
   1814      if (match) {
   1815        return match;
   1816      }
   1817    }
   1818    return null;
   1819  }
   1820 
   1821  /**
   1822   * Determines if this scope is a direct child of a parent variables view,
   1823   * scope, variable or property.
   1824   *
   1825   * @param {VariablesView | Scope | Variable | Property} aParent
   1826   *        The parent to check.
   1827   * @return {boolean}
   1828   *         True if the specified item is a direct child, false otherwise.
   1829   */
   1830  isChildOf(aParent) {
   1831    return this.ownerView == aParent;
   1832  }
   1833 
   1834  /**
   1835   * Determines if this scope is a descendant of a parent variables view,
   1836   * scope, variable or property.
   1837   *
   1838   * @param {VariablesView | Scope | Variable | Property} aParent
   1839   *        The parent to check.
   1840   * @return {boolean}
   1841   *         True if the specified item is a descendant, false otherwise.
   1842   */
   1843  isDescendantOf(aParent) {
   1844    if (this.isChildOf(aParent)) {
   1845      return true;
   1846    }
   1847 
   1848    // Recurse to parent if it is a Scope, Variable, or Property.
   1849    if (this.ownerView instanceof Scope) {
   1850      return this.ownerView.isDescendantOf(aParent);
   1851    }
   1852 
   1853    return false;
   1854  }
   1855 
   1856  /**
   1857   * Shows the scope.
   1858   */
   1859  show() {
   1860    this._target.hidden = false;
   1861    this._isContentVisible = true;
   1862 
   1863    if (this.onshow) {
   1864      this.onshow(this);
   1865    }
   1866  }
   1867 
   1868  /**
   1869   * Hides the scope.
   1870   */
   1871  hide() {
   1872    this._target.hidden = true;
   1873    this._isContentVisible = false;
   1874 
   1875    if (this.onhide) {
   1876      this.onhide(this);
   1877    }
   1878  }
   1879 
   1880  /**
   1881   * Expands the scope, showing all the added details.
   1882   */
   1883  async expand() {
   1884    if (this._isExpanded) {
   1885      return;
   1886    }
   1887    if (this._variablesView._enumVisible) {
   1888      this._openEnum();
   1889    }
   1890    if (this._variablesView._nonEnumVisible) {
   1891      Services.tm.dispatchToMainThread({ run: this._openNonEnum });
   1892    }
   1893    this._isExpanded = true;
   1894 
   1895    if (this.onexpand) {
   1896      // We return onexpand as it sometimes returns a promise
   1897      // (up to the user of VariableView to do it)
   1898      // that can indicate when the view is done expanding
   1899      // and attributes are available. (Mostly used for tests)
   1900      await this.onexpand(this);
   1901    }
   1902  }
   1903 
   1904  /**
   1905   * Collapses the scope, hiding all the added details.
   1906   */
   1907  collapse() {
   1908    if (!this._isExpanded) {
   1909      return;
   1910    }
   1911    this._arrow.removeAttribute("open");
   1912    this._enum.removeAttribute("open");
   1913    this._nonenum.removeAttribute("open");
   1914    this._isExpanded = false;
   1915 
   1916    if (this.oncollapse) {
   1917      this.oncollapse(this);
   1918    }
   1919  }
   1920 
   1921  /**
   1922   * Toggles between the scope's collapsed and expanded state.
   1923   */
   1924  toggle(e) {
   1925    if (e && e.button != 0) {
   1926      // Only allow left-click to trigger this event.
   1927      return;
   1928    }
   1929    this.expanded ^= 1;
   1930 
   1931    // Make sure the scope and its contents are visibile.
   1932    for (const [, variable] of this._store) {
   1933      variable.header = true;
   1934      variable._matched = true;
   1935    }
   1936    if (this.ontoggle) {
   1937      this.ontoggle(this);
   1938    }
   1939  }
   1940 
   1941  /**
   1942   * Shows the scope's title header.
   1943   */
   1944  showHeader() {
   1945    if (this._isHeaderVisible || !this._nameString) {
   1946      return;
   1947    }
   1948    this._target.removeAttribute("untitled");
   1949    this._isHeaderVisible = true;
   1950  }
   1951 
   1952  /**
   1953   * Hides the scope's title header.
   1954   * This action will automatically expand the scope.
   1955   */
   1956  hideHeader() {
   1957    if (!this._isHeaderVisible) {
   1958      return;
   1959    }
   1960    this.expand();
   1961    this._target.setAttribute("untitled", "");
   1962    this._isHeaderVisible = false;
   1963  }
   1964 
   1965  /**
   1966   * Sort in ascending order
   1967   * This only needs to compare non-numbers since it is dealing with an array
   1968   * which numeric-based indices are placed in order.
   1969   *
   1970   * @param {string} a
   1971   * @param {string} b
   1972   * @return {number}
   1973   *         -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
   1974   */
   1975  _naturalSort(a, b) {
   1976    if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
   1977      return a < b ? -1 : 1;
   1978    }
   1979    return 0;
   1980  }
   1981 
   1982  /**
   1983   * Shows the scope's expand/collapse arrow.
   1984   */
   1985  showArrow() {
   1986    if (this._isArrowVisible) {
   1987      return;
   1988    }
   1989    this._arrow.removeAttribute("invisible");
   1990    this._isArrowVisible = true;
   1991  }
   1992 
   1993  /**
   1994   * Hides the scope's expand/collapse arrow.
   1995   */
   1996  hideArrow() {
   1997    if (!this._isArrowVisible) {
   1998      return;
   1999    }
   2000    this._arrow.setAttribute("invisible", "");
   2001    this._isArrowVisible = false;
   2002  }
   2003 
   2004  /**
   2005   * Gets the visibility state.
   2006   *
   2007   * @return {boolean}
   2008   */
   2009  get visible() {
   2010    return this._isContentVisible;
   2011  }
   2012 
   2013  /**
   2014   * Gets the expanded state.
   2015   *
   2016   * @return {boolean}
   2017   */
   2018  get expanded() {
   2019    return this._isExpanded;
   2020  }
   2021 
   2022  /**
   2023   * Gets the header visibility state.
   2024   *
   2025   * @return {boolean}
   2026   */
   2027  get header() {
   2028    return this._isHeaderVisible;
   2029  }
   2030 
   2031  /**
   2032   * Gets the twisty visibility state.
   2033   *
   2034   * @return {boolean}
   2035   */
   2036  get twisty() {
   2037    return this._isArrowVisible;
   2038  }
   2039  /**
   2040   * Sets the visibility state.
   2041   *
   2042   * @param {boolean} aFlag
   2043   */
   2044  set visible(aFlag) {
   2045    aFlag ? this.show() : this.hide();
   2046  }
   2047 
   2048  /**
   2049   * Sets the expanded state.
   2050   *
   2051   * @param {boolean} aFlag
   2052   */
   2053  set expanded(aFlag) {
   2054    aFlag ? this.expand() : this.collapse();
   2055  }
   2056 
   2057  /**
   2058   * Sets the header visibility state.
   2059   *
   2060   * @param {boolean} aFlag
   2061   */
   2062  set header(aFlag) {
   2063    aFlag ? this.showHeader() : this.hideHeader();
   2064  }
   2065 
   2066  /**
   2067   * Sets the twisty visibility state.
   2068   *
   2069   * @param {boolean} aFlag
   2070   */
   2071  set twisty(aFlag) {
   2072    aFlag ? this.showArrow() : this.hideArrow();
   2073  }
   2074 
   2075  /**
   2076   * Specifies if this target node may be focused.
   2077   *
   2078   * @return {boolean}
   2079   */
   2080  get focusable() {
   2081    // Check if this target node is actually visibile.
   2082    if (
   2083      !this._nameString ||
   2084      !this._isContentVisible ||
   2085      !this._isHeaderVisible ||
   2086      !this._isMatch
   2087    ) {
   2088      return false;
   2089    }
   2090    // Check if all parent objects are expanded.
   2091    let item = this;
   2092 
   2093    // Recurse while parent is a Scope, Variable, or Property
   2094    while ((item = item.ownerView) && item instanceof Scope) {
   2095      if (!item._isExpanded) {
   2096        return false;
   2097      }
   2098    }
   2099    return true;
   2100  }
   2101 
   2102  /**
   2103   * Focus this scope.
   2104   */
   2105  focus() {
   2106    this._variablesView._focusItem(this);
   2107  }
   2108 
   2109  /**
   2110   * Adds an event listener for a certain event on this scope's title.
   2111   *
   2112   * @param {string} aName
   2113   * @param {Function} aCallback
   2114   * @param {boolean} aCapture
   2115   */
   2116  addEventListener(aName, aCallback, aCapture) {
   2117    this._title.addEventListener(aName, aCallback, aCapture);
   2118  }
   2119 
   2120  /**
   2121   * Removes an event listener for a certain event on this scope's title.
   2122   *
   2123   * @param {string} aName
   2124   * @param {Function} aCallback
   2125   * @param {boolean} aCapture
   2126   */
   2127  removeEventListener(aName, aCallback, aCapture) {
   2128    this._title.removeEventListener(aName, aCallback, aCapture);
   2129  }
   2130 
   2131  /**
   2132   * Gets the id associated with this item.
   2133   *
   2134   * @return {string}
   2135   */
   2136  get id() {
   2137    return this._idString;
   2138  }
   2139 
   2140  /**
   2141   * Gets the name associated with this item.
   2142   *
   2143   * @return {string}
   2144   */
   2145  get name() {
   2146    return this._nameString;
   2147  }
   2148 
   2149  /**
   2150   * Gets the displayed value for this item.
   2151   *
   2152   * @return {string}
   2153   */
   2154  get displayValue() {
   2155    return this._valueString;
   2156  }
   2157 
   2158  /**
   2159   * Gets the class names used for the displayed value.
   2160   *
   2161   * @return {string}
   2162   */
   2163  get displayValueClassName() {
   2164    return this._valueClassName;
   2165  }
   2166 
   2167  /**
   2168   * Gets the element associated with this item.
   2169   *
   2170   * @return {Node}
   2171   */
   2172  get target() {
   2173    return this._target;
   2174  }
   2175 
   2176  /**
   2177   * Initializes this scope's id, view and binds event listeners.
   2178   *
   2179   * @param {string} l10nId
   2180   *        The scope localized string id.
   2181   * @param {object} [aFlags]
   2182   *        Additional options or flags for this scope.
   2183   */
   2184  _init(l10nId, aFlags) {
   2185    this._idString = generateId((this._nameString = l10nId));
   2186    this._displayScope({
   2187      l10nId,
   2188      targetClassName: `${this.targetClassName} ${aFlags.customClass}`,
   2189      titleClassName: "devtools-toolbar",
   2190    });
   2191    this._addEventListeners();
   2192    this.parentNode.appendChild(this._target);
   2193  }
   2194 
   2195  /**
   2196   * Creates the necessary nodes for this scope.
   2197   *
   2198   * @param {object} options
   2199   * @param {string} [options.l10nId]
   2200   *        The scope localized string id.
   2201   * @param {string} [options.value]
   2202   *        The scope's name. Either this or l10nId need to be passed
   2203   * @param {string} options.targetClassName
   2204   *        A custom class name for this scope's target element.
   2205   * @param {string} [options.titleClassName]
   2206   *        A custom class name for this scope's title element.
   2207   */
   2208  _displayScope({ l10nId, value, targetClassName, titleClassName = "" }) {
   2209    const document = this.document;
   2210 
   2211    const element = (this._target = document.createXULElement("vbox"));
   2212    element.id = this._idString;
   2213    element.className = targetClassName;
   2214 
   2215    const arrow = (this._arrow = document.createXULElement("hbox"));
   2216    arrow.className = "arrow theme-twisty";
   2217 
   2218    const name = (this._name = document.createXULElement("label"));
   2219    name.className = "name";
   2220    if (l10nId) {
   2221      document.l10n.setAttributes(name, l10nId);
   2222    } else {
   2223      name.setAttribute("value", value);
   2224    }
   2225    name.setAttribute("crop", "end");
   2226 
   2227    const title = (this._title = document.createXULElement("hbox"));
   2228    title.className = "title " + titleClassName;
   2229    title.setAttribute("align", "center");
   2230 
   2231    const enumerable = (this._enum = document.createXULElement("vbox"));
   2232    const nonenum = (this._nonenum = document.createXULElement("vbox"));
   2233    enumerable.className = "variables-view-element-details enum";
   2234    nonenum.className = "variables-view-element-details nonenum";
   2235 
   2236    title.appendChild(arrow);
   2237    title.appendChild(name);
   2238 
   2239    element.appendChild(title);
   2240    element.appendChild(enumerable);
   2241    element.appendChild(nonenum);
   2242  }
   2243 
   2244  /**
   2245   * Adds the necessary event listeners for this scope.
   2246   */
   2247  _addEventListeners() {
   2248    this._title.addEventListener("mousedown", this._onClick);
   2249  }
   2250 
   2251  /**
   2252   * The click listener for this scope's title.
   2253   */
   2254  _onClick(e) {
   2255    if (e.button != 0) {
   2256      return;
   2257    }
   2258    this.toggle();
   2259    this.focus();
   2260  }
   2261 
   2262  /**
   2263   * Opens the enumerable items container.
   2264   */
   2265  _openEnum() {
   2266    this._arrow.setAttribute("open", "");
   2267    this._enum.setAttribute("open", "");
   2268  }
   2269 
   2270  /**
   2271   * Opens the non-enumerable items container.
   2272   */
   2273  _openNonEnum() {
   2274    this._nonenum.setAttribute("open", "");
   2275  }
   2276 
   2277  /**
   2278   * Specifies if enumerable properties and variables should be displayed.
   2279   *
   2280   * @param {boolean} aFlag
   2281   */
   2282  set _enumVisible(aFlag) {
   2283    for (const [, variable] of this._store) {
   2284      variable._enumVisible = aFlag;
   2285 
   2286      if (!this._isExpanded) {
   2287        continue;
   2288      }
   2289      if (aFlag) {
   2290        this._enum.setAttribute("open", "");
   2291      } else {
   2292        this._enum.removeAttribute("open");
   2293      }
   2294    }
   2295  }
   2296 
   2297  /**
   2298   * Specifies if non-enumerable properties and variables should be displayed.
   2299   *
   2300   * @param {boolean} aFlag
   2301   */
   2302  set _nonEnumVisible(aFlag) {
   2303    for (const [, variable] of this._store) {
   2304      variable._nonEnumVisible = aFlag;
   2305 
   2306      if (!this._isExpanded) {
   2307        continue;
   2308      }
   2309      if (aFlag) {
   2310        this._nonenum.setAttribute("open", "");
   2311      } else {
   2312        this._nonenum.removeAttribute("open");
   2313      }
   2314    }
   2315  }
   2316 
   2317  /**
   2318   * Performs a case insensitive search for variables or properties matching
   2319   * the query, and hides non-matched items.
   2320   *
   2321   * @param {string} aLowerCaseQuery
   2322   *        The lowercased name of the variable or property to search for.
   2323   */
   2324  _performSearch(aLowerCaseQuery) {
   2325    for (let [, variable] of this._store) {
   2326      const currentObject = variable;
   2327      const lowerCaseName = variable._nameString.toLowerCase();
   2328      const lowerCaseValue = variable._valueString.toLowerCase();
   2329 
   2330      // Non-matched variables or properties require a corresponding attribute.
   2331      if (
   2332        !lowerCaseName.includes(aLowerCaseQuery) &&
   2333        !lowerCaseValue.includes(aLowerCaseQuery)
   2334      ) {
   2335        variable._matched = false;
   2336      } else {
   2337        // Variable or property is matched.
   2338        variable._matched = true;
   2339 
   2340        // If the variable was ever expanded, there's a possibility it may
   2341        // contain some matched properties, so make sure they're visible
   2342        // ("expand downwards").
   2343        if (variable._store.size) {
   2344          variable.expand();
   2345        }
   2346 
   2347        // If the variable is contained in another Scope, Variable, or Property,
   2348        // the parent may not be a match, thus hidden. It should be visible
   2349        // ("expand upwards").
   2350        while ((variable = variable.ownerView) && variable instanceof Scope) {
   2351          variable._matched = true;
   2352          variable.expand();
   2353        }
   2354      }
   2355 
   2356      // Proceed with the search recursively inside this variable or property.
   2357      if (
   2358        currentObject._store.size ||
   2359        currentObject.getter ||
   2360        currentObject.setter
   2361      ) {
   2362        currentObject._performSearch(aLowerCaseQuery);
   2363      }
   2364    }
   2365  }
   2366 
   2367  /**
   2368   * Sets if this object instance is a matched or non-matched item.
   2369   *
   2370   * @param {boolean} aStatus
   2371   */
   2372  set _matched(aStatus) {
   2373    if (this._isMatch == aStatus) {
   2374      return;
   2375    }
   2376    if (aStatus) {
   2377      this._isMatch = true;
   2378      this.target.removeAttribute("unmatched");
   2379    } else {
   2380      this._isMatch = false;
   2381      this.target.setAttribute("unmatched", "");
   2382    }
   2383  }
   2384 
   2385  /**
   2386   * Find the first item in the tree of visible items in this item that matches
   2387   * the predicate. Searches in visual order (the order seen by the user).
   2388   * Tests itself, then descends into first the enumerable children and then
   2389   * the non-enumerable children (since they are presented in separate groups).
   2390   *
   2391   * @param {Function} aPredicate
   2392   *        A function that returns true when a match is found.
   2393   * @return {Scope | Variable | Property}
   2394   *         The first visible scope, variable or property, or null if nothing
   2395   *         is found.
   2396   */
   2397  _findInVisibleItems(aPredicate) {
   2398    if (aPredicate(this)) {
   2399      return this;
   2400    }
   2401 
   2402    if (this._isExpanded) {
   2403      if (this._variablesView._enumVisible) {
   2404        for (const item of this._enumItems) {
   2405          const result = item._findInVisibleItems(aPredicate);
   2406          if (result) {
   2407            return result;
   2408          }
   2409        }
   2410      }
   2411 
   2412      if (this._variablesView._nonEnumVisible) {
   2413        for (const item of this._nonEnumItems) {
   2414          const result = item._findInVisibleItems(aPredicate);
   2415          if (result) {
   2416            return result;
   2417          }
   2418        }
   2419      }
   2420    }
   2421 
   2422    return null;
   2423  }
   2424 
   2425  /**
   2426   * Find the last item in the tree of visible items in this item that matches
   2427   * the predicate. Searches in reverse visual order (opposite of the order
   2428   * seen by the user). Descends into first the non-enumerable children, then
   2429   * the enumerable children (since they are presented in separate groups), and
   2430   * finally tests itself.
   2431   *
   2432   * @param {Function} aPredicate
   2433   *        A function that returns true when a match is found.
   2434   * @return {Scope | Variable | Property}
   2435   *         The last visible scope, variable or property, or null if nothing
   2436   *         is found.
   2437   */
   2438  _findInVisibleItemsReverse(aPredicate) {
   2439    if (this._isExpanded) {
   2440      if (this._variablesView._nonEnumVisible) {
   2441        for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
   2442          const item = this._nonEnumItems[i];
   2443          const result = item._findInVisibleItemsReverse(aPredicate);
   2444          if (result) {
   2445            return result;
   2446          }
   2447        }
   2448      }
   2449 
   2450      if (this._variablesView._enumVisible) {
   2451        for (let i = this._enumItems.length - 1; i >= 0; i--) {
   2452          const item = this._enumItems[i];
   2453          const result = item._findInVisibleItemsReverse(aPredicate);
   2454          if (result) {
   2455            return result;
   2456          }
   2457        }
   2458      }
   2459    }
   2460 
   2461    if (aPredicate(this)) {
   2462      return this;
   2463    }
   2464 
   2465    return null;
   2466  }
   2467 
   2468  /**
   2469   * Gets top level variables view instance.
   2470   *
   2471   * @return {VariablesView}
   2472   */
   2473  get _variablesView() {
   2474    return (
   2475      this._topView ||
   2476      (this._topView = (() => {
   2477        let parentView = this.ownerView;
   2478        let topView;
   2479 
   2480        while ((topView = parentView.ownerView)) {
   2481          parentView = topView;
   2482        }
   2483        return parentView;
   2484      })())
   2485    );
   2486  }
   2487 
   2488  /**
   2489   * Gets the parent node holding this scope.
   2490   *
   2491   * @return {Node}
   2492   */
   2493  get parentNode() {
   2494    return this.ownerView._list;
   2495  }
   2496 
   2497  /**
   2498   * Gets the owner document holding this scope.
   2499   *
   2500   * @return {HTMLDocument}
   2501   */
   2502  get document() {
   2503    return this._document || (this._document = this.ownerView.document);
   2504  }
   2505 
   2506  /**
   2507   * Gets the default window holding this scope.
   2508   *
   2509   * @return {Window}
   2510   */
   2511  get window() {
   2512    return this._window || (this._window = this.ownerView.window);
   2513  }
   2514 
   2515  _topView = null;
   2516  _document = null;
   2517  _window = null;
   2518 
   2519  ownerView = null;
   2520  contextMenuId = "";
   2521  separatorStr = "";
   2522 
   2523  _fetched = false;
   2524  _isExpanded = false;
   2525  _isContentVisible = true;
   2526  _isHeaderVisible = true;
   2527  _isArrowVisible = true;
   2528  _isMatch = true;
   2529  _idString = "";
   2530  _nameString = "";
   2531  _target = null;
   2532  _arrow = null;
   2533  _name = null;
   2534  _title = null;
   2535  _enum = null;
   2536  _nonenum = null;
   2537 
   2538  *[Symbol.iterator]() {
   2539    yield* this._store;
   2540  }
   2541 }
   2542 
   2543 // Creating maps and arrays thousands of times for variables or properties
   2544 // with a large number of children fills up a lot of memory. Make sure
   2545 // these are instantiated only if needed.
   2546 DevToolsUtils.defineLazyPrototypeGetter(
   2547  Scope.prototype,
   2548  "_store",
   2549  () => new Map()
   2550 );
   2551 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
   2552 DevToolsUtils.defineLazyPrototypeGetter(
   2553  Scope.prototype,
   2554  "_nonEnumItems",
   2555  Array
   2556 );
   2557 
   2558 /**
   2559 * A Variable is a Scope holding Property instances.
   2560 * Iterable via "for (let [name, property] of instance) { }".
   2561 */
   2562 class Variable extends Scope {
   2563  /**
   2564   * @param {Scope} aScope
   2565   *        The scope to contain this variable.
   2566   * @param {string} aName
   2567   *        The variable's name.
   2568   * @param {object} aDescriptor
   2569   *        The variable's descriptor.
   2570   * @param {object} aOptions
   2571   *        Options of the form accepted by Scope.addItem
   2572   */
   2573  constructor(aScope, aName, aDescriptor, aOptions) {
   2574    // Treat safe getter descriptors as descriptors with a value.
   2575    if ("getterValue" in aDescriptor) {
   2576      aDescriptor.value = aDescriptor.getterValue;
   2577      delete aDescriptor.get;
   2578      delete aDescriptor.set;
   2579    }
   2580 
   2581    super(aScope, aName, {
   2582      _internalItem: aOptions.internalItem,
   2583      _initialDescriptor: aDescriptor,
   2584    });
   2585 
   2586    this.setGrip(aDescriptor.value);
   2587  }
   2588 
   2589  /**
   2590   * Whether this Variable should be prefetched when it is remoted.
   2591   */
   2592  get shouldPrefetch() {
   2593    return this.name == "window" || this.name == "this";
   2594  }
   2595 
   2596  /**
   2597   * Whether this Variable should paginate its contents.
   2598   */
   2599  get allowPaginate() {
   2600    return this.name != "window" && this.name != "this";
   2601  }
   2602 
   2603  /**
   2604   * The class name applied to this variable's target element.
   2605   */
   2606  get targetClassName() {
   2607    return "variables-view-variable variable-or-property";
   2608  }
   2609 
   2610  /**
   2611   * Create a new Property that is a child of Variable.
   2612   *
   2613   * @param {string} aName
   2614   *        The name of the new Property.
   2615   * @param {object} aDescriptor
   2616   *        The property's descriptor.
   2617   * @param {object} aOptions
   2618   *        Options of the form accepted by Scope.addItem
   2619   * @return {Property}
   2620   *         The newly created child Property.
   2621   */
   2622  _createChild(aName, aDescriptor, aOptions) {
   2623    return new Property(this, aName, aDescriptor, aOptions);
   2624  }
   2625 
   2626  /**
   2627   * Remove this Variable from its parent and remove all children recursively.
   2628   */
   2629  remove() {
   2630    this.ownerView._store.delete(this._nameString);
   2631    this._variablesView._itemsByElement.delete(this._target);
   2632    this._variablesView._testOnlyHierarchy.delete(this.absoluteName);
   2633 
   2634    this._target.remove();
   2635 
   2636    for (const property of this._store.values()) {
   2637      property.remove();
   2638    }
   2639  }
   2640 
   2641  /**
   2642   * Populates this variable to contain all the properties of an object.
   2643   *
   2644   * @param {object} aObject
   2645   *        The raw object you want to display.
   2646   * @param {object} [aOptions={}]
   2647   *        Additional options for adding the properties. Supported options:
   2648   *        - sorted: true to sort all the properties before adding them
   2649   *        - expanded: true to expand all the properties after adding them
   2650   */
   2651  populate(aObject, aOptions = {}) {
   2652    // Retrieve the properties only once.
   2653    if (this._fetched) {
   2654      return;
   2655    }
   2656    this._fetched = true;
   2657 
   2658    const propertyNames = Object.getOwnPropertyNames(aObject);
   2659    const prototype = Object.getPrototypeOf(aObject);
   2660 
   2661    // Sort all of the properties before adding them, if preferred.
   2662    if (aOptions.sorted) {
   2663      propertyNames.sort(this._naturalSort);
   2664    }
   2665 
   2666    // Add all the variable properties.
   2667    for (const name of propertyNames) {
   2668      const descriptor = Object.getOwnPropertyDescriptor(aObject, name);
   2669      if (descriptor.get || descriptor.set) {
   2670        const prop = this._addRawNonValueProperty(name, descriptor);
   2671        if (aOptions.expanded) {
   2672          prop.expanded = true;
   2673        }
   2674      } else {
   2675        const prop = this._addRawValueProperty(name, descriptor, aObject[name]);
   2676        if (aOptions.expanded) {
   2677          prop.expanded = true;
   2678        }
   2679      }
   2680    }
   2681    // Add the variable's __proto__.
   2682    if (prototype) {
   2683      this._addRawValueProperty("__proto__", {}, prototype);
   2684    }
   2685  }
   2686 
   2687  /**
   2688   * Populates a specific variable or property instance to contain all the
   2689   * properties of an object
   2690   *
   2691   * @param {Variable | Property} aVar
   2692   *        The target variable to populate.
   2693   * @param {object} [aObject=aVar._sourceValue]
   2694   *        The raw object you want to display. If unspecified, the object is
   2695   *        assumed to be defined in a _sourceValue property on the target.
   2696   */
   2697  _populateTarget(aVar, aObject = aVar._sourceValue) {
   2698    aVar.populate(aObject);
   2699  }
   2700 
   2701  /**
   2702   * Adds a property for this variable based on a raw value descriptor.
   2703   *
   2704   * @param {string} aName
   2705   *        The property's name.
   2706   * @param {object} aDescriptor
   2707   *        Specifies the exact property descriptor as returned by a call to
   2708   *        Object.getOwnPropertyDescriptor.
   2709   * @param {object} aValue
   2710   *        The raw property value you want to display.
   2711   * @return {Property}
   2712   *         The newly added property instance.
   2713   */
   2714  _addRawValueProperty(aName, aDescriptor, aValue) {
   2715    const descriptor = Object.create(aDescriptor);
   2716    descriptor.value = VariablesView.getGrip(aValue);
   2717 
   2718    const propertyItem = this.addItem(aName, descriptor);
   2719    propertyItem._sourceValue = aValue;
   2720 
   2721    // Add an 'onexpand' callback for the property, lazily handling
   2722    // the addition of new child properties.
   2723    if (!VariablesView.isPrimitive(descriptor)) {
   2724      propertyItem.onexpand = this._populateTarget;
   2725    }
   2726    return propertyItem;
   2727  }
   2728 
   2729  /**
   2730   * Adds a property for this variable based on a getter/setter descriptor.
   2731   *
   2732   * @param {string} aName
   2733   *        The property's name.
   2734   * @param {object} aDescriptor
   2735   *        Specifies the exact property descriptor as returned by a call to
   2736   *        Object.getOwnPropertyDescriptor.
   2737   * @return {Property}
   2738   *         The newly added property instance.
   2739   */
   2740  _addRawNonValueProperty(aName, aDescriptor) {
   2741    const descriptor = Object.create(aDescriptor);
   2742    descriptor.get = VariablesView.getGrip(aDescriptor.get);
   2743    descriptor.set = VariablesView.getGrip(aDescriptor.set);
   2744 
   2745    return this.addItem(aName, descriptor);
   2746  }
   2747 
   2748  /**
   2749   * Gets this variable's path to the topmost scope in the form of a string
   2750   * meant for use via eval() or a similar approach.
   2751   * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
   2752   *
   2753   * @return {string}
   2754   */
   2755  get symbolicName() {
   2756    return this._nameString || "";
   2757  }
   2758 
   2759  /**
   2760   * Gets full path to this variable, including name of the scope.
   2761   *
   2762   * @return {string}
   2763   */
   2764  get absoluteName() {
   2765    if (this._absoluteName) {
   2766      return this._absoluteName;
   2767    }
   2768 
   2769    this._absoluteName =
   2770      this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
   2771    return this._absoluteName;
   2772  }
   2773 
   2774  /**
   2775   * Gets this variable's symbolic path to the topmost scope.
   2776   *
   2777   * @return {Array}
   2778   * @see Variable._buildSymbolicPath
   2779   */
   2780  get symbolicPath() {
   2781    if (this._symbolicPath) {
   2782      return this._symbolicPath;
   2783    }
   2784    this._symbolicPath = this._buildSymbolicPath();
   2785    return this._symbolicPath;
   2786  }
   2787 
   2788  /**
   2789   * Build this variable's path to the topmost scope in form of an array of
   2790   * strings, one for each segment of the path.
   2791   * For example, a symbolic path may look like ["0", "foo", "bar"].
   2792   *
   2793   * @return {Array}
   2794   */
   2795  _buildSymbolicPath(path = []) {
   2796    if (this.name) {
   2797      path.unshift(this.name);
   2798      if (this.ownerView instanceof Variable) {
   2799        return this.ownerView._buildSymbolicPath(path);
   2800      }
   2801    }
   2802    return path;
   2803  }
   2804 
   2805  /**
   2806   * Returns this variable's value from the descriptor if available.
   2807   *
   2808   * @return {any}
   2809   */
   2810  get value() {
   2811    return this._initialDescriptor.value;
   2812  }
   2813 
   2814  /**
   2815   * Returns this variable's getter from the descriptor if available.
   2816   *
   2817   * @return {object}
   2818   */
   2819  get getter() {
   2820    return this._initialDescriptor.get;
   2821  }
   2822 
   2823  /**
   2824   * Returns this variable's getter from the descriptor if available.
   2825   *
   2826   * @return {object}
   2827   */
   2828  get setter() {
   2829    return this._initialDescriptor.set;
   2830  }
   2831 
   2832  /**
   2833   * Sets the specific grip for this variable (applies the text content and
   2834   * class name to the value label).
   2835   *
   2836   * The grip should contain the value or the type & class, as defined in the
   2837   * remote debugger protocol. For convenience, undefined and null are
   2838   * both considered types.
   2839   *
   2840   * @param {any} aGrip
   2841   *        Specifies the value and/or type & class of the variable.
   2842   *        e.g. - 42
   2843   *             - true
   2844   *             - "nasu"
   2845   *             - { type: "undefined" }
   2846   *             - { type: "null" }
   2847   *             - { type: "object", class: "Object" }
   2848   */
   2849  setGrip(aGrip) {
   2850    // Don't allow displaying grip information if there's no name available
   2851    // or the grip is malformed.
   2852    if (
   2853      this._nameString === undefined ||
   2854      aGrip === undefined ||
   2855      aGrip === null
   2856    ) {
   2857      return;
   2858    }
   2859    // Getters and setters should display grip information in sub-properties.
   2860    if (this.getter || this.setter) {
   2861      return;
   2862    }
   2863 
   2864    const prevGrip = this._valueGrip;
   2865    if (prevGrip) {
   2866      this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
   2867    }
   2868    this._valueGrip = aGrip;
   2869 
   2870    if (
   2871      aGrip &&
   2872      (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)
   2873    ) {
   2874      if (aGrip.optimizedOut) {
   2875        this._valueString = L10N.getStr("variablesViewOptimizedOut");
   2876      } else if (aGrip.uninitialized) {
   2877        this._valueString = L10N.getStr("variablesViewUninitialized");
   2878      } else if (aGrip.missingArguments) {
   2879        this._valueString = L10N.getStr("variablesViewMissingArgs");
   2880      }
   2881      this.eval = null;
   2882    } else {
   2883      this._valueString = VariablesView.getString(aGrip, {
   2884        concise: true,
   2885        noEllipsis: true,
   2886      });
   2887      this.eval = this.ownerView.eval;
   2888    }
   2889 
   2890    this._valueClassName = VariablesView.getClass(aGrip);
   2891 
   2892    this._valueLabel.classList.add(this._valueClassName);
   2893    this._valueLabel.setAttribute("value", this._valueString);
   2894    this._separatorLabel.hidden = false;
   2895  }
   2896 
   2897  /**
   2898   * Initializes this variable's id, view and binds event listeners.
   2899   *
   2900   * @override
   2901   * @param {string} aName
   2902   *        The variable's name.
   2903   * @param {object} options
   2904   * @param {object} options._internalItem
   2905   * @param {object} options._initialDescriptor
   2906   */
   2907  _init(aName, { _internalItem, _initialDescriptor }) {
   2908    this._internalItem = _internalItem;
   2909    this._initialDescriptor = _initialDescriptor;
   2910 
   2911    this._idString = generateId((this._nameString = aName));
   2912    this._displayScope({ value: aName, targetClassName: this.targetClassName });
   2913    this._displayVariable();
   2914 
   2915    if (this.ownerView.contextMenuId) {
   2916      this._title.setAttribute("context", this.ownerView.contextMenuId);
   2917    }
   2918 
   2919    this._addEventListeners();
   2920 
   2921    if (
   2922      this._initialDescriptor.enumerable ||
   2923      this._nameString == "this" ||
   2924      this._internalItem
   2925    ) {
   2926      this.ownerView._enum.appendChild(this._target);
   2927      this.ownerView._enumItems.push(this);
   2928    } else {
   2929      this.ownerView._nonenum.appendChild(this._target);
   2930      this.ownerView._nonEnumItems.push(this);
   2931    }
   2932  }
   2933 
   2934  /**
   2935   * Creates the necessary nodes for this variable.
   2936   */
   2937  _displayVariable() {
   2938    const document = this.document;
   2939    const descriptor = this._initialDescriptor;
   2940 
   2941    const separatorLabel = (this._separatorLabel =
   2942      document.createXULElement("label"));
   2943    separatorLabel.className = "separator";
   2944    separatorLabel.setAttribute("value", this.separatorStr + " ");
   2945 
   2946    const valueLabel = (this._valueLabel = document.createXULElement("label"));
   2947    valueLabel.className = "value";
   2948    valueLabel.setAttribute("flex", "1");
   2949    valueLabel.setAttribute("crop", "center");
   2950 
   2951    this._title.appendChild(separatorLabel);
   2952    this._title.appendChild(valueLabel);
   2953 
   2954    if (VariablesView.isPrimitive(descriptor)) {
   2955      this.hideArrow();
   2956    }
   2957 
   2958    // If no value will be displayed, we don't need the separator.
   2959    if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
   2960      separatorLabel.hidden = true;
   2961    }
   2962 
   2963    // If this is a getter/setter property, create two child pseudo-properties
   2964    // called "get" and "set" that display the corresponding functions.
   2965    if (descriptor.get || descriptor.set) {
   2966      separatorLabel.hidden = true;
   2967      valueLabel.hidden = true;
   2968 
   2969      const getter = this.addItem("get", { value: descriptor.get });
   2970      const setter = this.addItem("set", { value: descriptor.set });
   2971 
   2972      getter.hideArrow();
   2973      setter.hideArrow();
   2974      this.expand();
   2975    }
   2976  }
   2977 
   2978  /**
   2979   * Adds the necessary event listeners for this variable.
   2980   */
   2981  _addEventListeners() {
   2982    this._title.addEventListener("mousedown", this._onClick);
   2983  }
   2984 
   2985  _symbolicName = null;
   2986  _symbolicPath = null;
   2987  _absoluteName = null;
   2988 
   2989  _spacer = null;
   2990  _valueGrip = null;
   2991  _valueString = "";
   2992  _valueClassName = "";
   2993  _prevExpandable = false;
   2994  _prevExpanded = false;
   2995 }
   2996 
   2997 /**
   2998 * A Property is a Variable holding additional child Property instances.
   2999 * Iterable via "for (let [name, property] of instance) { }".
   3000 */
   3001 class Property extends Variable {
   3002  /**
   3003   * @param {Variable} aVar
   3004   *        The variable to contain this property.
   3005   * @param {string} aName
   3006   *        The property's name.
   3007   * @param {object} aDescriptor
   3008   *        The property's descriptor.
   3009   * @param {object} aOptions
   3010   *        Options of the form accepted by Scope.addItem
   3011   */
   3012  constructor(aVar, aName, aDescriptor, aOptions) {
   3013    super(aVar, aName, aDescriptor, aOptions);
   3014  }
   3015 
   3016  /**
   3017   * The class name applied to this property's target element.
   3018   */
   3019  get targetClassName() {
   3020    return "variables-view-property variable-or-property";
   3021  }
   3022 
   3023  /**
   3024   * @see Variable.symbolicName
   3025   * @return {string}
   3026   */
   3027  get symbolicName() {
   3028    if (this._symbolicName) {
   3029      return this._symbolicName;
   3030    }
   3031 
   3032    this._symbolicName =
   3033      this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
   3034    return this._symbolicName;
   3035  }
   3036 
   3037  /**
   3038   * @see Variable.absoluteName
   3039   * @return {string}
   3040   */
   3041  get absoluteName() {
   3042    if (this._absoluteName) {
   3043      return this._absoluteName;
   3044    }
   3045 
   3046    this._absoluteName =
   3047      this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
   3048    return this._absoluteName;
   3049  }
   3050 }
   3051 
   3052 // Match the function name from the result of toString() or toSource().
   3053 //
   3054 // Examples:
   3055 // (function foobar(a, b) { ...
   3056 // function foobar2(a) { ...
   3057 // function() { ...
   3058 const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
   3059 
   3060 /**
   3061 * Helper function to deduce the name of the provided function.
   3062 *
   3063 * @param {Function} function
   3064 *        The function whose name will be returned.
   3065 * @return {string}
   3066 *         Function name.
   3067 */
   3068 function getFunctionName(func) {
   3069  let name = null;
   3070  if (func.name) {
   3071    name = func.name;
   3072  } else {
   3073    let desc;
   3074    try {
   3075      desc = func.getOwnPropertyDescriptor("displayName");
   3076    } catch (ex) {
   3077      // Ignore.
   3078    }
   3079    if (desc && typeof desc.value == "string") {
   3080      name = desc.value;
   3081    }
   3082  }
   3083  if (!name) {
   3084    try {
   3085      const str = (func.toString() || func.toSource()) + "";
   3086      name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
   3087    } catch (ex) {
   3088      // Ignore.
   3089    }
   3090  }
   3091  return name;
   3092 }
   3093 
   3094 /**
   3095 * Get the object class name. For example, the |window| object has the Window
   3096 * class name (based on [object Window]).
   3097 *
   3098 * @param {object} object
   3099 *        The object you want to get the class name for.
   3100 * @return {string}
   3101 *         The object class name.
   3102 */
   3103 function getObjectClassName(object) {
   3104  if (object === null) {
   3105    return "null";
   3106  }
   3107  if (object === undefined) {
   3108    return "undefined";
   3109  }
   3110 
   3111  const type = typeof object;
   3112  if (type != "object") {
   3113    // Grip class names should start with an uppercase letter.
   3114    return type.charAt(0).toUpperCase() + type.substr(1);
   3115  }
   3116 
   3117  let className;
   3118 
   3119  try {
   3120    className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
   3121    if (!className) {
   3122      className = ((object.constructor + "").match(/^\[object (\S+)\]$/) ||
   3123        [])[1];
   3124    }
   3125    if (!className && typeof object.constructor == "function") {
   3126      className = getFunctionName(object.constructor);
   3127    }
   3128  } catch (ex) {
   3129    // Ignore.
   3130  }
   3131 
   3132  return className;
   3133 }
   3134 
   3135 /**
   3136 * A monotonically-increasing counter, that guarantees the uniqueness of scope,
   3137 * variables and properties ids.
   3138 *
   3139 * @param string aName
   3140 *        An optional string to prefix the id with.
   3141 * @return number
   3142 *         A unique id.
   3143 */
   3144 var generateId = (function () {
   3145  let count = 0;
   3146  return function (aName = "") {
   3147    return aName.toLowerCase().trim().replace(/\s+/g, "-") + ++count;
   3148  };
   3149 })();
   3150 
   3151 /**
   3152 * Quote and escape a string. The result will be another string containing an
   3153 * ECMAScript StringLiteral which will produce the original one when evaluated
   3154 * by `eval` or similar.
   3155 *
   3156 * @param string aString
   3157 *       An optional string to be escaped. If no string is passed, the function
   3158 *       returns an empty string.
   3159 * @return string
   3160 */
   3161 function escapeString(aString) {
   3162  if (typeof aString !== "string") {
   3163    return "";
   3164  }
   3165  // U+2028 and U+2029 are allowed in JSON but not in ECMAScript string literals.
   3166  return JSON.stringify(aString)
   3167    .replace(/\u2028/g, "\\u2028")
   3168    .replace(/\u2029/g, "\\u2029");
   3169 }
   3170 
   3171 /**
   3172 * Escape some HTML special characters. We do not need full HTML serialization
   3173 * here, we just want to make strings safe to display in HTML attributes, for
   3174 * the stringifiers.
   3175 *
   3176 * @param string aString
   3177 * @return string
   3178 */
   3179 export function escapeHTML(aString) {
   3180  return aString
   3181    .replace(/&/g, "&amp;")
   3182    .replace(/"/g, "&quot;")
   3183    .replace(/</g, "&lt;")
   3184    .replace(/>/g, "&gt;");
   3185 }