UNPKG

chrome-devtools-frontend

Version:
1,235 lines (1,117 loc) 77.4 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; } 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 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; } async populateChildrenIfNeeded(): Promise<NodeChildren> { if (!this.#children) { this.#children = await this.populateChildrenIfNeededImpl(); } return this.#children; } 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})); 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})); const internalProperties = objectInternalProperties?.map( p => new ObjectTreeNode( p, effectiveParent, {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED})); 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})); properties?.push(...this.extraProperties); properties?.sort(ObjectPropertiesSection.compareProperties); const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties); const internalProperties = objectInternalProperties?.map( p => new ObjectTreeNode( p, effectiveParent, {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED})); 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}))); } static getGettersAndSetters(properties: ObjectTreeNode[]): 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})); } 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})); } } } 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; } } class ArrayGroupTreeNode extends ObjectTreeNodeBase { readonly #object: SDK.RemoteObject.RemoteObject; readonly #range: {fromIndex: number, toIndex: number, count: number}; constructor( object: SDK.RemoteObject.RemoteObject, range: {fromIndex: number, toIndex: number, count: number}, 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})); 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})); properties?.push(...this.extraProperties); properties?.sort(ObjectPropertiesSection.compareProperties); const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties); return {properties, accessors}; } get singular(): boolean { return this.#range.fromIndex === this.#range.toIndex; } get range(): {fromIndex: number, toIndex: number, count: number} { 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); } override onexpand(): void { if (this.treeOutline) { this.treeOutline.element.classList.add('expanded'); } } override oncollapse(): void { 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), this.collapseChildren.bind(this), {jslogContext: 'collapse-children'}); contextMenu.viewSection().appendCheckboxItem(i18n.i18n.lockedString('Show all'), () => { this.object.includeNullOrUndefinedValues = !this.object.includeNullOrUndefinedValues; }, {checked: this.object.includeNullOrUndefinedValues, jslogContext: 'show-all'}); void contextMenu.show(); } override async onpopulate(): Promise<void> { this.removeChildren(); const treeOutline = (this.treeOutline as ObjectPropertiesSection | null); const skipProto = treeOutline ? Boolean(treeOutline.skipProtoInternal) : false; return await ObjectPropertyTreeElement.populate( this, this.object, skipProto, false, this.linkifier, this.emptyPlaceholder); } } /** * Number of initially visible children in an ObjectPropertyTreeElement. * Remaining children are shown as soon as requested via a show more properties button. **/ export const InitialVisibleChildrenLimit = 200; export interface ObjectPropertyViewInput { editable: boolean; startEditing(): unknown; invokeGetter(getter: SDK.RemoteObject.RemoteObject): unknown; onAutoComplete(expression: string, filter: string, force: boolean): unknown; linkifier: Components.Linkifier.Linkifier|undefined; completions: string[]; expanded: boolean; editing: boolean; editingEnded(): unknown; editingCommitted(detail: string): unknown; node: ObjectTreeNode; } interface ObjectPropertyViewOutput { valueElement: Element|undefined; nameElement: Element|undefined; } type ObjectPropertyView = (input: ObjectPropertyViewInput, output: ObjectPropertyViewOutput, target: HTMLElement) => void; export const OBJECT_PROPERTY_DEFAULT_VIEW: ObjectPropertyView = (input, output, target) => { const {property} = input.node; const isInternalEntries = property.synthetic && input.node.name === '[[Entries]]'; const completionsId = `completions-${input.node.parent?.object?.objectId?.replaceAll('.', '-')}-${input.node.name}`; const onAutoComplete = async(e: UI.TextPrompt.TextPromptElement.BeforeAutoCompleteEvent): Promise<void> => { if (!(e.target instanceof UI.TextPrompt.TextPromptElement)) { return; } input.onAutoComplete(e.detail.expression, e.detail.filter, e.detail.force); }; const nameClasses = classMap({ name: true, 'object-properties-section-dimmed': !property.enumerable, 'own-property': property.isOwn, 'synthetic-property': property.synthetic, }); const quotedName = /^\s|\s$|^$|\n/.test(property.name) ? `"${property.name.replace(/\n/g, '\u21B5')}"` : property.name; const isExpandable = !isInternalEntries && property.value && !property.wasThrown && property.value.hasChildren && !property.value.customPreview() && property.value.subtype !== 'node' && property.value.type !== 'function' && (property.value.type !== 'object' || property.value.preview); const value = (): LitTemplate|HTMLElement => { const valueRef = ref(e => { output.valueElement = e; }); if (isInternalEntries) { return html`<span ${valueRef} class=value></span>`; } if (property.value) { const showPreview = property.name !== '[[Prototype]]'; const value = ObjectPropertiesSection.createPropertyValueWithCustomSupport( property.value, property.wasThrown, showPreview, input.linkifier, property.synthetic, input.node.path /* variableName */, input.node.includeNullOrUndefinedValues); output.valueElement = value; return value; } if (property.getter) { const getter = property.getter; const invokeGetter = (event: Event): void => { event.consume(); input.invokeGetter(getter); }; return html`<span ${valueRef}><span class=object-value-calculate-value-button title=${i18nString(UIStrings.invokePropertyGetter)} @click=${invokeGetter} >${i18nString(UIStrings.dots)}</span></span>`; } return html`<span ${valueRef} class=object-value-unavailable title=${i18nString(UIStrings.valueNotAccessibleToTheDebugger)}>${ i18nString(UIStrings.valueUnavailable)}</span>`; }; const onActivate = (event: MouseEvent|KeyboardEvent): void => { if (event instanceof KeyboardEvent && !Platform.KeyboardUtilities.isEnterOrSpaceKey(event)) { return; } event.consume(true); if (input.editable && property.value && !property.value.customPreview() && (property.writable || property.setter)) { input.startEditing(); } }; // clang-format off render( html`<span class=name-and-value><span ${ref(e => { output.nameElement = e; })} class=${nameClasses} title=${input.node.path}>${property.private ? html`<span class="private-property-hash">${property.name[0]}</span>${ property.name.substring(1)}</span>` : quotedName}</span>${ isInternalEntries ? nothing : html`<span class='separator'>: </span><devtools-prompt @commit=${(e: UI.TextPrompt.TextPromptElement.CommitEvent) => input.editingCommitted(e.detail)} @cancel=${() => input.editingEnded()} @beforeautocomplete=${onAutoComplete} @dblclick=${onActivate} @keydown=${onActivate} completions=${completionsId} placeholder=${i18nString(UIStrings.stringIsTooLargeToEdit)} ?editing=${input.editing}> ${input.expanded && isExpandable && property.value ? html`<span class="value object-value-${property.value.subtype || property.value.type}" title=${ifDefined(property.value.description)}>${ property.value.description === 'Object' ? '' : Platform.StringUtilities.trimMiddle(property.value.description ?? '', maxRenderableStringLength)}${ property.synthetic ? nothing : ObjectPropertiesSection.getMemoryIcon(property.value)}</span>` : value() } <datalist id=${completionsId}>${repeat(input.completions, c => html`<option>${c}</option>`)}</datalist> </devtools-prompt></span>`}</span>`, target); // clang-format on }; export class ObjectPropertyWidget extends UI.Widget.Widget { #highlightChanges: Highlighting.HighlightChange[] = []; #property?: ObjectTreeNode; #nameElement?: Element; #valueElement?: Element; #completions: string[] = []; #editing = false; readonly #view: ObjectPropertyView; #expanded = false; #linkifier: Components.Linkifier.Linkifier|undefined; #editable = false; constructor(target?: HTMLElement, view = OBJECT_PROPERTY_DEFAULT_VIEW) { super(target); this.#view = view; } get property(): ObjectTreeNode|undefined { return this.#property; } set property(property: ObjectTreeNode) { if (this.#property) { this.#property.removeEventListener(ObjectTreeNodeBase.Events.VALUE_CHANGED, this.requestUpdate, this); this.#property.removeEventListener(ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.requestUpdate, this); this.#property.removeEventListener(ObjectTreeNodeBase.Events.FILTER_CHANGED, this.requestUpdate, this); } this.#property = property; this.#property.addEventListener(ObjectTreeNodeBase.Events.VALUE_CHANGED, this.requestUpdate, this); this.#property.addEventListener(ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.requestUpdate, this); this.#property.addEventListener(ObjectTreeNodeBase.Events.FILTER_CHANGED, this.requestUpdate, this); this.requestUpdate(); } get expanded(): boolean { return this.#expanded; } set expanded(expanded: boolean) { this.#expanded = expanded; this.requestUpdate(); } get linkifier(): Components.Linkifier.Linkifier|undefined { return this.#linkifier; } set linkifier(linkifier: Components.Linkifier.Linkifier|undefined) { this.#linkifier = linkifier; this.requestUpdate(); } get editable(): boolean { return this.#editable; } set editable(val: boolean) { this.#editable = val; this.requestUpdate(); } override performUpdate(): void { if (!this.#property) {