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"use strict";
5
6const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
7const LAZY_EMPTY_DELAY = 150; // ms
8const SCROLL_PAGE_SIZE_DEFAULT = 0;
9const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
10const PAGE_SIZE_MAX_JUMPS = 30;
11const SEARCH_ACTION_MAX_DELAY = 300; // ms
12const ITEM_FLASH_DURATION = 300; // ms
13
14const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
15const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
16const EventEmitter = require("devtools/shared/event-emitter");
17const DevToolsUtils = require("devtools/shared/DevToolsUtils");
18const Services = require("Services");
19const { getSourceNames } = require("devtools/client/shared/source-utils");
20const promise = require("promise");
21const { extend } = require("devtools/shared/extend");
22const {
23  ViewHelpers,
24  setNamedTimeout,
25} = require("devtools/client/shared/widgets/view-helpers");
26const nodeConstants = require("devtools/shared/dom-node-constants");
27const { KeyCodes } = require("devtools/client/shared/keycodes");
28const { PluralForm } = require("devtools/shared/plural-form");
29const { LocalizationHelper, ELLIPSIS } = require("devtools/shared/l10n");
30const L10N = new LocalizationHelper(DBG_STRINGS_URI);
31const HTML_NS = "http://www.w3.org/1999/xhtml";
32
33XPCOMUtils.defineLazyServiceGetter(
34  this,
35  "clipboardHelper",
36  "@mozilla.org/widget/clipboardhelper;1",
37  "nsIClipboardHelper"
38);
39
40const EXPORTED_SYMBOLS = ["VariablesView", "escapeHTML"];
41
42/**
43 * A tree view for inspecting scopes, objects and properties.
44 * Iterable via "for (let [id, scope] of instance) { }".
45 * Requires the devtools common.css and debugger.css skin stylesheets.
46 *
47 * To allow replacing variable or property values in this view, provide an
48 * "eval" function property. To allow replacing variable or property names,
49 * provide a "switch" function. To handle deleting variables or properties,
50 * provide a "delete" function.
51 *
52 * @param Node aParentNode
53 *        The parent node to hold this view.
54 * @param object aFlags [optional]
55 *        An object contaning initialization options for this view.
56 *        e.g. { lazyEmpty: true, searchEnabled: true ... }
57 */
58function VariablesView(aParentNode, aFlags = {}) {
59  this._store = []; // Can't use a Map because Scope names needn't be unique.
60  this._itemsByElement = new WeakMap();
61  this._prevHierarchy = new Map();
62  this._currHierarchy = new Map();
63
64  this._parent = aParentNode;
65  this._parent.classList.add("variables-view-container");
66  this._parent.classList.add("theme-body");
67  this._appendEmptyNotice();
68
69  this._onSearchboxInput = this._onSearchboxInput.bind(this);
70  this._onSearchboxKeyDown = this._onSearchboxKeyDown.bind(this);
71  this._onViewKeyDown = this._onViewKeyDown.bind(this);
72
73  // Create an internal scrollbox container.
74  this._list = this.document.createXULElement("scrollbox");
75  this._list.setAttribute("orient", "vertical");
76  this._list.addEventListener("keydown", this._onViewKeyDown);
77  this._parent.appendChild(this._list);
78
79  for (const name in aFlags) {
80    this[name] = aFlags[name];
81  }
82
83  EventEmitter.decorate(this);
84}
85
86VariablesView.prototype = {
87  /**
88   * Helper setter for populating this container with a raw object.
89   *
90   * @param object aObject
91   *        The raw object to display. You can only provide this object
92   *        if you want the variables view to work in sync mode.
93   */
94  set rawObject(aObject) {
95    this.empty();
96    this.addScope()
97      .addItem(undefined, { enumerable: true })
98      .populate(aObject, { sorted: true });
99  },
100
101  /**
102   * Adds a scope to contain any inspected variables.
103   *
104   * This new scope will be considered the parent of any other scope
105   * added afterwards.
106   *
107   * @param string aName
108   *        The scope's name (e.g. "Local", "Global" etc.).
109   * @param string aCustomClass
110   *        An additional class name for the containing element.
111   * @return Scope
112   *         The newly created Scope instance.
113   */
114  addScope: function(aName = "", aCustomClass = "") {
115    this._removeEmptyNotice();
116    this._toggleSearchVisibility(true);
117
118    const scope = new Scope(this, aName, { customClass: aCustomClass });
119    this._store.push(scope);
120    this._itemsByElement.set(scope._target, scope);
121    this._currHierarchy.set(aName, scope);
122    scope.header = !!aName;
123
124    return scope;
125  },
126
127  /**
128   * Removes all items from this container.
129   *
130   * @param number aTimeout [optional]
131   *        The number of milliseconds to delay the operation if
132   *        lazy emptying of this container is enabled.
133   */
134  empty: function(aTimeout = this.lazyEmptyDelay) {
135    // If there are no items in this container, emptying is useless.
136    if (!this._store.length) {
137      return;
138    }
139
140    this._store.length = 0;
141    this._itemsByElement = new WeakMap();
142    this._prevHierarchy = this._currHierarchy;
143    this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
144
145    // Check if this empty operation may be executed lazily.
146    if (this.lazyEmpty && aTimeout > 0) {
147      this._emptySoon(aTimeout);
148      return;
149    }
150
151    while (this._list.hasChildNodes()) {
152      this._list.firstChild.remove();
153    }
154
155    this._appendEmptyNotice();
156    this._toggleSearchVisibility(false);
157  },
158
159  /**
160   * Emptying this container and rebuilding it immediately afterwards would
161   * result in a brief redraw flicker, because the previously expanded nodes
162   * may get asynchronously re-expanded, after fetching the prototype and
163   * properties from a server.
164   *
165   * To avoid such behaviour, a normal container list is rebuild, but not
166   * immediately attached to the parent container. The old container list
167   * is kept around for a short period of time, hopefully accounting for the
168   * data fetching delay. In the meantime, any operations can be executed
169   * normally.
170   *
171   * @see VariablesView.empty
172   * @see VariablesView.commitHierarchy
173   */
174  _emptySoon: function(aTimeout) {
175    const prevList = this._list;
176    const currList = (this._list = this.document.createXULElement("scrollbox"));
177
178    this.window.setTimeout(() => {
179      prevList.removeEventListener("keydown", this._onViewKeyDown);
180      currList.addEventListener("keydown", this._onViewKeyDown);
181      currList.setAttribute("orient", "vertical");
182
183      this._parent.removeChild(prevList);
184      this._parent.appendChild(currList);
185
186      if (!this._store.length) {
187        this._appendEmptyNotice();
188        this._toggleSearchVisibility(false);
189      }
190    }, aTimeout);
191  },
192
193  /**
194   * Optional DevTools toolbox containing this VariablesView. Used to
195   * communicate with the inspector and highlighter.
196   */
197  toolbox: null,
198
199  /**
200   * The controller for this VariablesView, if it has one.
201   */
202  controller: null,
203
204  /**
205   * The amount of time (in milliseconds) it takes to empty this view lazily.
206   */
207  lazyEmptyDelay: LAZY_EMPTY_DELAY,
208
209  /**
210   * Specifies if this view may be emptied lazily.
211   * @see VariablesView.prototype.empty
212   */
213  lazyEmpty: false,
214
215  /**
216   * Specifies if nodes in this view may be searched lazily.
217   */
218  lazySearch: true,
219
220  /**
221   * The number of elements in this container to jump when Page Up or Page Down
222   * keys are pressed. If falsy, then the page size will be based on the
223   * container height.
224   */
225  scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
226
227  /**
228   * Function called each time a variable or property's value is changed via
229   * user interaction. If null, then value changes are disabled.
230   *
231   * This property is applied recursively onto each scope in this view and
232   * affects only the child nodes when they're created.
233   */
234  eval: null,
235
236  /**
237   * Function called each time a variable or property's name is changed via
238   * user interaction. If null, then name changes are disabled.
239   *
240   * This property is applied recursively onto each scope in this view and
241   * affects only the child nodes when they're created.
242   */
243  switch: null,
244
245  /**
246   * Function called each time a variable or property is deleted via
247   * user interaction. If null, then deletions are disabled.
248   *
249   * This property is applied recursively onto each scope in this view and
250   * affects only the child nodes when they're created.
251   */
252  delete: null,
253
254  /**
255   * Function called each time a property is added via user interaction. If
256   * null, then property additions are disabled.
257   *
258   * This property is applied recursively onto each scope in this view and
259   * affects only the child nodes when they're created.
260   */
261  new: null,
262
263  /**
264   * Specifies if after an eval or switch operation, the variable or property
265   * which has been edited should be disabled.
266   */
267  preventDisableOnChange: false,
268
269  /**
270   * Specifies if, whenever a variable or property descriptor is available,
271   * configurable, enumerable, writable, frozen, sealed and extensible
272   * attributes should not affect presentation.
273   *
274   * This flag is applied recursively onto each scope in this view and
275   * affects only the child nodes when they're created.
276   */
277  preventDescriptorModifiers: false,
278
279  /**
280   * The tooltip text shown on a variable or property's value if an |eval|
281   * function is provided, in order to change the variable or property's value.
282   *
283   * This flag is applied recursively onto each scope in this view and
284   * affects only the child nodes when they're created.
285   */
286  editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),
287
288  /**
289   * The tooltip text shown on a variable or property's name if a |switch|
290   * function is provided, in order to change the variable or property's name.
291   *
292   * This flag is applied recursively onto each scope in this view and
293   * affects only the child nodes when they're created.
294   */
295  editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),
296
297  /**
298   * The tooltip text shown on a variable or property's edit button if an
299   * |eval| function is provided and a getter/setter descriptor is present,
300   * in order to change the variable or property to a plain value.
301   *
302   * This flag is applied recursively onto each scope in this view and
303   * affects only the child nodes when they're created.
304   */
305  editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),
306
307  /**
308   * The tooltip text shown on a variable or property's value if that value is
309   * a DOMNode that can be highlighted and selected in the inspector.
310   *
311   * This flag is applied recursively onto each scope in this view and
312   * affects only the child nodes when they're created.
313   */
314  domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),
315
316  /**
317   * The tooltip text shown on a variable or property's delete button if a
318   * |delete| function is provided, in order to delete the variable or property.
319   *
320   * This flag is applied recursively onto each scope in this view and
321   * affects only the child nodes when they're created.
322   */
323  deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),
324
325  /**
326   * Specifies the context menu attribute set on variables and properties.
327   *
328   * This flag is applied recursively onto each scope in this view and
329   * affects only the child nodes when they're created.
330   */
331  contextMenuId: "",
332
333  /**
334   * The separator label between the variables or properties name and value.
335   *
336   * This flag is applied recursively onto each scope in this view and
337   * affects only the child nodes when they're created.
338   */
339  separatorStr: L10N.getStr("variablesSeparatorLabel"),
340
341  /**
342   * Specifies if enumerable properties and variables should be displayed.
343   * These variables and properties are visible by default.
344   * @param boolean aFlag
345   */
346  set enumVisible(aFlag) {
347    this._enumVisible = aFlag;
348
349    for (const scope of this._store) {
350      scope._enumVisible = aFlag;
351    }
352  },
353
354  /**
355   * Specifies if non-enumerable properties and variables should be displayed.
356   * These variables and properties are visible by default.
357   * @param boolean aFlag
358   */
359  set nonEnumVisible(aFlag) {
360    this._nonEnumVisible = aFlag;
361
362    for (const scope of this._store) {
363      scope._nonEnumVisible = aFlag;
364    }
365  },
366
367  /**
368   * Specifies if only enumerable properties and variables should be displayed.
369   * Both types of these variables and properties are visible by default.
370   * @param boolean aFlag
371   */
372  set onlyEnumVisible(aFlag) {
373    if (aFlag) {
374      this.enumVisible = true;
375      this.nonEnumVisible = false;
376    } else {
377      this.enumVisible = true;
378      this.nonEnumVisible = true;
379    }
380  },
381
382  /**
383   * Sets if the variable and property searching is enabled.
384   * @param boolean aFlag
385   */
386  set searchEnabled(aFlag) {
387    aFlag ? this._enableSearch() : this._disableSearch();
388  },
389
390  /**
391   * Gets if the variable and property searching is enabled.
392   * @return boolean
393   */
394  get searchEnabled() {
395    return !!this._searchboxContainer;
396  },
397
398  /**
399   * Sets the text displayed for the searchbox in this container.
400   * @param string aValue
401   */
402  set searchPlaceholder(aValue) {
403    if (this._searchboxNode) {
404      this._searchboxNode.setAttribute("placeholder", aValue);
405    }
406    this._searchboxPlaceholder = aValue;
407  },
408
409  /**
410   * Gets the text displayed for the searchbox in this container.
411   * @return string
412   */
413  get searchPlaceholder() {
414    return this._searchboxPlaceholder;
415  },
416
417  /**
418   * Enables variable and property searching in this view.
419   * Use the "searchEnabled" setter to enable searching.
420   */
421  _enableSearch: function() {
422    // If searching was already enabled, no need to re-enable it again.
423    if (this._searchboxContainer) {
424      return;
425    }
426    const document = this.document;
427    const ownerNode = this._parent.parentNode;
428
429    const container = (this._searchboxContainer = document.createXULElement(
430      "hbox"
431    ));
432    container.className = "devtools-toolbar devtools-input-toolbar";
433
434    // Hide the variables searchbox container if there are no variables or
435    // properties to display.
436    container.hidden = !this._store.length;
437
438    const searchbox = (this._searchboxNode = document.createElementNS(
439      HTML_NS,
440      "input"
441    ));
442    searchbox.className = "variables-view-searchinput devtools-filterinput";
443    searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
444    searchbox.addEventListener("input", this._onSearchboxInput);
445    searchbox.addEventListener("keydown", this._onSearchboxKeyDown);
446
447    container.appendChild(searchbox);
448    ownerNode.insertBefore(container, this._parent);
449  },
450
451  /**
452   * Disables variable and property searching in this view.
453   * Use the "searchEnabled" setter to disable searching.
454   */
455  _disableSearch: function() {
456    // If searching was already disabled, no need to re-disable it again.
457    if (!this._searchboxContainer) {
458      return;
459    }
460    this._searchboxContainer.remove();
461    this._searchboxNode.removeEventListener("input", this._onSearchboxInput);
462    this._searchboxNode.removeEventListener(
463      "keydown",
464      this._onSearchboxKeyDown
465    );
466
467    this._searchboxContainer = null;
468    this._searchboxNode = null;
469  },
470
471  /**
472   * Sets the variables searchbox container hidden or visible.
473   * It's hidden by default.
474   *
475   * @param boolean aVisibleFlag
476   *        Specifies the intended visibility.
477   */
478  _toggleSearchVisibility: function(aVisibleFlag) {
479    // If searching was already disabled, there's no need to hide it.
480    if (!this._searchboxContainer) {
481      return;
482    }
483    this._searchboxContainer.hidden = !aVisibleFlag;
484  },
485
486  /**
487   * Listener handling the searchbox input event.
488   */
489  _onSearchboxInput: function() {
490    this.scheduleSearch(this._searchboxNode.value);
491  },
492
493  /**
494   * Listener handling the searchbox keydown event.
495   */
496  _onSearchboxKeyDown: function(e) {
497    switch (e.keyCode) {
498      case KeyCodes.DOM_VK_RETURN:
499        this._onSearchboxInput();
500        return;
501      case KeyCodes.DOM_VK_ESCAPE:
502        this._searchboxNode.value = "";
503        this._onSearchboxInput();
504    }
505  },
506
507  /**
508   * Schedules searching for variables or properties matching the query.
509   *
510   * @param string aToken
511   *        The variable or property to search for.
512   * @param number aWait
513   *        The amount of milliseconds to wait until draining.
514   */
515  scheduleSearch: function(aToken, aWait) {
516    // Check if this search operation may not be executed lazily.
517    if (!this.lazySearch) {
518      this._doSearch(aToken);
519      return;
520    }
521
522    // The amount of time to wait for the requests to settle.
523    const maxDelay = SEARCH_ACTION_MAX_DELAY;
524    const delay = aWait === undefined ? maxDelay / aToken.length : aWait;
525
526    // Allow requests to settle down first.
527    setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
528  },
529
530  /**
531   * Performs a case insensitive search for variables or properties matching
532   * the query, and hides non-matched items.
533   *
534   * If aToken is falsy, then all the scopes are unhidden and expanded,
535   * while the available variables and properties inside those scopes are
536   * just unhidden.
537   *
538   * @param string aToken
539   *        The variable or property to search for.
540   */
541  _doSearch: function(aToken) {
542    if (this.controller && this.controller.supportsSearch()) {
543      // Retrieve the main Scope in which we add attributes
544      const scope = this._store[0]._store.get(undefined);
545      if (!aToken) {
546        // Prune the view from old previous content
547        // so that we delete the intermediate search results
548        // we created in previous searches
549        for (const property of scope._store.values()) {
550          property.remove();
551        }
552      }
553      // Retrieve new attributes eventually hidden in splits
554      this.controller.performSearch(scope, aToken);
555      // Filter already displayed attributes
556      if (aToken) {
557        scope._performSearch(aToken.toLowerCase());
558      }
559      return;
560    }
561    for (const scope of this._store) {
562      switch (aToken) {
563        case "":
564        case null:
565        case undefined:
566          scope.expand();
567          scope._performSearch("");
568          break;
569        default:
570          scope._performSearch(aToken.toLowerCase());
571          break;
572      }
573    }
574  },
575
576  /**
577   * Find the first item in the tree of visible items in this container that
578   * matches the predicate. Searches in visual order (the order seen by the
579   * user). Descends into each scope to check the scope and its children.
580   *
581   * @param function aPredicate
582   *        A function that returns true when a match is found.
583   * @return Scope | Variable | Property
584   *         The first visible scope, variable or property, or null if nothing
585   *         is found.
586   */
587  _findInVisibleItems: function(aPredicate) {
588    for (const scope of this._store) {
589      const result = scope._findInVisibleItems(aPredicate);
590      if (result) {
591        return result;
592      }
593    }
594    return null;
595  },
596
597  /**
598   * Find the last item in the tree of visible items in this container that
599   * matches the predicate. Searches in reverse visual order (opposite of the
600   * order seen by the user). Descends into each scope to check the scope and
601   * its children.
602   *
603   * @param function aPredicate
604   *        A function that returns true when a match is found.
605   * @return Scope | Variable | Property
606   *         The last visible scope, variable or property, or null if nothing
607   *         is found.
608   */
609  _findInVisibleItemsReverse: function(aPredicate) {
610    for (let i = this._store.length - 1; i >= 0; i--) {
611      const scope = this._store[i];
612      const result = scope._findInVisibleItemsReverse(aPredicate);
613      if (result) {
614        return result;
615      }
616    }
617    return null;
618  },
619
620  /**
621   * Gets the scope at the specified index.
622   *
623   * @param number aIndex
624   *        The scope's index.
625   * @return Scope
626   *         The scope if found, undefined if not.
627   */
628  getScopeAtIndex: function(aIndex) {
629    return this._store[aIndex];
630  },
631
632  /**
633   * Recursively searches this container for the scope, variable or property
634   * displayed by the specified node.
635   *
636   * @param Node aNode
637   *        The node to search for.
638   * @return Scope | Variable | Property
639   *         The matched scope, variable or property, or null if nothing is found.
640   */
641  getItemForNode: function(aNode) {
642    return this._itemsByElement.get(aNode);
643  },
644
645  /**
646   * Gets the scope owning a Variable or Property.
647   *
648   * @param Variable | Property
649   *        The variable or property to retrieven the owner scope for.
650   * @return Scope
651   *         The owner scope.
652   */
653  getOwnerScopeForVariableOrProperty: function(aItem) {
654    if (!aItem) {
655      return null;
656    }
657    // If this is a Scope, return it.
658    if (!(aItem instanceof Variable)) {
659      return aItem;
660    }
661    // If this is a Variable or Property, find its owner scope.
662    if (aItem instanceof Variable && aItem.ownerView) {
663      return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
664    }
665    return null;
666  },
667
668  /**
669   * Gets the parent scopes for a specified Variable or Property.
670   * The returned list will not include the owner scope.
671   *
672   * @param Variable | Property
673   *        The variable or property for which to find the parent scopes.
674   * @return array
675   *         A list of parent Scopes.
676   */
677  getParentScopesForVariableOrProperty: function(aItem) {
678    const scope = this.getOwnerScopeForVariableOrProperty(aItem);
679    return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
680  },
681
682  /**
683   * Gets the currently focused scope, variable or property in this view.
684   *
685   * @return Scope | Variable | Property
686   *         The focused scope, variable or property, or null if nothing is found.
687   */
688  getFocusedItem: function() {
689    const focused = this.document.commandDispatcher.focusedElement;
690    return this.getItemForNode(focused);
691  },
692
693  /**
694   * Focuses the first visible scope, variable, or property in this container.
695   */
696  focusFirstVisibleItem: function() {
697    const focusableItem = this._findInVisibleItems(item => item.focusable);
698    if (focusableItem) {
699      this._focusItem(focusableItem);
700    }
701    this._parent.scrollTop = 0;
702    this._parent.scrollLeft = 0;
703  },
704
705  /**
706   * Focuses the last visible scope, variable, or property in this container.
707   */
708  focusLastVisibleItem: function() {
709    const focusableItem = this._findInVisibleItemsReverse(
710      item => item.focusable
711    );
712    if (focusableItem) {
713      this._focusItem(focusableItem);
714    }
715    this._parent.scrollTop = this._parent.scrollHeight;
716    this._parent.scrollLeft = 0;
717  },
718
719  /**
720   * Focuses the next scope, variable or property in this view.
721   */
722  focusNextItem: function() {
723    this.focusItemAtDelta(+1);
724  },
725
726  /**
727   * Focuses the previous scope, variable or property in this view.
728   */
729  focusPrevItem: function() {
730    this.focusItemAtDelta(-1);
731  },
732
733  /**
734   * Focuses another scope, variable or property in this view, based on
735   * the index distance from the currently focused item.
736   *
737   * @param number aDelta
738   *        A scalar specifying by how many items should the selection change.
739   */
740  focusItemAtDelta: function(aDelta) {
741    const direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
742    let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
743    while (distance--) {
744      if (!this._focusChange(direction)) {
745        break; // Out of bounds.
746      }
747    }
748  },
749
750  /**
751   * Focuses the next or previous scope, variable or property in this view.
752   *
753   * @param string aDirection
754   *        Either "advanceFocus" or "rewindFocus".
755   * @return boolean
756   *         False if the focus went out of bounds and the first or last element
757   *         in this view was focused instead.
758   */
759  _focusChange: function(aDirection) {
760    const commandDispatcher = this.document.commandDispatcher;
761    const prevFocusedElement = commandDispatcher.focusedElement;
762    let currFocusedItem = null;
763
764    do {
765      commandDispatcher[aDirection]();
766
767      // Make sure the newly focused item is a part of this view.
768      // If the focus goes out of bounds, revert the previously focused item.
769      if (!(currFocusedItem = this.getFocusedItem())) {
770        prevFocusedElement.focus();
771        return false;
772      }
773    } while (!currFocusedItem.focusable);
774
775    // Focus remained within bounds.
776    return true;
777  },
778
779  /**
780   * Focuses a scope, variable or property and makes sure it's visible.
781   *
782   * @param aItem Scope | Variable | Property
783   *        The item to focus.
784   * @param boolean aCollapseFlag
785   *        True if the focused item should also be collapsed.
786   * @return boolean
787   *         True if the item was successfully focused.
788   */
789  _focusItem: function(aItem, aCollapseFlag) {
790    if (!aItem.focusable) {
791      return false;
792    }
793    if (aCollapseFlag) {
794      aItem.collapse();
795    }
796    aItem._target.focus();
797    aItem._arrow.scrollIntoView({ block: "nearest" });
798    return true;
799  },
800
801  /**
802   * Copy current selection to clipboard.
803   */
804  _copyItem: function() {
805    const item = this.getFocusedItem();
806    clipboardHelper.copyString(
807      item._nameString + item.separatorStr + item._valueString
808    );
809  },
810
811  /**
812   * Listener handling a key down event on the view.
813   */
814  // eslint-disable-next-line complexity
815  _onViewKeyDown: function(e) {
816    const item = this.getFocusedItem();
817
818    // Prevent scrolling when pressing navigation keys.
819    ViewHelpers.preventScrolling(e);
820
821    switch (e.keyCode) {
822      case KeyCodes.DOM_VK_C:
823        if (e.ctrlKey || e.metaKey) {
824          this._copyItem();
825        }
826        return;
827
828      case KeyCodes.DOM_VK_UP:
829        // Always rewind focus.
830        this.focusPrevItem(true);
831        return;
832
833      case KeyCodes.DOM_VK_DOWN:
834        // Always advance focus.
835        this.focusNextItem(true);
836        return;
837
838      case KeyCodes.DOM_VK_LEFT:
839        // Collapse scopes, variables and properties before rewinding focus.
840        if (item._isExpanded && item._isArrowVisible) {
841          item.collapse();
842        } else {
843          this._focusItem(item.ownerView);
844        }
845        return;
846
847      case KeyCodes.DOM_VK_RIGHT:
848        // Nothing to do here if this item never expands.
849        if (!item._isArrowVisible) {
850          return;
851        }
852        // Expand scopes, variables and properties before advancing focus.
853        if (!item._isExpanded) {
854          item.expand();
855        } else {
856          this.focusNextItem(true);
857        }
858        return;
859
860      case KeyCodes.DOM_VK_PAGE_UP:
861        // Rewind a certain number of elements based on the container height.
862        this.focusItemAtDelta(
863          -(
864            this.scrollPageSize ||
865            Math.min(
866              Math.floor(
867                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
868              ),
869              PAGE_SIZE_MAX_JUMPS
870            )
871          )
872        );
873        return;
874
875      case KeyCodes.DOM_VK_PAGE_DOWN:
876        // Advance a certain number of elements based on the container height.
877        this.focusItemAtDelta(
878          +(
879            this.scrollPageSize ||
880            Math.min(
881              Math.floor(
882                this._list.scrollHeight / PAGE_SIZE_SCROLL_HEIGHT_RATIO
883              ),
884              PAGE_SIZE_MAX_JUMPS
885            )
886          )
887        );
888        return;
889
890      case KeyCodes.DOM_VK_HOME:
891        this.focusFirstVisibleItem();
892        return;
893
894      case KeyCodes.DOM_VK_END:
895        this.focusLastVisibleItem();
896        return;
897
898      case KeyCodes.DOM_VK_RETURN:
899        // Start editing the value or name of the Variable or Property.
900        if (item instanceof Variable) {
901          if (e.metaKey || e.altKey || e.shiftKey) {
902            item._activateNameInput();
903          } else {
904            item._activateValueInput();
905          }
906        }
907        return;
908
909      case KeyCodes.DOM_VK_DELETE:
910      case KeyCodes.DOM_VK_BACK_SPACE:
911        // Delete the Variable or Property if allowed.
912        if (item instanceof Variable) {
913          item._onDelete(e);
914        }
915        return;
916
917      case KeyCodes.DOM_VK_INSERT:
918        item._onAddProperty(e);
919    }
920  },
921
922  /**
923   * Sets the text displayed in this container when there are no available items.
924   * @param string aValue
925   */
926  set emptyText(aValue) {
927    if (this._emptyTextNode) {
928      this._emptyTextNode.setAttribute("value", aValue);
929    }
930    this._emptyTextValue = aValue;
931    this._appendEmptyNotice();
932  },
933
934  /**
935   * Creates and appends a label signaling that this container is empty.
936   */
937  _appendEmptyNotice: function() {
938    if (this._emptyTextNode || !this._emptyTextValue) {
939      return;
940    }
941
942    const label = this.document.createXULElement("label");
943    label.className = "variables-view-empty-notice";
944    label.setAttribute("value", this._emptyTextValue);
945
946    this._parent.appendChild(label);
947    this._emptyTextNode = label;
948  },
949
950  /**
951   * Removes the label signaling that this container is empty.
952   */
953  _removeEmptyNotice: function() {
954    if (!this._emptyTextNode) {
955      return;
956    }
957
958    this._parent.removeChild(this._emptyTextNode);
959    this._emptyTextNode = null;
960  },
961
962  /**
963   * Gets if all values should be aligned together.
964   * @return boolean
965   */
966  get alignedValues() {
967    return this._alignedValues;
968  },
969
970  /**
971   * Sets if all values should be aligned together.
972   * @param boolean aFlag
973   */
974  set alignedValues(aFlag) {
975    this._alignedValues = aFlag;
976    if (aFlag) {
977      this._parent.setAttribute("aligned-values", "");
978    } else {
979      this._parent.removeAttribute("aligned-values");
980    }
981  },
982
983  /**
984   * Gets if action buttons (like delete) should be placed at the beginning or
985   * end of a line.
986   * @return boolean
987   */
988  get actionsFirst() {
989    return this._actionsFirst;
990  },
991
992  /**
993   * Sets if action buttons (like delete) should be placed at the beginning or
994   * end of a line.
995   * @param boolean aFlag
996   */
997  set actionsFirst(aFlag) {
998    this._actionsFirst = aFlag;
999    if (aFlag) {
1000      this._parent.setAttribute("actions-first", "");
1001    } else {
1002      this._parent.removeAttribute("actions-first");
1003    }
1004  },
1005
1006  /**
1007   * Gets the parent node holding this view.
1008   * @return Node
1009   */
1010  get parentNode() {
1011    return this._parent;
1012  },
1013
1014  /**
1015   * Gets the owner document holding this view.
1016   * @return HTMLDocument
1017   */
1018  get document() {
1019    return this._document || (this._document = this._parent.ownerDocument);
1020  },
1021
1022  /**
1023   * Gets the default window holding this view.
1024   * @return nsIDOMWindow
1025   */
1026  get window() {
1027    return this._window || (this._window = this.document.defaultView);
1028  },
1029
1030  _document: null,
1031  _window: null,
1032
1033  _store: null,
1034  _itemsByElement: null,
1035  _prevHierarchy: null,
1036  _currHierarchy: null,
1037
1038  _enumVisible: true,
1039  _nonEnumVisible: true,
1040  _alignedValues: false,
1041  _actionsFirst: false,
1042
1043  _parent: null,
1044  _list: null,
1045  _searchboxNode: null,
1046  _searchboxContainer: null,
1047  _searchboxPlaceholder: "",
1048  _emptyTextNode: null,
1049  _emptyTextValue: "",
1050};
1051
1052VariablesView.NON_SORTABLE_CLASSES = [
1053  "Array",
1054  "Int8Array",
1055  "Uint8Array",
1056  "Uint8ClampedArray",
1057  "Int16Array",
1058  "Uint16Array",
1059  "Int32Array",
1060  "Uint32Array",
1061  "Float32Array",
1062  "Float64Array",
1063  "NodeList",
1064];
1065
1066/**
1067 * Determine whether an object's properties should be sorted based on its class.
1068 *
1069 * @param string aClassName
1070 *        The class of the object.
1071 */
1072VariablesView.isSortable = function(aClassName) {
1073  return !VariablesView.NON_SORTABLE_CLASSES.includes(aClassName);
1074};
1075
1076/**
1077 * Generates the string evaluated when performing simple value changes.
1078 *
1079 * @param Variable | Property aItem
1080 *        The current variable or property.
1081 * @param string aCurrentString
1082 *        The trimmed user inputted string.
1083 * @param string aPrefix [optional]
1084 *        Prefix for the symbolic name.
1085 * @return string
1086 *         The string to be evaluated.
1087 */
1088VariablesView.simpleValueEvalMacro = function(
1089  aItem,
1090  aCurrentString,
1091  aPrefix = ""
1092) {
1093  return aPrefix + aItem.symbolicName + "=" + aCurrentString;
1094};
1095
1096/**
1097 * Generates the string evaluated when overriding getters and setters with
1098 * plain values.
1099 *
1100 * @param Property aItem
1101 *        The current getter or setter property.
1102 * @param string aCurrentString
1103 *        The trimmed user inputted string.
1104 * @param string aPrefix [optional]
1105 *        Prefix for the symbolic name.
1106 * @return string
1107 *         The string to be evaluated.
1108 */
1109VariablesView.overrideValueEvalMacro = function(
1110  aItem,
1111  aCurrentString,
1112  aPrefix = ""
1113) {
1114  const property = escapeString(aItem._nameString);
1115  const parent = aPrefix + aItem.ownerView.symbolicName || "this";
1116
1117  return (
1118    "Object.defineProperty(" +
1119    parent +
1120    "," +
1121    property +
1122    "," +
1123    "{ value: " +
1124    aCurrentString +
1125    ", enumerable: " +
1126    parent +
1127    ".propertyIsEnumerable(" +
1128    property +
1129    ")" +
1130    ", configurable: true" +
1131    ", writable: true" +
1132    "})"
1133  );
1134};
1135
1136/**
1137 * Generates the string evaluated when performing getters and setters changes.
1138 *
1139 * @param Property aItem
1140 *        The current getter or setter property.
1141 * @param string aCurrentString
1142 *        The trimmed user inputted string.
1143 * @param string aPrefix [optional]
1144 *        Prefix for the symbolic name.
1145 * @return string
1146 *         The string to be evaluated.
1147 */
1148VariablesView.getterOrSetterEvalMacro = function(
1149  aItem,
1150  aCurrentString,
1151  aPrefix = ""
1152) {
1153  const type = aItem._nameString;
1154  const propertyObject = aItem.ownerView;
1155  const parentObject = propertyObject.ownerView;
1156  const property = escapeString(propertyObject._nameString);
1157  const parent = aPrefix + parentObject.symbolicName || "this";
1158
1159  switch (aCurrentString) {
1160    case "":
1161    case "null":
1162    case "undefined":
1163      const mirrorType = type == "get" ? "set" : "get";
1164      const mirrorLookup =
1165        type == "get" ? "__lookupSetter__" : "__lookupGetter__";
1166
1167      // If the parent object will end up without any getter or setter,
1168      // morph it into a plain value.
1169      if (
1170        (type == "set" && propertyObject.getter.type == "undefined") ||
1171        (type == "get" && propertyObject.setter.type == "undefined")
1172      ) {
1173        // Make sure the right getter/setter to value override macro is applied
1174        // to the target object.
1175        return propertyObject.evaluationMacro(
1176          propertyObject,
1177          "undefined",
1178          aPrefix
1179        );
1180      }
1181
1182      // Construct and return the getter/setter removal evaluation string.
1183      // e.g: Object.defineProperty(foo, "bar", {
1184      //   get: foo.__lookupGetter__("bar"),
1185      //   set: undefined,
1186      //   enumerable: true,
1187      //   configurable: true
1188      // })
1189      return (
1190        "Object.defineProperty(" +
1191        parent +
1192        "," +
1193        property +
1194        "," +
1195        "{" +
1196        mirrorType +
1197        ":" +
1198        parent +
1199        "." +
1200        mirrorLookup +
1201        "(" +
1202        property +
1203        ")" +
1204        "," +
1205        type +
1206        ":" +
1207        undefined +
1208        ", enumerable: " +
1209        parent +
1210        ".propertyIsEnumerable(" +
1211        property +
1212        ")" +
1213        ", configurable: true" +
1214        "})"
1215      );
1216
1217    default:
1218      // Wrap statements inside a function declaration if not already wrapped.
1219      if (!aCurrentString.startsWith("function")) {
1220        const header = "function(" + (type == "set" ? "value" : "") + ")";
1221        let body = "";
1222        // If there's a return statement explicitly written, always use the
1223        // standard function definition syntax
1224        if (aCurrentString.includes("return ")) {
1225          body = "{" + aCurrentString + "}";
1226        } else if (aCurrentString.startsWith("{")) {
1227          // If block syntax is used, use the whole string as the function body.
1228          body = aCurrentString;
1229        } else {
1230          // Prefer an expression closure.
1231          body = "(" + aCurrentString + ")";
1232        }
1233        aCurrentString = header + body;
1234      }
1235
1236      // Determine if a new getter or setter should be defined.
1237      const defineType =
1238        type == "get" ? "__defineGetter__" : "__defineSetter__";
1239
1240      // Make sure all quotes are escaped in the expression's syntax,
1241      const defineFunc =
1242        'eval("(' + aCurrentString.replace(/"/g, "\\$&") + ')")';
1243
1244      // Construct and return the getter/setter evaluation string.
1245      // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
1246      return (
1247        parent + "." + defineType + "(" + property + "," + defineFunc + ")"
1248      );
1249  }
1250};
1251
1252/**
1253 * Function invoked when a getter or setter is deleted.
1254 *
1255 * @param Property aItem
1256 *        The current getter or setter property.
1257 */
1258VariablesView.getterOrSetterDeleteCallback = function(aItem) {
1259  aItem._disable();
1260
1261  // Make sure the right getter/setter to value override macro is applied
1262  // to the target object.
1263  aItem.ownerView.eval(aItem, "");
1264
1265  return true; // Don't hide the element.
1266};
1267
1268/**
1269 * A Scope is an object holding Variable instances.
1270 * Iterable via "for (let [name, variable] of instance) { }".
1271 *
1272 * @param VariablesView aView
1273 *        The view to contain this scope.
1274 * @param string aName
1275 *        The scope's name.
1276 * @param object aFlags [optional]
1277 *        Additional options or flags for this scope.
1278 */
1279function Scope(aView, aName, aFlags = {}) {
1280  this.ownerView = aView;
1281
1282  this._onClick = this._onClick.bind(this);
1283  this._openEnum = this._openEnum.bind(this);
1284  this._openNonEnum = this._openNonEnum.bind(this);
1285
1286  // Inherit properties and flags from the parent view. You can override
1287  // each of these directly onto any scope, variable or property instance.
1288  this.scrollPageSize = aView.scrollPageSize;
1289  this.eval = aView.eval;
1290  this.switch = aView.switch;
1291  this.delete = aView.delete;
1292  this.new = aView.new;
1293  this.preventDisableOnChange = aView.preventDisableOnChange;
1294  this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
1295  this.editableNameTooltip = aView.editableNameTooltip;
1296  this.editableValueTooltip = aView.editableValueTooltip;
1297  this.editButtonTooltip = aView.editButtonTooltip;
1298  this.deleteButtonTooltip = aView.deleteButtonTooltip;
1299  this.domNodeValueTooltip = aView.domNodeValueTooltip;
1300  this.contextMenuId = aView.contextMenuId;
1301  this.separatorStr = aView.separatorStr;
1302
1303  this._init(aName, aFlags);
1304}
1305
1306Scope.prototype = {
1307  /**
1308   * Whether this Scope should be prefetched when it is remoted.
1309   */
1310  shouldPrefetch: true,
1311
1312  /**
1313   * Whether this Scope should paginate its contents.
1314   */
1315  allowPaginate: false,
1316
1317  /**
1318   * The class name applied to this scope's target element.
1319   */
1320  targetClassName: "variables-view-scope",
1321
1322  /**
1323   * Create a new Variable that is a child of this Scope.
1324   *
1325   * @param string aName
1326   *        The name of the new Property.
1327   * @param object aDescriptor
1328   *        The variable's descriptor.
1329   * @param object aOptions
1330   *        Options of the form accepted by addItem.
1331   * @return Variable
1332   *         The newly created child Variable.
1333   */
1334  _createChild: function(aName, aDescriptor, aOptions) {
1335    return new Variable(this, aName, aDescriptor, aOptions);
1336  },
1337
1338  /**
1339   * Adds a child to contain any inspected properties.
1340   *
1341   * @param string aName
1342   *        The child's name.
1343   * @param object aDescriptor
1344   *        Specifies the value and/or type & class of the child,
1345   *        or 'get' & 'set' accessor properties. If the type is implicit,
1346   *        it will be inferred from the value. If this parameter is omitted,
1347   *        a property without a value will be added (useful for branch nodes).
1348   *        e.g. - { value: 42 }
1349   *             - { value: true }
1350   *             - { value: "nasu" }
1351   *             - { value: { type: "undefined" } }
1352   *             - { value: { type: "null" } }
1353   *             - { value: { type: "object", class: "Object" } }
1354   *             - { get: { type: "object", class: "Function" },
1355   *                 set: { type: "undefined" } }
1356   * @param object aOptions
1357   *        Specifies some options affecting the new variable.
1358   *        Recognized properties are
1359   *        * boolean relaxed  true if name duplicates should be allowed.
1360   *                           You probably shouldn't do it. Use this
1361   *                           with caution.
1362   *        * boolean internalItem  true if the item is internally generated.
1363   *                           This is used for special variables
1364   *                           like <return> or <exception> and distinguishes
1365   *                           them from ordinary properties that happen
1366   *                           to have the same name
1367   * @return Variable
1368   *         The newly created Variable instance, null if it already exists.
1369   */
1370  addItem: function(aName, aDescriptor = {}, aOptions = {}) {
1371    const { relaxed } = aOptions;
1372    if (this._store.has(aName) && !relaxed) {
1373      return this._store.get(aName);
1374    }
1375
1376    const child = this._createChild(aName, aDescriptor, aOptions);
1377    this._store.set(aName, child);
1378    this._variablesView._itemsByElement.set(child._target, child);
1379    this._variablesView._currHierarchy.set(child.absoluteName, child);
1380    child.header = aName !== undefined;
1381
1382    return child;
1383  },
1384
1385  /**
1386   * Adds items for this variable.
1387   *
1388   * @param object aItems
1389   *        An object containing some { name: descriptor } data properties,
1390   *        specifying the value and/or type & class of the variable,
1391   *        or 'get' & 'set' accessor properties. If the type is implicit,
1392   *        it will be inferred from the value.
1393   *        e.g. - { someProp0: { value: 42 },
1394   *                 someProp1: { value: true },
1395   *                 someProp2: { value: "nasu" },
1396   *                 someProp3: { value: { type: "undefined" } },
1397   *                 someProp4: { value: { type: "null" } },
1398   *                 someProp5: { value: { type: "object", class: "Object" } },
1399   *                 someProp6: { get: { type: "object", class: "Function" },
1400   *                              set: { type: "undefined" } } }
1401   * @param object aOptions [optional]
1402   *        Additional options for adding the properties. Supported options:
1403   *        - sorted: true to sort all the properties before adding them
1404   *        - callback: function invoked after each item is added
1405   */
1406  addItems: function(aItems, aOptions = {}) {
1407    const names = Object.keys(aItems);
1408
1409    // Sort all of the properties before adding them, if preferred.
1410    if (aOptions.sorted) {
1411      names.sort(this._naturalSort);
1412    }
1413
1414    // Add the properties to the current scope.
1415    for (const name of names) {
1416      const descriptor = aItems[name];
1417      const item = this.addItem(name, descriptor);
1418
1419      if (aOptions.callback) {
1420        aOptions.callback(item, descriptor && descriptor.value);
1421      }
1422    }
1423  },
1424
1425  /**
1426   * Remove this Scope from its parent and remove all children recursively.
1427   */
1428  remove: function() {
1429    const view = this._variablesView;
1430    view._store.splice(view._store.indexOf(this), 1);
1431    view._itemsByElement.delete(this._target);
1432    view._currHierarchy.delete(this._nameString);
1433
1434    this._target.remove();
1435
1436    for (const variable of this._store.values()) {
1437      variable.remove();
1438    }
1439  },
1440
1441  /**
1442   * Gets the variable in this container having the specified name.
1443   *
1444   * @param string aName
1445   *        The name of the variable to get.
1446   * @return Variable
1447   *         The matched variable, or null if nothing is found.
1448   */
1449  get: function(aName) {
1450    return this._store.get(aName);
1451  },
1452
1453  /**
1454   * Recursively searches for the variable or property in this container
1455   * displayed by the specified node.
1456   *
1457   * @param Node aNode
1458   *        The node to search for.
1459   * @return Variable | Property
1460   *         The matched variable or property, or null if nothing is found.
1461   */
1462  find: function(aNode) {
1463    for (const [, variable] of this._store) {
1464      let match;
1465      if (variable._target == aNode) {
1466        match = variable;
1467      } else {
1468        match = variable.find(aNode);
1469      }
1470      if (match) {
1471        return match;
1472      }
1473    }
1474    return null;
1475  },
1476
1477  /**
1478   * Determines if this scope is a direct child of a parent variables view,
1479   * scope, variable or property.
1480   *
1481   * @param VariablesView | Scope | Variable | Property
1482   *        The parent to check.
1483   * @return boolean
1484   *         True if the specified item is a direct child, false otherwise.
1485   */
1486  isChildOf: function(aParent) {
1487    return this.ownerView == aParent;
1488  },
1489
1490  /**
1491   * Determines if this scope is a descendant of a parent variables view,
1492   * scope, variable or property.
1493   *
1494   * @param VariablesView | Scope | Variable | Property
1495   *        The parent to check.
1496   * @return boolean
1497   *         True if the specified item is a descendant, false otherwise.
1498   */
1499  isDescendantOf: function(aParent) {
1500    if (this.isChildOf(aParent)) {
1501      return true;
1502    }
1503
1504    // Recurse to parent if it is a Scope, Variable, or Property.
1505    if (this.ownerView instanceof Scope) {
1506      return this.ownerView.isDescendantOf(aParent);
1507    }
1508
1509    return false;
1510  },
1511
1512  /**
1513   * Shows the scope.
1514   */
1515  show: function() {
1516    this._target.hidden = false;
1517    this._isContentVisible = true;
1518
1519    if (this.onshow) {
1520      this.onshow(this);
1521    }
1522  },
1523
1524  /**
1525   * Hides the scope.
1526   */
1527  hide: function() {
1528    this._target.hidden = true;
1529    this._isContentVisible = false;
1530
1531    if (this.onhide) {
1532      this.onhide(this);
1533    }
1534  },
1535
1536  /**
1537   * Expands the scope, showing all the added details.
1538   */
1539  expand: function() {
1540    if (this._isExpanded || this._isLocked) {
1541      return;
1542    }
1543    if (this._variablesView._enumVisible) {
1544      this._openEnum();
1545    }
1546    if (this._variablesView._nonEnumVisible) {
1547      Services.tm.dispatchToMainThread({ run: this._openNonEnum });
1548    }
1549    this._isExpanded = true;
1550
1551    if (this.onexpand) {
1552      // We return onexpand as it sometimes returns a promise
1553      // (up to the user of VariableView to do it)
1554      // that can indicate when the view is done expanding
1555      // and attributes are available. (Mostly used for tests)
1556      return this.onexpand(this);
1557    }
1558  },
1559
1560  /**
1561   * Collapses the scope, hiding all the added details.
1562   */
1563  collapse: function() {
1564    if (!this._isExpanded || this._isLocked) {
1565      return;
1566    }
1567    this._arrow.removeAttribute("open");
1568    this._enum.removeAttribute("open");
1569    this._nonenum.removeAttribute("open");
1570    this._isExpanded = false;
1571
1572    if (this.oncollapse) {
1573      this.oncollapse(this);
1574    }
1575  },
1576
1577  /**
1578   * Toggles between the scope's collapsed and expanded state.
1579   */
1580  toggle: function(e) {
1581    if (e && e.button != 0) {
1582      // Only allow left-click to trigger this event.
1583      return;
1584    }
1585    this.expanded ^= 1;
1586
1587    // Make sure the scope and its contents are visibile.
1588    for (const [, variable] of this._store) {
1589      variable.header = true;
1590      variable._matched = true;
1591    }
1592    if (this.ontoggle) {
1593      this.ontoggle(this);
1594    }
1595  },
1596
1597  /**
1598   * Shows the scope's title header.
1599   */
1600  showHeader: function() {
1601    if (this._isHeaderVisible || !this._nameString) {
1602      return;
1603    }
1604    this._target.removeAttribute("untitled");
1605    this._isHeaderVisible = true;
1606  },
1607
1608  /**
1609   * Hides the scope's title header.
1610   * This action will automatically expand the scope.
1611   */
1612  hideHeader: function() {
1613    if (!this._isHeaderVisible) {
1614      return;
1615    }
1616    this.expand();
1617    this._target.setAttribute("untitled", "");
1618    this._isHeaderVisible = false;
1619  },
1620
1621  /**
1622   * Sort in ascending order
1623   * This only needs to compare non-numbers since it is dealing with an array
1624   * which numeric-based indices are placed in order.
1625   *
1626   * @param string a
1627   * @param string b
1628   * @return number
1629   *         -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
1630   */
1631  _naturalSort: function(a, b) {
1632    if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
1633      return a < b ? -1 : 1;
1634    }
1635  },
1636
1637  /**
1638   * Shows the scope's expand/collapse arrow.
1639   */
1640  showArrow: function() {
1641    if (this._isArrowVisible) {
1642      return;
1643    }
1644    this._arrow.removeAttribute("invisible");
1645    this._isArrowVisible = true;
1646  },
1647
1648  /**
1649   * Hides the scope's expand/collapse arrow.
1650   */
1651  hideArrow: function() {
1652    if (!this._isArrowVisible) {
1653      return;
1654    }
1655    this._arrow.setAttribute("invisible", "");
1656    this._isArrowVisible = false;
1657  },
1658
1659  /**
1660   * Gets the visibility state.
1661   * @return boolean
1662   */
1663  get visible() {
1664    return this._isContentVisible;
1665  },
1666
1667  /**
1668   * Gets the expanded state.
1669   * @return boolean
1670   */
1671  get expanded() {
1672    return this._isExpanded;
1673  },
1674
1675  /**
1676   * Gets the header visibility state.
1677   * @return boolean
1678   */
1679  get header() {
1680    return this._isHeaderVisible;
1681  },
1682
1683  /**
1684   * Gets the twisty visibility state.
1685   * @return boolean
1686   */
1687  get twisty() {
1688    return this._isArrowVisible;
1689  },
1690
1691  /**
1692   * Gets the expand lock state.
1693   * @return boolean
1694   */
1695  get locked() {
1696    return this._isLocked;
1697  },
1698
1699  /**
1700   * Sets the visibility state.
1701   * @param boolean aFlag
1702   */
1703  set visible(aFlag) {
1704    aFlag ? this.show() : this.hide();
1705  },
1706
1707  /**
1708   * Sets the expanded state.
1709   * @param boolean aFlag
1710   */
1711  set expanded(aFlag) {
1712    aFlag ? this.expand() : this.collapse();
1713  },
1714
1715  /**
1716   * Sets the header visibility state.
1717   * @param boolean aFlag
1718   */
1719  set header(aFlag) {
1720    aFlag ? this.showHeader() : this.hideHeader();
1721  },
1722
1723  /**
1724   * Sets the twisty visibility state.
1725   * @param boolean aFlag
1726   */
1727  set twisty(aFlag) {
1728    aFlag ? this.showArrow() : this.hideArrow();
1729  },
1730
1731  /**
1732   * Sets the expand lock state.
1733   * @param boolean aFlag
1734   */
1735  set locked(aFlag) {
1736    this._isLocked = aFlag;
1737  },
1738
1739  /**
1740   * Specifies if this target node may be focused.
1741   * @return boolean
1742   */
1743  get focusable() {
1744    // Check if this target node is actually visibile.
1745    if (
1746      !this._nameString ||
1747      !this._isContentVisible ||
1748      !this._isHeaderVisible ||
1749      !this._isMatch
1750    ) {
1751      return false;
1752    }
1753    // Check if all parent objects are expanded.
1754    let item = this;
1755
1756    // Recurse while parent is a Scope, Variable, or Property
1757    while ((item = item.ownerView) && item instanceof Scope) {
1758      if (!item._isExpanded) {
1759        return false;
1760      }
1761    }
1762    return true;
1763  },
1764
1765  /**
1766   * Focus this scope.
1767   */
1768  focus: function() {
1769    this._variablesView._focusItem(this);
1770  },
1771
1772  /**
1773   * Adds an event listener for a certain event on this scope's title.
1774   * @param string aName
1775   * @param function aCallback
1776   * @param boolean aCapture
1777   */
1778  addEventListener: function(aName, aCallback, aCapture) {
1779    this._title.addEventListener(aName, aCallback, aCapture);
1780  },
1781
1782  /**
1783   * Removes an event listener for a certain event on this scope's title.
1784   * @param string aName
1785   * @param function aCallback
1786   * @param boolean aCapture
1787   */
1788  removeEventListener: function(aName, aCallback, aCapture) {
1789    this._title.removeEventListener(aName, aCallback, aCapture);
1790  },
1791
1792  /**
1793   * Gets the id associated with this item.
1794   * @return string
1795   */
1796  get id() {
1797    return this._idString;
1798  },
1799
1800  /**
1801   * Gets the name associated with this item.
1802   * @return string
1803   */
1804  get name() {
1805    return this._nameString;
1806  },
1807
1808  /**
1809   * Gets the displayed value for this item.
1810   * @return string
1811   */
1812  get displayValue() {
1813    return this._valueString;
1814  },
1815
1816  /**
1817   * Gets the class names used for the displayed value.
1818   * @return string
1819   */
1820  get displayValueClassName() {
1821    return this._valueClassName;
1822  },
1823
1824  /**
1825   * Gets the element associated with this item.
1826   * @return Node
1827   */
1828  get target() {
1829    return this._target;
1830  },
1831
1832  /**
1833   * Initializes this scope's id, view and binds event listeners.
1834   *
1835   * @param string aName
1836   *        The scope's name.
1837   * @param object aFlags [optional]
1838   *        Additional options or flags for this scope.
1839   */
1840  _init: function(aName, aFlags) {
1841    this._idString = generateId((this._nameString = aName));
1842    this._displayScope(
1843      aName,
1844      `${this.targetClassName} ${aFlags.customClass}`,
1845      "devtools-toolbar"
1846    );
1847    this._addEventListeners();
1848    this.parentNode.appendChild(this._target);
1849  },
1850
1851  /**
1852   * Creates the necessary nodes for this scope.
1853   *
1854   * @param string aName
1855   *        The scope's name.
1856   * @param string aTargetClassName
1857   *        A custom class name for this scope's target element.
1858   * @param string aTitleClassName [optional]
1859   *        A custom class name for this scope's title element.
1860   */
1861  _displayScope: function(aName = "", aTargetClassName, aTitleClassName = "") {
1862    const document = this.document;
1863
1864    const element = (this._target = document.createXULElement("vbox"));
1865    element.id = this._idString;
1866    element.className = aTargetClassName;
1867
1868    const arrow = (this._arrow = document.createXULElement("hbox"));
1869    arrow.className = "arrow theme-twisty";
1870
1871    const name = (this._name = document.createXULElement("label"));
1872    name.className = "plain name";
1873    name.setAttribute("value", aName.trim());
1874    name.setAttribute("crop", "end");
1875
1876    const title = (this._title = document.createXULElement("hbox"));
1877    title.className = "title " + aTitleClassName;
1878    title.setAttribute("align", "center");
1879
1880    const enumerable = (this._enum = document.createXULElement("vbox"));
1881    const nonenum = (this._nonenum = document.createXULElement("vbox"));
1882    enumerable.className = "variables-view-element-details enum";
1883    nonenum.className = "variables-view-element-details nonenum";
1884
1885    title.appendChild(arrow);
1886    title.appendChild(name);
1887
1888    element.appendChild(title);
1889    element.appendChild(enumerable);
1890    element.appendChild(nonenum);
1891  },
1892
1893  /**
1894   * Adds the necessary event listeners for this scope.
1895   */
1896  _addEventListeners: function() {
1897    this._title.addEventListener("mousedown", this._onClick);
1898  },
1899
1900  /**
1901   * The click listener for this scope's title.
1902   */
1903  _onClick: function(e) {
1904    if (
1905      this.editing ||
1906      e.button != 0 ||
1907      e.target == this._editNode ||
1908      e.target == this._deleteNode ||
1909      e.target == this._addPropertyNode
1910    ) {
1911      return;
1912    }
1913    this.toggle();
1914    this.focus();
1915  },
1916
1917  /**
1918   * Opens the enumerable items container.
1919   */
1920  _openEnum: function() {
1921    this._arrow.setAttribute("open", "");
1922    this._enum.setAttribute("open", "");
1923  },
1924
1925  /**
1926   * Opens the non-enumerable items container.
1927   */
1928  _openNonEnum: function() {
1929    this._nonenum.setAttribute("open", "");
1930  },
1931
1932  /**
1933   * Specifies if enumerable properties and variables should be displayed.
1934   * @param boolean aFlag
1935   */
1936  set _enumVisible(aFlag) {
1937    for (const [, variable] of this._store) {
1938      variable._enumVisible = aFlag;
1939
1940      if (!this._isExpanded) {
1941        continue;
1942      }
1943      if (aFlag) {
1944        this._enum.setAttribute("open", "");
1945      } else {
1946        this._enum.removeAttribute("open");
1947      }
1948    }
1949  },
1950
1951  /**
1952   * Specifies if non-enumerable properties and variables should be displayed.
1953   * @param boolean aFlag
1954   */
1955  set _nonEnumVisible(aFlag) {
1956    for (const [, variable] of this._store) {
1957      variable._nonEnumVisible = aFlag;
1958
1959      if (!this._isExpanded) {
1960        continue;
1961      }
1962      if (aFlag) {
1963        this._nonenum.setAttribute("open", "");
1964      } else {
1965        this._nonenum.removeAttribute("open");
1966      }
1967    }
1968  },
1969
1970  /**
1971   * Performs a case insensitive search for variables or properties matching
1972   * the query, and hides non-matched items.
1973   *
1974   * @param string aLowerCaseQuery
1975   *        The lowercased name of the variable or property to search for.
1976   */
1977  _performSearch: function(aLowerCaseQuery) {
1978    for (let [, variable] of this._store) {
1979      const currentObject = variable;
1980      const lowerCaseName = variable._nameString.toLowerCase();
1981      const lowerCaseValue = variable._valueString.toLowerCase();
1982
1983      // Non-matched variables or properties require a corresponding attribute.
1984      if (
1985        !lowerCaseName.includes(aLowerCaseQuery) &&
1986        !lowerCaseValue.includes(aLowerCaseQuery)
1987      ) {
1988        variable._matched = false;
1989      } else {
1990        // Variable or property is matched.
1991        variable._matched = true;
1992
1993        // If the variable was ever expanded, there's a possibility it may
1994        // contain some matched properties, so make sure they're visible
1995        // ("expand downwards").
1996        if (variable._store.size) {
1997          variable.expand();
1998        }
1999
2000        // If the variable is contained in another Scope, Variable, or Property,
2001        // the parent may not be a match, thus hidden. It should be visible
2002        // ("expand upwards").
2003        while ((variable = variable.ownerView) && variable instanceof Scope) {
2004          variable._matched = true;
2005          variable.expand();
2006        }
2007      }
2008
2009      // Proceed with the search recursively inside this variable or property.
2010      if (
2011        currentObject._store.size ||
2012        currentObject.getter ||
2013        currentObject.setter
2014      ) {
2015        currentObject._performSearch(aLowerCaseQuery);
2016      }
2017    }
2018  },
2019
2020  /**
2021   * Sets if this object instance is a matched or non-matched item.
2022   * @param boolean aStatus
2023   */
2024  set _matched(aStatus) {
2025    if (this._isMatch == aStatus) {
2026      return;
2027    }
2028    if (aStatus) {
2029      this._isMatch = true;
2030      this.target.removeAttribute("unmatched");
2031    } else {
2032      this._isMatch = false;
2033      this.target.setAttribute("unmatched", "");
2034    }
2035  },
2036
2037  /**
2038   * Find the first item in the tree of visible items in this item that matches
2039   * the predicate. Searches in visual order (the order seen by the user).
2040   * Tests itself, then descends into first the enumerable children and then
2041   * the non-enumerable children (since they are presented in separate groups).
2042   *
2043   * @param function aPredicate
2044   *        A function that returns true when a match is found.
2045   * @return Scope | Variable | Property
2046   *         The first visible scope, variable or property, or null if nothing
2047   *         is found.
2048   */
2049  _findInVisibleItems: function(aPredicate) {
2050    if (aPredicate(this)) {
2051      return this;
2052    }
2053
2054    if (this._isExpanded) {
2055      if (this._variablesView._enumVisible) {
2056        for (const item of this._enumItems) {
2057          const result = item._findInVisibleItems(aPredicate);
2058          if (result) {
2059            return result;
2060          }
2061        }
2062      }
2063
2064      if (this._variablesView._nonEnumVisible) {
2065        for (const item of this._nonEnumItems) {
2066          const result = item._findInVisibleItems(aPredicate);
2067          if (result) {
2068            return result;
2069          }
2070        }
2071      }
2072    }
2073
2074    return null;
2075  },
2076
2077  /**
2078   * Find the last item in the tree of visible items in this item that matches
2079   * the predicate. Searches in reverse visual order (opposite of the order
2080   * seen by the user). Descends into first the non-enumerable children, then
2081   * the enumerable children (since they are presented in separate groups), and
2082   * finally tests itself.
2083   *
2084   * @param function aPredicate
2085   *        A function that returns true when a match is found.
2086   * @return Scope | Variable | Property
2087   *         The last visible scope, variable or property, or null if nothing
2088   *         is found.
2089   */
2090  _findInVisibleItemsReverse: function(aPredicate) {
2091    if (this._isExpanded) {
2092      if (this._variablesView._nonEnumVisible) {
2093        for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
2094          const item = this._nonEnumItems[i];
2095          const result = item._findInVisibleItemsReverse(aPredicate);
2096          if (result) {
2097            return result;
2098          }
2099        }
2100      }
2101
2102      if (this._variablesView._enumVisible) {
2103        for (let i = this._enumItems.length - 1; i >= 0; i--) {
2104          const item = this._enumItems[i];
2105          const result = item._findInVisibleItemsReverse(aPredicate);
2106          if (result) {
2107            return result;
2108          }
2109        }
2110      }
2111    }
2112
2113    if (aPredicate(this)) {
2114      return this;
2115    }
2116
2117    return null;
2118  },
2119
2120  /**
2121   * Gets top level variables view instance.
2122   * @return VariablesView
2123   */
2124  get _variablesView() {
2125    return (
2126      this._topView ||
2127      (this._topView = (() => {
2128        let parentView = this.ownerView;
2129        let topView;
2130
2131        while ((topView = parentView.ownerView)) {
2132          parentView = topView;
2133        }
2134        return parentView;
2135      })())
2136    );
2137  },
2138
2139  /**
2140   * Gets the parent node holding this scope.
2141   * @return Node
2142   */
2143  get parentNode() {
2144    return this.ownerView._list;
2145  },
2146
2147  /**
2148   * Gets the owner document holding this scope.
2149   * @return HTMLDocument
2150   */
2151  get document() {
2152    return this._document || (this._document = this.ownerView.document);
2153  },
2154
2155  /**
2156   * Gets the default window holding this scope.
2157   * @return nsIDOMWindow
2158   */
2159  get window() {
2160    return this._window || (this._window = this.ownerView.window);
2161  },
2162
2163  _topView: null,
2164  _document: null,
2165  _window: null,
2166
2167  ownerView: null,
2168  eval: null,
2169  switch: null,
2170  delete: null,
2171  new: null,
2172  preventDisableOnChange: false,
2173  preventDescriptorModifiers: false,
2174  editing: false,
2175  editableNameTooltip: "",
2176  editableValueTooltip: "",
2177  editButtonTooltip: "",
2178  deleteButtonTooltip: "",
2179  domNodeValueTooltip: "",
2180  contextMenuId: "",
2181  separatorStr: "",
2182
2183  _store: null,
2184  _enumItems: null,
2185  _nonEnumItems: null,
2186  _fetched: false,
2187  _committed: false,
2188  _isLocked: false,
2189  _isExpanded: false,
2190  _isContentVisible: true,
2191  _isHeaderVisible: true,
2192  _isArrowVisible: true,
2193  _isMatch: true,
2194  _idString: "",
2195  _nameString: "",
2196  _target: null,
2197  _arrow: null,
2198  _name: null,
2199  _title: null,
2200  _enum: null,
2201  _nonenum: null,
2202};
2203
2204// Creating maps and arrays thousands of times for variables or properties
2205// with a large number of children fills up a lot of memory. Make sure
2206// these are instantiated only if needed.
2207DevToolsUtils.defineLazyPrototypeGetter(
2208  Scope.prototype,
2209  "_store",
2210  () => new Map()
2211);
2212DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
2213DevToolsUtils.defineLazyPrototypeGetter(
2214  Scope.prototype,
2215  "_nonEnumItems",
2216  Array
2217);
2218
2219/**
2220 * A Variable is a Scope holding Property instances.
2221 * Iterable via "for (let [name, property] of instance) { }".
2222 *
2223 * @param Scope aScope
2224 *        The scope to contain this variable.
2225 * @param string aName
2226 *        The variable's name.
2227 * @param object aDescriptor
2228 *        The variable's descriptor.
2229 * @param object aOptions
2230 *        Options of the form accepted by Scope.addItem
2231 */
2232function Variable(aScope, aName, aDescriptor, aOptions) {
2233  this._setTooltips = this._setTooltips.bind(this);
2234  this._activateNameInput = this._activateNameInput.bind(this);
2235  this._activateValueInput = this._activateValueInput.bind(this);
2236  this.openNodeInInspector = this.openNodeInInspector.bind(this);
2237  this.highlightDomNode = this.highlightDomNode.bind(this);
2238  this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
2239  this._internalItem = aOptions.internalItem;
2240
2241  // Treat safe getter descriptors as descriptors with a value.
2242  if ("getterValue" in aDescriptor) {
2243    aDescriptor.value = aDescriptor.getterValue;
2244    delete aDescriptor.get;
2245    delete aDescriptor.set;
2246  }
2247
2248  Scope.call(this, aScope, aName, (this._initialDescriptor = aDescriptor));
2249  this.setGrip(aDescriptor.value);
2250}
2251
2252Variable.prototype = extend(Scope.prototype, {
2253  /**
2254   * Whether this Variable should be prefetched when it is remoted.
2255   */
2256  get shouldPrefetch() {
2257    return this.name == "window" || this.name == "this";
2258  },
2259
2260  /**
2261   * Whether this Variable should paginate its contents.
2262   */
2263  get allowPaginate() {
2264    return this.name != "window" && this.name != "this";
2265  },
2266
2267  /**
2268   * The class name applied to this variable's target element.
2269   */
2270  targetClassName: "variables-view-variable variable-or-property",
2271
2272  /**
2273   * Create a new Property that is a child of Variable.
2274   *
2275   * @param string aName
2276   *        The name of the new Property.
2277   * @param object aDescriptor
2278   *        The property's descriptor.
2279   * @param object aOptions
2280   *        Options of the form accepted by Scope.addItem
2281   * @return Property
2282   *         The newly created child Property.
2283   */
2284  _createChild: function(aName, aDescriptor, aOptions) {
2285    return new Property(this, aName, aDescriptor, aOptions);
2286  },
2287
2288  /**
2289   * Remove this Variable from its parent and remove all children recursively.
2290   */
2291  remove: function() {
2292    if (this._linkedToInspector) {
2293      this.unhighlightDomNode();
2294      this._valueLabel.removeEventListener("mouseover", this.highlightDomNode);
2295      this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode);
2296      this._openInspectorNode.removeEventListener(
2297        "mousedown",
2298        this.openNodeInInspector
2299      );
2300    }
2301
2302    this.ownerView._store.delete(this._nameString);
2303    this._variablesView._itemsByElement.delete(this._target);
2304    this._variablesView._currHierarchy.delete(this.absoluteName);
2305
2306    this._target.remove();
2307
2308    for (const property of this._store.values()) {
2309      property.remove();
2310    }
2311  },
2312
2313  /**
2314   * Populates this variable to contain all the properties of an object.
2315   *
2316   * @param object aObject
2317   *        The raw object you want to display.
2318   * @param object aOptions [optional]
2319   *        Additional options for adding the properties. Supported options:
2320   *        - sorted: true to sort all the properties before adding them
2321   *        - expanded: true to expand all the properties after adding them
2322   */
2323  populate: function(aObject, aOptions = {}) {
2324    // Retrieve the properties only once.
2325    if (this._fetched) {
2326      return;
2327    }
2328    this._fetched = true;
2329
2330    const propertyNames = Object.getOwnPropertyNames(aObject);
2331    const prototype = Object.getPrototypeOf(aObject);
2332
2333    // Sort all of the properties before adding them, if preferred.
2334    if (aOptions.sorted) {
2335      propertyNames.sort(this._naturalSort);
2336    }
2337
2338    // Add all the variable properties.
2339    for (const name of propertyNames) {
2340      const descriptor = Object.getOwnPropertyDescriptor(aObject, name);
2341      if (descriptor.get || descriptor.set) {
2342        const prop = this._addRawNonValueProperty(name, descriptor);
2343        if (aOptions.expanded) {
2344          prop.expanded = true;
2345        }
2346      } else {
2347        const prop = this._addRawValueProperty(name, descriptor, aObject[name]);
2348        if (aOptions.expanded) {
2349          prop.expanded = true;
2350        }
2351      }
2352    }
2353    // Add the variable's __proto__.
2354    if (prototype) {
2355      this._addRawValueProperty("__proto__", {}, prototype);
2356    }
2357  },
2358
2359  /**
2360   * Populates a specific variable or property instance to contain all the
2361   * properties of an object
2362   *
2363   * @param Variable | Property aVar
2364   *        The target variable to populate.
2365   * @param object aObject [optional]
2366   *        The raw object you want to display. If unspecified, the object is
2367   *        assumed to be defined in a _sourceValue property on the target.
2368   */
2369  _populateTarget: function(aVar, aObject = aVar._sourceValue) {
2370    aVar.populate(aObject);
2371  },
2372
2373  /**
2374   * Adds a property for this variable based on a raw value descriptor.
2375   *
2376   * @param string aName
2377   *        The property's name.
2378   * @param object aDescriptor
2379   *        Specifies the exact property descriptor as returned by a call to
2380   *        Object.getOwnPropertyDescriptor.
2381   * @param object aValue
2382   *        The raw property value you want to display.
2383   * @return Property
2384   *         The newly added property instance.
2385   */
2386  _addRawValueProperty: function(aName, aDescriptor, aValue) {
2387    const descriptor = Object.create(aDescriptor);
2388    descriptor.value = VariablesView.getGrip(aValue);
2389
2390    const propertyItem = this.addItem(aName, descriptor);
2391    propertyItem._sourceValue = aValue;
2392
2393    // Add an 'onexpand' callback for the property, lazily handling
2394    // the addition of new child properties.
2395    if (!VariablesView.isPrimitive(descriptor)) {
2396      propertyItem.onexpand = this._populateTarget;
2397    }
2398    return propertyItem;
2399  },
2400
2401  /**
2402   * Adds a property for this variable based on a getter/setter descriptor.
2403   *
2404   * @param string aName
2405   *        The property's name.
2406   * @param object aDescriptor
2407   *        Specifies the exact property descriptor as returned by a call to
2408   *        Object.getOwnPropertyDescriptor.
2409   * @return Property
2410   *         The newly added property instance.
2411   */
2412  _addRawNonValueProperty: function(aName, aDescriptor) {
2413    const descriptor = Object.create(aDescriptor);
2414    descriptor.get = VariablesView.getGrip(aDescriptor.get);
2415    descriptor.set = VariablesView.getGrip(aDescriptor.set);
2416
2417    return this.addItem(aName, descriptor);
2418  },
2419
2420  /**
2421   * Gets this variable's path to the topmost scope in the form of a string
2422   * meant for use via eval() or a similar approach.
2423   * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
2424   * @return string
2425   */
2426  get symbolicName() {
2427    return this._nameString || "";
2428  },
2429
2430  /**
2431   * Gets full path to this variable, including name of the scope.
2432   * @return string
2433   */
2434  get absoluteName() {
2435    if (this._absoluteName) {
2436      return this._absoluteName;
2437    }
2438
2439    this._absoluteName =
2440      this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
2441    return this._absoluteName;
2442  },
2443
2444  /**
2445   * Gets this variable's symbolic path to the topmost scope.
2446   * @return array
2447   * @see Variable._buildSymbolicPath
2448   */
2449  get symbolicPath() {
2450    if (this._symbolicPath) {
2451      return this._symbolicPath;
2452    }
2453    this._symbolicPath = this._buildSymbolicPath();
2454    return this._symbolicPath;
2455  },
2456
2457  /**
2458   * Build this variable's path to the topmost scope in form of an array of
2459   * strings, one for each segment of the path.
2460   * For example, a symbolic path may look like ["0", "foo", "bar"].
2461   * @return array
2462   */
2463  _buildSymbolicPath: function(path = []) {
2464    if (this.name) {
2465      path.unshift(this.name);
2466      if (this.ownerView instanceof Variable) {
2467        return this.ownerView._buildSymbolicPath(path);
2468      }
2469    }
2470    return path;
2471  },
2472
2473  /**
2474   * Returns this variable's value from the descriptor if available.
2475   * @return any
2476   */
2477  get value() {
2478    return this._initialDescriptor.value;
2479  },
2480
2481  /**
2482   * Returns this variable's getter from the descriptor if available.
2483   * @return object
2484   */
2485  get getter() {
2486    return this._initialDescriptor.get;
2487  },
2488
2489  /**
2490   * Returns this variable's getter from the descriptor if available.
2491   * @return object
2492   */
2493  get setter() {
2494    return this._initialDescriptor.set;
2495  },
2496
2497  /**
2498   * Sets the specific grip for this variable (applies the text content and
2499   * class name to the value label).
2500   *
2501   * The grip should contain the value or the type & class, as defined in the
2502   * remote debugger protocol. For convenience, undefined and null are
2503   * both considered types.
2504   *
2505   * @param any aGrip
2506   *        Specifies the value and/or type & class of the variable.
2507   *        e.g. - 42
2508   *             - true
2509   *             - "nasu"
2510   *             - { type: "undefined" }
2511   *             - { type: "null" }
2512   *             - { type: "object", class: "Object" }
2513   */
2514  setGrip: function(aGrip) {
2515    // Don't allow displaying grip information if there's no name available
2516    // or the grip is malformed.
2517    if (
2518      this._nameString === undefined ||
2519      aGrip === undefined ||
2520      aGrip === null
2521    ) {
2522      return;
2523    }
2524    // Getters and setters should display grip information in sub-properties.
2525    if (this.getter || this.setter) {
2526      return;
2527    }
2528
2529    const prevGrip = this._valueGrip;
2530    if (prevGrip) {
2531      this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
2532    }
2533    this._valueGrip = aGrip;
2534
2535    if (
2536      aGrip &&
2537      (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)
2538    ) {
2539      if (aGrip.optimizedOut) {
2540        this._valueString = L10N.getStr("variablesViewOptimizedOut");
2541      } else if (aGrip.uninitialized) {
2542        this._valueString = L10N.getStr("variablesViewUninitialized");
2543      } else if (aGrip.missingArguments) {
2544        this._valueString = L10N.getStr("variablesViewMissingArgs");
2545      }
2546      this.eval = null;
2547    } else {
2548      this._valueString = VariablesView.getString(aGrip, {
2549        concise: true,
2550        noEllipsis: true,
2551      });
2552      this.eval = this.ownerView.eval;
2553    }
2554
2555    this._valueClassName = VariablesView.getClass(aGrip);
2556
2557    this._valueLabel.classList.add(this._valueClassName);
2558    this._valueLabel.setAttribute("value", this._valueString);
2559    this._separatorLabel.hidden = false;
2560
2561    // DOMNodes get special treatment since they can be linked to the inspector
2562    if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
2563      this._linkToInspector();
2564    }
2565  },
2566
2567  /**
2568   * Marks this variable as overridden.
2569   *
2570   * @param boolean aFlag
2571   *        Whether this variable is overridden or not.
2572   */
2573  setOverridden: function(aFlag) {
2574    if (aFlag) {
2575      this._target.setAttribute("overridden", "");
2576    } else {
2577      this._target.removeAttribute("overridden");
2578    }
2579  },
2580
2581  /**
2582   * Briefly flashes this variable.
2583   *
2584   * @param number aDuration [optional]
2585   *        An optional flash animation duration.
2586   */
2587  flash: function(aDuration = ITEM_FLASH_DURATION) {
2588    const fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
2589    const fadeOutDelay = fadeInDelay + aDuration;
2590
2591    setNamedTimeout("vview-flash-in" + this.absoluteName, fadeInDelay, () =>
2592      this._target.setAttribute("changed", "")
2593    );
2594
2595    setNamedTimeout("vview-flash-out" + this.absoluteName, fadeOutDelay, () =>
2596      this._target.removeAttribute("changed")
2597    );
2598  },
2599
2600  /**
2601   * Initializes this variable's id, view and binds event listeners.
2602   *
2603   * @param string aName
2604   *        The variable's name.
2605   * @param object aDescriptor
2606   *        The variable's descriptor.
2607   */
2608  _init: function(aName, aDescriptor) {
2609    this._idString = generateId((this._nameString = aName));
2610    this._displayScope(aName, this.targetClassName);
2611    this._displayVariable();
2612    this._customizeVariable();
2613    this._prepareTooltips();
2614    this._setAttributes();
2615    this._addEventListeners();
2616
2617    if (
2618      this._initialDescriptor.enumerable ||
2619      this._nameString == "this" ||
2620      this._internalItem
2621    ) {
2622      this.ownerView._enum.appendChild(this._target);
2623      this.ownerView._enumItems.push(this);
2624    } else {
2625      this.ownerView._nonenum.appendChild(this._target);
2626      this.ownerView._nonEnumItems.push(this);
2627    }
2628  },
2629
2630  /**
2631   * Creates the necessary nodes for this variable.
2632   */
2633  _displayVariable: function() {
2634    const document = this.document;
2635    const descriptor = this._initialDescriptor;
2636
2637    const separatorLabel = (this._separatorLabel = document.createXULElement(
2638      "label"
2639    ));
2640    separatorLabel.className = "plain separator";
2641    separatorLabel.setAttribute("value", this.separatorStr + " ");
2642
2643    const valueLabel = (this._valueLabel = document.createXULElement("label"));
2644    valueLabel.className = "plain value";
2645    valueLabel.setAttribute("flex", "1");
2646    valueLabel.setAttribute("crop", "center");
2647
2648    this._title.appendChild(separatorLabel);
2649    this._title.appendChild(valueLabel);
2650
2651    if (VariablesView.isPrimitive(descriptor)) {
2652      this.hideArrow();
2653    }
2654
2655    // If no value will be displayed, we don't need the separator.
2656    if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
2657      separatorLabel.hidden = true;
2658    }
2659
2660    // If this is a getter/setter property, create two child pseudo-properties
2661    // called "get" and "set" that display the corresponding functions.
2662    if (descriptor.get || descriptor.set) {
2663      separatorLabel.hidden = true;
2664      valueLabel.hidden = true;
2665
2666      // Changing getter/setter names is never allowed.
2667      this.switch = null;
2668
2669      // Getter/setter properties require special handling when it comes to
2670      // evaluation and deletion.
2671      if (this.ownerView.eval) {
2672        this.delete = VariablesView.getterOrSetterDeleteCallback;
2673        this.evaluationMacro = VariablesView.overrideValueEvalMacro;
2674      } else {
2675        // Deleting getters and setters individually is not allowed if no
2676        // evaluation method is provided.
2677        this.delete = null;
2678        this.evaluationMacro = null;
2679      }
2680
2681      const getter = this.addItem("get", { value: descriptor.get });
2682      const setter = this.addItem("set", { value: descriptor.set });
2683      getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
2684      setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
2685
2686      getter.hideArrow();
2687      setter.hideArrow();
2688      this.expand();
2689    }
2690  },
2691
2692  /**
2693   * Adds specific nodes for this variable based on custom flags.
2694   */
2695  _customizeVariable: function() {
2696    const ownerView = this.ownerView;
2697    const descriptor = this._initialDescriptor;
2698
2699    if ((ownerView.eval && this.getter) || this.setter) {
2700      const editNode = (this._editNode = this.document.createXULElement(
2701        "toolbarbutton"
2702      ));
2703      editNode.className = "plain variables-view-edit";
2704      editNode.addEventListener("mousedown", this._onEdit.bind(this));
2705      this._title.insertBefore(editNode, this._spacer);
2706    }
2707
2708    if (ownerView.delete) {
2709      const deleteNode = (this._deleteNode = this.document.createXULElement(
2710        "toolbarbutton"
2711      ));
2712      deleteNode.className = "plain variables-view-delete";
2713      deleteNode.addEventListener("click", this._onDelete.bind(this));
2714      this._title.appendChild(deleteNode);
2715    }
2716
2717    if (ownerView.new) {
2718      const addPropertyNode = (this._addPropertyNode = this.document.createXULElement(
2719        "toolbarbutton"
2720      ));
2721      addPropertyNode.className = "plain variables-view-add-property";
2722      addPropertyNode.addEventListener(
2723        "mousedown",
2724        this._onAddProperty.bind(this)
2725      );
2726      this._title.appendChild(addPropertyNode);
2727
2728      // Can't add properties to primitive values, hide the node in those cases.
2729      if (VariablesView.isPrimitive(descriptor)) {
2730        addPropertyNode.setAttribute("invisible", "");
2731      }
2732    }
2733
2734    if (ownerView.contextMenuId) {
2735      this._title.setAttribute("context", ownerView.contextMenuId);
2736    }
2737
2738    if (ownerView.preventDescriptorModifiers) {
2739      return;
2740    }
2741
2742    if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
2743      const nonWritableIcon = this.document.createXULElement("hbox");
2744      nonWritableIcon.className =
2745        "plain variable-or-property-non-writable-icon";
2746      nonWritableIcon.setAttribute("optional-visibility", "");
2747      this._title.appendChild(nonWritableIcon);
2748    }
2749    if (descriptor.value && typeof descriptor.value == "object") {
2750      if (descriptor.value.frozen) {
2751        const frozenLabel = this.document.createXULElement("label");
2752        frozenLabel.className = "plain variable-or-property-frozen-label";
2753        frozenLabel.setAttribute("optional-visibility", "");
2754        frozenLabel.setAttribute("value", "F");
2755        this._title.appendChild(frozenLabel);
2756      }
2757      if (descriptor.value.sealed) {
2758        const sealedLabel = this.document.createXULElement("label");
2759        sealedLabel.className = "plain variable-or-property-sealed-label";
2760        sealedLabel.setAttribute("optional-visibility", "");
2761        sealedLabel.setAttribute("value", "S");
2762        this._title.appendChild(sealedLabel);
2763      }
2764      if (!descriptor.value.extensible) {
2765        const nonExtensibleLabel = this.document.createXULElement("label");
2766        nonExtensibleLabel.className =
2767          "plain variable-or-property-non-extensible-label";
2768        nonExtensibleLabel.setAttribute("optional-visibility", "");
2769        nonExtensibleLabel.setAttribute("value", "N");
2770        this._title.appendChild(nonExtensibleLabel);
2771      }
2772    }
2773  },
2774
2775  /**
2776   * Prepares all tooltips for this variable.
2777   */
2778  _prepareTooltips: function() {
2779    this._target.addEventListener("mouseover", this._setTooltips);
2780  },
2781
2782  /**
2783   * Sets all tooltips for this variable.
2784   */
2785  _setTooltips: function() {
2786    this._target.removeEventListener("mouseover", this._setTooltips);
2787
2788    const ownerView = this.ownerView;
2789    if (ownerView.preventDescriptorModifiers) {
2790      return;
2791    }
2792
2793    const tooltip = this.document.createXULElement("tooltip");
2794    tooltip.id = "tooltip-" + this._idString;
2795    tooltip.setAttribute("orient", "horizontal");
2796
2797    const labels = [
2798      "configurable",
2799      "enumerable",
2800      "writable",
2801      "frozen",
2802      "sealed",
2803      "extensible",
2804      "overridden",
2805      "WebIDL",
2806    ];
2807
2808    for (const type of labels) {
2809      const labelElement = this.document.createXULElement("label");
2810      labelElement.className = type;
2811      labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
2812      tooltip.appendChild(labelElement);
2813    }
2814
2815    this._target.appendChild(tooltip);
2816    this._target.setAttribute("tooltip", tooltip.id);
2817
2818    if (this._editNode && ownerView.eval) {
2819      this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
2820    }
2821    if (this._openInspectorNode && this._linkedToInspector) {
2822      this._openInspectorNode.setAttribute(
2823        "tooltiptext",
2824        this.ownerView.domNodeValueTooltip
2825      );
2826    }
2827    if (this._valueLabel && ownerView.eval) {
2828      this._valueLabel.setAttribute(
2829        "tooltiptext",
2830        ownerView.editableValueTooltip
2831      );
2832    }
2833    if (this._name && ownerView.switch) {
2834      this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
2835    }
2836    if (this._deleteNode && ownerView.delete) {
2837      this._deleteNode.setAttribute(
2838        "tooltiptext",
2839        ownerView.deleteButtonTooltip
2840      );
2841    }
2842  },
2843
2844  /**
2845   * Get the parent variablesview toolbox, if any.
2846   */
2847  get toolbox() {
2848    return this._variablesView.toolbox;
2849  },
2850
2851  /**
2852   * Checks if this variable is a DOMNode and is part of a variablesview that
2853   * has been linked to the toolbox, so that highlighting and jumping to the
2854   * inspector can be done.
2855   */
2856  _isLinkableToInspector: function() {
2857    const isDomNode =
2858      this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
2859    const hasBeenLinked = this._linkedToInspector;
2860    const hasToolbox = !!this.toolbox;
2861
2862    return isDomNode && !hasBeenLinked && hasToolbox;
2863  },
2864
2865  /**
2866   * If the variable is a DOMNode, and if a toolbox is set, then link it to the
2867   * inspector (highlight on hover, and jump to markup-view on click)
2868   */
2869  _linkToInspector: function() {
2870    if (!this._isLinkableToInspector()) {
2871      return;
2872    }
2873
2874    // Listen to value mouseover/click events to highlight and jump
2875    this._valueLabel.addEventListener("mouseover", this.highlightDomNode);
2876    this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode);
2877
2878    // Add a button to open the node in the inspector
2879    this._openInspectorNode = this.document.createXULElement("toolbarbutton");
2880    this._openInspectorNode.className = "plain variables-view-open-inspector";
2881    this._openInspectorNode.addEventListener(
2882      "mousedown",
2883      this.openNodeInInspector
2884    );
2885    this._title.appendChild(this._openInspectorNode);
2886
2887    this._linkedToInspector = true;
2888  },
2889
2890  /**
2891   * In case this variable is a DOMNode and part of a variablesview that has been
2892   * linked to the toolbox's inspector, then select the corresponding node in
2893   * the inspector, and switch the inspector tool in the toolbox
2894   * @return a promise that resolves when the node is selected and the inspector
2895   * has been switched to and is ready
2896   */
2897  openNodeInInspector: function(event) {
2898    if (!this.toolbox) {
2899      return promise.reject(new Error("Toolbox not available"));
2900    }
2901
2902    event && event.stopPropagation();
2903
2904    return async function() {
2905      let nodeFront = this._nodeFront;
2906      if (!nodeFront) {
2907        const inspectorFront = await this.toolbox.target.getFront("inspector");
2908        nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
2909          this._valueGrip
2910        );
2911      }
2912
2913      if (nodeFront) {
2914        await this.toolbox.selectTool("inspector");
2915
2916        const inspectorReady = new Promise(resolve => {
2917          this.toolbox.getPanel("inspector").once("inspector-updated", resolve);
2918        });
2919
2920        await this.toolbox.selection.setNodeFront(nodeFront, {
2921          reason: "variables-view",
2922        });
2923        await inspectorReady;
2924      }
2925    }.bind(this)();
2926  },
2927
2928  /**
2929   * In case this variable is a DOMNode and part of a variablesview that has been
2930   * linked to the toolbox's inspector, then highlight the corresponding node
2931   */
2932  highlightDomNode: async function() {
2933    if (!this.toolbox) {
2934      return;
2935    }
2936
2937    if (!this._nodeFront) {
2938      const inspectorFront = await this.toolbox.target.getFront("inspector");
2939      this._nodeFront = await inspectorFront.getNodeFrontFromNodeGrip(
2940        this._valueGrip
2941      );
2942    }
2943
2944    await this.toolbox.getHighlighter().highlight(this._nodeFront);
2945  },
2946
2947  /**
2948   * Unhighlight a previously highlit node
2949   * @see highlightDomNode
2950   */
2951  unhighlightDomNode: function() {
2952    if (!this.toolbox) {
2953      return;
2954    }
2955
2956    this.toolbox.getHighlighter().unhighlight();
2957  },
2958
2959  /**
2960   * Sets a variable's configurable, enumerable and writable attributes,
2961   * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
2962   * reference.
2963   */
2964  // eslint-disable-next-line complexity
2965  _setAttributes: function() {
2966    const ownerView = this.ownerView;
2967    if (ownerView.preventDescriptorModifiers) {
2968      return;
2969    }
2970
2971    const descriptor = this._initialDescriptor;
2972    const target = this._target;
2973    const name = this._nameString;
2974
2975    if (ownerView.eval) {
2976      target.setAttribute("editable", "");
2977    }
2978
2979    if (!descriptor.configurable) {
2980      target.setAttribute("non-configurable", "");
2981    }
2982    if (!descriptor.enumerable) {
2983      target.setAttribute("non-enumerable", "");
2984    }
2985    if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
2986      target.setAttribute("non-writable", "");
2987    }
2988
2989    if (descriptor.value && typeof descriptor.value == "object") {
2990      if (descriptor.value.frozen) {
2991        target.setAttribute("frozen", "");
2992      }
2993      if (descriptor.value.sealed) {
2994        target.setAttribute("sealed", "");
2995      }
2996      if (!descriptor.value.extensible) {
2997        target.setAttribute("non-extensible", "");
2998      }
2999    }
3000
3001    if (descriptor && "getterValue" in descriptor) {
3002      target.setAttribute("safe-getter", "");
3003    }
3004
3005    if (name == "this") {
3006      target.setAttribute("self", "");
3007    } else if (this._internalItem && name == "<exception>") {
3008      target.setAttribute("exception", "");
3009      target.setAttribute("pseudo-item", "");
3010    } else if (this._internalItem && name == "<return>") {
3011      target.setAttribute("return", "");
3012      target.setAttribute("pseudo-item", "");
3013    } else if (name == "__proto__") {
3014      target.setAttribute("proto", "");
3015      target.setAttribute("pseudo-item", "");
3016    }
3017
3018    if (Object.keys(descriptor).length == 0) {
3019      target.setAttribute("pseudo-item", "");
3020    }
3021  },
3022
3023  /**
3024   * Adds the necessary event listeners for this variable.
3025   */
3026  _addEventListeners: function() {
3027    this._name.addEventListener("dblclick", this._activateNameInput);
3028    this._valueLabel.addEventListener("mousedown", this._activateValueInput);
3029    this._title.addEventListener("mousedown", this._onClick);
3030  },
3031
3032  /**
3033   * Makes this variable's name editable.
3034   */
3035  _activateNameInput: function(e) {
3036    if (!this._variablesView.alignedValues) {
3037      this._separatorLabel.hidden = true;
3038      this._valueLabel.hidden = true;
3039    }
3040
3041    EditableName.create(
3042      this,
3043      {
3044        onSave: aKey => {
3045          if (!this._variablesView.preventDisableOnChange) {
3046            this._disable();
3047          }
3048          this.ownerView.switch(this, aKey);
3049        },
3050        onCleanup: () => {
3051          if (!this._variablesView.alignedValues) {
3052            this._separatorLabel.hidden = false;
3053            this._valueLabel.hidden = false;
3054          }
3055        },
3056      },
3057      e
3058    );
3059  },
3060
3061  /**
3062   * Makes this variable's value editable.
3063   */
3064  _activateValueInput: function(e) {
3065    EditableValue.create(
3066      this,
3067      {
3068        onSave: aString => {
3069          if (this._linkedToInspector) {
3070            this.unhighlightDomNode();
3071          }
3072          if (!this._variablesView.preventDisableOnChange) {
3073            this._disable();
3074          }
3075          this.ownerView.eval(this, aString);
3076        },
3077      },
3078      e
3079    );
3080  },
3081
3082  /**
3083   * Disables this variable prior to a new name switch or value evaluation.
3084   */
3085  _disable: function() {
3086    // Prevent the variable from being collapsed or expanded.
3087    this.hideArrow();
3088
3089    // Hide any nodes that may offer information about the variable.
3090    for (const node of this._title.childNodes) {
3091      node.hidden = node != this._arrow && node != this._name;
3092    }
3093    this._enum.hidden = true;
3094    this._nonenum.hidden = true;
3095  },
3096
3097  /**
3098   * The current macro used to generate the string evaluated when performing
3099   * a variable or property value change.
3100   */
3101  evaluationMacro: VariablesView.simpleValueEvalMacro,
3102
3103  /**
3104   * The click listener for the edit button.
3105   */
3106  _onEdit: function(e) {
3107    if (e.button != 0) {
3108      return;
3109    }
3110
3111    e.preventDefault();
3112    e.stopPropagation();
3113    this._activateValueInput();
3114  },
3115
3116  /**
3117   * The click listener for the delete button.
3118   */
3119  _onDelete: function(e) {
3120    if ("button" in e && e.button != 0) {
3121      return;
3122    }
3123
3124    e.preventDefault();
3125    e.stopPropagation();
3126
3127    if (this.ownerView.delete) {
3128      if (!this.ownerView.delete(this)) {
3129        this.hide();
3130      }
3131    }
3132  },
3133
3134  /**
3135   * The click listener for the add property button.
3136   */
3137  _onAddProperty: function(e) {
3138    if ("button" in e && e.button != 0) {
3139      return;
3140    }
3141
3142    e.preventDefault();
3143    e.stopPropagation();
3144
3145    this.expanded = true;
3146
3147    const item = this.addItem(
3148      " ",
3149      {
3150        value: undefined,
3151        configurable: true,
3152        enumerable: true,
3153        writable: true,
3154      },
3155      { relaxed: true }
3156    );
3157
3158    // Force showing the separator.
3159    item._separatorLabel.hidden = false;
3160
3161    EditableNameAndValue.create(
3162      item,
3163      {
3164        onSave: ([aKey, aValue]) => {
3165          if (!this._variablesView.preventDisableOnChange) {
3166            this._disable();
3167          }
3168          this.ownerView.new(this, aKey, aValue);
3169        },
3170      },
3171      e
3172    );
3173  },
3174
3175  _symbolicName: null,
3176  _symbolicPath: null,
3177  _absoluteName: null,
3178  _initialDescriptor: null,
3179  _separatorLabel: null,
3180  _valueLabel: null,
3181  _spacer: null,
3182  _editNode: null,
3183  _deleteNode: null,
3184  _addPropertyNode: null,
3185  _tooltip: null,
3186  _valueGrip: null,
3187  _valueString: "",
3188  _valueClassName: "",
3189  _prevExpandable: false,
3190  _prevExpanded: false,
3191});
3192
3193/**
3194 * A Property is a Variable holding additional child Property instances.
3195 * Iterable via "for (let [name, property] of instance) { }".
3196 *
3197 * @param Variable aVar
3198 *        The variable to contain this property.
3199 * @param string aName
3200 *        The property's name.
3201 * @param object aDescriptor
3202 *        The property's descriptor.
3203 * @param object aOptions
3204 *        Options of the form accepted by Scope.addItem
3205 */
3206function Property(aVar, aName, aDescriptor, aOptions) {
3207  Variable.call(this, aVar, aName, aDescriptor, aOptions);
3208}
3209
3210Property.prototype = extend(Variable.prototype, {
3211  /**
3212   * The class name applied to this property's target element.
3213   */
3214  targetClassName: "variables-view-property variable-or-property",
3215
3216  /**
3217   * @see Variable.symbolicName
3218   * @return string
3219   */
3220  get symbolicName() {
3221    if (this._symbolicName) {
3222      return this._symbolicName;
3223    }
3224
3225    this._symbolicName =
3226      this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
3227    return this._symbolicName;
3228  },
3229
3230  /**
3231   * @see Variable.absoluteName
3232   * @return string
3233   */
3234  get absoluteName() {
3235    if (this._absoluteName) {
3236      return this._absoluteName;
3237    }
3238
3239    this._absoluteName =
3240      this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
3241    return this._absoluteName;
3242  },
3243});
3244
3245/**
3246 * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
3247 */
3248VariablesView.prototype[Symbol.iterator] = Scope.prototype[
3249  Symbol.iterator
3250] = Variable.prototype[Symbol.iterator] = Property.prototype[
3251  Symbol.iterator
3252] = function*() {
3253  yield* this._store;
3254};
3255
3256/**
3257 * Forget everything recorded about added scopes, variables or properties.
3258 * @see VariablesView.commitHierarchy
3259 */
3260VariablesView.prototype.clearHierarchy = function() {
3261  this._prevHierarchy.clear();
3262  this._currHierarchy.clear();
3263};
3264
3265/**
3266 * Perform operations on all the VariablesView Scopes, Variables and Properties
3267 * after you've added all the items you wanted.
3268 *
3269 * Calling this method is optional, and does the following:
3270 *   - styles the items overridden by other items in parent scopes
3271 *   - reopens the items which were previously expanded
3272 *   - flashes the items whose values changed
3273 */
3274VariablesView.prototype.commitHierarchy = function() {
3275  for (const [, currItem] of this._currHierarchy) {
3276    // Avoid performing expensive operations.
3277    if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
3278      continue;
3279    }
3280    const overridden = this.isOverridden(currItem);
3281    if (overridden) {
3282      currItem.setOverridden(true);
3283    }
3284    const expanded = !currItem._committed && this.wasExpanded(currItem);
3285    if (expanded) {
3286      currItem.expand();
3287    }
3288    const changed = !currItem._committed && this.hasChanged(currItem);
3289    if (changed) {
3290      currItem.flash();
3291    }
3292    currItem._committed = true;
3293  }
3294  if (this.oncommit) {
3295    this.oncommit(this);
3296  }
3297};
3298
3299// Some variables are likely to contain a very large number of properties.
3300// It would be a bad idea to re-expand them or perform expensive operations.
3301VariablesView.prototype.commitHierarchyIgnoredItems = extend(null, {
3302  window: true,
3303  this: true,
3304});
3305
3306/**
3307 * Checks if the an item was previously expanded, if it existed in a
3308 * previous hierarchy.
3309 *
3310 * @param Scope | Variable | Property aItem
3311 *        The item to verify.
3312 * @return boolean
3313 *         Whether the item was expanded.
3314 */
3315VariablesView.prototype.wasExpanded = function(aItem) {
3316  if (!(aItem instanceof Scope)) {
3317    return false;
3318  }
3319  const prevItem = this._prevHierarchy.get(
3320    aItem.absoluteName || aItem._nameString
3321  );
3322  return prevItem ? prevItem._isExpanded : false;
3323};
3324
3325/**
3326 * Checks if the an item's displayed value (a representation of the grip)
3327 * has changed, if it existed in a previous hierarchy.
3328 *
3329 * @param Variable | Property aItem
3330 *        The item to verify.
3331 * @return boolean
3332 *         Whether the item has changed.
3333 */
3334VariablesView.prototype.hasChanged = function(aItem) {
3335  // Only analyze Variables and Properties for displayed value changes.
3336  // Scopes are just collections of Variables and Properties and
3337  // don't have a "value", so they can't change.
3338  if (!(aItem instanceof Variable)) {
3339    return false;
3340  }
3341  const prevItem = this._prevHierarchy.get(aItem.absoluteName);
3342  return prevItem ? prevItem._valueString != aItem._valueString : false;
3343};
3344
3345/**
3346 * Checks if the an item was previously expanded, if it existed in a
3347 * previous hierarchy.
3348 *
3349 * @param Scope | Variable | Property aItem
3350 *        The item to verify.
3351 * @return boolean
3352 *         Whether the item was expanded.
3353 */
3354VariablesView.prototype.isOverridden = function(aItem) {
3355  // Only analyze Variables for being overridden in different Scopes.
3356  if (!(aItem instanceof Variable) || aItem instanceof Property) {
3357    return false;
3358  }
3359  const currVariableName = aItem._nameString;
3360  const parentScopes = this.getParentScopesForVariableOrProperty(aItem);
3361
3362  for (const otherScope of parentScopes) {
3363    for (const [otherVariableName] of otherScope) {
3364      if (otherVariableName == currVariableName) {
3365        return true;
3366      }
3367    }
3368  }
3369  return false;
3370};
3371
3372/**
3373 * Returns true if the descriptor represents an undefined, null or
3374 * primitive value.
3375 *
3376 * @param object aDescriptor
3377 *        The variable's descriptor.
3378 */
3379VariablesView.isPrimitive = function(aDescriptor) {
3380  // For accessor property descriptors, the getter and setter need to be
3381  // contained in 'get' and 'set' properties.
3382  const getter = aDescriptor.get;
3383  const setter = aDescriptor.set;
3384  if (getter || setter) {
3385    return false;
3386  }
3387
3388  // As described in the remote debugger protocol, the value grip
3389  // must be contained in a 'value' property.
3390  const grip = aDescriptor.value;
3391  if (typeof grip != "object") {
3392    return true;
3393  }
3394
3395  // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
3396  // strings are considered types.
3397  const type = grip.type;
3398  if (
3399    type == "undefined" ||
3400    type == "null" ||
3401    type == "Infinity" ||
3402    type == "-Infinity" ||
3403    type == "NaN" ||
3404    type == "-0" ||
3405    type == "symbol" ||
3406    type == "longString"
3407  ) {
3408    return true;
3409  }
3410
3411  return false;
3412};
3413
3414/**
3415 * Returns true if the descriptor represents an undefined value.
3416 *
3417 * @param object aDescriptor
3418 *        The variable's descriptor.
3419 */
3420VariablesView.isUndefined = function(aDescriptor) {
3421  // For accessor property descriptors, the getter and setter need to be
3422  // contained in 'get' and 'set' properties.
3423  const getter = aDescriptor.get;
3424  const setter = aDescriptor.set;
3425  if (
3426    typeof getter == "object" &&
3427    getter.type == "undefined" &&
3428    typeof setter == "object" &&
3429    setter.type == "undefined"
3430  ) {
3431    return true;
3432  }
3433
3434  // As described in the remote debugger protocol, the value grip
3435  // must be contained in a 'value' property.
3436  const grip = aDescriptor.value;
3437  if (typeof grip == "object" && grip.type == "undefined") {
3438    return true;
3439  }
3440
3441  return false;
3442};
3443
3444/**
3445 * Returns true if the descriptor represents a falsy value.
3446 *
3447 * @param object aDescriptor
3448 *        The variable's descriptor.
3449 */
3450VariablesView.isFalsy = function(aDescriptor) {
3451  // As described in the remote debugger protocol, the value grip
3452  // must be contained in a 'value' property.
3453  const grip = aDescriptor.value;
3454  if (typeof grip != "object") {
3455    return !grip;
3456  }
3457
3458  // For convenience, undefined, null, NaN, and -0 are all considered types.
3459  const type = grip.type;
3460  if (type == "undefined" || type == "null" || type == "NaN" || type == "-0") {
3461    return true;
3462  }
3463
3464  return false;
3465};
3466
3467/**
3468 * Returns true if the value is an instance of Variable or Property.
3469 *
3470 * @param any aValue
3471 *        The value to test.
3472 */
3473VariablesView.isVariable = function(aValue) {
3474  return aValue instanceof Variable;
3475};
3476
3477/**
3478 * Returns a standard grip for a value.
3479 *
3480 * @param any aValue
3481 *        The raw value to get a grip for.
3482 * @return any
3483 *         The value's grip.
3484 */
3485VariablesView.getGrip = function(aValue) {
3486  switch (typeof aValue) {
3487    case "boolean":
3488    case "string":
3489      return aValue;
3490    case "number":
3491      if (aValue === Infinity) {
3492        return { type: "Infinity" };
3493      } else if (aValue === -Infinity) {
3494        return { type: "-Infinity" };
3495      } else if (Number.isNaN(aValue)) {
3496        return { type: "NaN" };
3497      } else if (1 / aValue === -Infinity) {
3498        return { type: "-0" };
3499      }
3500      return aValue;
3501    case "undefined":
3502      // document.all is also "undefined"
3503      if (aValue === undefined) {
3504        return { type: "undefined" };
3505      }
3506    // fall through
3507    case "object":
3508      if (aValue === null) {
3509        return { type: "null" };
3510      }
3511    // fall through
3512    case "function":
3513      return { type: "object", class: getObjectClassName(aValue) };
3514    default:
3515      console.error(
3516        "Failed to provide a grip for value of " + typeof value + ": " + aValue
3517      );
3518      return null;
3519  }
3520};
3521
3522// Match the function name from the result of toString() or toSource().
3523//
3524// Examples:
3525// (function foobar(a, b) { ...
3526// function foobar2(a) { ...
3527// function() { ...
3528const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
3529
3530/**
3531 * Helper function to deduce the name of the provided function.
3532 *
3533 * @param function function
3534 *        The function whose name will be returned.
3535 * @return string
3536 *         Function name.
3537 */
3538function getFunctionName(func) {
3539  let name = null;
3540  if (func.name) {
3541    name = func.name;
3542  } else {
3543    let desc;
3544    try {
3545      desc = func.getOwnPropertyDescriptor("displayName");
3546    } catch (ex) {
3547      // Ignore.
3548    }
3549    if (desc && typeof desc.value == "string") {
3550      name = desc.value;
3551    }
3552  }
3553  if (!name) {
3554    try {
3555      const str = (func.toString() || func.toSource()) + "";
3556      name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
3557    } catch (ex) {
3558      // Ignore.
3559    }
3560  }
3561  return name;
3562}
3563
3564/**
3565 * Get the object class name. For example, the |window| object has the Window
3566 * class name (based on [object Window]).
3567 *
3568 * @param object object
3569 *        The object you want to get the class name for.
3570 * @return string
3571 *         The object class name.
3572 */
3573function getObjectClassName(object) {
3574  if (object === null) {
3575    return "null";
3576  }
3577  if (object === undefined) {
3578    return "undefined";
3579  }
3580
3581  const type = typeof object;
3582  if (type != "object") {
3583    // Grip class names should start with an uppercase letter.
3584    return type.charAt(0).toUpperCase() + type.substr(1);
3585  }
3586
3587  let className;
3588
3589  try {
3590    className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
3591    if (!className) {
3592      className = ((object.constructor + "").match(/^\[object (\S+)\]$/) ||
3593        [])[1];
3594    }
3595    if (!className && typeof object.constructor == "function") {
3596      className = getFunctionName(object.constructor);
3597    }
3598  } catch (ex) {
3599    // Ignore.
3600  }
3601
3602  return className;
3603}
3604
3605/**
3606 * Returns a custom formatted property string for a grip.
3607 *
3608 * @param any aGrip
3609 *        @see Variable.setGrip
3610 * @param object aOptions
3611 *        Options:
3612 *        - concise: boolean that tells you want a concisely formatted string.
3613 *        - noStringQuotes: boolean that tells to not quote strings.
3614 *        - noEllipsis: boolean that tells to not add an ellipsis after the
3615 *        initial text of a longString.
3616 * @return string
3617 *         The formatted property string.
3618 */
3619VariablesView.getString = function(aGrip, aOptions = {}) {
3620  if (aGrip && typeof aGrip == "object") {
3621    switch (aGrip.type) {
3622      case "undefined":
3623      case "null":
3624      case "NaN":
3625      case "Infinity":
3626      case "-Infinity":
3627      case "-0":
3628        return aGrip.type;
3629      default:
3630        const stringifier = VariablesView.stringifiers.byType[aGrip.type];
3631        if (stringifier) {
3632          const result = stringifier(aGrip, aOptions);
3633          if (result != null) {
3634            return result;
3635          }
3636        }
3637
3638        if (aGrip.displayString) {
3639          return VariablesView.getString(aGrip.displayString, aOptions);
3640        }
3641
3642        if (aGrip.type == "object" && aOptions.concise) {
3643          return aGrip.class;
3644        }
3645
3646        return "[" + aGrip.type + " " + aGrip.class + "]";
3647    }
3648  }
3649
3650  switch (typeof aGrip) {
3651    case "string":
3652      return VariablesView.stringifiers.byType.string(aGrip, aOptions);
3653    case "boolean":
3654      return aGrip ? "true" : "false";
3655    case "number":
3656      if (!aGrip && 1 / aGrip === -Infinity) {
3657        return "-0";
3658      }
3659    // fall through
3660    default:
3661      return aGrip + "";
3662  }
3663};
3664
3665/**
3666 * The VariablesView stringifiers are used by VariablesView.getString(). These
3667 * are organized by object type, object class and by object actor preview kind.
3668 * Some objects share identical ways for previews, for example Arrays, Sets and
3669 * NodeLists.
3670 *
3671 * Any stringifier function must return a string. If null is returned, * then
3672 * the default stringifier will be used. When invoked, the stringifier is
3673 * given the same two arguments as those given to VariablesView.getString().
3674 */
3675VariablesView.stringifiers = {};
3676
3677VariablesView.stringifiers.byType = {
3678  string: function(aGrip, { noStringQuotes }) {
3679    if (noStringQuotes) {
3680      return aGrip;
3681    }
3682    return '"' + aGrip + '"';
3683  },
3684
3685  longString: function({ initial }, { noStringQuotes, noEllipsis }) {
3686    const ellipsis = noEllipsis ? "" : ELLIPSIS;
3687    if (noStringQuotes) {
3688      return initial + ellipsis;
3689    }
3690    const result = '"' + initial + '"';
3691    if (!ellipsis) {
3692      return result;
3693    }
3694    return result.substr(0, result.length - 1) + ellipsis + '"';
3695  },
3696
3697  object: function(aGrip, aOptions) {
3698    const { preview } = aGrip;
3699    let stringifier;
3700    if (aGrip.class) {
3701      stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
3702    }
3703    if (!stringifier && preview && preview.kind) {
3704      stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
3705    }
3706    if (stringifier) {
3707      return stringifier(aGrip, aOptions);
3708    }
3709    return null;
3710  },
3711
3712  symbol: function(aGrip, aOptions) {
3713    const name = aGrip.name || "";
3714    return "Symbol(" + name + ")";
3715  },
3716
3717  mapEntry: function(aGrip, { concise }) {
3718    const {
3719      preview: { key, value },
3720    } = aGrip;
3721
3722    const keyString = VariablesView.getString(key, {
3723      concise: true,
3724      noStringQuotes: true,
3725    });
3726    const valueString = VariablesView.getString(value, { concise: true });
3727
3728    return keyString + " \u2192 " + valueString;
3729  },
3730}; // VariablesView.stringifiers.byType
3731
3732VariablesView.stringifiers.byObjectClass = {
3733  Function: function(aGrip, { concise }) {
3734    // TODO: Bug 948484 - support arrow functions and ES6 generators
3735
3736    let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
3737    name = VariablesView.getString(name, { noStringQuotes: true });
3738
3739    // TODO: Bug 948489 - Support functions with destructured parameters and
3740    // rest parameters
3741    const params = aGrip.parameterNames || "";
3742    if (!concise) {
3743      return "function " + name + "(" + params + ")";
3744    }
3745    return (name || "function ") + "(" + params + ")";
3746  },
3747
3748  RegExp: function({ displayString }) {
3749    return VariablesView.getString(displayString, { noStringQuotes: true });
3750  },
3751
3752  Date: function({ preview }) {
3753    if (!preview || !("timestamp" in preview)) {
3754      return null;
3755    }
3756
3757    if (typeof preview.timestamp != "number") {
3758      return new Date(preview.timestamp).toString(); // invalid date
3759    }
3760
3761    return "Date " + new Date(preview.timestamp).toISOString();
3762  },
3763
3764  Number: function(aGrip) {
3765    const { preview } = aGrip;
3766    if (preview === undefined) {
3767      return null;
3768    }
3769    return (
3770      aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) + " }"
3771    );
3772  },
3773}; // VariablesView.stringifiers.byObjectClass
3774
3775VariablesView.stringifiers.byObjectClass.Boolean =
3776  VariablesView.stringifiers.byObjectClass.Number;
3777
3778VariablesView.stringifiers.byObjectKind = {
3779  ArrayLike: function(aGrip, { concise }) {
3780    const { preview } = aGrip;
3781    if (concise) {
3782      return aGrip.class + "[" + preview.length + "]";
3783    }
3784
3785    if (!preview.items) {
3786      return null;
3787    }
3788
3789    let shown = 0,
3790      lastHole = null;
3791    const result = [];
3792    for (const item of preview.items) {
3793      if (item === null) {
3794        if (lastHole !== null) {
3795          result[lastHole] += ",";
3796        } else {
3797          result.push("");
3798        }
3799        lastHole = result.length - 1;
3800      } else {
3801        lastHole = null;
3802        result.push(VariablesView.getString(item, { concise: true }));
3803      }
3804      shown++;
3805    }
3806
3807    if (shown < preview.length) {
3808      const n = preview.length - shown;
3809      result.push(VariablesView.stringifiers._getNMoreString(n));
3810    } else if (lastHole !== null) {
3811      // make sure we have the right number of commas...
3812      result[lastHole] += ",";
3813    }
3814
3815    const prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
3816    return prefix + "[" + result.join(", ") + "]";
3817  },
3818
3819  MapLike: function(aGrip, { concise }) {
3820    const { preview } = aGrip;
3821    if (concise || !preview.entries) {
3822      const size =
3823        typeof preview.size == "number" ? "[" + preview.size + "]" : "";
3824      return aGrip.class + size;
3825    }
3826
3827    const entries = [];
3828    for (const [key, value] of preview.entries) {
3829      const keyString = VariablesView.getString(key, {
3830        concise: true,
3831        noStringQuotes: true,
3832      });
3833      const valueString = VariablesView.getString(value, { concise: true });
3834      entries.push(keyString + ": " + valueString);
3835    }
3836
3837    if (typeof preview.size == "number" && preview.size > entries.length) {
3838      const n = preview.size - entries.length;
3839      entries.push(VariablesView.stringifiers._getNMoreString(n));
3840    }
3841
3842    return aGrip.class + " {" + entries.join(", ") + "}";
3843  },
3844
3845  ObjectWithText: function(aGrip, { concise }) {
3846    if (concise) {
3847      return aGrip.class;
3848    }
3849
3850    return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
3851  },
3852
3853  ObjectWithURL: function(aGrip, { concise }) {
3854    let result = aGrip.class;
3855    const url = aGrip.preview.url;
3856    if (!VariablesView.isFalsy({ value: url })) {
3857      result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
3858    }
3859    return result;
3860  },
3861
3862  // Stringifier for any kind of object.
3863  Object: function(aGrip, { concise }) {
3864    if (concise) {
3865      return aGrip.class;
3866    }
3867
3868    const { preview } = aGrip;
3869    const props = [];
3870
3871    if (aGrip.class == "Promise" && aGrip.promiseState) {
3872      const { state, value, reason } = aGrip.promiseState;
3873      props.push("<state>: " + VariablesView.getString(state));
3874      if (state == "fulfilled") {
3875        props.push(
3876          "<value>: " + VariablesView.getString(value, { concise: true })
3877        );
3878      } else if (state == "rejected") {
3879        props.push(
3880          "<reason>: " + VariablesView.getString(reason, { concise: true })
3881        );
3882      }
3883    }
3884
3885    for (const key of Object.keys(preview.ownProperties || {})) {
3886      const value = preview.ownProperties[key];
3887      let valueString = "";
3888      if (value.get) {
3889        valueString = "Getter";
3890      } else if (value.set) {
3891        valueString = "Setter";
3892      } else {
3893        valueString = VariablesView.getString(value.value, { concise: true });
3894      }
3895      props.push(key + ": " + valueString);
3896    }
3897
3898    for (const key of Object.keys(preview.safeGetterValues || {})) {
3899      const value = preview.safeGetterValues[key];
3900      const valueString = VariablesView.getString(value.getterValue, {
3901        concise: true,
3902      });
3903      props.push(key + ": " + valueString);
3904    }
3905
3906    if (!props.length) {
3907      return null;
3908    }
3909
3910    if (preview.ownPropertiesLength) {
3911      const previewLength = Object.keys(preview.ownProperties).length;
3912      const diff = preview.ownPropertiesLength - previewLength;
3913      if (diff > 0) {
3914        props.push(VariablesView.stringifiers._getNMoreString(diff));
3915      }
3916    }
3917
3918    const prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
3919    return prefix + "{" + props.join(", ") + "}";
3920  }, // Object
3921
3922  Error: function(aGrip, { concise }) {
3923    const { preview } = aGrip;
3924    const name = VariablesView.getString(preview.name, {
3925      noStringQuotes: true,
3926    });
3927    if (concise) {
3928      return name || aGrip.class;
3929    }
3930
3931    let msg =
3932      name +
3933      ": " +
3934      VariablesView.getString(preview.message, { noStringQuotes: true });
3935
3936    if (!VariablesView.isFalsy({ value: preview.stack })) {
3937      msg +=
3938        "\n" +
3939        L10N.getStr("variablesViewErrorStacktrace") +
3940        "\n" +
3941        preview.stack;
3942    }
3943
3944    return msg;
3945  },
3946
3947  DOMException: function(aGrip, { concise }) {
3948    const { preview } = aGrip;
3949    if (concise) {
3950      return preview.name || aGrip.class;
3951    }
3952
3953    let msg =
3954      aGrip.class +
3955      " [" +
3956      preview.name +
3957      ": " +
3958      VariablesView.getString(preview.message) +
3959      "\n" +
3960      "code: " +
3961      preview.code +
3962      "\n" +
3963      "nsresult: 0x" +
3964      (+preview.result).toString(16);
3965
3966    if (preview.filename) {
3967      msg += "\nlocation: " + preview.filename;
3968      if (preview.lineNumber) {
3969        msg += ":" + preview.lineNumber;
3970      }
3971    }
3972
3973    return msg + "]";
3974  },
3975
3976  DOMEvent: function(aGrip, { concise }) {
3977    const { preview } = aGrip;
3978    if (!preview.type) {
3979      return null;
3980    }
3981
3982    if (concise) {
3983      return aGrip.class + " " + preview.type;
3984    }
3985
3986    let result = preview.type;
3987
3988    if (
3989      preview.eventKind == "key" &&
3990      preview.modifiers &&
3991      preview.modifiers.length
3992    ) {
3993      result += " " + preview.modifiers.join("-");
3994    }
3995
3996    const props = [];
3997    if (preview.target) {
3998      const target = VariablesView.getString(preview.target, { concise: true });
3999      props.push("target: " + target);
4000    }
4001
4002    for (const prop in preview.properties) {
4003      const value = preview.properties[prop];
4004      props.push(
4005        prop + ": " + VariablesView.getString(value, { concise: true })
4006      );
4007    }
4008
4009    return result + " {" + props.join(", ") + "}";
4010  }, // DOMEvent
4011
4012  DOMNode: function(aGrip, { concise }) {
4013    const { preview } = aGrip;
4014
4015    switch (preview.nodeType) {
4016      case nodeConstants.DOCUMENT_NODE: {
4017        let result = aGrip.class;
4018        if (preview.location) {
4019          result += ` \u2192 ${
4020            getSourceNames(preview.location)[concise ? "short" : "long"]
4021          }`;
4022        }
4023
4024        return result;
4025      }
4026
4027      case nodeConstants.ATTRIBUTE_NODE: {
4028        const value = VariablesView.getString(preview.value, {
4029          noStringQuotes: true,
4030        });
4031        return preview.nodeName + '="' + escapeHTML(value) + '"';
4032      }
4033
4034      case nodeConstants.TEXT_NODE:
4035        return (
4036          preview.nodeName + " " + VariablesView.getString(preview.textContent)
4037        );
4038
4039      case nodeConstants.COMMENT_NODE: {
4040        const comment = VariablesView.getString(preview.textContent, {
4041          noStringQuotes: true,
4042        });
4043        return "<!--" + comment + "-->";
4044      }
4045
4046      case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
4047        if (concise || !preview.childNodes) {
4048          return aGrip.class + "[" + preview.childNodesLength + "]";
4049        }
4050        const nodes = [];
4051        for (const node of preview.childNodes) {
4052          nodes.push(VariablesView.getString(node));
4053        }
4054        if (nodes.length < preview.childNodesLength) {
4055          const n = preview.childNodesLength - nodes.length;
4056          nodes.push(VariablesView.stringifiers._getNMoreString(n));
4057        }
4058        return aGrip.class + " [" + nodes.join(", ") + "]";
4059      }
4060
4061      case nodeConstants.ELEMENT_NODE: {
4062        const attrs = preview.attributes;
4063        if (!concise) {
4064          let n = 0,
4065            result = "<" + preview.nodeName;
4066          for (const name in attrs) {
4067            const value = VariablesView.getString(attrs[name], {
4068              noStringQuotes: true,
4069            });
4070            result += " " + name + '="' + escapeHTML(value) + '"';
4071            n++;
4072          }
4073          if (preview.attributesLength > n) {
4074            result += " " + ELLIPSIS;
4075          }
4076          return result + ">";
4077        }
4078
4079        let result = "<" + preview.nodeName;
4080        if (attrs.id) {
4081          result += "#" + attrs.id;
4082        }
4083
4084        if (attrs.class) {
4085          result += "." + attrs.class.trim().replace(/\s+/, ".");
4086        }
4087        return result + ">";
4088      }
4089
4090      default:
4091        return null;
4092    }
4093  }, // DOMNode
4094}; // VariablesView.stringifiers.byObjectKind
4095
4096/**
4097 * Get the "N more…" formatted string, given an N. This is used for displaying
4098 * how many elements are not displayed in an object preview (eg. an array).
4099 *
4100 * @private
4101 * @param number aNumber
4102 * @return string
4103 */
4104VariablesView.stringifiers._getNMoreString = function(aNumber) {
4105  const str = L10N.getStr("variablesViewMoreObjects");
4106  return PluralForm.get(aNumber, str).replace("#1", aNumber);
4107};
4108
4109/**
4110 * Returns a custom class style for a grip.
4111 *
4112 * @param any aGrip
4113 *        @see Variable.setGrip
4114 * @return string
4115 *         The custom class style.
4116 */
4117VariablesView.getClass = function(aGrip) {
4118  if (aGrip && typeof aGrip == "object") {
4119    if (aGrip.preview) {
4120      switch (aGrip.preview.kind) {
4121        case "DOMNode":
4122          return "token-domnode";
4123      }
4124    }
4125
4126    switch (aGrip.type) {
4127      case "undefined":
4128        return "token-undefined";
4129      case "null":
4130        return "token-null";
4131      case "Infinity":
4132      case "-Infinity":
4133      case "NaN":
4134      case "-0":
4135        return "token-number";
4136      case "longString":
4137        return "token-string";
4138    }
4139  }
4140  switch (typeof aGrip) {
4141    case "string":
4142      return "token-string";
4143    case "boolean":
4144      return "token-boolean";
4145    case "number":
4146      return "token-number";
4147    default:
4148      return "token-other";
4149  }
4150};
4151
4152/**
4153 * A monotonically-increasing counter, that guarantees the uniqueness of scope,
4154 * variables and properties ids.
4155 *
4156 * @param string aName
4157 *        An optional string to prefix the id with.
4158 * @return number
4159 *         A unique id.
4160 */
4161var generateId = (function() {
4162  let count = 0;
4163  return function(aName = "") {
4164    return (
4165      aName
4166        .toLowerCase()
4167        .trim()
4168        .replace(/\s+/g, "-") + ++count
4169    );
4170  };
4171})();
4172
4173/**
4174 * Quote and escape a string. The result will be another string containing an
4175 * ECMAScript StringLiteral which will produce the original one when evaluated
4176 * by `eval` or similar.
4177 *
4178 * @param string aString
4179 *       An optional string to be escaped. If no string is passed, the function
4180 *       returns an empty string.
4181 * @return string
4182 */
4183function escapeString(aString) {
4184  if (typeof aString !== "string") {
4185    return "";
4186  }
4187  // U+2028 and U+2029 are allowed in JSON but not in ECMAScript string literals.
4188  return JSON.stringify(aString)
4189    .replace(/\u2028/g, "\\u2028")
4190    .replace(/\u2029/g, "\\u2029");
4191}
4192
4193/**
4194 * Escape some HTML special characters. We do not need full HTML serialization
4195 * here, we just want to make strings safe to display in HTML attributes, for
4196 * the stringifiers.
4197 *
4198 * @param string aString
4199 * @return string
4200 */
4201function escapeHTML(aString) {
4202  return aString
4203    .replace(/&/g, "&amp;")
4204    .replace(/"/g, "&quot;")
4205    .replace(/</g, "&lt;")
4206    .replace(/>/g, "&gt;");
4207}
4208
4209/**
4210 * An Editable encapsulates the UI of an edit box that overlays a label,
4211 * allowing the user to edit the value.
4212 *
4213 * @param Variable aVariable
4214 *        The Variable or Property to make editable.
4215 * @param object aOptions
4216 *        - onSave
4217 *          The callback to call with the value when editing is complete.
4218 *        - onCleanup
4219 *          The callback to call when the editable is removed for any reason.
4220 */
4221function Editable(aVariable, aOptions) {
4222  this._variable = aVariable;
4223  this._onSave = aOptions.onSave;
4224  this._onCleanup = aOptions.onCleanup;
4225}
4226
4227Editable.create = function(aVariable, aOptions, aEvent) {
4228  const editable = new this(aVariable, aOptions);
4229  editable.activate(aEvent);
4230  return editable;
4231};
4232
4233Editable.prototype = {
4234  /**
4235   * The class name for targeting this Editable type's label element. Overridden
4236   * by inheriting classes.
4237   */
4238  className: null,
4239
4240  /**
4241   * Boolean indicating whether this Editable should activate. Overridden by
4242   * inheriting classes.
4243   */
4244  shouldActivate: null,
4245
4246  /**
4247   * The label element for this Editable. Overridden by inheriting classes.
4248   */
4249  label: null,
4250
4251  /**
4252   * Activate this editable by replacing the input box it overlays and
4253   * initialize the handlers.
4254   *
4255   * @param Event e [optional]
4256   *        Optionally, the Event object that was used to activate the Editable.
4257   */
4258  activate: function(e) {
4259    if (!this.shouldActivate) {
4260      this._onCleanup && this._onCleanup();
4261      return;
4262    }
4263
4264    const { label } = this;
4265    const initialString = label.getAttribute("value");
4266
4267    if (e) {
4268      e.preventDefault();
4269      e.stopPropagation();
4270    }
4271
4272    // Create a texbox input element which will be shown in the current
4273    // element's specified label location.
4274    const input = (this._input = this._variable.document.createElementNS(
4275      HTML_NS,
4276      "input"
4277    ));
4278    input.className = this.className;
4279    input.setAttribute("value", initialString);
4280
4281    // Replace the specified label with a textbox input element.
4282    label.parentNode.replaceChild(input, label);
4283    input.scrollIntoView({ block: "nearest" });
4284    input.select();
4285
4286    // When the value is a string (displayed as "value"), then we probably want
4287    // to change it to another string in the textbox, so to avoid typing the ""
4288    // again, tackle with the selection bounds just a bit.
4289    if (initialString.match(/^".+"$/)) {
4290      input.selectionEnd--;
4291      input.selectionStart++;
4292    }
4293
4294    this._onKeydown = this._onKeydown.bind(this);
4295    this._onBlur = this._onBlur.bind(this);
4296    input.addEventListener("keydown", this._onKeydown);
4297    input.addEventListener("blur", this._onBlur);
4298
4299    this._prevExpandable = this._variable.twisty;
4300    this._prevExpanded = this._variable.expanded;
4301    this._variable.collapse();
4302    this._variable.hideArrow();
4303    this._variable.locked = true;
4304    this._variable.editing = true;
4305  },
4306
4307  /**
4308   * Remove the input box and restore the Variable or Property to its previous
4309   * state.
4310   */
4311  deactivate: function() {
4312    this._input.removeEventListener("keydown", this._onKeydown);
4313    this._input.removeEventListener("blur", this.deactivate);
4314    this._input.parentNode.replaceChild(this.label, this._input);
4315    this._input = null;
4316
4317    const scrollbox = this._variable._variablesView._list;
4318    scrollbox.scrollBy(-this._variable._target, 0);
4319    this._variable.locked = false;
4320    this._variable.twisty = this._prevExpandable;
4321    this._variable.expanded = this._prevExpanded;
4322    this._variable.editing = false;
4323    this._onCleanup && this._onCleanup();
4324  },
4325
4326  /**
4327   * Save the current value and deactivate the Editable.
4328   */
4329  _save: function() {
4330    const initial = this.label.getAttribute("value");
4331    const current = this._input.value.trim();
4332    this.deactivate();
4333    if (initial != current) {
4334      this._onSave(current);
4335    }
4336  },
4337
4338  /**
4339   * Called when tab is pressed, allowing subclasses to link different
4340   * behavior to tabbing if desired.
4341   */
4342  _next: function() {
4343    this._save();
4344  },
4345
4346  /**
4347   * Called when escape is pressed, indicating a cancelling of editing without
4348   * saving.
4349   */
4350  _reset: function() {
4351    this.deactivate();
4352    this._variable.focus();
4353  },
4354
4355  /**
4356   * Event handler for when the input loses focus.
4357   */
4358  _onBlur: function() {
4359    this.deactivate();
4360  },
4361
4362  /**
4363   * Event handler for when the input receives a key press.
4364   */
4365  _onKeydown: function(e) {
4366    e.stopPropagation();
4367
4368    switch (e.keyCode) {
4369      case KeyCodes.DOM_VK_TAB:
4370        this._next();
4371        break;
4372      case KeyCodes.DOM_VK_RETURN:
4373        this._save();
4374        break;
4375      case KeyCodes.DOM_VK_ESCAPE:
4376        this._reset();
4377        break;
4378    }
4379  },
4380};
4381
4382/**
4383 * An Editable specific to editing the name of a Variable or Property.
4384 */
4385function EditableName(aVariable, aOptions) {
4386  Editable.call(this, aVariable, aOptions);
4387}
4388
4389EditableName.create = Editable.create;
4390
4391EditableName.prototype = extend(Editable.prototype, {
4392  className: "element-name-input",
4393
4394  get label() {
4395    return this._variable._name;
4396  },
4397
4398  get shouldActivate() {
4399    return !!this._variable.ownerView.switch;
4400  },
4401});
4402
4403/**
4404 * An Editable specific to editing the value of a Variable or Property.
4405 */
4406function EditableValue(aVariable, aOptions) {
4407  Editable.call(this, aVariable, aOptions);
4408}
4409
4410EditableValue.create = Editable.create;
4411
4412EditableValue.prototype = extend(Editable.prototype, {
4413  className: "element-value-input",
4414
4415  get label() {
4416    return this._variable._valueLabel;
4417  },
4418
4419  get shouldActivate() {
4420    return !!this._variable.ownerView.eval;
4421  },
4422});
4423
4424/**
4425 * An Editable specific to editing the key and value of a new property.
4426 */
4427function EditableNameAndValue(aVariable, aOptions) {
4428  EditableName.call(this, aVariable, aOptions);
4429}
4430
4431EditableNameAndValue.create = Editable.create;
4432
4433EditableNameAndValue.prototype = extend(EditableName.prototype, {
4434  _reset: function(e) {
4435    // Hide the Variable or Property if the user presses escape.
4436    this._variable.remove();
4437    this.deactivate();
4438  },
4439
4440  _next: function(e) {
4441    // Override _next so as to set both key and value at the same time.
4442    const key = this._input.value;
4443    this.label.setAttribute("value", key);
4444
4445    const valueEditable = EditableValue.create(this._variable, {
4446      onSave: aValue => {
4447        this._onSave([key, aValue]);
4448      },
4449    });
4450    valueEditable._reset = () => {
4451      this._variable.remove();
4452      valueEditable.deactivate();
4453    };
4454  },
4455
4456  _save: function(e) {
4457    // Both _save and _next activate the value edit box.
4458    this._next(e);
4459  },
4460});
4461