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