| // 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; |