mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 01:38:07 +08:00
DevTools: Give argument hints in the console prompt
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
This commit is contained in:
committed by
Commit Bot
parent
4566e31350
commit
2fc0fb7ea3
@@ -703,6 +703,7 @@ all_devtools_files = [
|
||||
"front_end/terminal/xterm.js/build/xterm.js",
|
||||
"front_end/test_runner/module.json",
|
||||
"front_end/test_runner/TestRunner.js",
|
||||
"front_end/text_editor/autocompleteTooltip.css",
|
||||
"front_end/text_editor/cmdevtools.css",
|
||||
"front_end/text_editor/CodeMirrorTextEditor.js",
|
||||
"front_end/text_editor/CodeMirrorUtils.js",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// 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.
|
||||
/**
|
||||
* @unrestricted
|
||||
*/
|
||||
|
||||
Console.ConsolePrompt = class extends UI.Widget {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -11,6 +9,7 @@ Console.ConsolePrompt = class extends UI.Widget {
|
||||
this._history = new Console.ConsoleHistoryManager();
|
||||
|
||||
this._initialText = '';
|
||||
/** @type {?UI.TextEditor} */
|
||||
this._editor = null;
|
||||
this._isBelowPromptEnabled = Runtime.experiments.isEnabled('consoleBelowPrompt');
|
||||
this._eagerPreviewElement = createElementWithClass('div', 'console-eager-preview');
|
||||
@@ -25,6 +24,8 @@ Console.ConsolePrompt = class extends UI.Widget {
|
||||
this._eagerPreviewElement.classList.toggle('hidden', !this._eagerEvalSetting.get());
|
||||
|
||||
this.element.tabIndex = 0;
|
||||
/** @type {?Promise} */
|
||||
this._previewRequestForTest = null;
|
||||
|
||||
self.runtime.extension(UI.TextEditorFactory).instance().then(gotFactory.bind(this));
|
||||
|
||||
@@ -39,6 +40,7 @@ Console.ConsolePrompt = class extends UI.Widget {
|
||||
this._editor.configureAutocomplete({
|
||||
substituteRangeCallback: this._substituteRange.bind(this),
|
||||
suggestionsCallback: this._wordsWithQuery.bind(this),
|
||||
tooltipCallback: (lineNumber, columnNumber) => this._tooltipCallback(lineNumber, columnNumber)
|
||||
});
|
||||
this._editor.widget().element.addEventListener('keydown', this._editorKeyDown.bind(this), true);
|
||||
this._editor.widget().show(this.element);
|
||||
@@ -371,6 +373,35 @@ Console.ConsolePrompt = class extends UI.Widget {
|
||||
.then(words => words.concat(historyWords));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 argumentsElement = createElement('span');
|
||||
for (let i = 0; i < result.args.length; i++) {
|
||||
if (i === result.argumentIndex || (i < result.argumentIndex && result.args[i].startsWith('...'))) {
|
||||
const boldElement = createElement('b');
|
||||
boldElement.textContent = result.args[i];
|
||||
argumentsElement.appendChild(boldElement);
|
||||
} else {
|
||||
argumentsElement.createTextChild(result.args[i]);
|
||||
}
|
||||
if (i < result.args.length - 1)
|
||||
argumentsElement.createTextChild(', ');
|
||||
}
|
||||
const tooltip = createElementWithClass('span', 'source-code');
|
||||
tooltip.createTextChild('\u0192(');
|
||||
tooltip.appendChild(argumentsElement);
|
||||
tooltip.createTextChild(')');
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
_editorSetForTest() {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -239,6 +239,23 @@ Formatter.FormatterWorkerPool = class {
|
||||
return /** @type {!Promise<?{baseExpression: string, possibleSideEffects:boolean}>} */ (
|
||||
this._runTask('findLastExpression', {content}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Promise<?{baseExpression: string, possibleSideEffects:boolean, argumentIndex: number}>}
|
||||
*/
|
||||
findLastFunctionCall(content) {
|
||||
return /** @type {!Promise<?{baseExpression: string, possibleSideEffects:boolean, argumentIndex: number}>} */ (
|
||||
this._runTask('findLastFunctionCall', {content}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
argumentsList(content) {
|
||||
return /** @type {!Promise<!Array<string>>} */ (this._runTask('argumentsList', {content}));
|
||||
}
|
||||
};
|
||||
|
||||
Formatter.FormatterWorkerPool.MaxWorkers = 2;
|
||||
|
||||
@@ -87,6 +87,12 @@ self.onmessage = function(event) {
|
||||
case 'findLastExpression':
|
||||
postMessage(FormatterWorker.findLastExpression(params.content));
|
||||
break;
|
||||
case 'findLastFunctionCall':
|
||||
postMessage(FormatterWorker.findLastFunctionCall(params.content));
|
||||
break;
|
||||
case 'argumentsList':
|
||||
postMessage(FormatterWorker.argumentsList(params.content));
|
||||
break;
|
||||
default:
|
||||
console.error('Unsupport method name: ' + method);
|
||||
}
|
||||
@@ -320,6 +326,95 @@ FormatterWorker.format = function(mimeType, text, indentString) {
|
||||
postMessage(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {?{baseExpression: string, possibleSideEffects:boolean, argumentIndex: number}}
|
||||
*/
|
||||
FormatterWorker.findLastFunctionCall = function(content) {
|
||||
if (content.length > 10000)
|
||||
return null;
|
||||
try {
|
||||
const tokenizer = acorn.tokenizer(content, {ecmaVersion: 9});
|
||||
while (tokenizer.getToken().type !== acorn.tokTypes.eof) {
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = '000)';
|
||||
const base = FormatterWorker._lastCompleteExpression(content, suffix, new Set(['CallExpression', 'NewExpression']));
|
||||
if (!base)
|
||||
return null;
|
||||
const callee = base.baseNode['callee'];
|
||||
const argumentIndex = base.baseNode['arguments'].length - 1;
|
||||
const baseExpression = base.baseExpression.substring(callee.start, callee.end);
|
||||
const possibleSideEffects = FormatterWorker._nodeHasPossibleSideEffects(callee);
|
||||
return {baseExpression, possibleSideEffects, argumentIndex};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
FormatterWorker.argumentsList = function(content) {
|
||||
if (content.length > 10000)
|
||||
return [];
|
||||
let parsed = null;
|
||||
try {
|
||||
// Try to parse as a function, anonymous function, or arrow function.
|
||||
parsed = acorn.parse(`(${content})`, {ecmaVersion: 9});
|
||||
} catch (e) {
|
||||
}
|
||||
if (!parsed) {
|
||||
try {
|
||||
// Try to parse as a method.
|
||||
parsed = acorn.parse(`({${content}})`, {ecmaVersion: 9});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
if (!parsed || !parsed.body || !parsed.body[0] || !parsed.body[0].expression)
|
||||
return [];
|
||||
const expression = parsed.body[0].expression;
|
||||
let params = null;
|
||||
switch (expression.type) {
|
||||
case 'ClassExpression':
|
||||
if (!expression.body.body)
|
||||
break;
|
||||
const constructor = expression.body.body.find(method => method.kind === 'constructor');
|
||||
if (constructor)
|
||||
params = constructor.value.params;
|
||||
break;
|
||||
case 'ObjectExpression':
|
||||
if (!expression.properties[0] || !expression.properties[0].value)
|
||||
break;
|
||||
params = expression.properties[0].value.params;
|
||||
break;
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
params = expression.params;
|
||||
break;
|
||||
}
|
||||
if (!params)
|
||||
return [];
|
||||
return params.map(paramName);
|
||||
|
||||
function paramName(param) {
|
||||
switch (param.type) {
|
||||
case 'Identifier':
|
||||
return param.name;
|
||||
case 'AssignmentPattern':
|
||||
return '?' + paramName(param.left);
|
||||
case 'ObjectPattern':
|
||||
return 'obj';
|
||||
case 'ArrayPattern':
|
||||
return 'arr';
|
||||
case 'RestElement':
|
||||
return '...' + paramName(param.argument);
|
||||
}
|
||||
return '?';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {?{baseExpression: string, possibleSideEffects:boolean}}
|
||||
@@ -335,8 +430,6 @@ FormatterWorker.findLastExpression = function(content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @type {!ESTree.Node} */
|
||||
let ast;
|
||||
const suffix = '.DEVTOOLS';
|
||||
try {
|
||||
acorn.parse(content + suffix, {ecmaVersion: 9});
|
||||
@@ -345,6 +438,23 @@ FormatterWorker.findLastExpression = function(content) {
|
||||
if (parseError.message.startsWith('Unexpected token') && parseError.pos === content.length)
|
||||
return null;
|
||||
}
|
||||
const base = FormatterWorker._lastCompleteExpression(content, suffix, new Set(['MemberExpression', 'Identifier']));
|
||||
if (!base)
|
||||
return null;
|
||||
const {baseExpression, baseNode} = base;
|
||||
const possibleSideEffects = FormatterWorker._nodeHasPossibleSideEffects(baseNode);
|
||||
return {baseExpression, possibleSideEffects};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} suffix
|
||||
* @param {!Set<string>} types
|
||||
* @return {?{baseNode: !ESTree.Node, baseExpression: string}}
|
||||
*/
|
||||
FormatterWorker._lastCompleteExpression = function(content, suffix, types) {
|
||||
/** @type {!ESTree.Node} */
|
||||
let ast;
|
||||
let parsedContent = '';
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
try {
|
||||
@@ -357,7 +467,6 @@ FormatterWorker.findLastExpression = function(content) {
|
||||
}
|
||||
if (!ast)
|
||||
return null;
|
||||
const types = new Set(['MemberExpression', 'Identifier']);
|
||||
let baseNode = null;
|
||||
const walker = new FormatterWorker.ESTreeWalker(node => {
|
||||
if (baseNode || node.end < ast.end)
|
||||
@@ -371,6 +480,14 @@ FormatterWorker.findLastExpression = function(content) {
|
||||
let baseExpression = parsedContent.substring(baseNode.start, parsedContent.length - suffix.length);
|
||||
if (baseExpression.startsWith('{'))
|
||||
baseExpression = `(${baseExpression})`;
|
||||
return {baseNode, baseExpression};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!ESTree.Node} baseNode
|
||||
* @return {boolean}
|
||||
*/
|
||||
FormatterWorker._nodeHasPossibleSideEffects = function(baseNode) {
|
||||
const sideEffectFreeTypes = new Set([
|
||||
'MemberExpression', 'Identifier', 'BinaryExpression', 'Literal', 'TemplateLiteral', 'TemplateElement',
|
||||
'ObjectExpression', 'ArrayExpression', 'Property', 'ThisExpression'
|
||||
@@ -383,7 +500,7 @@ FormatterWorker.findLastExpression = function(content) {
|
||||
return FormatterWorker.ESTreeWalker.SkipSubtree;
|
||||
});
|
||||
sideEffectwalker.walk(/** @type {!ESTree.Node} */ (baseNode));
|
||||
return {baseExpression, possibleSideEffects};
|
||||
return possibleSideEffects;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,43 @@ ObjectUI.JavaScriptAutocomplete = class {
|
||||
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
|
||||
|
||||
@@ -22,7 +22,10 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
this._changes = this._changes.bind(this);
|
||||
this._blur = this._blur.bind(this);
|
||||
this._beforeChange = this._beforeChange.bind(this);
|
||||
this._mouseDown = this.clearAutocomplete.bind(this);
|
||||
this._mouseDown = () => {
|
||||
this.clearAutocomplete();
|
||||
this._tooltipGlassPane.hide();
|
||||
};
|
||||
this._codeMirror.on('changes', this._changes);
|
||||
this._lastHintText = '';
|
||||
/** @type {?UI.SuggestBox} */
|
||||
@@ -30,6 +33,14 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
/** @type {?string} */
|
||||
this._currentSuggestion = null;
|
||||
this._hintElement = createElementWithClass('span', 'auto-complete-text');
|
||||
|
||||
this._tooltipGlassPane = new UI.GlassPane();
|
||||
this._tooltipGlassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
|
||||
this._tooltipGlassPane.setOutsideClickCallback(this._tooltipGlassPane.hide.bind(this._tooltipGlassPane));
|
||||
this._tooltipElement = createElementWithClass('div', 'autocomplete-tooltip');
|
||||
const shadowRoot =
|
||||
UI.createShadowRootWithCoreStyles(this._tooltipGlassPane.contentElement, 'text_editor/autocompleteTooltip.css');
|
||||
shadowRoot.appendChild(this._tooltipElement);
|
||||
}
|
||||
|
||||
_initializeIfNeeded() {
|
||||
@@ -220,14 +231,14 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
autocomplete(force) {
|
||||
this._initializeIfNeeded();
|
||||
if (this._codeMirror.somethingSelected()) {
|
||||
this.clearAutocomplete();
|
||||
this._hideSuggestBox();
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = this._codeMirror.getCursor('head');
|
||||
const substituteRange = this._substituteRange(cursor.line, cursor.ch);
|
||||
if (!substituteRange || !this._validateSelectionsContexts(substituteRange)) {
|
||||
this.clearAutocomplete();
|
||||
this._hideSuggestBox();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -246,7 +257,7 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
function wordsAcquired(wordsWithQuery) {
|
||||
if (!wordsWithQuery.length || (wordsWithQuery.length === 1 && query === wordsWithQuery[0].text) ||
|
||||
(!this._suggestBox && hadSuggestBox)) {
|
||||
this.clearAutocomplete();
|
||||
this._hideSuggestBox();
|
||||
this._onSuggestionsShownForTest([]);
|
||||
return;
|
||||
}
|
||||
@@ -259,6 +270,8 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
queryRange.startColumn !== oldQueryRange.startColumn)
|
||||
this._updateAnchorBox();
|
||||
this._suggestBox.updateSuggestions(this._anchorBox, wordsWithQuery, true, !this._isCursorAtEndOfLine(), query);
|
||||
if (this._suggestBox.visible)
|
||||
this._tooltipGlassPane.hide();
|
||||
this._onSuggestionsShownForTest(wordsWithQuery);
|
||||
}
|
||||
}
|
||||
@@ -311,6 +324,11 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
}
|
||||
|
||||
clearAutocomplete() {
|
||||
this._tooltipGlassPane.hide();
|
||||
this._hideSuggestBox();
|
||||
}
|
||||
|
||||
_hideSuggestBox() {
|
||||
if (!this._suggestBox)
|
||||
return;
|
||||
this._suggestBox.hide();
|
||||
@@ -328,6 +346,10 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
* @return {boolean}
|
||||
*/
|
||||
keyDown(event) {
|
||||
if (this._tooltipGlassPane.isShowing() && event.keyCode === UI.KeyboardShortcut.Keys.Esc.code) {
|
||||
this._tooltipGlassPane.hide();
|
||||
return true;
|
||||
}
|
||||
if (!this._suggestBox)
|
||||
return false;
|
||||
switch (event.keyCode) {
|
||||
@@ -417,6 +439,7 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
}
|
||||
|
||||
_onScroll() {
|
||||
this._tooltipGlassPane.hide();
|
||||
if (!this._suggestBox)
|
||||
return;
|
||||
const cursor = this._codeMirror.getCursor();
|
||||
@@ -431,7 +454,34 @@ TextEditor.TextEditorAutocompleteController = class {
|
||||
}
|
||||
}
|
||||
|
||||
async _updateTooltip() {
|
||||
const cursor = this._codeMirror.getCursor();
|
||||
const tooltip = this._config.tooltipCallback ? await this._config.tooltipCallback(cursor.line, cursor.ch) : null;
|
||||
const newCursor = this._codeMirror.getCursor();
|
||||
|
||||
if (newCursor.line !== cursor.line && newCursor.ch !== cursor.ch)
|
||||
return;
|
||||
if (this._suggestBox && this._suggestBox.visible)
|
||||
return;
|
||||
|
||||
if (!tooltip) {
|
||||
this._tooltipGlassPane.hide();
|
||||
return;
|
||||
}
|
||||
const metrics = this._textEditor.cursorPositionToCoordinates(cursor.line, cursor.ch);
|
||||
if (!metrics) {
|
||||
this._tooltipGlassPane.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this._tooltipGlassPane.setContentAnchorBox(new AnchorBox(metrics.x, metrics.y, 0, metrics.height));
|
||||
this._tooltipElement.removeChildren();
|
||||
this._tooltipElement.appendChild(tooltip);
|
||||
this._tooltipGlassPane.show(/** @type {!Document} */ (this._textEditor.element.ownerDocument));
|
||||
}
|
||||
|
||||
_onCursorActivity() {
|
||||
this._updateTooltip();
|
||||
if (!this._suggestBox)
|
||||
return;
|
||||
const cursor = this._codeMirror.getCursor();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2018 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.
|
||||
*/
|
||||
.autocomplete-tooltip {
|
||||
background-color: #FFFFFF;
|
||||
pointer-events: none;
|
||||
margin-left: -3px;
|
||||
box-shadow: var(--drop-shadow);
|
||||
overflow-x: hidden;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.autocomplete-tooltip > * {
|
||||
padding: 0px 4px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
line-height: 20px;
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"CodeMirrorTextEditor.js"
|
||||
],
|
||||
"resources": [
|
||||
"autocompleteTooltip.css",
|
||||
"cmdevtools.css"
|
||||
],
|
||||
"skip_compilation": [
|
||||
|
||||
@@ -116,6 +116,7 @@ UI.TextEditor.Options;
|
||||
/**
|
||||
* @typedef {{
|
||||
* substituteRangeCallback: ((function(number, number):?TextUtils.TextRange)|undefined),
|
||||
* tooltipCallback: ((function(number, number):?Element)|undefined),
|
||||
* suggestionsCallback: ((function(!TextUtils.TextRange, !TextUtils.TextRange, boolean=):?Promise.<!UI.SuggestBox.Suggestions>)|undefined),
|
||||
* isWordChar: ((function(string):boolean)|undefined)
|
||||
* }}
|
||||
|
||||
Reference in New Issue
Block a user