mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 01:38:07 +08:00
https://i.imgur.com/Y3OlPM2.png Change-Id: Ic1d9ef1d47ff93ce77bfc3ae7173689bccf0cb2c Reviewed-on: https://chromium-review.googlesource.com/1031096 Reviewed-by: Andrey Lushnikov <lushnikov@chromium.org> Commit-Queue: Joel Einbinder <einbinder@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#557030} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 66bdf19b13c9a1dffbd47121101e0b8aedde3495
498 lines
19 KiB
JavaScript
498 lines
19 KiB
JavaScript
// 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.
|
|
|
|
ObjectUI.JavaScriptAutocomplete = class {
|
|
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<string>, argumentIndex: number}>}
|
|
*/
|
|
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')
|
|
return null;
|
|
executionContext.runtimeModel.releaseObjectGroup('argumentsHint');
|
|
|
|
const description = result.object.description;
|
|
if (description.endsWith('{ [native code] }'))
|
|
return null; // TODO(einbinder) support native function argument hints
|
|
const args = await Formatter.formatterWorkerPool().argumentsList(description);
|
|
|
|
if (!args.length)
|
|
return null;
|
|
return {args, argumentIndex: functionCall.argumentIndex};
|
|
}
|
|
|
|
/**
|
|
* @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.getOwnPropertiesPromise(false);
|
|
const internalProperties = properties.internalProperties || [];
|
|
const entriesProperty = internalProperties.find(property => property.name === '[[Entries]]');
|
|
if (!entriesProperty)
|
|
return [];
|
|
const keysObj = await entriesProperty.value.callFunctionJSONPromise(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 {ObjectUI.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.getOwnPropertiesPromise(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.callFunctionJSONPromise(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 = 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' && 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()
|
|
.getAllPropertiesPromise(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'
|
|
];
|
|
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) {
|
|
const keywords = [
|
|
'break', 'case', 'catch', 'continue', 'default', 'delete', 'do', 'else', 'finally',
|
|
'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this',
|
|
'throw', 'try', 'typeof', 'var', 'void', 'while', 'with'
|
|
];
|
|
propertyGroups.push({title: Common.UIString('keywords'), items: keywords});
|
|
}
|
|
|
|
/** @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: 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);
|
|
}
|
|
};
|
|
|
|
/** @typedef {{title:(string|undefined), items:Array<string>}} */
|
|
ObjectUI.JavaScriptAutocomplete.CompletionGroup;
|
|
|
|
ObjectUI.javaScriptAutocomplete = new ObjectUI.JavaScriptAutocomplete(); |