UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,422 lines (1,284 loc) 49.5 kB
/* * 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. */ /** * @unrestricted */ ObjectUI.ObjectPropertiesSection = class extends UI.TreeOutlineInShadow { /** * @param {!SDK.RemoteObject} object * @param {?string|!Element=} title * @param {!Components.Linkifier=} linkifier * @param {?string=} emptyPlaceholder * @param {boolean=} ignoreHasOwnProperty * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties */ constructor(object, title, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties) { super(); this._object = object; this._editable = true; this.hideOverflow(); this.setFocusable(false); this._objectTreeElement = new ObjectUI.ObjectPropertiesSection.RootElement( object, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties); 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); } this.element._section = this; this.registerRequiredCSS('object_ui/objectValue.css'); this.registerRequiredCSS('object_ui/objectPropertiesSection.css'); this.rootElement().childrenListElement.classList.add('source-code', 'object-properties-section'); } /** * @param {!SDK.RemoteObject} object * @param {!Components.Linkifier=} linkifier * @param {boolean=} skipProto * @return {!Element} */ static defaultObjectPresentation(object, linkifier, skipProto) { const componentRoot = createElementWithClass('span', 'source-code'); const shadowRoot = UI.createShadowRootWithCoreStyles(componentRoot, 'object_ui/objectValue.css'); shadowRoot.appendChild( ObjectUI.ObjectPropertiesSection.createValueElement(object, false /* wasThrown */, true /* showPreview */)); if (!object.hasChildren) return componentRoot; const objectPropertiesSection = new ObjectUI.ObjectPropertiesSection(object, componentRoot, linkifier); objectPropertiesSection.editable = false; if (skipProto) objectPropertiesSection.skipProto(); return objectPropertiesSection.element; } /** * @param {!SDK.RemoteObjectProperty} propertyA * @param {!SDK.RemoteObjectProperty} propertyB * @return {number} */ static CompareProperties(propertyA, propertyB) { const a = propertyA.name; const b = propertyB.name; if (a === '__proto__') return 1; if (b === '__proto__') return -1; if (!propertyA.enumerable && propertyB.enumerable) return 1; if (!propertyB.enumerable && propertyA.enumerable) return -1; if (a.startsWith('_') && !b.startsWith('_')) return 1; if (b.startsWith('_') && !a.startsWith('_')) return -1; if (propertyA.symbol && !propertyB.symbol) return 1; if (propertyB.symbol && !propertyA.symbol) return -1; return String.naturalOrderComparator(a, b); } /** * @param {?string} name * @return {!Element} */ static createNameElement(name) { const nameElement = createElementWithClass('span', 'name'); if (/^\s|\s$|^$|\n/.test(name)) nameElement.createTextChildren('"', name.replace(/\n/g, '\u21B5'), '"'); else nameElement.textContent = name; return nameElement; } /** * @param {?string=} description * @param {boolean=} includePreview * @param {string=} defaultName * @return {!Element} valueElement */ static valueElementForFunctionDescription(description, includePreview, defaultName) { const valueElement = createElementWithClass('span', 'object-value-function'); description = description || ''; const text = description.replace(/^function [gs]et /, 'function ') .replace(/^function [gs]et\(/, 'function\(') .replace(/^[gs]et /, ''); defaultName = defaultName || ''; // 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; let textAfterPrefix; if (isClass) { textAfterPrefix = text.substring('class'.length); const classNameMatch = /^[^{\s]+/.exec(textAfterPrefix.trim()); let className = defaultName; if (classNameMatch) className = classNameMatch[0].trim() || defaultName; addElements('class', textAfterPrefix, className); } else if (asyncMatch) { textAfterPrefix = text.substring(asyncMatch[1].length); addElements('async \u0192', textAfterPrefix, nameAndArguments(textAfterPrefix)); } else if (isGenerator) { textAfterPrefix = text.substring('function*'.length); addElements('\u0192*', textAfterPrefix, nameAndArguments(textAfterPrefix)); } else if (isGeneratorShorthand) { textAfterPrefix = text.substring('*'.length); addElements('\u0192*', textAfterPrefix, nameAndArguments(textAfterPrefix)); } else if (isBasic) { textAfterPrefix = text.substring('function'.length); addElements('\u0192', textAfterPrefix, nameAndArguments(textAfterPrefix)); } else if (isArrow) { const maxArrowFunctionCharacterLength = 60; let abbreviation = text; if (defaultName) abbreviation = defaultName + '()'; else if (text.length > maxArrowFunctionCharacterLength) abbreviation = text.substring(0, firstArrowIndex + 2) + ' {\u2026}'; addElements('', text, abbreviation); } else { addElements('\u0192', text, nameAndArguments(text)); } valueElement.title = description.trimEnd(500); return valueElement; /** * @param {string} contents * @return {string} */ function nameAndArguments(contents) { const startOfArgumentsIndex = contents.indexOf('('); const endOfArgumentsMatch = contents.match(/\)\s*{/); if (startOfArgumentsIndex !== -1 && endOfArgumentsMatch && endOfArgumentsMatch.index > startOfArgumentsIndex) { const name = contents.substring(0, startOfArgumentsIndex).trim() || defaultName; const args = contents.substring(startOfArgumentsIndex, endOfArgumentsMatch.index + 1); return name + args; } return defaultName + '()'; } /** * @param {string} prefix * @param {string} body * @param {string} abbreviation */ function addElements(prefix, body, abbreviation) { const maxFunctionBodyLength = 200; if (prefix.length) valueElement.createChild('span', 'object-value-function-prefix').textContent = prefix + ' '; if (includePreview) valueElement.createTextChild(body.trim().trimEnd(maxFunctionBodyLength)); else valueElement.createTextChild(abbreviation.replace(/\n/g, ' ')); } } /** * @param {!SDK.RemoteObject} value * @param {boolean} wasThrown * @param {boolean} showPreview * @param {!Element=} parentElement * @param {!Components.Linkifier=} linkifier * @return {!Element} */ static createValueElementWithCustomSupport(value, wasThrown, showPreview, parentElement, linkifier) { if (value.customPreview()) { const result = (new ObjectUI.CustomPreviewComponent(value)).element; result.classList.add('object-properties-section-custom-section'); return result; } return ObjectUI.ObjectPropertiesSection.createValueElement(value, wasThrown, showPreview, parentElement, linkifier); } /** * @param {!SDK.RemoteObject} value * @param {boolean} wasThrown * @param {boolean} showPreview * @param {!Element=} parentElement * @param {!Components.Linkifier=} linkifier * @return {!Element} */ static createValueElement(value, wasThrown, showPreview, parentElement, linkifier) { let valueElement; const type = value.type; const subtype = value.subtype; const description = value.description; if (type === 'object' && subtype === 'internal#location') { const rawLocation = value.debuggerModel().createRawLocationByScriptId( value.value.scriptId, value.value.lineNumber, value.value.columnNumber); if (rawLocation && linkifier) return linkifier.linkifyRawLocation(rawLocation, ''); valueElement = createUnknownInternalLocationElement(); } else if (type === 'string' && typeof description === 'string') { valueElement = createStringElement(); } else if (type === 'function') { valueElement = ObjectUI.ObjectPropertiesSection.valueElementForFunctionDescription(description); } else if (type === 'object' && subtype === 'node' && description) { valueElement = createNodeElement(); } else if (type === 'number' && description && description.indexOf('e') !== -1) { valueElement = createNumberWithExponentElement(); if (parentElement) // FIXME: do it in the caller. parentElement.classList.add('hbox'); } else { valueElement = createElementWithClass('span', 'object-value-' + (subtype || type)); valueElement.title = description || ''; if (value.preview && showPreview) { const previewFormatter = new ObjectUI.RemoteObjectPreviewFormatter(); previewFormatter.appendObjectPreview(valueElement, value.preview, false /* isEntry */); } else if (description.length > ObjectUI.ObjectPropertiesSection._maxRenderableStringLength) { valueElement.appendChild(UI.createExpandableText(description, 50)); } else { valueElement.textContent = description; } } if (wasThrown) { const wrapperElement = createElementWithClass('span', 'error value'); wrapperElement.createTextChild('[' + Common.UIString('Exception') + ': '); wrapperElement.appendChild(valueElement); wrapperElement.createTextChild(']'); return wrapperElement; } valueElement.classList.add('value'); return valueElement; /** * @return {!Element} */ function createUnknownInternalLocationElement() { const valueElement = createElementWithClass('span'); valueElement.textContent = '<' + Common.UIString('unknown') + '>'; valueElement.title = description || ''; return valueElement; } /** * @return {!Element} */ function createStringElement() { const valueElement = createElementWithClass('span', 'object-value-string'); const text = description.replace(/\n/g, '\u21B5'); valueElement.createChild('span', 'object-value-string-quote').textContent = '"'; if (description.length > ObjectUI.ObjectPropertiesSection._maxRenderableStringLength) valueElement.appendChild(UI.createExpandableText(text, 50)); else valueElement.createTextChild(text); valueElement.createChild('span', 'object-value-string-quote').textContent = '"'; valueElement.title = description || ''; return valueElement; } /** * @return {!Element} */ function createNodeElement() { const valueElement = createElementWithClass('span', 'object-value-node'); ObjectUI.RemoteObjectPreviewFormatter.createSpansForNodeTitle(valueElement, /** @type {string} */ (description)); valueElement.addEventListener('click', event => { Common.Revealer.reveal(value); event.consume(true); }, false); valueElement.addEventListener('mousemove', () => SDK.OverlayModel.highlightObjectAsDOMNode(value), false); valueElement.addEventListener('mouseleave', () => SDK.OverlayModel.hideDOMNodeHighlight(), false); return valueElement; } /** * @return {!Element} */ function createNumberWithExponentElement() { const valueElement = createElementWithClass('span', 'object-value-number'); const numberParts = description.split('e'); valueElement.createChild('span', 'object-value-scientific-notation-mantissa').textContent = numberParts[0]; valueElement.createChild('span', 'object-value-scientific-notation-exponent').textContent = 'e' + numberParts[1]; valueElement.classList.add('object-value-scientific-notation-number'); valueElement.title = description || ''; return valueElement; } } /** * @param {!SDK.RemoteObject} func * @param {!Element} element * @param {boolean} linkify * @param {boolean=} includePreview */ static formatObjectAsFunction(func, element, linkify, includePreview) { func.debuggerModel().functionDetailsPromise(func).then(didGetDetails); /** * @param {?SDK.DebuggerModel.FunctionDetails} response */ function didGetDetails(response) { if (linkify && response && response.location) { element.classList.add('linkified'); element.addEventListener('click', () => Common.Revealer.reveal(response.location) && false); } // The includePreview flag is false for formats such as console.dir(). let defaultName = includePreview ? '' : 'anonymous'; if (response && response.functionName) defaultName = response.functionName; const valueElement = ObjectUI.ObjectPropertiesSection.valueElementForFunctionDescription( func.description, includePreview, defaultName); element.appendChild(valueElement); } } skipProto() { this._skipProto = true; } expand() { this._objectTreeElement.expand(); } /** * @param {boolean} value */ setEditable(value) { this._editable = value; } /** * @return {!UI.TreeElement} */ objectTreeElement() { return this._objectTreeElement; } enableContextMenu() { this.element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); } _contextMenuEventFired(event) { const contextMenu = new UI.ContextMenu(event); contextMenu.appendApplicableItems(this._object); if (this._object instanceof SDK.LocalJSONObject) { contextMenu.viewSection().appendItem( ls`Expand recursively`, this._objectTreeElement.expandRecursively.bind(this._objectTreeElement, Number.MAX_VALUE)); contextMenu.viewSection().appendItem( ls`Collapse children`, this._objectTreeElement.collapseChildren.bind(this._objectTreeElement)); } contextMenu.show(); } titleLessMode() { this._objectTreeElement.listItemElement.classList.add('hidden'); this._objectTreeElement.childrenListElement.classList.add('title-less-mode'); this._objectTreeElement.expand(); } }; /** @const */ ObjectUI.ObjectPropertiesSection._arrayLoadThreshold = 100; /** @const */ ObjectUI.ObjectPropertiesSection._maxRenderableStringLength = 10000; /** * @unrestricted */ ObjectUI.ObjectPropertiesSection.RootElement = class extends UI.TreeElement { /** * @param {!SDK.RemoteObject} object * @param {!Components.Linkifier=} linkifier * @param {?string=} emptyPlaceholder * @param {boolean=} ignoreHasOwnProperty * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties */ constructor(object, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties) { const contentElement = createElement('slot'); super(contentElement); this._object = object; this._extraProperties = extraProperties || []; this._ignoreHasOwnProperty = !!ignoreHasOwnProperty; this._emptyPlaceholder = emptyPlaceholder; this.setExpandable(true); this.selectable = false; this.toggleOnClick = true; this.listItemElement.classList.add('object-properties-section-root-element'); this._linkifier = linkifier; } /** * @override */ onexpand() { if (this.treeOutline) this.treeOutline.element.classList.add('expanded'); } /** * @override */ oncollapse() { if (this.treeOutline) this.treeOutline.element.classList.remove('expanded'); } /** * @override * @param {!Event} e * @return {boolean} */ ondblclick(e) { return true; } /** * @override */ onpopulate() { ObjectUI.ObjectPropertyTreeElement._populate( this, this._object, !!this.treeOutline._skipProto, this._linkifier, this._emptyPlaceholder, this._ignoreHasOwnProperty, this._extraProperties); } }; /** * @unrestricted */ ObjectUI.ObjectPropertyTreeElement = class extends UI.TreeElement { /** * @param {!SDK.RemoteObjectProperty} property * @param {!Components.Linkifier=} linkifier */ constructor(property, linkifier) { // Pass an empty title, the title gets made later in onattach. super(); this.property = property; this.toggleOnClick = true; this.selectable = false; /** @type {!Array.<!Object>} */ this._highlightChanges = []; this._linkifier = linkifier; this.listItemElement.addEventListener('contextmenu', this._contextMenuFired.bind(this), false); } /** * @param {!UI.TreeElement} treeElement * @param {!SDK.RemoteObject} value * @param {boolean} skipProto * @param {!Components.Linkifier=} linkifier * @param {?string=} emptyPlaceholder * @param {boolean=} flattenProtoChain * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties * @param {!SDK.RemoteObject=} targetValue */ static _populate( treeElement, value, skipProto, linkifier, emptyPlaceholder, flattenProtoChain, extraProperties, targetValue) { if (value.arrayLength() > ObjectUI.ObjectPropertiesSection._arrayLoadThreshold) { treeElement.removeChildren(); ObjectUI.ArrayGroupingTreeElement._populateArray(treeElement, value, 0, value.arrayLength() - 1, linkifier); return; } /** * @param {?Array.<!SDK.RemoteObjectProperty>} properties * @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties */ function callback(properties, internalProperties) { treeElement.removeChildren(); if (!properties) return; extraProperties = extraProperties || []; for (let i = 0; i < extraProperties.length; ++i) properties.push(extraProperties[i]); ObjectUI.ObjectPropertyTreeElement.populateWithProperties( treeElement, properties, internalProperties, skipProto, targetValue || value, linkifier, emptyPlaceholder); } if (flattenProtoChain) value.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */, callback); else SDK.RemoteObject.loadFromObjectPerProto(value, true /* generatePreview */, callback); } /** * @param {!UI.TreeElement} treeNode * @param {!Array.<!SDK.RemoteObjectProperty>} properties * @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties * @param {boolean} skipProto * @param {?SDK.RemoteObject} value * @param {!Components.Linkifier=} linkifier * @param {?string=} emptyPlaceholder */ static populateWithProperties( treeNode, properties, internalProperties, skipProto, value, linkifier, emptyPlaceholder) { properties.sort(ObjectUI.ObjectPropertiesSection.CompareProperties); const tailProperties = []; let protoProperty = null; for (let i = 0; i < properties.length; ++i) { const property = properties[i]; property.parentObject = value; if (property.name === '__proto__' && !property.isAccessorProperty()) { protoProperty = property; continue; } if (property.isOwn && property.getter) { const getterProperty = new SDK.RemoteObjectProperty('get ' + property.name, property.getter, false); getterProperty.parentObject = value; tailProperties.push(getterProperty); } if (property.isOwn && property.setter) { const setterProperty = new SDK.RemoteObjectProperty('set ' + property.name, property.setter, false); setterProperty.parentObject = value; tailProperties.push(setterProperty); } const canShowProperty = property.getter || !property.isAccessorProperty(); if (canShowProperty && property.name !== '__proto__') treeNode.appendChild(new ObjectUI.ObjectPropertyTreeElement(property, linkifier)); } for (let i = 0; i < tailProperties.length; ++i) treeNode.appendChild(new ObjectUI.ObjectPropertyTreeElement(tailProperties[i], linkifier)); if (!skipProto && protoProperty) treeNode.appendChild(new ObjectUI.ObjectPropertyTreeElement(protoProperty, linkifier)); if (internalProperties) { for (let i = 0; i < internalProperties.length; i++) { internalProperties[i].parentObject = value; const treeElement = new ObjectUI.ObjectPropertyTreeElement(internalProperties[i], linkifier); if (internalProperties[i].name === '[[Entries]]') { treeElement.setExpandable(true); treeElement.expand(); } treeNode.appendChild(treeElement); } } ObjectUI.ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder); } /** * @param {!UI.TreeElement} treeNode * @param {?string=} emptyPlaceholder */ static _appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder) { if (treeNode.childCount()) return; const title = createElementWithClass('div', 'gray-info-message'); title.textContent = emptyPlaceholder || Common.UIString('No properties'); const infoElement = new UI.TreeElement(title); treeNode.appendChild(infoElement); } /** * @param {?SDK.RemoteObject} object * @param {!Array.<string>} propertyPath * @param {function(?SDK.RemoteObject, boolean=)} callback * @return {!Element} */ static createRemoteObjectAccessorPropertySpan(object, propertyPath, callback) { const rootElement = createElement('span'); const element = rootElement.createChild('span'); element.textContent = Common.UIString('(...)'); if (!object) return rootElement; element.classList.add('object-value-calculate-value-button'); element.title = Common.UIString('Invoke property getter'); element.addEventListener('click', onInvokeGetterClick, false); function onInvokeGetterClick(event) { event.consume(); object.getProperty(propertyPath, callback); } return rootElement; } /** * @param {!RegExp} regex * @param {string=} additionalCssClassName * @return {boolean} */ setSearchRegex(regex, additionalCssClassName) { let cssClasses = UI.highlightedSearchResultClassName; if (additionalCssClassName) cssClasses += ' ' + additionalCssClassName; this.revertHighlightChanges(); this._applySearch(regex, this.nameElement, cssClasses); const valueType = this.property.value.type; if (valueType !== 'object') this._applySearch(regex, this.valueElement, cssClasses); return !!this._highlightChanges.length; } /** * @param {!RegExp} regex * @param {!Element} element * @param {string} cssClassName */ _applySearch(regex, element, cssClassName) { const ranges = []; const content = element.textContent; regex.lastIndex = 0; let match = regex.exec(content); while (match) { ranges.push(new TextUtils.SourceRange(match.index, match[0].length)); match = regex.exec(content); } if (ranges.length) UI.highlightRangesWithStyleClass(element, ranges, cssClassName, this._highlightChanges); } revertHighlightChanges() { UI.revertDomChanges(this._highlightChanges); this._highlightChanges = []; } /** * @override */ onpopulate() { const propertyValue = /** @type {!SDK.RemoteObject} */ (this.property.value); console.assert(propertyValue); const skipProto = this.treeOutline ? this.treeOutline._skipProto : true; const targetValue = this.property.name !== '__proto__' ? propertyValue : this.property.parentObject; ObjectUI.ObjectPropertyTreeElement._populate( this, propertyValue, skipProto, this._linkifier, undefined, undefined, undefined, targetValue); } /** * @override * @return {boolean} */ ondblclick(event) { const inEditableElement = event.target.isSelfOrDescendant(this.valueElement) || (this.expandedValueElement && event.target.isSelfOrDescendant(this.expandedValueElement)); if (!this.property.value.customPreview() && inEditableElement && (this.property.writable || this.property.setter)) this._startEditing(); return false; } /** * @override */ onattach() { this.update(); this._updateExpandable(); } /** * @override */ onexpand() { this._showExpandedValueElement(true); } /** * @override */ oncollapse() { this._showExpandedValueElement(false); } /** * @param {boolean} value */ _showExpandedValueElement(value) { if (!this.expandedValueElement) return; if (value) this._rowContainer.replaceChild(this.expandedValueElement, this.valueElement); else this._rowContainer.replaceChild(this.valueElement, this.expandedValueElement); } /** * @param {!SDK.RemoteObject} value * @return {?Element} */ _createExpandedValueElement(value) { const needsAlternateValue = value.hasChildren && !value.customPreview() && value.subtype !== 'node' && value.type !== 'function' && (value.type !== 'object' || value.preview); if (!needsAlternateValue) return null; const valueElement = createElementWithClass('span', 'value'); if (value.description === 'Object') valueElement.textContent = ''; else valueElement.setTextContentTruncatedIfNeeded(value.description || ''); valueElement.classList.add('object-value-' + (value.subtype || value.type)); valueElement.title = value.description || ''; return valueElement; } update() { this.nameElement = ObjectUI.ObjectPropertiesSection.createNameElement(this.property.name); if (!this.property.enumerable) this.nameElement.classList.add('object-properties-section-dimmed'); if (this.property.synthetic) this.nameElement.classList.add('synthetic-property'); this._updatePropertyPath(); if (this.property.value) { const showPreview = this.property.name !== '__proto__'; this.valueElement = ObjectUI.ObjectPropertiesSection.createValueElementWithCustomSupport( this.property.value, this.property.wasThrown, showPreview, this.listItemElement, this._linkifier); } else if (this.property.getter) { this.valueElement = ObjectUI.ObjectPropertyTreeElement.createRemoteObjectAccessorPropertySpan( this.property.parentObject, [this.property.name], this._onInvokeGetterClick.bind(this)); } else { this.valueElement = createElementWithClass('span', 'object-value-undefined'); this.valueElement.textContent = Common.UIString('<unreadable>'); this.valueElement.title = Common.UIString('No property getter'); } const valueText = this.valueElement.textContent; if (this.property.value && valueText && !this.property.wasThrown) this.expandedValueElement = this._createExpandedValueElement(this.property.value); this.listItemElement.removeChildren(); this._rowContainer = UI.html`<span>${this.nameElement}: ${this.valueElement}</span>`; this.listItemElement.appendChild(this._rowContainer); } _updatePropertyPath() { if (this.nameElement.title) return; const name = this.property.name; if (this.property.synthetic) { this.nameElement.title = name; return; } const useDotNotation = /^(_|\$|[A-Z])(_|\$|[A-Z]|\d)*$/i; const isInteger = /^[1-9]\d*$/; const parentPath = (this.parent.nameElement && !this.parent.property.synthetic) ? this.parent.nameElement.title : ''; if (useDotNotation.test(name)) this.nameElement.title = parentPath ? `${parentPath}.${name}` : name; else if (isInteger.test(name)) this.nameElement.title = parentPath + '[' + name + ']'; else this.nameElement.title = parentPath + '["' + JSON.stringify(name) + '"]'; } /** * @param {!Event} event */ _contextMenuFired(event) { const contextMenu = new UI.ContextMenu(event); contextMenu.appendApplicableItems(this); if (this.property.symbol) contextMenu.appendApplicableItems(this.property.symbol); if (this.property.value) contextMenu.appendApplicableItems(this.property.value); if (!this.property.synthetic && this.nameElement && this.nameElement.title) { const copyPathHandler = InspectorFrontendHost.copyText.bind(InspectorFrontendHost, this.nameElement.title); contextMenu.clipboardSection().appendItem(ls`Copy property path`, copyPathHandler); } if (this.property.parentObject instanceof SDK.LocalJSONObject) { contextMenu.viewSection().appendItem(ls`Expand recursively`, this.expandRecursively.bind(this, Number.MAX_VALUE)); contextMenu.viewSection().appendItem(ls`Collapse children`, this.collapseChildren.bind(this)); } contextMenu.show(); } _startEditing() { if (this._prompt || !this.treeOutline._editable || this._readOnly) return; this._editableDiv = this._rowContainer.createChild('span', 'editable-div'); let text = this.property.value.description; if (this.property.value.type === 'string' && typeof text === 'string') text = '"' + text + '"'; this._editableDiv.setTextContentTruncatedIfNeeded(text, Common.UIString('<string is too large to edit>')); const originalContent = this._editableDiv.textContent; // Lie about our children to prevent expanding on double click and to collapse subproperties. this.setExpandable(false); this.listItemElement.classList.add('editing-sub-part'); this.valueElement.classList.add('hidden'); this._prompt = new ObjectUI.ObjectPropertyPrompt(); const proxyElement = this._prompt.attachAndStartEditing(this._editableDiv, this._editingCommitted.bind(this, originalContent)); this.listItemElement.getComponentSelection().selectAllChildren(this._editableDiv); proxyElement.addEventListener('keydown', this._promptKeyDown.bind(this, originalContent), false); } _editingEnded() { this._prompt.detach(); delete this._prompt; this._editableDiv.remove(); this._updateExpandable(); this.listItemElement.scrollLeft = 0; this.listItemElement.classList.remove('editing-sub-part'); } _editingCancelled() { this.valueElement.classList.remove('hidden'); this._editingEnded(); } /** * @param {string} originalContent */ _editingCommitted(originalContent) { const userInput = this._prompt.text(); if (userInput === originalContent) { this._editingCancelled(); // nothing changed, so cancel return; } this._editingEnded(); this._applyExpression(userInput); } /** * @param {string} originalContent * @param {!Event} event */ _promptKeyDown(originalContent, event) { if (isEnterKey(event)) { event.consume(); this._editingCommitted(originalContent); return; } if (event.key === 'Escape') { event.consume(); this._editingCancelled(); return; } } /** * @param {string} expression */ async _applyExpression(expression) { const property = SDK.RemoteObject.toCallArgument(this.property.symbol || this.property.name); expression = SDK.RuntimeModel.wrapObjectLiteralExpressionIfNeeded(expression.trim()); if (this.property.synthetic) { let invalidate = false; if (expression) invalidate = await this.property.setSyntheticValue(expression); if (invalidate) { const parent = this.parent; parent.invalidateChildren(); parent.onpopulate(); } else { this.update(); } return; } const errorPromise = expression ? this.property.parentObject.setPropertyValue(property, expression) : this.property.parentObject.deleteProperty(property); const error = await errorPromise; if (error) { this.update(); return; } if (!expression) { // The property was deleted, so remove this tree element. this.parent.removeChild(this); } else { // Call updateSiblings since their value might be based on the value that just changed. const parent = this.parent; parent.invalidateChildren(); parent.onpopulate(); } } /** * @param {?SDK.RemoteObject} result * @param {boolean=} wasThrown */ _onInvokeGetterClick(result, wasThrown) { if (!result) return; this.property.value = result; this.property.wasThrown = wasThrown; this.update(); this.invalidateChildren(); this._updateExpandable(); } _updateExpandable() { if (this.property.value) { this.setExpandable( !this.property.value.customPreview() && this.property.value.hasChildren && !this.property.wasThrown); } else { this.setExpandable(false); } } /** * @return {string} */ path() { return this.nameElement.title; } }; /** * @unrestricted */ ObjectUI.ArrayGroupingTreeElement = class extends UI.TreeElement { /** * @param {!SDK.RemoteObject} object * @param {number} fromIndex * @param {number} toIndex * @param {number} propertyCount * @param {!Components.Linkifier=} linkifier */ constructor(object, fromIndex, toIndex, propertyCount, linkifier) { super(String.sprintf('[%d \u2026 %d]', fromIndex, toIndex), true); this.toggleOnClick = true; this.selectable = false; this._fromIndex = fromIndex; this._toIndex = toIndex; this._object = object; this._readOnly = true; this._propertyCount = propertyCount; this._linkifier = linkifier; } /** * @param {!UI.TreeElement} treeNode * @param {!SDK.RemoteObject} object * @param {number} fromIndex * @param {number} toIndex * @param {!Components.Linkifier=} linkifier */ static _populateArray(treeNode, object, fromIndex, toIndex, linkifier) { ObjectUI.ArrayGroupingTreeElement._populateRanges(treeNode, object, fromIndex, toIndex, true, linkifier); } /** * @param {!UI.TreeElement} treeNode * @param {!SDK.RemoteObject} object * @param {number} fromIndex * @param {number} toIndex * @param {boolean} topLevel * @param {!Components.Linkifier=} linkifier * @this {ObjectUI.ArrayGroupingTreeElement} */ static _populateRanges(treeNode, object, fromIndex, toIndex, topLevel, linkifier) { object.callFunctionJSON( packRanges, [ {value: fromIndex}, {value: toIndex}, {value: ObjectUI.ArrayGroupingTreeElement._bucketThreshold}, {value: ObjectUI.ArrayGroupingTreeElement._sparseIterationThreshold}, {value: ObjectUI.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold} ], callback); /** * Note: must declare params as optional. * @param {number=} fromIndex * @param {number=} toIndex * @param {number=} bucketThreshold * @param {number=} sparseIterationThreshold * @param {number=} getOwnPropertyNamesThreshold * @suppressReceiverCheck * @this {Object} */ function packRanges(fromIndex, toIndex, bucketThreshold, sparseIterationThreshold, getOwnPropertyNamesThreshold) { let ownPropertyNames = null; const consecutiveRange = (toIndex - fromIndex >= sparseIterationThreshold) && ArrayBuffer.isView(this); const skipGetOwnPropertyNames = consecutiveRange && (toIndex - fromIndex >= getOwnPropertyNamesThreshold); function* arrayIndexes(object) { if (toIndex - fromIndex < sparseIterationThreshold) { for (let i = fromIndex; i <= toIndex; ++i) { if (i in object) yield i; } } else { ownPropertyNames = ownPropertyNames || Object.getOwnPropertyNames(object); for (let i = 0; i < ownPropertyNames.length; ++i) { const name = ownPropertyNames[i]; const index = name >>> 0; if (('' + index) === name && fromIndex <= index && index <= toIndex) yield index; } } } let count = 0; if (consecutiveRange) { count = toIndex - fromIndex + 1; } else { for (const i of arrayIndexes(this)) // eslint-disable-line ++count; } let bucketSize = count; if (count <= bucketThreshold) bucketSize = count; else bucketSize = Math.pow(bucketThreshold, Math.ceil(Math.log(count) / Math.log(bucketThreshold)) - 1); const ranges = []; if (consecutiveRange) { for (let i = fromIndex; i <= toIndex; i += bucketSize) { const groupStart = i; let groupEnd = groupStart + bucketSize - 1; if (groupEnd > toIndex) groupEnd = toIndex; ranges.push([groupStart, groupEnd, groupEnd - groupStart + 1]); } } else { count = 0; let groupStart = -1; let groupEnd = 0; for (const i of arrayIndexes(this)) { if (groupStart === -1) groupStart = i; groupEnd = i; if (++count === bucketSize) { ranges.push([groupStart, groupEnd, count]); count = 0; groupStart = -1; } } if (count > 0) ranges.push([groupStart, groupEnd, count]); } return {ranges: ranges, skipGetOwnPropertyNames: skipGetOwnPropertyNames}; } function callback(result) { if (!result) return; const ranges = /** @type {!Array.<!Array.<number>>} */ (result.ranges); if (ranges.length === 1) { ObjectUI.ArrayGroupingTreeElement._populateAsFragment(treeNode, object, ranges[0][0], ranges[0][1], linkifier); } else { for (let i = 0; i < ranges.length; ++i) { const fromIndex = ranges[i][0]; const toIndex = ranges[i][1]; const count = ranges[i][2]; if (fromIndex === toIndex) ObjectUI.ArrayGroupingTreeElement._populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier); else treeNode.appendChild(new ObjectUI.ArrayGroupingTreeElement(object, fromIndex, toIndex, count, linkifier)); } } if (topLevel) { ObjectUI.ArrayGroupingTreeElement._populateNonIndexProperties( treeNode, object, result.skipGetOwnPropertyNames, linkifier); } } } /** * @param {!UI.TreeElement} treeNode * @param {!SDK.RemoteObject} object * @param {number} fromIndex * @param {number} toIndex * @param {!Components.Linkifier=} linkifier * @this {ObjectUI.ArrayGroupingTreeElement} */ static _populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier) { object.callFunction( buildArrayFragment, [{value: fromIndex}, {value: toIndex}, {value: ObjectUI.ArrayGroupingTreeElement._sparseIterationThreshold}], processArrayFragment.bind(this)); /** * @suppressReceiverCheck * @this {Object} * @param {number=} fromIndex // must declare optional * @param {number=} toIndex // must declare optional * @param {number=} sparseIterationThreshold // must declare optional */ function buildArrayFragment(fromIndex, toIndex, sparseIterationThreshold) { const result = Object.create(null); if (toIndex - fromIndex < sparseIterationThreshold) { for (let i = fromIndex; i <= toIndex; ++i) { if (i in this) result[i] = this[i]; } } else { const ownPropertyNames = Object.getOwnPropertyNames(this); for (let i = 0; i < ownPropertyNames.length; ++i) { const name = ownPropertyNames[i]; const index = name >>> 0; if (String(index) === name && fromIndex <= index && index <= toIndex) result[index] = this[index]; } } return result; } /** * @param {?SDK.RemoteObject} arrayFragment * @param {boolean=} wasThrown * @this {ObjectUI.ArrayGroupingTreeElement} */ function processArrayFragment(arrayFragment, wasThrown) { if (!arrayFragment || wasThrown) return; arrayFragment.getAllProperties( false /* accessorPropertiesOnly */, true /* generatePreview */, processProperties.bind(this)); } /** @this {ObjectUI.ArrayGroupingTreeElement} */ function processProperties(properties, internalProperties) { if (!properties) return; properties.sort(ObjectUI.ObjectPropertiesSection.CompareProperties); for (let i = 0; i < properties.length; ++i) { properties[i].parentObject = this._object; const childTreeElement = new ObjectUI.ObjectPropertyTreeElement(properties[i], linkifier); childTreeElement._readOnly = true; treeNode.appendChild(childTreeElement); } } } /** * @param {!UI.TreeElement} treeNode * @param {!SDK.RemoteObject} object * @param {boolean} skipGetOwnPropertyNames * @param {!Components.Linkifier=} linkifier * @this {ObjectUI.ArrayGroupingTreeElement} */ static _populateNonIndexProperties(treeNode, object, skipGetOwnPropertyNames, linkifier) { object.callFunction(buildObjectFragment, [{value: skipGetOwnPropertyNames}], processObjectFragment.bind(this)); /** * @param {boolean=} skipGetOwnPropertyNames * @suppressReceiverCheck * @this {Object} */ function buildObjectFragment(skipGetOwnPropertyNames) { const result = {__proto__: this.__proto__}; if (skipGetOwnPropertyNames) return result; const names = Object.getOwnPropertyNames(this); for (let i = 0; i < names.length; ++i) { const name = names[i]; // Array index check according to the ES5-15.4. if (String(name >>> 0) === name && name >>> 0 !== 0xffffffff) continue; const descriptor = Object.getOwnPropertyDescriptor(this, name); if (descriptor) Object.defineProperty(result, name, descriptor); } return result; } /** * @param {?SDK.RemoteObject} arrayFragment * @param {boolean=} wasThrown * @this {ObjectUI.ArrayGroupingTreeElement} */ function processObjectFragment(arrayFragment, wasThrown) { if (!arrayFragment || wasThrown) return; arrayFragment.getOwnProperties(true /* generatePreview */, processProperties.bind(this)); } /** * @param {?Array.<!SDK.RemoteObjectProperty>} properties * @param {?Array.<!SDK.RemoteObjectProperty>=} internalProperties * @this {ObjectUI.ArrayGroupingTreeElement} */ function processProperties(properties, internalProperties) { if (!properties) return; properties.sort(ObjectUI.ObjectPropertiesSection.CompareProperties); for (let i = 0; i < properties.length; ++i) { properties[i].parentObject = this._object; const childTreeElement = new ObjectUI.ObjectPropertyTreeElement(properties[i], linkifier); childTreeElement._readOnly = true; treeNode.appendChild(childTreeElement); } } } /** * @override */ onpopulate() { if (this._propertyCount >= ObjectUI.ArrayGroupingTreeElement._bucketThreshold) { ObjectUI.ArrayGroupingTreeElement._populateRanges( this, this._object, this._fromIndex, this._toIndex, false, this._linkifier); return; } ObjectUI.ArrayGroupingTreeElement._populateAsFragment( this, this._object, this._fromIndex, this._toIndex, this._linkifier); } /** * @override */ onattach() { this.listItemElement.classList.add('object-properties-section-name'); } }; ObjectUI.ArrayGroupingTreeElement._bucketThreshold = 100; ObjectUI.ArrayGroupingTreeElement._sparseIterationThreshold = 250000; ObjectUI.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold = 500000; /** * @unrestricted */ ObjectUI.ObjectPropertyPrompt = class extends UI.TextPrompt { constructor() { super(); this.initialize( ObjectUI.javaScriptAutocomplete.completionsForTextInCurrentContext.bind(ObjectUI.javaScriptAutocomplete)); } }; /** * @unrestricted */ ObjectUI.ObjectPropertiesSectionExpandController = class { constructor() { /** @type {!Set.<string>} */ this._expandedProperties = new Set(); } /** * @param {string} id * @param {!ObjectUI.ObjectPropertiesSection} section */ watchSection(id, section) { section.addEventListener(UI.TreeOutline.Events.ElementAttached, this._elementAttached, this); section.addEventListener(UI.TreeOutline.Events.ElementExpanded, this._elementExpanded, this); section.addEventListener(UI.TreeOutline.Events.ElementCollapsed, this._elementCollapsed, this); section[ObjectUI.ObjectPropertiesSectionExpandController._treeOutlineId] = id; if (this._expandedProperties.has(id)) section.expand(); } /** * @param {string} id */ stopWatchSectionsWithId(id) { for (const property of this._expandedProperties) { if (property.startsWith(id + ':')) this._expandedProperties.delete(property); } } /** * @param {!Common.Event} event */ _elementAttached(event) { const element = /** @type {!UI.TreeElement} */ (event.data); if (element.isExpandable() && this._expandedProperties.has(this._propertyPath(element))) element.expand(); } /** * @param {!Common.Event} event */ _elementExpanded(event) { const element = /** @type {!UI.TreeElement} */ (event.data); this._expandedProperties.add(this._propertyPath(element)); } /** * @param {!Common.Event} event */ _elementCollapsed(event) { const element = /** @type {!UI.TreeElement} */ (event.data); this._expandedProperties.delete(this._propertyPath(element)); } /** * @param {!UI.TreeElement} treeElement * @return {string} */ _propertyPath(treeElement) { const cachedPropertyPath = treeElement[ObjectUI.ObjectPropertiesSectionExpandController._cachedPathSymbol]; if (cachedPropertyPath) return cachedPropertyPath; let current = treeElement; const rootElement = treeElement.treeOutline.objectTreeElement(); let result; while (current !== rootElement) { let currentName = ''; if (current.property) currentName = current.property.name; else currentName = typeof current.title === 'string' ? current.title : current.title.textContent; result = currentName + (result ? '.' + result : ''); current = current.parent; } const treeOutlineId = treeElement.treeOutline[ObjectUI.ObjectPropertiesSectionExpandController._treeOutlineId]; result = treeOutlineId + (result ? ':' + result : ''); treeElement[ObjectUI.ObjectPropertiesSectionExpandController._cachedPathSymbol] = result; return result; } }; ObjectUI.ObjectPropertiesSectionExpandController._cachedPathSymbol = Symbol('cachedPath'); ObjectUI.ObjectPropertiesSectionExpandController._treeOutlineId = Symbol('treeOutlineId'); /** * @implements {Common.Renderer} */ ObjectUI.ObjectPropertiesSection.Renderer = class { /** * @override * @param {!Object} object * @param {!Common.Renderer.Options=} options * @return {!Promise<?Node>} */ render(object, options) { if (!(object instanceof SDK.RemoteObject)) return Promise.reject(new Error('Can\'t render ' + object)); options = options || {}; const title = options.title; const section = new ObjectUI.ObjectPropertiesSection(object, title); if (!title) section.titleLessMode(); if (options.expanded) section.expand(); section.editable = !!options.editable; return Promise.resolve(section.element); } };