UNPKG

chrome-devtools-frontend

Version:
1,299 lines (1,168 loc) 86.2 kB
// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ /* * Copyright (C) 2008 Apple Inc. All Rights Reserved. * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../../../../core/common/common.js'; import * as Host from '../../../../core/host/host.js'; import * as i18n from '../../../../core/i18n/i18n.js'; import * as Platform from '../../../../core/platform/platform.js'; import * as SDK from '../../../../core/sdk/sdk.js'; import type * as Protocol from '../../../../generated/protocol.js'; import * as TextUtils from '../../../../models/text_utils/text_utils.js'; import * as uiI18n from '../../../../ui/i18n/i18n.js'; import * as Highlighting from '../../../components/highlighting/highlighting.js'; import * as TextEditor from '../../../components/text_editor/text_editor.js'; import {Directives, html, type LitTemplate, nothing, render} from '../../../lit/lit.js'; import * as VisualLogging from '../../../visual_logging/visual_logging.js'; import * as UI from '../../legacy.js'; import type * as Components from '../utils/utils.js'; import {CustomPreviewComponent} from './CustomPreviewComponent.js'; import {JavaScriptREPL} from './JavaScriptREPL.js'; import objectPropertiesSectionStyles from './objectPropertiesSection.css.js'; import objectValueStyles from './objectValue.css.js'; import {RemoteObjectPreviewFormatter, renderNodeTitle} from './RemoteObjectPreviewFormatter.js'; export {objectPropertiesSectionStyles, objectValueStyles}; const {widget} = UI.Widget; const {ref, repeat, ifDefined, classMap} = Directives; const UIStrings = { /** * @description Text in Object Properties Section * @example {function alert() [native code] } PH1 */ exceptionS: '[Exception: {PH1}]', /** * @description Text in Object Properties Section */ unknown: 'unknown', /** * @description Text to expand something recursively */ expandRecursively: 'Expand recursively', /** * @description Text to collapse children of a parent group */ collapseChildren: 'Collapse children', /** * @description Text in Object Properties Section */ noProperties: 'No properties', /** * @description Element text content in Object Properties Section */ dots: '(...)', /** * @description Element title in Object Properties Section */ invokePropertyGetter: 'Invoke property getter', /** * @description Show all text content in Show More Data Grid Node of a data grid * @example {50} PH1 */ showAllD: 'Show all {PH1}', /** * @description Value element text content in Object Properties Section. Shown when the developer is * viewing a variable in the Scope view, whose value is not available (i.e. because it was optimized * out) by the JavaScript engine, or inspecting a JavaScript object accessor property, which has no * getter. This string should be translated. */ valueUnavailable: '<value unavailable>', /** * @description Tooltip for value elements in the Scope view that refer to variables whose values * aren't accessible to the debugger (potentially due to being optimized out by the JavaScript * engine), or for JavaScript object accessor properties which have no getter. */ valueNotAccessibleToTheDebugger: 'Value is not accessible to the debugger', /** * @description A context menu item in the Watch Expressions Sidebar Pane of the Sources panel and Network pane request. */ copyValue: 'Copy value', /** * @description A context menu item in the Object Properties Section */ copyPropertyPath: 'Copy property path', /** * @description Text shown when displaying a JavaScript object that has a string property that is * too large for DevTools to properly display a text editor. This is shown instead of the string in * question. Should be translated. */ stringIsTooLargeToEdit: '<string is too large to edit>', /** * @description Text of attribute value when text is too long * @example {30 MB} PH1 */ showMoreS: 'Show more ({PH1})', /** * @description Text of attribute value when text is too long * @example {30 MB} PH1 */ longTextWasTruncatedS: 'long text was truncated ({PH1})', /** * @description Text for copying */ copy: 'Copy', /** * @description A tooltip text that shows when hovering over a button next to value objects, * which are based on bytes and can be shown in a hexadecimal viewer. * Clicking on the button will display that object in the Memory inspector panel. */ openInMemoryInpector: 'Open in Memory inspector panel', } as const; const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/object_ui/ObjectPropertiesSection.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const EXPANDABLE_MAX_DEPTH = 100; const objectPropertiesSectionMap = new WeakMap<Element, ObjectPropertiesSection>(); interface NodeChildren { properties?: ObjectTreeNode[]; internalProperties?: ObjectTreeNode[]; arrayRanges?: ArrayGroupTreeNode[]; accessors?: ObjectTreeNode[]; } export interface ObjectTreeOptions { readonly propertiesMode: ObjectPropertiesMode; readonly readOnly: boolean; readonly expansionTracker?: ObjectTreeExpansionTracker; } type KeyTypes = { [K in keyof Required<NodeChildren>]: Key<Required<NodeChildren>[K][0]>; }; type Key<T> = T extends ObjectTreeNode ? string : T extends ArrayGroupTreeNode ? ArrayGroupRange : never; interface TrackingKey { type: keyof KeyTypes; key: KeyTypes[keyof KeyTypes]; } class NodeExpansionLog { properties = new Map<string, NodeExpansionLog>(); internalProperties = new Map<string, NodeExpansionLog>(); arrayRanges = new Map<string, NodeExpansionLog>(); accessors = new Map<string, NodeExpansionLog>(); remove(key: TrackingKey): boolean { return this[key.type].delete(NodeExpansionLog.#serializeKey(key)); } get(key: TrackingKey): NodeExpansionLog|undefined { return this[key.type].get(NodeExpansionLog.#serializeKey(key)); } getOrInsert(key: TrackingKey): NodeExpansionLog { const map = this[key.type]; const serializedKey = NodeExpansionLog.#serializeKey(key); const log = map.get(serializedKey) ?? new NodeExpansionLog(); if (!map.has(serializedKey)) { map.set(serializedKey, log); } return log; } clear(type: keyof KeyTypes): void { this[type].clear(); } clearMissing(type: keyof KeyTypes, seen: TrackingKey[]): void { const seenSet = new Set(seen.map(NodeExpansionLog.#serializeKey)); const map = this[type]; for (const key of map.keys()) { if (!seenSet.has(key)) { map.delete(key); } } } get empty(): boolean { return this.properties.size === 0 && this.internalProperties.size === 0 && this.arrayRanges.size === 0 && this.accessors.size === 0; } static #serializeKey(key: TrackingKey): string { if (typeof key.key === 'string') { return `${key.type}:${key.key}`; } return `${key.type}:${key.key.fromIndex}-${key.key.toIndex}`; } } export class ObjectTreeExpansionTracker { #log: NodeExpansionLog|null = null; /** * For nodes physically nested within a parent's [[Prototype]] internal property, the node's * parent property points to the logical parent (skipping the [[Prototype]] node). This helper * finds that skipped [[Prototype]] node if it contains the given node. */ static #protoParent(node: ObjectTreeNodeBase): ObjectTreeNode|undefined { if (!(node instanceof ObjectTreeNode)) { return undefined; } return node.parent?.children?.internalProperties?.find( p => p.name === '[[Prototype]]' && p.children?.properties?.includes(node)); } static #keyType(node: ObjectTreeNodeBase): keyof NodeChildren|null { if (node instanceof ObjectTreeNode) { if (node.parent?.children?.properties?.includes(node)) { return 'properties'; } if (node.parent?.children?.internalProperties?.includes(node)) { return 'internalProperties'; } if (node.parent?.children?.accessors?.includes(node)) { return 'accessors'; } const proto = ObjectTreeExpansionTracker.#protoParent(node); if (proto) { return 'properties'; } } if (node instanceof ArrayGroupTreeNode && node.parent?.children?.arrayRanges?.includes(node)) { return 'arrayRanges'; } return null; } static #key(type: keyof KeyTypes, node: ObjectTreeNodeBase): TrackingKey|null { switch (type) { case 'arrayRanges': return node instanceof ArrayGroupTreeNode ? {type, key: node.range} : null; default: return node instanceof ObjectTreeNode ? {type, key: node.name} : null; } } clear(): void { this.#log = null; } async #apply(log: NodeExpansionLog, node: ObjectTreeNodeBase): Promise<void> { const apply = async<KeyType extends keyof KeyTypes>(type: KeyType): Promise<void> => { const nodes = node.children?.[type]; if (!nodes) { log.clear(type); return; } const seen: TrackingKey[] = []; for (const childNode of nodes) { const key = ObjectTreeExpansionTracker.#key(type, childNode); if (!key) { continue; } const childLog = log.get(key); if (childLog) { await this.#apply(childLog, childNode); seen.push(key); } } log.clearMissing(type, seen); }; node.expanded = true; await node.populateChildrenIfNeeded(); await apply('properties'); await apply('internalProperties'); await apply('arrayRanges'); await apply('accessors'); } async apply(node: ObjectTree): Promise<void> { if (this.#log) { return await this.#apply(this.#log, node); } } static * #path(node: ObjectTreeNodeBase|undefined): Generator<TrackingKey> { if (!node) { return; } const proto = ObjectTreeExpansionTracker.#protoParent(node); if (proto) { yield* this.#path(proto); } else { yield* this.#path(node.parent); } const keyType = this.#keyType(node); const key = keyType && ObjectTreeExpansionTracker.#key(keyType, node); if (key) { yield key; } } collapse(node: ObjectTreeNodeBase): void { if (!this.#log) { return; } if (node instanceof ObjectTree) { this.#log = null; return; } let lastKey; let parent: NodeExpansionLog|null = null; let log: NodeExpansionLog = this.#log; for (const key of ObjectTreeExpansionTracker.#path(node)) { lastKey = key; parent = log; const nextLog = log.get(key); if (!nextLog) { return; } log = nextLog; } if (lastKey && parent) { parent.remove(lastKey); } } expand(node: ObjectTreeNodeBase): void { if (!this.#log) { this.#log = new NodeExpansionLog(); } let log: NodeExpansionLog = this.#log; for (const key of ObjectTreeExpansionTracker.#path(node)) { log = log.getOrInsert(key); } } } export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrapper<ObjectTreeNodeBase.EventTypes> { #children?: NodeChildren; protected filter: {includeNullOrUndefinedValues: boolean, regex: RegExp|null}|null = null; protected extraProperties: ObjectTreeNode[] = []; #expanded = false; constructor(readonly parent: ObjectTreeNodeBase|undefined, protected readonly options: ObjectTreeOptions) { super(); this.filter = parent?.filter ?? null; } get expanded(): boolean { return this.#expanded; } set expanded(val: boolean) { if (val) { this.options.expansionTracker?.expand(this); } else { this.options.expansionTracker?.collapse(this); } this.#expanded = val; } get readOnly(): boolean { return this.options.readOnly; } get propertiesMode(): ObjectPropertiesMode { return this.options.propertiesMode; } get includeNullOrUndefinedValues(): boolean { return this.filter?.includeNullOrUndefinedValues ?? true; } set includeNullOrUndefinedValues(value: boolean) { this.setFilter({includeNullOrUndefinedValues: value, regex: this.filter?.regex ?? null}); } // Performs a pre-order tree traversal over the populated children. If any children need to be populated, callers must // do that while walking (pre-order visitation enables that). * #walk(maxDepth = -1): Generator<ObjectTreeNodeBase> { function* walkChildren(children: ObjectTreeNodeBase[]|undefined): Generator<ObjectTreeNodeBase> { if (children) { for (const child of children) { yield* child.#walk(Math.max(-1, maxDepth - 1)); } } } yield this; if (maxDepth !== 0) { yield* walkChildren(this.#children?.properties); yield* walkChildren(this.#children?.arrayRanges); yield* walkChildren(this.#children?.internalProperties); } } async expandRecursively(maxDepth: number): Promise<void> { for (const node of this.#walk(maxDepth)) { await node.populateChildrenIfNeeded(); node.expanded = true; } } collapseRecursively(): void { for (const node of this.#walk()) { node.expanded = false; } } setFilter(filter: {includeNullOrUndefinedValues: boolean, regex: RegExp|null}|null): void { this.filter = filter; this.dispatchEventToListeners(ObjectTreeNodeBase.Events.FILTER_CHANGED); this.#walk().forEach(c => { c.filter = filter; c.dispatchEventToListeners(ObjectTreeNodeBase.Events.FILTER_CHANGED); }); } abstract get object(): SDK.RemoteObject.RemoteObject|undefined; removeChildren(): void { this.#children = undefined; this.dispatchEventToListeners(ObjectTreeNodeBase.Events.CHILDREN_CHANGED); } removeChild(child: ObjectTreeNodeBase): void { remove(this.#children?.arrayRanges, child); remove(this.#children?.internalProperties, child); remove(this.#children?.properties, child); this.dispatchEventToListeners(ObjectTreeNodeBase.Events.CHILDREN_CHANGED); function remove<T>(array: T[]|undefined, element: T): void { if (!array) { return; } const index = array.indexOf(element); if (index >= 0) { array.splice(index, 1); } } } protected selfOrParentIfInternal(): ObjectTreeNodeBase { return this; } get children(): NodeChildren|undefined { return this.#children; } #populatePromise?: Promise<NodeChildren>; async populateChildrenIfNeeded(): Promise<NodeChildren> { if (this.#children) { return this.#children; } if (!this.#populatePromise) { this.#populatePromise = this.populateChildrenIfNeededImpl() .then(children => { this.#children = children; return children; }) .finally(() => { this.#populatePromise = undefined; }); } return await this.#populatePromise; } protected async populateChildrenIfNeededImpl(): Promise<NodeChildren> { const object = this.object; if (!object) { return {}; } const effectiveParent = this.selfOrParentIfInternal(); if (this.arrayLength > ARRAY_LOAD_THRESHOLD) { const ranges = await arrayRangeGroups(object, 0, this.arrayLength - 1); const arrayRanges = ranges?.ranges.map( ([fromIndex, toIndex, count]) => new ArrayGroupTreeNode(object, {fromIndex, toIndex, count}, effectiveParent, { readOnly: this.readOnly, propertiesMode: this.propertiesMode, expansionTracker: this.options.expansionTracker })); if (!arrayRanges) { return {}; } const {properties: objectProperties, internalProperties: objectInternalProperties} = await SDK.RemoteObject.RemoteObject.loadFromObjectPerProto( this.object, true /* generatePreview */, true /* nonIndexedPropertiesOnly */); const properties = objectProperties?.map(p => new ObjectTreeNode(p, effectiveParent, { readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, expansionTracker: this.options.expansionTracker })); const internalProperties = objectInternalProperties?.map(p => new ObjectTreeNode(p, effectiveParent, { readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, expansionTracker: this.options.expansionTracker })); return {arrayRanges, properties, internalProperties}; } let objectProperties: SDK.RemoteObject.RemoteObjectProperty[]|null = null; let objectInternalProperties: SDK.RemoteObject.RemoteObjectProperty[]|null = null; switch (this.propertiesMode) { case ObjectPropertiesMode.ALL: ({properties: objectProperties} = await object.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */)); break; case ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED: ({properties: objectProperties, internalProperties: objectInternalProperties} = await SDK.RemoteObject.RemoteObject.loadFromObjectPerProto(object, true /* generatePreview */)); break; } const properties = objectProperties?.map(p => new ObjectTreeNode(p, effectiveParent, { readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, expansionTracker: this.options.expansionTracker })); properties?.push(...this.extraProperties); properties?.sort(ObjectPropertiesSection.compareProperties); const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties, this.options); const internalProperties = objectInternalProperties?.map(p => new ObjectTreeNode(p, effectiveParent, { readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, expansionTracker: this.options.expansionTracker })); return {properties, internalProperties, accessors}; } get hasChildren(): boolean { return this.object?.hasChildren ?? false; } get arrayLength(): number { return this.object?.arrayLength() ?? 0; } // This is used in web tests async setPropertyValue(name: string|Protocol.Runtime.CallArgument, value: string): Promise<string|undefined> { return await this.object?.setPropertyValue(name, value); } addExtraProperties(...properties: SDK.RemoteObject.RemoteObjectProperty[]): void { this.extraProperties.push(...properties.map(p => new ObjectTreeNode(p, this, { readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, expansionTracker: this.options.expansionTracker }))); } static getGettersAndSetters(properties: ObjectTreeNode[], options: ObjectTreeOptions): ObjectTreeNode[] { const gettersAndSetters = []; for (const property of properties) { if (property.property.isOwn) { if (property.property.getter) { const getterProperty = new SDK.RemoteObject.RemoteObjectProperty( 'get ' + property.property.name, property.property.getter, false); gettersAndSetters.push(new ObjectTreeNode(getterProperty, property.parent, { propertiesMode: property.propertiesMode, readOnly: property.readOnly, expansionTracker: options.expansionTracker })); } if (property.property.setter) { const setterProperty = new SDK.RemoteObject.RemoteObjectProperty( 'set ' + property.property.name, property.property.setter, false); gettersAndSetters.push(new ObjectTreeNode(setterProperty, property.parent, { propertiesMode: property.propertiesMode, readOnly: property.readOnly, expansionTracker: options.expansionTracker })); } } } return gettersAndSetters; } } export namespace ObjectTreeNodeBase { export const enum Events { VALUE_CHANGED = 'value-changed', CHILDREN_CHANGED = 'children-changed', FILTER_CHANGED = 'filter-changed', } export interface EventTypes { [Events.VALUE_CHANGED]: void; [Events.CHILDREN_CHANGED]: void; [Events.FILTER_CHANGED]: void; } } export class ObjectTree extends ObjectTreeNodeBase { readonly #object: SDK.RemoteObject.RemoteObject; constructor(object: SDK.RemoteObject.RemoteObject, options: ObjectTreeOptions) { super(undefined, options); this.#object = object; } override get object(): SDK.RemoteObject.RemoteObject { return this.#object; } } interface ArrayGroupRange { fromIndex: number; toIndex: number; count: number; } export class ArrayGroupTreeNode extends ObjectTreeNodeBase { readonly #object: SDK.RemoteObject.RemoteObject; readonly #range: ArrayGroupRange; constructor( object: SDK.RemoteObject.RemoteObject, range: ArrayGroupRange, parent: ObjectTreeNodeBase, options: ObjectTreeOptions) { super(parent, options); this.#object = object; this.#range = range; } override async populateChildrenIfNeededImpl(): Promise<NodeChildren> { if (this.#range.count > ArrayGroupingTreeElement.bucketThreshold) { const ranges = await arrayRangeGroups(this.object, this.#range.fromIndex, this.#range.toIndex); const arrayRanges = ranges?.ranges.map( ([fromIndex, toIndex, count]) => new ArrayGroupTreeNode(this.object, {fromIndex, toIndex, count}, this, { readOnly: this.readOnly, propertiesMode: this.propertiesMode, expansionTracker: this.options.expansionTracker })); return {arrayRanges}; } const result = await this.#object.callFunction(buildArrayFragment, [ {value: this.#range.fromIndex}, {value: this.#range.toIndex}, {value: ArrayGroupingTreeElement.sparseIterationThreshold}, ]); if (!result.object || result.wasThrown) { return {}; } const arrayFragment = result.object; const allProperties = await arrayFragment.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */); arrayFragment.release(); const properties = allProperties.properties?.map(p => new ObjectTreeNode(p, this, { propertiesMode: this.propertiesMode, readOnly: this.readOnly, expansionTracker: this.options.expansionTracker })); properties?.push(...this.extraProperties); properties?.sort(ObjectPropertiesSection.compareProperties); const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties, this.options); return {properties, accessors}; } get singular(): boolean { return this.#range.fromIndex === this.#range.toIndex; } get range(): ArrayGroupRange { return this.#range; } override get object(): SDK.RemoteObject.RemoteObject { return this.#object; } } export class ObjectTreeNode extends ObjectTreeNodeBase { #path?: string; constructor( readonly property: SDK.RemoteObject.RemoteObjectProperty, parent: ObjectTreeNodeBase|undefined, options: ObjectTreeOptions, readonly nonSyntheticParent?: SDK.RemoteObject.RemoteObject, ) { super(parent, options); } override get object(): SDK.RemoteObject.RemoteObject|undefined { return this.property.value; } get isFiltered(): boolean { return Boolean(this.filter && !this.property.match(this.filter)); } get name(): string { return this.property.name; } get path(): string { if (!this.#path) { if (this.property.synthetic) { this.#path = this.name; return this.name; } // https://tc39.es/ecma262/#prod-IdentifierName const useDotNotation = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; const isInteger = /^(?:0|[1-9]\d*)$/; const parentPath = (this.parent instanceof ObjectTreeNode && !this.parent.property.synthetic) ? this.parent.path : ''; if (this.property.private || useDotNotation.test(this.name)) { this.#path = parentPath ? `${parentPath}.${this.name}` : this.name; } else if (isInteger.test(this.name)) { this.#path = `${parentPath}[${this.name}]`; } else { this.#path = `${parentPath}[${JSON.stringify(this.name)}]`; } } return this.#path; } override selfOrParentIfInternal(): ObjectTreeNodeBase { return this.name === '[[Prototype]]' ? (this.parent ?? this) : this; } async setValue(expression: string): Promise<void> { const property = SDK.RemoteObject.RemoteObject.toCallArgument(this.property.symbol || this.name); expression = JavaScriptREPL.wrapObjectLiteral(expression.trim()); if (this.property.synthetic) { let invalidate = false; if (expression) { invalidate = await this.property.setSyntheticValue(expression); } if (invalidate) { this.parent?.removeChildren(); } else { this.dispatchEventToListeners(ObjectTreeNodeBase.Events.VALUE_CHANGED); } return; } const parentObject = this.parent?.object as SDK.RemoteObject.RemoteObject; const errorPromise = expression ? parentObject.setPropertyValue(property, expression) : parentObject.deleteProperty(property); const error = await errorPromise; if (error) { this.dispatchEventToListeners(ObjectTreeNodeBase.Events.VALUE_CHANGED); return; } if (!expression) { this.parent?.removeChild(this); } else { this.parent?.removeChildren(); } } async invokeGetter(getter: SDK.RemoteObject.RemoteObject): Promise<void> { const invokeGetter = ` function invokeGetter(getter) { return Reflect.apply(getter, this, []); }`; // Also passing a string instead of a Function to avoid coverage implementation messing with it. const result = await this.parent ?.object // @ts-expect-error No way to teach TypeScript to preserve the Function-ness of `getter`. ?.callFunction(invokeGetter, [SDK.RemoteObject.RemoteObject.toCallArgument(getter)]); if (!result?.object) { return; } this.property.value = result.object; this.property.wasThrown = result.wasThrown || false; this.dispatchEventToListeners(ObjectTreeNodeBase.Events.VALUE_CHANGED); } } export const getObjectPropertiesSectionFrom = (element: Element): ObjectPropertiesSection|undefined => { return objectPropertiesSectionMap.get(element); }; export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow { readonly root: ObjectTree; readonly #objectTreeElement: RootElement; titleElement: Element; skipProtoInternal?: boolean; constructor( object: SDK.RemoteObject.RemoteObject, title?: string|Element|null, linkifier?: Components.Linkifier.Linkifier, showOverflow?: boolean, editable = true) { super(); this.root = new ObjectTree(object, { readOnly: !editable, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED, }); if (!showOverflow) { this.setHideOverflow(true); } this.setFocusable(true); this.setShowSelectionOnKeyboardFocus(true); this.#objectTreeElement = new RootElement(this.root, linkifier); this.appendChild(this.#objectTreeElement); if (typeof title === 'string' || !title) { this.titleElement = this.element.createChild('span'); this.titleElement.textContent = title || ''; } else { this.titleElement = title; this.element.appendChild(title); } if (this.titleElement instanceof HTMLElement && !this.titleElement.hasAttribute('tabIndex')) { this.titleElement.tabIndex = -1; } objectPropertiesSectionMap.set(this.element, this); this.registerRequiredCSS(objectValueStyles, objectPropertiesSectionStyles); this.rootElement().childrenListElement.classList.add('source-code', 'object-properties-section'); } static defaultObjectPresentation( object: SDK.RemoteObject.RemoteObject, linkifier?: Components.Linkifier.Linkifier, skipProto?: boolean, readOnly?: boolean): Element { const objectPropertiesSection = ObjectPropertiesSection.defaultObjectPropertiesSection(object, linkifier, skipProto, readOnly); if (!object.hasChildren) { return objectPropertiesSection.titleElement; } return objectPropertiesSection.element; } static defaultObjectPropertiesSection( object: SDK.RemoteObject.RemoteObject, linkifier?: Components.Linkifier.Linkifier, skipProto?: boolean, readOnly?: boolean): ObjectPropertiesSection { const titleElement = document.createElement('span'); titleElement.classList.add('source-code'); const shadowRoot = UI.UIUtils.createShadowRootWithCoreStyles(titleElement, {cssFile: objectValueStyles}); const propertyValue = ObjectPropertiesSection.createPropertyValue(object, /* wasThrown */ false, /* showPreview */ true); shadowRoot.appendChild(propertyValue); const objectPropertiesSection = new ObjectPropertiesSection(object, titleElement, linkifier, undefined, !readOnly); if (skipProto) { objectPropertiesSection.skipProto(); } return objectPropertiesSection; } // The RemoteObjectProperty overload is kept for web test compatibility for now. static compareProperties( propertyA: ObjectTreeNode|SDK.RemoteObject.RemoteObjectProperty, propertyB: ObjectTreeNode|SDK.RemoteObject.RemoteObjectProperty): number { if (propertyA instanceof ObjectTreeNode) { propertyA = propertyA.property; } if (propertyB instanceof ObjectTreeNode) { propertyB = propertyB.property; } if (!propertyA.synthetic && propertyB.synthetic) { return 1; } if (!propertyB.synthetic && propertyA.synthetic) { return -1; } if (!propertyA.isOwn && propertyB.isOwn) { return 1; } if (!propertyB.isOwn && propertyA.isOwn) { return -1; } if (!propertyA.enumerable && propertyB.enumerable) { return 1; } if (!propertyB.enumerable && propertyA.enumerable) { return -1; } if (propertyA.symbol && !propertyB.symbol) { return 1; } if (propertyB.symbol && !propertyA.symbol) { return -1; } if (propertyA.private && !propertyB.private) { return 1; } if (propertyB.private && !propertyA.private) { return -1; } const a = propertyA.name; const b = propertyB.name; if (a.startsWith('_') && !b.startsWith('_')) { return 1; } if (b.startsWith('_') && !a.startsWith('_')) { return -1; } return Platform.StringUtilities.naturalOrderComparator(a, b); } static createNameElement(name: string|null, isPrivate?: boolean): Element { const element = document.createElement('span'); element.classList.add('name'); if (name === null) { return element; } if (/^\s|\s$|^$|\n/.test(name)) { element.textContent = `"${name.replace(/\n/g, '\u21B5')}"`; return element; } if (isPrivate) { const privatePropertyHash = document.createElement('span'); privatePropertyHash.classList.add('private-property-hash'); privatePropertyHash.textContent = name[0]; element.appendChild(privatePropertyHash); element.appendChild(document.createTextNode(name.substring(1))); return element; } element.textContent = name; return element; } static valueElementForFunctionDescription( description?: string, includePreview?: boolean, defaultName?: string, className?: string): LitTemplate { const contents = (description: string, defaultName: string): {prefix: string, abbreviation: string, body: string} => { const text = description.replace(/^function [gs]et /, 'function ') .replace(/^function [gs]et\(/, 'function\(') .replace(/^[gs]et /, ''); // This set of best-effort regular expressions captures common function descriptions. // Ideally, some parser would provide prefix, arguments, function body text separately. const asyncMatch = text.match(/^(async\s+function)/); const isGenerator = text.startsWith('function*'); const isGeneratorShorthand = text.startsWith('*'); const isBasic = !isGenerator && text.startsWith('function'); const isClass = text.startsWith('class ') || text.startsWith('class{'); const firstArrowIndex = text.indexOf('=>'); const isArrow = !asyncMatch && !isGenerator && !isBasic && !isClass && firstArrowIndex > 0; if (isClass) { const body = text.substring('class'.length); const classNameMatch = /^[^{\s]+/.exec(body.trim()); let className: string = defaultName; if (classNameMatch) { className = classNameMatch[0].trim() || defaultName; } return {prefix: 'class', body, abbreviation: className}; } if (asyncMatch) { const body = text.substring(asyncMatch[1].length); return {prefix: 'async \u0192', body, abbreviation: nameAndArguments(body)}; } if (isGenerator) { const body = text.substring('function*'.length); return {prefix: '\u0192*', body, abbreviation: nameAndArguments(body)}; } if (isGeneratorShorthand) { const body = text.substring('*'.length); return {prefix: '\u0192*', body, abbreviation: nameAndArguments(body)}; } if (isBasic) { const body = text.substring('function'.length); return {prefix: '\u0192', body, abbreviation: nameAndArguments(body)}; } if (isArrow) { const maxArrowFunctionCharacterLength = 60; let abbreviation: string = text; if (defaultName) { abbreviation = defaultName + '()'; } else if (text.length > maxArrowFunctionCharacterLength) { abbreviation = text.substring(0, firstArrowIndex + 2) + ' {…}'; } return {prefix: '', body: text, abbreviation}; } return {prefix: '\u0192', body: text, abbreviation: nameAndArguments(text)}; }; const {prefix, body, abbreviation} = contents(description ?? '', defaultName ?? ''); const maxFunctionBodyLength = 200; return html`<span class="object-value-function ${className ?? ''}" title=${Platform.StringUtilities.trimEndWithMaxLength(description ?? '', 500)}>${ prefix && html`<span class=object-value-function-prefix>${prefix} </span>`}${ includePreview ? Platform.StringUtilities.trimEndWithMaxLength(body.trim(), maxFunctionBodyLength) : abbreviation.replace(/\n/g, ' ')}</span>`; function nameAndArguments(contents: string): string { const startOfArgumentsIndex = contents.indexOf('('); const endOfArgumentsMatch = contents.match(/\)\s*{/); if (startOfArgumentsIndex !== -1 && endOfArgumentsMatch?.index !== undefined && endOfArgumentsMatch.index > startOfArgumentsIndex) { const name = contents.substring(0, startOfArgumentsIndex).trim() || (defaultName ?? ''); const args = contents.substring(startOfArgumentsIndex, endOfArgumentsMatch.index + 1); return name + args; } return defaultName + '()'; } } static createPropertyValueWithCustomSupport( value: SDK.RemoteObject.RemoteObject, wasThrown: boolean, showPreview: boolean, linkifier?: Components.Linkifier.Linkifier, isSyntheticProperty?: boolean, variableName?: string, includeNullOrUndefined?: boolean): HTMLElement { if (value.customPreview()) { const result = (new CustomPreviewComponent(value)).element; result.classList.add('object-properties-section-custom-section'); return result; } return ObjectPropertiesSection.createPropertyValue( value, wasThrown, showPreview, linkifier, isSyntheticProperty, variableName, includeNullOrUndefined); } static getMemoryIcon(object: SDK.RemoteObject.RemoteObject, expression?: string): LitTemplate { // Directly set styles on memory icon, so that the memory icon is also // styled within the context of code mirror. // clang-format off return !object.isLinearMemoryInspectable() ? nothing : html`<devtools-icon name=memory style="width: var(--sys-size-8); height: 13px; vertical-align: sub; cursor: pointer;" @click=${(event: Event) => { event.consume(); void Common.Revealer.reveal(new SDK.RemoteObject.LinearMemoryInspectable(object, expression)); }} jslog=${VisualLogging.action('open-memory-inspector').track({click: true})} title=${i18nString(UIStrings.openInMemoryInpector)} aria-label=${i18nString(UIStrings.openInMemoryInpector)}></devtools-icon>`; // clang-format on } static appendMemoryIcon(element: Element, object: SDK.RemoteObject.RemoteObject, expression?: string): void { const fragment = document.createDocumentFragment(); // eslint-disable-next-line @devtools/no-lit-render-outside-of-view render(ObjectPropertiesSection.getMemoryIcon(object, expression), fragment); element.appendChild(fragment); } static createPropertyValue( value: SDK.RemoteObject.RemoteObject, wasThrown: boolean, showPreview: boolean, linkifier?: Components.Linkifier.Linkifier, isSyntheticProperty = false, variableName?: string, includeNullOrUndefined?: boolean): HTMLElement { const propertyValue = document.createDocumentFragment(); const type = value.type; const subtype = value.subtype; const description = value.description || ''; const className = value.className; const contents = (): LitTemplate => { if (type === 'object' && subtype === 'internal#location') { const rawLocation = value.debuggerModel().createRawLocationByScriptId( value.value.scriptId, value.value.lineNumber, value.value.columnNumber); if (rawLocation && linkifier) { return html`${linkifier.linkifyRawLocation(rawLocation, Platform.DevToolsPath.EmptyUrlString, 'value')}`; } return html`<span class=value title=${description}>${'<' + i18nString(UIStrings.unknown) + '>'}</span>`; } if (type === 'string' && typeof description === 'string') { const text = JSON.stringify(description); const tooLong = description.length > maxRenderableStringLength; return html`<span class="value object-value-string" title=${ifDefined(tooLong ? undefined : description)}>${ tooLong ? widget(ExpandableTextPropertyValue, {text}) : text}</span>`; } if (type === 'object' && subtype === 'trustedtype') { const text = `${className} '${description}'`; const tooLong = text.length > maxRenderableStringLength; return html`<span class="value object-value-trustedtype" title=${ifDefined(tooLong ? undefined : text)}>${ tooLong ? widget(ExpandableTextPropertyValue, {text}) : html`${className} <span class=object-value-string title=${description}>${ JSON.stringify(description)}</span>`}</span>`; } if (type === 'function') { return ObjectPropertiesSection.valueElementForFunctionDescription(description, undefined, undefined, 'value'); } if (type === 'object' && subtype === 'node' && description) { return html`<span class="value object-value-node" @click=${(event: Event) => { void Common.Revealer.reveal(value); event.consume(true); }} @mousemove=${() => SDK.OverlayModel.OverlayModel.highlightObjectAsDOMNode(value)} @mouseleave=${() => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight()} >${renderNodeTitle(description)}</span>`; } if (description.length > maxRenderableStringLength) { // clang-format off return html`<span class="value object-value-${subtype || type}" title=${description}> ${widget(ExpandableTextPropertyValue, {text: description})} </span>`; // clang-format on } const hasPreview = value.preview && showPreview; return html`<span class="value object-value-${subtype || type}" title=${description}>${ hasPreview ? new RemoteObjectPreviewFormatter().renderObjectPreview(value.preview, includeNullOrUndefined) : description}${isSyntheticProperty ? nothing : this.getMemoryIcon(value, variableName)}</span>`; }; if (wasThrown) { // eslint-disable-next-line @devtools/no-lit-render-outside-of-view render( html`<span class="error value">${ uiI18n.getFormatLocalizedStringTemplate(str_, UIStrings.exceptionS, {PH1: contents()})}</span>`, propertyValue); } else { // eslint-disable-next-line @devtools/no-lit-render-outside-of-view render(contents(), propertyValue); } const child = propertyValue.firstElementChild; if (!(child instanceof HTMLElement)) { throw new Error('Expected an HTML element'); } return child; } static formatObjectAsFunction( func: SDK.RemoteObject.RemoteObject, element: Element, linkify: boolean, includePreview?: boolean): Promise<void> { return func.debuggerModel().functionDetailsPromise(func).then(didGetDetails); function didGetDetails(response: SDK.DebuggerModel.FunctionDetails|null): void { if (linkify && response?.location) { element.classList.add('linkified'); element.addEventListener('click', () => { void Common.Revealer.reveal(response.location); return false; }); } // The includePreview flag is false for formats such as console.dir(). let defaultName: string|('' | 'anonymous') = includePreview ? '' : 'anonymous'; if (response?.functionName) { defaultName = response.functionName; } const valueElement = document.createDocumentFragment(); // eslint-disable-next-line @devtools/no-lit-render-outside-of-view render( ObjectPropertiesSection.valueElementForFunctionDescription(func.description, includePreview, defaultName), valueElement); element.appendChild(valueElement); } } static isDisplayableProperty( property: SDK.RemoteObject.RemoteObjectProperty, parentProperty?: SDK.RemoteObject.RemoteObjectProperty): boolean { if (!parentProperty?.synthetic) { return true; } const name = property.name; const useless = (parentProperty.name === '[[Entries]]' && (name === 'length' || name === '__proto__')); return !useless; } skipProto(): void { this.skipProtoInternal = true; } expand(): void { this.#objectTreeElement.expand(); } objectTreeElement(): UI.TreeOutline.TreeElement { return this.#objectTreeElement; } enableContextMenu(): void { this.element.addEventListener('contextmenu', this.contextMenuEventFired.bind(this), false); } private contextMenuEventFired(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendApplicableItems(this.root); if (this.root.object instanceof SDK.RemoteObject.LocalJSONObject) { contextMenu.viewSection().appendItem( i18nString(UIStrings.expandRecursively), this.#objectTreeElement.expandRecursively.bind(this.#objectTreeElement, EXPANDABLE_MAX_DEPTH), {jslogContext: 'expand-recursively'}); contextMenu.viewSection().appendItem( i18nString(UIStrings.collapseChildren), this.#objectTreeElement.collapseChildren.bind(this.#objectTreeElement), {jslogContext: 'collapse-children'}); } void contextMenu.show(); } titleLessMode(): void { this.#objectTreeElement.listItemElement.classList.add('hidden'); this.#objectTreeElement.childrenListElement.classList.add('title-less-mode'); this.#objectTreeElement.expand(); } } /** @constant */ const ARRAY_LOAD_THRESHOLD = 100; const maxRenderableStringLength = 10000; export interface TreeOutlineOptions { readOnly?: boolean; } export class ObjectPropertiesSectionsTreeOutline extends UI.TreeOutline.TreeOutlineInShadow { constructor() { super(); this.registerRequiredCSS(objectValueStyles, objectPropertiesSectionStyles); this.contentElement.classList.add('source-code'); this.contentElement.classList.add('object-properties-section'); } } export const enum ObjectPropertiesMode { ALL = 0, // All properties, including prototype properties OWN_AND_INTERNAL_AND_INHERITED = 1, // Own, internal, and inherited properties } export class RootElement extends UI.TreeOutline.TreeElement { private readonly object: ObjectTree; private readonly linkifier: Components.Linkifier.Linkifier|undefined; private readonly emptyPlaceholder: string|null|undefined; override toggleOnClick: boolean; constructor(object: ObjectTree, linkifier?: Components.Linkifier.Linkifier, emptyPlaceholder?: string|null) { const contentElement = document.createElement('slot'); super(contentElement); this.object = object; this.object.addEventListener(ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.onpopulate, this); this.linkifier = linkifier; this.emptyPlaceholder = emptyPlaceholder; this.setExpandable(true); this.selectable = true; this.toggleOnClick = true; this.listItemElement.classList.add('object-properties-section-root-element'); this.listItemElement.addEventListener('contextmenu', this.onContextMenu.bind(this), false); if (object.expanded) { this.expand(); } } override onexpand(): void { this.object.expanded = true; if (this.treeOutline) { this.treeOutline.element.classList.add('expanded'); } } override oncollapse(): void { this.object.expanded = false; if (this.treeOutline) { this.treeOutline.element.classList.remove('expanded'); } } override ondblclick(_e: Event): boolean { return true; } private onContextMenu(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendApplicableItems(this.object.object); if (this.object instanceof SDK.RemoteObject.LocalJSONObject) { const {value} = this.object; const propertyValue = typeof value === 'object' ? JSON.stringify(value, null, 2) : value; const copyValueHandler = (): void => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelCopyValue); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText((propertyValue as string | undefined)); }; contextMenu.clipboardSection().appendItem( i18nString(UIStrings.copyValue), copyValueHandler, {jslogContext: 'copy-value'}); } contextMenu.viewSection().appendItem( i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, EXPANDABLE_MAX_DEPTH), {jslogContext: 'expand-recursively'}); contextMenu.viewSection().appendItem( i18nString(UIStrings.collapseChildren), t