| // Copyright 2021 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 */ |
| |
| import * as SDK from '../../../core/sdk/sdk.js'; |
| import * as Bindings from '../../../models/bindings/bindings.js'; |
| import * as JavaScriptMetaData from '../../../models/javascript_metadata/javascript_metadata.js'; |
| import * as SourceMapScopes from '../../../models/source_map_scopes/source_map_scopes.js'; |
| import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js'; |
| import * as UI from '../../legacy/legacy.js'; |
| |
| import {type ArgumentHintsTooltip, closeTooltip, cursorTooltip} from './cursor_tooltip.js'; |
| |
| export function completion(): CodeMirror.Extension { |
| return CodeMirror.javascript.javascriptLanguage.data.of({ |
| autocomplete: javascriptCompletionSource, |
| }); |
| } |
| |
| export async function completeInContext( |
| textBefore: string, query: string, force = false): Promise<UI.SuggestBox.Suggestions> { |
| const state = CodeMirror.EditorState.create({ |
| doc: textBefore + query, |
| selection: {anchor: textBefore.length}, |
| extensions: CodeMirror.javascript.javascriptLanguage, |
| }); |
| const result = await javascriptCompletionSource(new CodeMirror.CompletionContext(state, state.doc.length, force)); |
| return result ? result.options.filter(o => o.label.startsWith(query)).map(o => ({ |
| text: o.label, |
| priority: 100 + (o.boost || 0), |
| isSecondary: o.type === 'secondary', |
| })) : |
| []; |
| } |
| |
| class CompletionSet { |
| constructor( |
| readonly completions: CodeMirror.Completion[] = [], |
| readonly seen: Set<string> = new Set(), |
| ) { |
| } |
| |
| add(completion: CodeMirror.Completion): void { |
| if (!this.seen.has(completion.label)) { |
| this.seen.add(completion.label); |
| this.completions.push(completion); |
| } |
| } |
| |
| copy(): CompletionSet { |
| return new CompletionSet(this.completions.slice(), new Set(this.seen)); |
| } |
| } |
| |
| const javascriptKeywords = [ |
| 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', |
| 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', |
| 'instanceof', 'let', 'new', 'null', 'of', 'return', 'static', 'super', 'switch', 'this', 'throw', |
| 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', |
| ]; |
| const consoleBuiltinFunctions = [ |
| 'clear', |
| 'copy', |
| 'debug', |
| 'dir', |
| 'dirxml', |
| 'getEventListeners', |
| 'inspect', |
| 'keys', |
| 'monitor', |
| 'monitorEvents', |
| 'profile', |
| 'profileEnd', |
| 'queryObjects', |
| 'table', |
| 'undebug', |
| 'unmonitor', |
| 'unmonitorEvents', |
| 'values', |
| ]; |
| const consoleBuiltinVariables = ['$', '$$', '$x', '$0', '$_']; |
| |
| const baseCompletions = new CompletionSet(); |
| for (const kw of javascriptKeywords) { |
| baseCompletions.add({label: kw, type: 'keyword'}); |
| } |
| for (const builtin of consoleBuiltinFunctions) { |
| baseCompletions.add({label: builtin, type: 'function'}); |
| } |
| for (const varName of consoleBuiltinVariables) { |
| baseCompletions.add({label: varName, type: 'variable'}); |
| } |
| |
| const dontCompleteIn = new Set([ |
| 'TemplateString', |
| 'LineComment', |
| 'BlockComment', |
| 'TypeDefinition', |
| 'VariableDefinition', |
| 'PropertyDefinition', |
| 'TypeName', |
| ]); |
| |
| export const enum QueryType { |
| EXPRESSION = 0, |
| PROPERTY_NAME = 1, |
| PROPERTY_EXPRESSION = 2, |
| POTENTIALLY_RETRIEVING_FROM_MAP = 3, |
| } |
| |
| export function getQueryType(tree: CodeMirror.Tree, pos: number, doc: CodeMirror.Text): { |
| type: QueryType, |
| from?: number, |
| relatedNode?: CodeMirror.SyntaxNode, |
| }|null { |
| let node = tree.resolveInner(pos, -1); |
| const parent = node.parent; |
| if (dontCompleteIn.has(node.name)) { |
| return null; |
| } |
| |
| if (node.name === 'PropertyName' || node.name === 'PrivatePropertyName') { |
| return parent?.name !== 'MemberExpression' ? null : |
| {type: QueryType.PROPERTY_NAME, from: node.from, relatedNode: parent}; |
| } |
| if (node.name === 'VariableName' || |
| // Treat alphabetic keywords as variables |
| !node.firstChild && node.to - node.from < 20 && !/[^a-z]/.test(doc.sliceString(node.from, node.to))) { |
| return {type: QueryType.EXPRESSION, from: node.from}; |
| } |
| if (node.name === 'String') { |
| const parent = node.parent; |
| return parent?.name === 'MemberExpression' && parent.childBefore(node.from)?.name === '[' ? |
| {type: QueryType.PROPERTY_EXPRESSION, from: node.from, relatedNode: parent} : |
| null; |
| } |
| // Enter unfinished nodes before the position. |
| node = node.enterUnfinishedNodesBefore(pos); |
| // Normalize to parent node when pointing after a child of a member expr. |
| if (node.to === pos && node.parent?.name === 'MemberExpression') { |
| node = node.parent; |
| } |
| if (node.name === 'MemberExpression') { |
| const before = node.childBefore(Math.min(pos, node.to)); |
| if (before?.name === '[') { |
| return {type: QueryType.PROPERTY_EXPRESSION, relatedNode: node}; |
| } |
| if (before?.name === '.' || before?.name === '?.') { |
| return {type: QueryType.PROPERTY_NAME, relatedNode: node}; |
| } |
| } |
| if (node.name === '(') { |
| // map.get(<auto-complete> |
| if (parent?.name === 'ArgList' && parent?.parent?.name === 'CallExpression') { |
| // map.get |
| const callReceiver = parent?.parent?.firstChild; |
| if (callReceiver?.name === 'MemberExpression') { |
| // get |
| const propertyExpression = callReceiver?.lastChild; |
| if (propertyExpression && doc.sliceString(propertyExpression.from, propertyExpression.to) === 'get') { |
| // map |
| const potentiallyMapObject = callReceiver?.firstChild; |
| return {type: QueryType.POTENTIALLY_RETRIEVING_FROM_MAP, relatedNode: potentiallyMapObject || undefined}; |
| } |
| } |
| } |
| } |
| return {type: QueryType.EXPRESSION}; |
| } |
| |
| export async function javascriptCompletionSource(cx: CodeMirror.CompletionContext): |
| Promise<CodeMirror.CompletionResult|null> { |
| const query = getQueryType(CodeMirror.syntaxTree(cx.state), cx.pos, cx.state.doc); |
| if (!query || query.from === undefined && !cx.explicit && query.type === QueryType.EXPRESSION) { |
| return null; |
| } |
| |
| const script = getExecutionContext()?.debuggerModel.selectedCallFrame()?.script; |
| if (script && |
| Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().pluginManager.hasPluginForScript(script)) { |
| return null; |
| } |
| |
| let result: CompletionSet; |
| let quote: string|undefined = undefined; |
| if (query.type === QueryType.EXPRESSION) { |
| const [scope, global] = await Promise.all([ |
| completeExpressionInScope(), |
| completeExpressionGlobal(), |
| ]); |
| if (scope.completions.length) { |
| result = scope; |
| for (const r of global.completions) { |
| result.add(r); |
| } |
| } else { |
| result = global; |
| } |
| } else if (query.type === QueryType.PROPERTY_NAME || query.type === QueryType.PROPERTY_EXPRESSION) { |
| const objectExpr = (query.relatedNode as CodeMirror.SyntaxNode).getChild('Expression'); |
| if (query.type === QueryType.PROPERTY_EXPRESSION) { |
| quote = query.from === undefined ? '\'' : cx.state.sliceDoc(query.from, query.from + 1); |
| } |
| if (!objectExpr) { |
| return null; |
| } |
| result = await completeProperties( |
| cx.state.sliceDoc(objectExpr.from, objectExpr.to), quote, cx.state.sliceDoc(cx.pos, cx.pos + 1) === ']'); |
| } else if (query.type === QueryType.POTENTIALLY_RETRIEVING_FROM_MAP) { |
| const potentialMapObject = query.relatedNode; |
| if (!potentialMapObject) { |
| return null; |
| } |
| result = await maybeCompleteKeysFromMap(cx.state.sliceDoc(potentialMapObject.from, potentialMapObject.to)); |
| } else { |
| return null; |
| } |
| return { |
| from: query.from ?? cx.pos, |
| options: result.completions, |
| validFor: !quote ? SPAN_IDENT : |
| quote === '\'' ? SPAN_SINGLE_QUOTE : |
| SPAN_DOUBLE_QUOTE, |
| }; |
| } |
| |
| const SPAN_IDENT = /^#?(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u, |
| SPAN_SINGLE_QUOTE = /^\'(\\.|[^\\'\n])*'?$/, SPAN_DOUBLE_QUOTE = /^"(\\.|[^\\"\n])*"?$/; |
| |
| function getExecutionContext(): SDK.RuntimeModel.ExecutionContext|null { |
| return UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); |
| } |
| |
| async function evaluateExpression( |
| context: SDK.RuntimeModel.ExecutionContext, |
| expression: string, |
| group: string, |
| ): Promise<SDK.RemoteObject.RemoteObject|null> { |
| const result = await context.evaluate( |
| { |
| expression, |
| objectGroup: group, |
| includeCommandLineAPI: true, |
| silent: true, |
| returnByValue: false, |
| generatePreview: false, |
| throwOnSideEffect: true, |
| timeout: 500, |
| replMode: true, |
| }, |
| false, false); |
| if ('error' in result || result.exceptionDetails || !result.object) { |
| return null; |
| } |
| return result.object; |
| } |
| |
| const primitivePrototypes = new Map<string, string>([ |
| ['string', 'String'], |
| ['symbol', 'Symbol'], |
| ['number', 'Number'], |
| ['boolean', 'Boolean'], |
| ['bigint', 'BigInt'], |
| ]); |
| |
| const maxCacheAge = 30_000; |
| |
| let cacheInstance: PropertyCache|null = null; |
| |
| /** |
| * Store recent collections of property completions. The empty string |
| * is used to store the set of global bindings. |
| **/ |
| class PropertyCache { |
| readonly #cache = new Map<string, Promise<CompletionSet>>(); |
| |
| constructor() { |
| const clear = (): void => this.#cache.clear(); |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.CommandEvaluated, clear); |
| UI.Context.Context.instance().addFlavorChangeListener(SDK.RuntimeModel.ExecutionContext, clear); |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, clear); |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, clear); |
| } |
| |
| get(expression: string): Promise<CompletionSet>|undefined { |
| return this.#cache.get(expression); |
| } |
| |
| set(expression: string, value: Promise<CompletionSet>): void { |
| this.#cache.set(expression, value); |
| window.setTimeout(() => { |
| if (this.#cache.get(expression) === value) { |
| this.#cache.delete(expression); |
| } |
| }, maxCacheAge); |
| } |
| |
| static instance(): PropertyCache { |
| if (!cacheInstance) { |
| cacheInstance = new PropertyCache(); |
| } |
| return cacheInstance; |
| } |
| } |
| |
| async function maybeCompleteKeysFromMap(objectVariable: string): Promise<CompletionSet> { |
| const result = new CompletionSet(); |
| const context = getExecutionContext(); |
| if (!context) { |
| return result; |
| } |
| const maybeRetrieveKeys = |
| await evaluateExpression(context, `[...Map.prototype.keys.call(${objectVariable})]`, 'completion'); |
| if (!maybeRetrieveKeys) { |
| return result; |
| } |
| const properties = SDK.RemoteObject.RemoteArray.objectAsArray(maybeRetrieveKeys); |
| const numProperties = properties.length(); |
| for (let i = 0; i < numProperties; i++) { |
| result.add({ |
| label: `"${(await properties.at(i)).value}")`, |
| type: 'constant', |
| boost: i * -1, |
| }); |
| } |
| return result; |
| } |
| |
| async function completeProperties( |
| expression: string, |
| quoted?: string, |
| hasBracket = false, |
| ): Promise<CompletionSet> { |
| const cache = PropertyCache.instance(); |
| if (!quoted) { |
| const cached = cache.get(expression); |
| if (cached) { |
| return await cached; |
| } |
| } |
| const context = getExecutionContext(); |
| if (!context) { |
| return new CompletionSet(); |
| } |
| const result = completePropertiesInner(expression, context, quoted, hasBracket); |
| if (!quoted) { |
| cache.set(expression, result); |
| } |
| return await result; |
| } |
| |
| async function completePropertiesInner( |
| expression: string, |
| context: SDK.RuntimeModel.ExecutionContext, |
| quoted?: string, |
| hasBracket = false, |
| ): Promise<CompletionSet> { |
| const result = new CompletionSet(); |
| if (!context) { |
| return result; |
| } |
| let object = await evaluateExpression(context, expression, 'completion'); |
| if (!object) { |
| return result; |
| } |
| |
| while (object.type === 'object' && object.subtype === 'proxy') { |
| const properties = await object.getOwnProperties(false); |
| const innerObject = properties.internalProperties?.find(p => p.name === '[[Target]]')?.value; |
| if (!innerObject) { |
| break; |
| } |
| object = innerObject as SDK.RemoteObject.RemoteObject; |
| } |
| |
| const toPrototype = primitivePrototypes.get(object.type); |
| if (toPrototype) { |
| object = await evaluateExpression(context, toPrototype + '.prototype', 'completion'); |
| } |
| |
| const functionType = expression === 'globalThis' ? 'function' : 'method'; |
| const otherType = expression === 'globalThis' ? 'variable' : 'property'; |
| if (object && (object.type === 'object' || object.type === 'function')) { |
| const properties = await object.getAllProperties( |
| /* accessorPropertiesOnly */ false, /* generatePreview */ false, /* nonIndexedPropertiesOnly */ true); |
| const isFunction = object.type === 'function'; |
| for (const prop of properties.properties || []) { |
| if (!prop.symbol && !(isFunction && (prop.name === 'arguments' || prop.name === 'caller')) && |
| (quoted || SPAN_IDENT.test(prop.name))) { |
| const label = |
| quoted ? quoted + prop.name.replaceAll('\\', '\\\\').replaceAll(quoted, '\\' + quoted) + quoted : prop.name; |
| const apply = (quoted && !hasBracket) ? `${label}]` : undefined; |
| const boost = 2 * Number(prop.isOwn) + 1 * Number(prop.enumerable); |
| const type = prop.value?.type === 'function' ? functionType : otherType; |
| result.add({apply, label, type, boost}); |
| } |
| } |
| } |
| context.runtimeModel.releaseObjectGroup('completion'); |
| return result; |
| } |
| |
| async function completeExpressionInScope(): Promise<CompletionSet> { |
| const result = new CompletionSet(); |
| const selectedFrame = getExecutionContext()?.debuggerModel.selectedCallFrame(); |
| if (!selectedFrame) { |
| return result; |
| } |
| |
| const scopes = await Promise.all(selectedFrame.scopeChain().map( |
| scope => SourceMapScopes.NamesResolver.resolveScopeInObject(scope).getAllProperties(false, false))); |
| for (const scope of scopes) { |
| for (const property of scope.properties || []) { |
| result.add({ |
| label: property.name, |
| type: property.value?.type === 'function' ? 'function' : 'variable', |
| }); |
| } |
| } |
| return result; |
| } |
| |
| async function completeExpressionGlobal(): Promise<CompletionSet> { |
| const cache = PropertyCache.instance(); |
| const cached = cache.get(''); |
| if (cached) { |
| return await cached; |
| } |
| |
| const context = getExecutionContext(); |
| if (!context) { |
| return baseCompletions; |
| } |
| const result = baseCompletions.copy(); |
| |
| const fetchNames = completePropertiesInner('globalThis', context).then(fromWindow => { |
| return context.globalLexicalScopeNames().then(globals => { |
| for (const option of fromWindow.completions) { |
| result.add(option); |
| } |
| for (const name of globals || []) { |
| result.add({label: name, type: 'variable'}); |
| } |
| return result; |
| }); |
| }); |
| cache.set('', fetchNames); |
| return await fetchNames; |
| } |
| |
| export async function isExpressionComplete(expression: string): Promise<boolean> { |
| const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); |
| if (!currentExecutionContext) { |
| return true; |
| } |
| const result = |
| await currentExecutionContext.runtimeModel.compileScript(expression, '', false, currentExecutionContext.id); |
| if (!result?.exceptionDetails?.exception) { |
| return true; |
| } |
| const description = result.exceptionDetails.exception.description; |
| if (description) { |
| return !description.startsWith('SyntaxError: Unexpected end of input') && |
| !description.startsWith('SyntaxError: Unterminated template literal'); |
| } |
| return false; |
| } |
| |
| export function argumentHints(): ArgumentHintsTooltip { |
| return cursorTooltip(getArgumentHints); |
| } |
| |
| export function closeArgumentsHintsTooltip( |
| view: CodeMirror.EditorView, tooltip: CodeMirror.StateField<CodeMirror.Tooltip|null>): boolean { |
| // If the tooltip is currently showing, the state will reflect its properties. |
| // If it isn't showing, the state is explicitly set to `null`. |
| if (view.state.field(tooltip) === null) { |
| return false; |
| } |
| view.dispatch({effects: closeTooltip.of(null)}); |
| return true; |
| } |
| |
| async function getArgumentHints( |
| state: CodeMirror.EditorState, pos: number): Promise<(() => CodeMirror.TooltipView)|null> { |
| const node = CodeMirror.syntaxTree(state).resolveInner(pos).enterUnfinishedNodesBefore(pos); |
| |
| if (node.name !== 'ArgList') { |
| return null; |
| } |
| const callee = node.parent?.getChild('Expression'); |
| if (!callee) { |
| return null; |
| } |
| const argumentList = await getArgumentsForExpression(callee, state.doc); |
| if (!argumentList) { |
| return null; |
| } |
| |
| let argumentIndex = 0; |
| for (let scanPos = pos;;) { |
| const before = node.childBefore(scanPos); |
| if (!before) { |
| break; |
| } |
| if (before.type.is('Expression')) { |
| argumentIndex++; |
| } |
| scanPos = before.from; |
| } |
| return () => tooltipBuilder(argumentList, argumentIndex); |
| } |
| |
| async function getArgumentsForExpression( |
| callee: CodeMirror.SyntaxNode, doc: CodeMirror.Text): Promise<string[][]|null> { |
| const context = getExecutionContext(); |
| if (!context) { |
| return null; |
| } |
| const expression = doc.sliceString(callee.from, callee.to); |
| const result = await evaluateExpression(context, expression, 'argumentsHint'); |
| if (result?.type !== 'function') { |
| return null; |
| } |
| const objGetter = async(): Promise<SDK.RemoteObject.RemoteObject|null> => { |
| const first = callee.firstChild; |
| if (!first || callee.name !== 'MemberExpression') { |
| return null; |
| } |
| return await evaluateExpression(context, doc.sliceString(first.from, first.to), 'argumentsHint'); |
| }; |
| return await getArgumentsForFunctionValue(result, objGetter, expression) |
| .finally(() => context.runtimeModel.releaseObjectGroup('argumentsHint')); |
| } |
| |
| export function argumentsList(input: string): string[] { |
| function parseParamList(cursor: CodeMirror.TreeCursor): string[] { |
| while (cursor.name !== 'ParamList') { |
| cursor.nextSibling(); |
| } |
| const parameters = []; |
| if (cursor.name === 'ParamList' && cursor.firstChild()) { |
| let prefix = ''; |
| do { |
| switch (cursor.name as string) { |
| case 'ArrayPattern': |
| parameters.push(prefix + 'arr'); |
| prefix = ''; |
| break; |
| case 'ObjectPattern': |
| parameters.push(prefix + 'obj'); |
| prefix = ''; |
| break; |
| case 'VariableDefinition': |
| parameters.push(prefix + input.slice(cursor.from, cursor.to)); |
| prefix = ''; |
| break; |
| case 'Spread': |
| prefix = '...'; |
| break; |
| } |
| } while (cursor.nextSibling()); |
| } |
| return parameters; |
| } |
| try { |
| try { |
| // First check if the |input| can be parsed as a method definition. |
| const {parser} = CodeMirror.javascript.javascriptLanguage.configure({strict: true, top: 'SingleClassItem'}); |
| const cursor = parser.parse(input).cursor(); |
| if (cursor.firstChild() && cursor.name === 'MethodDeclaration' && cursor.firstChild()) { |
| return parseParamList(cursor); |
| } |
| throw new Error('SingleClassItem rule is expected to have exactly one MethodDeclaration child'); |
| } catch { |
| // Otherwise fall back to parsing as an expression. |
| const {parser} = CodeMirror.javascript.javascriptLanguage.configure({strict: true, top: 'SingleExpression'}); |
| const cursor = parser.parse(input).cursor(); |
| if (!cursor.firstChild()) { |
| throw new Error('SingleExpression rule is expected to have children'); |
| } |
| switch (cursor.name) { |
| case 'ArrowFunction': |
| case 'FunctionExpression': { |
| if (!cursor.firstChild()) { |
| throw new Error(`${cursor.name} rule is expected to have children`); |
| } |
| return parseParamList(cursor); |
| } |
| case 'ClassExpression': { |
| if (!cursor.firstChild()) { |
| throw new Error(`${cursor.name} rule is expected to have children`); |
| } |
| do { |
| cursor.nextSibling(); |
| } while (cursor.name as string !== 'ClassBody'); |
| if (cursor.name as string === 'ClassBody' && cursor.firstChild()) { |
| do { |
| if (cursor.name as string === 'MethodDeclaration' && cursor.firstChild()) { |
| if (cursor.name as string === 'PropertyDefinition' && |
| input.slice(cursor.from, cursor.to) === 'constructor') { |
| return parseParamList(cursor); |
| } |
| cursor.parent(); |
| } |
| } while (cursor.nextSibling()); |
| } |
| return []; |
| } |
| } |
| throw new Error('Unexpected expression'); |
| } |
| } catch (cause) { |
| throw new Error(`Failed to parse for arguments list: ${input}`, {cause}); |
| } |
| } |
| |
| async function getArgumentsForFunctionValue( |
| object: SDK.RemoteObject.RemoteObject, |
| receiverObjGetter: () => Promise<SDK.RemoteObject.RemoteObject|null>, |
| functionName?: string, |
| ): Promise<string[][]|null> { |
| const description = object.description; |
| if (!description) { |
| return null; |
| } |
| if (!description.endsWith('{ [native code] }')) { |
| return [argumentsList(description)]; |
| } |
| |
| // Check if this is a bound function. |
| if (description === 'function () { [native code] }') { |
| const fromBound = await getArgumentsForBoundFunction(object); |
| if (fromBound) { |
| return fromBound; |
| } |
| } |
| |
| const javaScriptMetadata = JavaScriptMetaData.JavaScriptMetadata.JavaScriptMetadataImpl.instance(); |
| |
| const descriptionRegexResult = /^function ([^(]*)\(/.exec(description); |
| const name = descriptionRegexResult?.[1] || functionName; |
| if (!name) { |
| return null; |
| } |
| const uniqueSignatures = javaScriptMetadata.signaturesForNativeFunction(name); |
| if (uniqueSignatures) { |
| return uniqueSignatures; |
| } |
| const receiverObj = await receiverObjGetter(); |
| if (!receiverObj) { |
| return null; |
| } |
| const className = receiverObj.className; |
| if (className) { |
| const instanceMethods = javaScriptMetadata.signaturesForInstanceMethod(name, className); |
| if (instanceMethods) { |
| return instanceMethods; |
| } |
| } |
| |
| // Check for static methods on a constructor. |
| if (receiverObj.description && receiverObj.type === 'function' && |
| receiverObj.description.endsWith('{ [native code] }')) { |
| const receiverDescriptionRegexResult = /^function ([^(]*)\(/.exec(receiverObj.description); |
| if (receiverDescriptionRegexResult) { |
| const receiverName = receiverDescriptionRegexResult[1]; |
| const staticSignatures = javaScriptMetadata.signaturesForStaticMethod(name, receiverName); |
| if (staticSignatures) { |
| return staticSignatures; |
| } |
| } |
| } |
| |
| for (const proto of await prototypesFromObject(receiverObj)) { |
| const instanceSignatures = javaScriptMetadata.signaturesForInstanceMethod(name, proto); |
| if (instanceSignatures) { |
| return instanceSignatures; |
| } |
| } |
| return null; |
| } |
| |
| async function prototypesFromObject(object: SDK.RemoteObject.RemoteObject): Promise<string[]> { |
| if (object.type === 'number') { |
| return ['Number', 'Object']; |
| } |
| if (object.type === 'string') { |
| return ['String', 'Object']; |
| } |
| if (object.type === 'symbol') { |
| return ['Symbol', 'Object']; |
| } |
| if (object.type === 'bigint') { |
| return ['BigInt', 'Object']; |
| } |
| if (object.type === 'boolean') { |
| return ['Boolean', 'Object']; |
| } |
| if (object.type === 'undefined' || object.subtype === 'null') { |
| return []; |
| } |
| return await object.callFunctionJSON(function(this: Object) { |
| const result = []; |
| for (let object = this; object; object = Object.getPrototypeOf(object)) { |
| if (typeof object === 'object' && object.constructor?.name) { |
| result[result.length] = object.constructor.name; |
| } |
| } |
| return result; |
| }, []) ?? []; |
| } |
| |
| /** |
| * Given a function object that is probably a bound function, try to |
| * retrieve the argument list from its target function. |
| **/ |
| async function getArgumentsForBoundFunction(object: SDK.RemoteObject.RemoteObject): Promise<string[][]|null> { |
| const {internalProperties} = await object.getOwnProperties(false); |
| if (!internalProperties) { |
| return null; |
| } |
| const target = internalProperties.find(p => p.name === '[[TargetFunction]]')?.value; |
| const args = internalProperties.find(p => p.name === '[[BoundArgs]]')?.value; |
| const thisValue = internalProperties.find(p => p.name === '[[BoundThis]]')?.value; |
| if (!thisValue || !target || !args) { |
| return null; |
| } |
| const originalSignatures = await getArgumentsForFunctionValue(target, () => Promise.resolve(thisValue)); |
| const boundArgsLength = SDK.RemoteObject.RemoteObject.arrayLength(args); |
| if (!originalSignatures) { |
| return null; |
| } |
| return originalSignatures.map(signature => { |
| const restIndex = signature.findIndex(arg => arg.startsWith('...')); |
| return restIndex > -1 && restIndex < boundArgsLength ? signature.slice(restIndex) : |
| signature.slice(boundArgsLength); |
| }); |
| } |
| |
| function tooltipBuilder(signatures: string[][], currentIndex: number): {dom: HTMLElement} { |
| const tooltip = document.createElement('div'); |
| tooltip.className = 'cm-argumentHints'; |
| for (const args of signatures) { |
| const argumentsElement = document.createElement('span'); |
| for (let i = 0; i < args.length; i++) { |
| if (i === currentIndex || (i < currentIndex && args[i].startsWith('...'))) { |
| const argElement = argumentsElement.appendChild(document.createElement('b')); |
| argElement.appendChild(document.createTextNode(args[i])); |
| } else { |
| argumentsElement.appendChild(document.createTextNode(args[i])); |
| } |
| if (i < args.length - 1) { |
| argumentsElement.appendChild(document.createTextNode(', ')); |
| } |
| } |
| const signatureElement = tooltip.appendChild(document.createElement('div')); |
| signatureElement.className = 'source-code'; |
| signatureElement.appendChild(document.createTextNode('\u0192(')); |
| signatureElement.appendChild(argumentsElement); |
| signatureElement.appendChild(document.createTextNode(')')); |
| } |
| return {dom: tooltip}; |
| } |