Commit c965d521 authored by Joel Einbinder's avatar Joel Einbinder Committed by Commit Bot

DevTools: Move autocomplete logic ConsolePrompt->JavaScriptAutocomplete

This makes the code reusable for other CodeMirrorTextEditors that
might want the same autocomplete experience we have in the Console.

Change-Id: I8660249c08d920d86d2b82524b7bd50d9b5deb05
Reviewed-on: https://chromium-review.googlesource.com/1092080
Commit-Queue: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Reviewed-by: default avatarErik Luo <luoe@chromium.org>
Cr-Commit-Position: refs/heads/master@{#572381}
parent 788597c2
......@@ -251,4 +251,11 @@ Checking 'dontRunThis().'
Checking 'stall().'
error: Execution was terminated
Checking 'shouldNot|FindThisFunction()'
Not Found: shouldNotFindThisFunction
Checking 'thePrefix'
Found: thePrefix
Found: thePrefixAndTheSuffix
......@@ -34,6 +34,8 @@
function stall() {
while(true);
}
var thePrefix = true;
var thePrefixAndTheSuffix = true;
`);
var consoleEditor;
......@@ -181,7 +183,8 @@
() => testCompletions('var x = "string".char', ['charAt']),
() => testCompletions('({abc: 123}).a', ['abc']),
() => testCompletions('{dontFindLabels: 123}.dont', ['dontFindLabels']),
() => testCompletions('const x = 5; {dontFindLabels: 123}.dont', ['dontFindLabels']),
() => testCompletions(
'const x = 5; {dontFindLabels: 123}.dont', ['dontFindLabels']),
() => testCompletions('const x = {abc: 123}.a', ['abc']),
() => testCompletions('x = {abc: 123}.', ['abc']),
() => testCompletions('[1,2,3].j', ['join']),
......@@ -199,5 +202,8 @@
() => testCompletions('(dontRunThis`asdf`).', []),
() => testCompletions('dontRunThis().', []),
() => testCompletions('stall().', []),
() => testCompletions(
'shouldNot|FindThisFunction()', ['shouldNotFindThisFunction']),
() => testCompletions('thePrefix', ['thePrefix', 'thePrefixAndTheSuffix']),
]).then(TestRunner.completeTest);
})();
......@@ -27,6 +27,9 @@ Console.ConsolePrompt = class extends UI.Widget {
/** @type {?Promise} */
this._previewRequestForTest = null;
/** @type {?UI.AutocompleteConfig} */
this._defaultAutocompleteConfig = null;
self.runtime.extension(UI.TextEditorFactory).instance().then(gotFactory.bind(this));
/**
......@@ -37,13 +40,12 @@ Console.ConsolePrompt = class extends UI.Widget {
this._editor =
factory.createEditor({lineNumbers: false, lineWrapping: true, mimeType: 'javascript', autoHeight: true});
this._editor.configureAutocomplete({
substituteRangeCallback: this._substituteRange.bind(this),
this._defaultAutocompleteConfig = ObjectUI.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor);
this._editor.configureAutocomplete(Object.assign({}, this._defaultAutocompleteConfig, {
suggestionsCallback: this._wordsWithQuery.bind(this),
tooltipCallback: (lineNumber, columnNumber) => this._tooltipCallback(lineNumber, columnNumber),
anchorBehavior: this._isBelowPromptEnabled ? UI.GlassPane.AnchorBehavior.PreferTop :
UI.GlassPane.AnchorBehavior.PreferBottom
});
}));
this._editor.widget().element.addEventListener('keydown', this._editorKeyDown.bind(this), true);
this._editor.widget().show(this.element);
this._editor.addEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this);
......@@ -331,25 +333,6 @@ Console.ConsolePrompt = class extends UI.Widget {
this.element.focus();
}
/**
* @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
......@@ -358,55 +341,11 @@ Console.ConsolePrompt = class extends UI.Widget {
*/
async _wordsWithQuery(queryRange, substituteRange, force) {
const query = this._editor.text(queryRange);
const before = this._editor.text(new TextUtils.TextRange(0, 0, queryRange.startLine, queryRange.startColumn));
const words = await this._defaultAutocompleteConfig.suggestionsCallback(queryRange, substituteRange, force);
const historyWords = this._historyCompletions(query, force);
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 historyWords;
}
const words = await ObjectUI.javaScriptAutocomplete.completionsForTextInCurrentContext(before, query, force);
if (!force && !this._isCaretAtEndOfPrompt()) {
const queryAndAfter = this._editor.line(queryRange.startLine).substring(queryRange.startColumn);
if (queryAndAfter && words.some(word => queryAndAfter.startsWith(word.text) && query.length !== word.text.length))
return [];
}
return 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 {argumentIndex} = result;
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;
}
_editorSetForTest() {
}
};
......
......@@ -602,3 +602,100 @@ ObjectUI.JavaScriptAutocomplete = class {
ObjectUI.JavaScriptAutocomplete.CompletionGroup;
ObjectUI.javaScriptAutocomplete = new ObjectUI.JavaScriptAutocomplete();
ObjectUI.JavaScriptAutocompleteConfig = class {
/**
* @param {!UI.TextEditor} editor
*/
constructor(editor) {
this._editor = editor;
}
/**
* @param {!UI.TextEditor} editor
* @return {!UI.AutocompleteConfig}
*/
static createConfigForEditor(editor) {
const autocomplete = new ObjectUI.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;
}
};
......@@ -116,7 +116,7 @@ UI.TextEditor.Options;
/**
* @typedef {{
* substituteRangeCallback: ((function(number, number):?TextUtils.TextRange)|undefined),
* tooltipCallback: ((function(number, number):?Element)|undefined),
* tooltipCallback: ((function(number, number):!Promise<?Element>)|undefined),
* suggestionsCallback: ((function(!TextUtils.TextRange, !TextUtils.TextRange, boolean=):?Promise.<!UI.SuggestBox.Suggestions>)|undefined),
* isWordChar: ((function(string):boolean)|undefined),
* anchorBehavior: (UI.GlassPane.AnchorBehavior|undefined)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment