| // Copyright 2016 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. |
| |
| export default class JavaScriptAutocomplete { |
| constructor() { |
| /** @type {!Map<string, {date: number, value: !Promise<?Object>}>} */ |
| this._expressionCache = new Map(); |
| SDK.consoleModel.addEventListener(SDK.ConsoleModel.Events.CommandEvaluated, this._clearCache, this); |
| UI.context.addFlavorChangeListener(SDK.ExecutionContext, this._clearCache, this); |
| SDK.targetManager.addModelListener( |
| SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, this._clearCache, this); |
| SDK.targetManager.addModelListener( |
| SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, this._clearCache, this); |
| } |
| |
| _clearCache() { |
| this._expressionCache.clear(); |
| } |
| |
| /** |
| * @param {string} fullText |
| * @param {string} query |
| * @param {boolean=} force |
| * @return {!Promise<!UI.SuggestBox.Suggestions>} |
| */ |
| async completionsForTextInCurrentContext(fullText, query, force) { |
| const trimmedText = fullText.trim(); |
| |
| const [mapCompletions, expressionCompletions] = await Promise.all( |
| [this._mapCompletions(trimmedText, query), this._completionsForExpression(trimmedText, query, force)]); |
| return mapCompletions.concat(expressionCompletions); |
| } |
| |
| /** |
| * @param {string} fullText |
| * @return {!Promise<?{args: !Array<!Array<string>>, argumentIndex: number}|undefined>} |
| */ |
| async argumentsHint(fullText) { |
| const functionCall = await Formatter.formatterWorkerPool().findLastFunctionCall(fullText); |
| if (!functionCall) { |
| return null; |
| } |
| const executionContext = UI.context.flavor(SDK.ExecutionContext); |
| if (!executionContext) { |
| return null; |
| } |
| const result = await executionContext.evaluate( |
| { |
| expression: functionCall.baseExpression, |
| objectGroup: 'argumentsHint', |
| includeCommandLineAPI: true, |
| silent: true, |
| returnByValue: false, |
| generatePreview: false, |
| throwOnSideEffect: functionCall.possibleSideEffects, |
| timeout: functionCall.possibleSideEffects ? 500 : undefined |
| }, |
| /* userGesture */ false, /* awaitPromise */ false); |
| if (!result || result.exceptionDetails || !result.object || result.object.type !== 'function') { |
| executionContext.runtimeModel.releaseObjectGroup('argumentsHint'); |
| return null; |
| } |
| |
| const args = await this._argumentsForFunction(result.object, async () => { |
| const result = await executionContext.evaluate( |
| { |
| expression: functionCall.receiver, |
| objectGroup: 'argumentsHint', |
| includeCommandLineAPI: true, |
| silent: true, |
| returnByValue: false, |
| generatePreview: false, |
| throwOnSideEffect: functionCall.possibleSideEffects, |
| timeout: functionCall.possibleSideEffects ? 500 : undefined |
| }, |
| /* userGesture */ false, /* awaitPromise */ false); |
| return (result && !result.exceptionDetails && result.object) ? result.object : null; |
| }, functionCall.functionName); |
| executionContext.runtimeModel.releaseObjectGroup('argumentsHint'); |
| if (!args.length || (args.length === 1 && !args[0].length)) { |
| return null; |
| } |
| return {args, argumentIndex: functionCall.argumentIndex}; |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} functionObject |
| * @param {function():!Promise<?SDK.RemoteObject>} receiverObjGetter |
| * @param {string=} parsedFunctionName |
| * @return {!Promise<!Array<!Array<string>>>} |
| */ |
| async _argumentsForFunction(functionObject, receiverObjGetter, parsedFunctionName) { |
| const description = functionObject.description; |
| if (!description.endsWith('{ [native code] }')) { |
| return [await Formatter.formatterWorkerPool().argumentsList(description)]; |
| } |
| |
| // Check if this is a bound function. |
| if (description === 'function () { [native code] }') { |
| const properties = await functionObject.getOwnProperties(false); |
| const internalProperties = properties.internalProperties || []; |
| const targetProperty = internalProperties.find(property => property.name === '[[TargetFunction]]'); |
| const argsProperty = internalProperties.find(property => property.name === '[[BoundArgs]]'); |
| const thisProperty = internalProperties.find(property => property.name === '[[BoundThis]]'); |
| if (thisProperty && targetProperty && argsProperty) { |
| const originalSignatures = |
| await this._argumentsForFunction(targetProperty.value, () => Promise.resolve(thisProperty.value)); |
| const boundArgsLength = SDK.RemoteObject.arrayLength(argsProperty.value); |
| const clippedArgs = []; |
| for (const signature of originalSignatures) { |
| const restIndex = signature.slice(0, boundArgsLength).findIndex(arg => arg.startsWith('...')); |
| if (restIndex !== -1) { |
| clippedArgs.push(signature.slice(restIndex)); |
| } else { |
| clippedArgs.push(signature.slice(boundArgsLength)); |
| } |
| } |
| return clippedArgs; |
| } |
| } |
| const javaScriptMetadata = await self.runtime.extension(Common.JavaScriptMetadata).instance(); |
| |
| const name = /^function ([^(]*)\(/.exec(description)[1] || parsedFunctionName; |
| if (!name) { |
| return []; |
| } |
| const uniqueSignatures = javaScriptMetadata.signaturesForNativeFunction(name); |
| if (uniqueSignatures) { |
| return uniqueSignatures; |
| } |
| const receiverObj = await receiverObjGetter(); |
| const className = receiverObj.className; |
| if (javaScriptMetadata.signaturesForInstanceMethod(name, className)) { |
| return javaScriptMetadata.signaturesForInstanceMethod(name, className); |
| } |
| |
| // Check for static methods on a constructor. |
| if (receiverObj.type === 'function' && receiverObj.description.endsWith('{ [native code] }')) { |
| const receiverName = /^function ([^(]*)\(/.exec(receiverObj.description)[1]; |
| const staticSignatures = javaScriptMetadata.signaturesForStaticMethod(name, receiverName); |
| if (staticSignatures) { |
| return staticSignatures; |
| } |
| } |
| |
| |
| let protoNames; |
| if (receiverObj.type === 'number') { |
| protoNames = ['Number', 'Object']; |
| } else if (receiverObj.type === 'string') { |
| protoNames = ['String', 'Object']; |
| } else if (receiverObj.type === 'symbol') { |
| protoNames = ['Symbol', 'Object']; |
| } else if (receiverObj.type === 'bigint') { |
| protoNames = ['BigInt', 'Object']; |
| } else if (receiverObj.type === 'boolean') { |
| protoNames = ['Boolean', 'Object']; |
| } else if (receiverObj.type === 'undefined' || receiverObj.subtype === 'null') { |
| protoNames = []; |
| } else { |
| protoNames = await receiverObj.callFunctionJSON(function() { |
| const result = []; |
| for (let object = this; object; object = Object.getPrototypeOf(object)) { |
| if (typeof object === 'object' && object.constructor && object.constructor.name) { |
| result[result.length] = object.constructor.name; |
| } |
| } |
| return result; |
| }, []); |
| } |
| for (const proto of protoNames) { |
| const instanceSignatures = javaScriptMetadata.signaturesForInstanceMethod(name, proto); |
| if (instanceSignatures) { |
| return instanceSignatures; |
| } |
| } |
| return []; |
| } |
| |
| /** |
| * @param {string} text |
| * @param {string} query |
| * @return {!Promise<!UI.SuggestBox.Suggestions>} |
| */ |
| async _mapCompletions(text, query) { |
| const mapMatch = text.match(/\.\s*(get|set|delete)\s*\(\s*$/); |
| const executionContext = UI.context.flavor(SDK.ExecutionContext); |
| if (!executionContext || !mapMatch) { |
| return []; |
| } |
| |
| const expression = await Formatter.formatterWorkerPool().findLastExpression(text.substring(0, mapMatch.index)); |
| if (!expression) { |
| return []; |
| } |
| |
| const result = await executionContext.evaluate( |
| { |
| expression: expression.baseExpression, |
| objectGroup: 'mapCompletion', |
| includeCommandLineAPI: true, |
| silent: true, |
| returnByValue: false, |
| generatePreview: false, |
| throwOnSideEffect: expression.possibleSideEffects, |
| timeout: expression.possibleSideEffects ? 500 : undefined |
| }, |
| /* userGesture */ false, /* awaitPromise */ false); |
| if (result.error || !!result.exceptionDetails || result.object.subtype !== 'map') { |
| return []; |
| } |
| const properties = await result.object.getOwnProperties(false); |
| const internalProperties = properties.internalProperties || []; |
| const entriesProperty = internalProperties.find(property => property.name === '[[Entries]]'); |
| if (!entriesProperty) { |
| return []; |
| } |
| const keysObj = await entriesProperty.value.callFunctionJSON(getEntries); |
| executionContext.runtimeModel.releaseObjectGroup('mapCompletion'); |
| return gotKeys(Object.keys(keysObj)); |
| |
| /** |
| * @suppressReceiverCheck |
| * @this {!Array<{key:?, value:?}>} |
| * @return {!Object} |
| */ |
| function getEntries() { |
| const result = {__proto__: null}; |
| for (let i = 0; i < this.length; i++) { |
| if (typeof this[i].key === 'string') { |
| result[this[i].key] = true; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @param {!Array<string>} rawKeys |
| * @return {!UI.SuggestBox.Suggestions} |
| */ |
| function gotKeys(rawKeys) { |
| const caseSensitivePrefix = []; |
| const caseInsensitivePrefix = []; |
| const caseSensitiveAnywhere = []; |
| const caseInsensitiveAnywhere = []; |
| let quoteChar = '"'; |
| if (query.startsWith('\'')) { |
| quoteChar = '\''; |
| } |
| let endChar = ')'; |
| if (mapMatch[0].indexOf('set') !== -1) { |
| endChar = ', '; |
| } |
| |
| const sorter = rawKeys.length < 1000 ? String.naturalOrderComparator : undefined; |
| const keys = rawKeys.sort(sorter).map(key => quoteChar + key + quoteChar); |
| |
| for (const key of keys) { |
| if (key.length < query.length) { |
| continue; |
| } |
| if (query.length && key.toLowerCase().indexOf(query.toLowerCase()) === -1) { |
| continue; |
| } |
| // Substitute actual newlines with newline characters. @see crbug.com/498421 |
| const title = key.split('\n').join('\\n'); |
| const text = title + endChar; |
| |
| if (key.startsWith(query)) { |
| caseSensitivePrefix.push({text: text, title: title, priority: 4}); |
| } else if (key.toLowerCase().startsWith(query.toLowerCase())) { |
| caseInsensitivePrefix.push({text: text, title: title, priority: 3}); |
| } else if (key.indexOf(query) !== -1) { |
| caseSensitiveAnywhere.push({text: text, title: title, priority: 2}); |
| } else { |
| caseInsensitiveAnywhere.push({text: text, title: title, priority: 1}); |
| } |
| } |
| const suggestions = |
| caseSensitivePrefix.concat(caseInsensitivePrefix, caseSensitiveAnywhere, caseInsensitiveAnywhere); |
| if (suggestions.length) { |
| suggestions[0].subtitle = Common.UIString('Keys'); |
| } |
| return suggestions; |
| } |
| } |
| |
| /** |
| * @param {string} fullText |
| * @param {string} query |
| * @param {boolean=} force |
| * @return {!Promise<!UI.SuggestBox.Suggestions>} |
| */ |
| async _completionsForExpression(fullText, query, force) { |
| const executionContext = UI.context.flavor(SDK.ExecutionContext); |
| if (!executionContext) { |
| return []; |
| } |
| let expression; |
| if (fullText.endsWith('.') || fullText.endsWith('[')) { |
| expression = await Formatter.formatterWorkerPool().findLastExpression(fullText.substring(0, fullText.length - 1)); |
| } |
| if (!expression) { |
| if (fullText.endsWith('.')) { |
| return []; |
| } |
| expression = {baseExpression: '', possibleSideEffects: false}; |
| } |
| const needsNoSideEffects = expression.possibleSideEffects; |
| const expressionString = expression.baseExpression; |
| |
| |
| const dotNotation = fullText.endsWith('.'); |
| const bracketNotation = !!expressionString && fullText.endsWith('['); |
| |
| // User is entering float value, do not suggest anything. |
| if ((expressionString && !isNaN(expressionString)) || (!expressionString && query && !isNaN(query))) { |
| return []; |
| } |
| |
| |
| if (!query && !expressionString && !force) { |
| return []; |
| } |
| const selectedFrame = executionContext.debuggerModel.selectedCallFrame(); |
| let completionGroups; |
| const TEN_SECONDS = 10000; |
| let cache = this._expressionCache.get(expressionString); |
| if (cache && cache.date + TEN_SECONDS > Date.now()) { |
| completionGroups = await cache.value; |
| } else if (!expressionString && selectedFrame) { |
| cache = {date: Date.now(), value: completionsOnPause(selectedFrame)}; |
| this._expressionCache.set(expressionString, cache); |
| completionGroups = await cache.value; |
| } else { |
| const resultPromise = executionContext.evaluate( |
| { |
| expression: expressionString, |
| objectGroup: 'completion', |
| includeCommandLineAPI: true, |
| silent: true, |
| returnByValue: false, |
| generatePreview: false, |
| throwOnSideEffect: needsNoSideEffects, |
| timeout: needsNoSideEffects ? 500 : undefined |
| }, |
| /* userGesture */ false, /* awaitPromise */ false); |
| cache = {date: Date.now(), value: resultPromise.then(result => completionsOnGlobal.call(this, result))}; |
| this._expressionCache.set(expressionString, cache); |
| completionGroups = await cache.value; |
| } |
| return this._receivedPropertyNames( |
| completionGroups.slice(0), dotNotation, bracketNotation, expressionString, query); |
| |
| /** |
| * @this {JavaScriptAutocomplete} |
| * @param {!SDK.RuntimeModel.EvaluationResult} result |
| * @return {!Promise<!Array<!ObjectUI.JavaScriptAutocomplete.CompletionGroup>>} |
| */ |
| async function completionsOnGlobal(result) { |
| if (result.error || !!result.exceptionDetails || !result.object) { |
| return []; |
| } |
| |
| let object = result.object; |
| while (object && object.type === 'object' && object.subtype === 'proxy') { |
| const properties = await object.getOwnProperties(false /* generatePreview */); |
| const internalProperties = properties.internalProperties || []; |
| const target = internalProperties.find(property => property.name === '[[Target]]'); |
| object = target ? target.value : null; |
| } |
| if (!object) { |
| return []; |
| } |
| let completions = []; |
| if (object.type === 'object' || object.type === 'function') { |
| completions = |
| await object.callFunctionJSON(getCompletions, [SDK.RemoteObject.toCallArgument(object.subtype)]) || []; |
| } else if ( |
| object.type === 'string' || object.type === 'number' || object.type === 'boolean' || |
| object.type === 'bigint') { |
| const evaluateResult = await executionContext.evaluate( |
| { |
| expression: '(' + getCompletions + ')("' + object.type + '")', |
| objectGroup: 'completion', |
| includeCommandLineAPI: false, |
| silent: true, |
| returnByValue: true, |
| generatePreview: false |
| }, |
| /* userGesture */ false, |
| /* awaitPromise */ false); |
| if (evaluateResult.object && !evaluateResult.exceptionDetails) { |
| completions = /** @type {!Iterable} */ (evaluateResult.object.value) || []; |
| } |
| } |
| executionContext.runtimeModel.releaseObjectGroup('completion'); |
| |
| if (!expressionString) { |
| const globalNames = await executionContext.globalLexicalScopeNames(); |
| // Merge lexical scope names with first completion group on global object: let a and let b should be in the same group. |
| if (completions.length) { |
| completions[0].items = completions[0].items.concat(globalNames); |
| } else { |
| completions.push({items: globalNames.sort(), title: Common.UIString('Lexical scope variables')}); |
| } |
| } |
| |
| for (const group of completions) { |
| for (let i = 0; i < group.items.length; i++) { |
| group.items[i] = group.items[i].replace(/\n/g, '\\n'); |
| } |
| |
| group.items.sort(group.items.length < 1000 ? this._itemComparator : undefined); |
| } |
| |
| return completions; |
| |
| /** |
| * @param {string=} type |
| * @return {!Object} |
| * @suppressReceiverCheck |
| * @this {Object} |
| */ |
| function getCompletions(type) { |
| let object; |
| if (type === 'string') { |
| object = new String(''); |
| } else if (type === 'number') { |
| object = new Number(0); |
| } |
| // Object-wrapped BigInts cannot be constructed via `new BigInt`. |
| else if (type === 'bigint') { |
| object = Object(BigInt(0)); |
| } else if (type === 'boolean') { |
| object = new Boolean(false); |
| } else { |
| object = this; |
| } |
| |
| const result = []; |
| try { |
| for (let o = object; o; o = Object.getPrototypeOf(o)) { |
| if ((type === 'array' || type === 'typedarray') && o === object && o.length > 9999) { |
| continue; |
| } |
| |
| const group = {items: [], __proto__: null}; |
| try { |
| if (typeof o === 'object' && Object.prototype.hasOwnProperty.call(o, 'constructor') && o.constructor && |
| o.constructor.name) { |
| group.title = o.constructor.name; |
| } |
| } catch (ee) { |
| // we could break upon cross origin check. |
| } |
| result[result.length] = group; |
| const names = Object.getOwnPropertyNames(o); |
| const isArray = Array.isArray(o); |
| for (let i = 0; i < names.length && group.items.length < 10000; ++i) { |
| // Skip array elements indexes. |
| if (isArray && /^[0-9]/.test(names[i])) { |
| continue; |
| } |
| group.items[group.items.length] = names[i]; |
| } |
| } |
| } catch (e) { |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * @param {!SDK.DebuggerModel.CallFrame} callFrame |
| * @return {!Promise<?Object>} |
| */ |
| async function completionsOnPause(callFrame) { |
| const result = [{items: ['this']}]; |
| const scopeChain = callFrame.scopeChain(); |
| const groupPromises = []; |
| for (const scope of scopeChain) { |
| groupPromises.push(scope.object() |
| .getAllProperties(false /* accessorPropertiesOnly */, false /* generatePreview */) |
| .then(result => ({properties: result.properties, name: scope.name()}))); |
| } |
| const fullScopes = await Promise.all(groupPromises); |
| executionContext.runtimeModel.releaseObjectGroup('completion'); |
| for (const scope of fullScopes) { |
| result.push({title: scope.name, items: scope.properties.map(property => property.name).sort()}); |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * @param {?Array<!ObjectUI.JavaScriptAutocomplete.CompletionGroup>} propertyGroups |
| * @param {boolean} dotNotation |
| * @param {boolean} bracketNotation |
| * @param {string} expressionString |
| * @param {string} query |
| * @return {!UI.SuggestBox.Suggestions} |
| */ |
| _receivedPropertyNames(propertyGroups, dotNotation, bracketNotation, expressionString, query) { |
| if (!propertyGroups) { |
| return []; |
| } |
| const includeCommandLineAPI = (!dotNotation && !bracketNotation); |
| if (includeCommandLineAPI) { |
| const commandLineAPI = [ |
| 'dir', |
| 'dirxml', |
| 'keys', |
| 'values', |
| 'profile', |
| 'profileEnd', |
| 'monitorEvents', |
| 'unmonitorEvents', |
| 'inspect', |
| 'copy', |
| 'clear', |
| 'getEventListeners', |
| 'debug', |
| 'undebug', |
| 'monitor', |
| 'unmonitor', |
| 'table', |
| 'queryObjects', |
| '$', |
| '$$', |
| '$x', |
| '$0', |
| '$_' |
| ]; |
| propertyGroups.push({items: commandLineAPI}); |
| } |
| return this._completionsForQuery(dotNotation, bracketNotation, expressionString, query, propertyGroups); |
| } |
| |
| /** |
| * @param {boolean} dotNotation |
| * @param {boolean} bracketNotation |
| * @param {string} expressionString |
| * @param {string} query |
| * @param {!Array<!ObjectUI.JavaScriptAutocomplete.CompletionGroup>} propertyGroups |
| * @return {!UI.SuggestBox.Suggestions} |
| */ |
| _completionsForQuery(dotNotation, bracketNotation, expressionString, query, propertyGroups) { |
| const quoteUsed = (bracketNotation && query.startsWith('\'')) ? '\'' : '"'; |
| |
| if (!expressionString) { |
| // See ES2017 spec: https://www.ecma-international.org/ecma-262/8.0/index.html |
| const keywords = [ |
| // Section 11.6.2.1 Reserved keywords. |
| 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', |
| 'exports', 'extends', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return', |
| 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', |
| |
| // Section 11.6.2.1's note mentions words treated as reserved in certain cases. |
| 'let', 'static', |
| |
| // Other keywords not explicitly reserved by spec. |
| 'async', 'of' |
| ]; |
| propertyGroups.push({title: ls`keywords`, items: keywords.sort()}); |
| } |
| |
| /** @type {!Set<string>} */ |
| const allProperties = new Set(); |
| let result = []; |
| let lastGroupTitle; |
| const regex = /^[a-zA-Z_$\u008F-\uFFFF][a-zA-Z0-9_$\u008F-\uFFFF]*$/; |
| const lowerCaseQuery = query.toLowerCase(); |
| for (const group of propertyGroups) { |
| const caseSensitivePrefix = []; |
| const caseInsensitivePrefix = []; |
| const caseSensitiveAnywhere = []; |
| const caseInsensitiveAnywhere = []; |
| |
| for (let i = 0; i < group.items.length; i++) { |
| let property = group.items[i]; |
| // Assume that all non-ASCII characters are letters and thus can be used as part of identifier. |
| if (!bracketNotation && !regex.test(property)) { |
| continue; |
| } |
| |
| if (bracketNotation) { |
| if (!/^[0-9]+$/.test(property)) { |
| property = quoteUsed + property.escapeCharacters(quoteUsed + '\\') + quoteUsed; |
| } |
| property += ']'; |
| } |
| if (allProperties.has(property)) { |
| continue; |
| } |
| |
| if (property.length < query.length) { |
| continue; |
| } |
| const lowerCaseProperty = property.toLowerCase(); |
| if (query.length && lowerCaseProperty.indexOf(lowerCaseQuery) === -1) { |
| continue; |
| } |
| |
| allProperties.add(property); |
| if (property.startsWith(query)) { |
| caseSensitivePrefix.push({text: property, priority: property === query ? 5 : 4}); |
| } else if (lowerCaseProperty.startsWith(lowerCaseQuery)) { |
| caseInsensitivePrefix.push({text: property, priority: 3}); |
| } else if (property.indexOf(query) !== -1) { |
| caseSensitiveAnywhere.push({text: property, priority: 2}); |
| } else { |
| caseInsensitiveAnywhere.push({text: property, priority: 1}); |
| } |
| } |
| const structuredGroup = |
| caseSensitivePrefix.concat(caseInsensitivePrefix, caseSensitiveAnywhere, caseInsensitiveAnywhere); |
| if (structuredGroup.length && group.title !== lastGroupTitle) { |
| structuredGroup[0].subtitle = group.title; |
| lastGroupTitle = group.title; |
| } |
| result = result.concat(structuredGroup); |
| result.forEach(item => { |
| if (item.text.endsWith(']')) { |
| item.title = item.text.substring(0, item.text.length - 1); |
| } |
| }); |
| } |
| return result; |
| } |
| |
| /** |
| * @param {string} a |
| * @param {string} b |
| * @return {number} |
| */ |
| _itemComparator(a, b) { |
| const aStartsWithUnderscore = a.startsWith('_'); |
| const bStartsWithUnderscore = b.startsWith('_'); |
| if (aStartsWithUnderscore && !bStartsWithUnderscore) { |
| return 1; |
| } |
| if (bStartsWithUnderscore && !aStartsWithUnderscore) { |
| return -1; |
| } |
| return String.naturalOrderComparator(a, b); |
| } |
| |
| /** |
| * @param {string} expression |
| * @return {!Promise<boolean>} |
| */ |
| static async isExpressionComplete(expression) { |
| const currentExecutionContext = UI.context.flavor(SDK.ExecutionContext); |
| if (!currentExecutionContext) { |
| return true; |
| } |
| const result = |
| await currentExecutionContext.runtimeModel.compileScript(expression, '', false, currentExecutionContext.id); |
| if (!result.exceptionDetails) { |
| return true; |
| } |
| const description = result.exceptionDetails.exception.description; |
| return !description.startsWith('SyntaxError: Unexpected end of input') && |
| !description.startsWith('SyntaxError: Unterminated template literal'); |
| } |
| } |
| |
| export class JavaScriptAutocompleteConfig { |
| /** |
| * @param {!UI.TextEditor} editor |
| */ |
| constructor(editor) { |
| this._editor = editor; |
| } |
| |
| /** |
| * @param {!UI.TextEditor} editor |
| * @return {!UI.AutocompleteConfig} |
| */ |
| static createConfigForEditor(editor) { |
| const autocomplete = new JavaScriptAutocompleteConfig(editor); |
| return { |
| substituteRangeCallback: autocomplete._substituteRange.bind(autocomplete), |
| suggestionsCallback: autocomplete._suggestionsCallback.bind(autocomplete), |
| tooltipCallback: autocomplete._tooltipCallback.bind(autocomplete), |
| }; |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @return {?TextUtils.TextRange} |
| */ |
| _substituteRange(lineNumber, columnNumber) { |
| const token = this._editor.tokenAtTextPosition(lineNumber, columnNumber); |
| if (token && token.type === 'js-string') { |
| return new TextUtils.TextRange(lineNumber, token.startColumn, lineNumber, columnNumber); |
| } |
| |
| const lineText = this._editor.line(lineNumber); |
| let index; |
| for (index = columnNumber - 1; index >= 0; index--) { |
| if (' =:[({;,!+-*/&|^<>.\t\r\n'.indexOf(lineText.charAt(index)) !== -1) { |
| break; |
| } |
| } |
| return new TextUtils.TextRange(lineNumber, index + 1, lineNumber, columnNumber); |
| } |
| |
| /** |
| * @param {!TextUtils.TextRange} queryRange |
| * @param {!TextUtils.TextRange} substituteRange |
| * @param {boolean=} force |
| * @return {!Promise<!UI.SuggestBox.Suggestions>} |
| */ |
| async _suggestionsCallback(queryRange, substituteRange, force) { |
| const query = this._editor.text(queryRange); |
| const before = this._editor.text(new TextUtils.TextRange(0, 0, queryRange.startLine, queryRange.startColumn)); |
| const token = this._editor.tokenAtTextPosition(substituteRange.startLine, substituteRange.startColumn); |
| if (token) { |
| const excludedTokens = new Set(['js-comment', 'js-string-2', 'js-def']); |
| const trimmedBefore = before.trim(); |
| if (!trimmedBefore.endsWith('[') && !trimmedBefore.match(/\.\s*(get|set|delete)\s*\(\s*$/)) { |
| excludedTokens.add('js-string'); |
| } |
| if (!trimmedBefore.endsWith('.')) { |
| excludedTokens.add('js-property'); |
| } |
| if (excludedTokens.has(token.type)) { |
| return []; |
| } |
| } |
| const queryAndAfter = this._editor.line(queryRange.startLine).substring(queryRange.startColumn); |
| |
| const words = await ObjectUI.javaScriptAutocomplete.completionsForTextInCurrentContext(before, query, force); |
| if (!force && queryAndAfter && queryAndAfter !== query && |
| words.some(word => queryAndAfter.startsWith(word.text) && query.length !== word.text.length)) { |
| return []; |
| } |
| return words; |
| } |
| |
| /** |
| * @param {number} lineNumber |
| * @param {number} columnNumber |
| * @return {!Promise<?Element>} |
| */ |
| async _tooltipCallback(lineNumber, columnNumber) { |
| const before = this._editor.text(new TextUtils.TextRange(0, 0, lineNumber, columnNumber)); |
| const result = await ObjectUI.javaScriptAutocomplete.argumentsHint(before); |
| if (!result) { |
| return null; |
| } |
| const argumentIndex = result.argumentIndex; |
| const tooltip = createElement('div'); |
| for (const args of result.args) { |
| const argumentsElement = createElement('span'); |
| for (let i = 0; i < args.length; i++) { |
| if (i === argumentIndex || (i < argumentIndex && args[i].startsWith('...'))) { |
| argumentsElement.appendChild(UI.html`<b>${args[i]}</b>`); |
| } else { |
| argumentsElement.createTextChild(args[i]); |
| } |
| if (i < args.length - 1) { |
| argumentsElement.createTextChild(', '); |
| } |
| } |
| tooltip.appendChild(UI.html`<div class='source-code'>\u0192(${argumentsElement})</div>`); |
| } |
| return tooltip; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.ObjectUI = self.ObjectUI || {}; |
| |
| /* Legacy exported object */ |
| ObjectUI = ObjectUI || {}; |
| |
| /** @constructor */ |
| ObjectUI.JavaScriptAutocomplete = JavaScriptAutocomplete; |
| |
| /** @constructor */ |
| ObjectUI.JavaScriptAutocompleteConfig = JavaScriptAutocompleteConfig; |
| |
| ObjectUI.javaScriptAutocomplete = new JavaScriptAutocomplete(); |
| |
| /** @typedef {{title:(string|undefined), items:Array<string>}} */ |
| ObjectUI.JavaScriptAutocomplete.CompletionGroup; |