chrome-devtools-frontend
Version:
Chrome DevTools UI
769 lines (712 loc) • 27.4 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Root from '../../../core/root/root.js';
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 {closeTooltip, cursorTooltip, type ArgumentHintsTooltip} 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: boolean = 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): boolean => o.label.startsWith(query)).map((o): UI.SuggestBox.Suggestion => ({
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,
PropertyName = 1,
PropertyExpression = 2,
PotentiallyRetrievingFromMap = 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.PropertyName, 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.PropertyExpression, 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.PropertyExpression, relatedNode: node};
}
if (before?.name === '.' || before?.name === '?.') {
return {type: QueryType.PropertyName, 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.PotentiallyRetrievingFromMap, 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.PropertyName || query.type === QueryType.PropertyExpression) {
const objectExpr = (query.relatedNode as CodeMirror.SyntaxNode).getChild('Expression');
if (query.type === QueryType.PropertyExpression) {
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.PotentiallyRetrievingFromMap) {
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,
},
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: Map<string, Promise<CompletionSet>> = new Map();
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: boolean = false,
): Promise<CompletionSet> {
const cache = PropertyCache.instance();
if (!quoted) {
const cached = cache.get(expression);
if (cached) {
return cached;
}
}
const context = getExecutionContext();
if (!context) {
return new CompletionSet();
}
const result = completePropertiesInner(expression, context, quoted, hasBracket);
if (!quoted) {
cache.set(expression, result);
}
return result;
}
async function completePropertiesInner(
expression: string,
context: SDK.RuntimeModel.ExecutionContext,
quoted?: string,
hasBracket: boolean = 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')) &&
(!prop.private || expression === 'this') && (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 scopeObjectForScope = (scope: SDK.DebuggerModel.Scope): SDK.RemoteObject.RemoteObject =>
// TODO(crbug.com/1444349): Inline into `map` call below when experiment is removed.
Root.Runtime.experiments.isEnabled('evaluateExpressionsWithSourceMaps') ?
SourceMapScopes.NamesResolver.resolveScopeInObject(scope) :
scope.object();
const scopes = await Promise.all(
selectedFrame.scopeChain().map(scope => scopeObjectForScope(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 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 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 || !result.exceptionDetails || !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 (): {dom: HTMLElement} => 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 || 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 evaluateExpression(context, doc.sliceString(first.from, first.to), 'argumentsHint');
};
return 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`);
}
while (cursor.nextSibling() && 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 && 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() {
const result = [];
for (let object: Object = this; object; object = Object.getPrototypeOf(object)) {
if (typeof object === 'object' && object.constructor && 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};
}