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