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