Files
react-native-devtools-frontend/front_end/text_editor/TextEditorAutocompleteController.js
T
Tim van der Lippe e4d64e7ae0 Remove unrestricted from comments that include other annotations
For all comments that contained both the unrestricted annotation
and any other potential annotation, remove only the unrestricted
annotation.

R=szuend@chromium.org

No-Presubmit: True
Bug: 1011811
Change-Id: I27dd67f657cdb1a9233d768eea81cabe2591f5f7
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2569759
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
2020-12-02 15:31:52 +00:00

625 lines
21 KiB
JavaScript

// Copyright (c) 2014 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.
import * as Common from '../common/common.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as UI from '../ui/ui.js';
import {CodeMirrorTextEditor} from './CodeMirrorTextEditor.js'; // eslint-disable-line no-unused-vars
import {changeObjectToEditOperation} from './CodeMirrorUtils.js';
/**
* @implements {UI.SuggestBox.SuggestBoxDelegate}
*/
export class TextEditorAutocompleteController {
// https://crbug.com/1151919 * = CodeMirror.Editor
/**
* @param {!CodeMirrorTextEditor} textEditor
* @param {*} codeMirror
* @param {!UI.TextEditor.AutocompleteConfig} config
*/
constructor(textEditor, codeMirror, config) {
this._textEditor = textEditor;
this._codeMirror = codeMirror;
this._config = config;
this._initialized = false;
this._onScroll = this._onScroll.bind(this);
this._onCursorActivity = this._onCursorActivity.bind(this);
this._changes = this._changes.bind(this);
this._blur = this._blur.bind(this);
this._beforeChange = this._beforeChange.bind(this);
this._mouseDown = () => {
this.clearAutocomplete();
this._tooltipGlassPane.hide();
};
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('changes', this._changes);
this._lastHintText = '';
/** @type {?UI.SuggestBox.SuggestBox} */
this._suggestBox = null;
/** @type {?UI.SuggestBox.Suggestion} */
this._currentSuggestion = null;
this._hintElement = document.createElement('span');
this._hintElement.classList.add('auto-complete-text');
this._tooltipGlassPane = new UI.GlassPane.GlassPane();
this._tooltipGlassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
this._tooltipGlassPane.setOutsideClickCallback(this._tooltipGlassPane.hide.bind(this._tooltipGlassPane));
this._tooltipElement = document.createElement('div');
this._tooltipElement.classList.add('autocomplete-tooltip');
const shadowRoot = UI.Utils.createShadowRootWithCoreStyles(
this._tooltipGlassPane.contentElement,
{cssFile: 'text_editor/autocompleteTooltip.css', enableLegacyPatching: true, delegatesFocus: undefined});
shadowRoot.appendChild(this._tooltipElement);
/** @type {?TextUtils.TextRange.TextRange} */
this._queryRange = null;
}
_initializeIfNeeded() {
if (this._initialized) {
return;
}
this._initialized = true;
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('scroll', this._onScroll);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('cursorActivity', this._onCursorActivity);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('mousedown', this._mouseDown);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('blur', this._blur);
if (this._config.isWordChar) {
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.on('beforeChange', this._beforeChange);
this._dictionary = new Common.TextDictionary.TextDictionary();
// @ts-ignore CodeMirror types are wrong.
this._addWordsFromText(this._codeMirror.getValue());
}
}
dispose() {
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('changes', this._changes);
if (this._initialized) {
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('scroll', this._onScroll);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('cursorActivity', this._onCursorActivity);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('mousedown', this._mouseDown);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('blur', this._blur);
}
if (this._dictionary) {
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.off('beforeChange', this._beforeChange);
this._dictionary.reset();
}
}
/**
* @param {!CodeMirror} codeMirror
* @param {*} changeObject (!CodeMirror.BeforeChangeObject)
*/
_beforeChange(codeMirror, changeObject) {
this._updatedLines = this._updatedLines || {};
for (let i = changeObject.from.line; i <= changeObject.to.line; ++i) {
if (this._updatedLines[i] === undefined) {
// @ts-ignore CodeMirror types are wrong.
this._updatedLines[i] = this._codeMirror.getLine(i);
}
}
}
/**
* @param {string} text
*/
_addWordsFromText(text) {
TextUtils.TextUtils.Utils.textToWords(
text, /** @type {function(string):boolean} */ (this._config.isWordChar), addWord.bind(this));
/**
* @param {string} word
* @this {TextEditorAutocompleteController}
*/
function addWord(word) {
if (this._dictionary && word.length && (word[0] < '0' || word[0] > '9')) {
this._dictionary.addWord(word);
}
}
}
/**
* @param {string} text
*/
_removeWordsFromText(text) {
TextUtils.TextUtils.Utils.textToWords(
text, /** @type {function(string):boolean} */ (this._config.isWordChar),
word => this._dictionary && this._dictionary.removeWord(word));
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?TextUtils.TextRange.TextRange}
*/
_substituteRange(lineNumber, columnNumber) {
let range =
this._config.substituteRangeCallback ? this._config.substituteRangeCallback(lineNumber, columnNumber) : null;
if (!range && this._config.isWordChar) {
range = this._textEditor.wordRangeForCursorPosition(lineNumber, columnNumber, this._config.isWordChar);
}
return range;
}
/**
* @param {!TextUtils.TextRange.TextRange} queryRange
* @param {!TextUtils.TextRange.TextRange} substituteRange
* @param {boolean=} force
* @return {!Promise.<!UI.SuggestBox.Suggestions>}
*/
_wordsWithQuery(queryRange, substituteRange, force) {
const external =
this._config.suggestionsCallback ? this._config.suggestionsCallback(queryRange, substituteRange, force) : null;
if (external) {
return external;
}
if (!this._dictionary || (!force && queryRange.isEmpty())) {
return Promise.resolve([]);
}
let completions = this._dictionary.wordsWithPrefix(this._textEditor.text(queryRange));
const substituteWord = this._textEditor.text(substituteRange);
if (this._dictionary.wordCount(substituteWord) === 1) {
completions = completions.filter(word => word !== substituteWord);
}
const dictionary = this._dictionary;
completions.sort((a, b) => dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length);
return Promise.resolve(completions.map(item => ({
text: item,
title: undefined,
subtitle: undefined,
iconType: undefined,
priority: undefined,
isSecondary: undefined,
subtitleRenderer: undefined,
selectionRange: undefined,
hideGhostText: undefined,
iconElement: undefined
})));
}
/**
* @param {!CodeMirror} codeMirror
* @param {!Array.<*>} changes (CodeMirror.ChangeObject)
*/
_changes(codeMirror, changes) {
if (!changes.length) {
return;
}
if (this._dictionary && this._updatedLines) {
for (const lineNumber in this._updatedLines) {
this._removeWordsFromText(this._updatedLines[lineNumber]);
}
delete this._updatedLines;
/** @type {!Object.<string, string>} */
const linesToUpdate = {};
for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) {
const changeObject = changes[changeIndex];
const editInfo = changeObjectToEditOperation(changeObject);
for (let i = editInfo.newRange.startLine; i <= editInfo.newRange.endLine; ++i) {
// @ts-ignore CodeMirror types are wrong.
linesToUpdate[String(i)] = this._codeMirror.getLine(i);
}
}
for (const lineNumber in linesToUpdate) {
this._addWordsFromText(linesToUpdate[lineNumber]);
}
}
let singleCharInput = false;
let singleCharDelete = false;
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor('head');
for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) {
const changeObject = changes[changeIndex];
if (changeObject.origin === '+input' && changeObject.text.length === 1 && changeObject.text[0].length === 1 &&
changeObject.to.line === cursor.line && changeObject.to.ch + 1 === cursor.ch) {
singleCharInput = true;
break;
}
if (changeObject.origin === '+delete' && changeObject.removed.length === 1 &&
changeObject.removed[0].length === 1 && changeObject.to.line === cursor.line &&
changeObject.to.ch - 1 === cursor.ch) {
singleCharDelete = true;
break;
}
}
if (this._queryRange) {
if (singleCharInput) {
this._queryRange.endColumn++;
} else if (singleCharDelete) {
this._queryRange.endColumn--;
}
if (singleCharDelete || singleCharInput) {
this._setHint(this._lastHintText);
}
}
if (singleCharInput || singleCharDelete) {
queueMicrotask(() => {
this.autocomplete();
});
} else {
this.clearAutocomplete();
}
}
_blur() {
this.clearAutocomplete();
}
/**
* @param {!TextUtils.TextRange.TextRange} mainSelection
* @return {boolean}
*/
_validateSelectionsContexts(mainSelection) {
// @ts-ignore CodeMirror types are wrong.
const selections = this._codeMirror.listSelections();
if (selections.length <= 1) {
return true;
}
const mainSelectionContext = this._textEditor.text(mainSelection);
for (let i = 0; i < selections.length; ++i) {
const wordRange = this._substituteRange(selections[i].head.line, selections[i].head.ch);
if (!wordRange) {
return false;
}
const context = this._textEditor.text(wordRange);
if (context !== mainSelectionContext) {
return false;
}
}
return true;
}
/**
* @param {boolean=} force
*/
autocomplete(force) {
this._initializeIfNeeded();
// @ts-ignore CodeMirror types are wrong.
if (this._codeMirror.somethingSelected()) {
this._hideSuggestBox();
return;
}
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor('head');
const substituteRange = this._substituteRange(cursor.line, cursor.ch);
if (!substituteRange || !this._validateSelectionsContexts(substituteRange)) {
this._hideSuggestBox();
return;
}
const queryRange = substituteRange.clone();
queryRange.endColumn = cursor.ch;
const query = this._textEditor.text(queryRange);
let hadSuggestBox = false;
if (this._suggestBox) {
hadSuggestBox = true;
}
this._wordsWithQuery(queryRange, substituteRange, force).then(wordsWithQuery => {
return wordsAcquired(wordsWithQuery);
});
/**
* @param {!UI.SuggestBox.Suggestions} wordsWithQuery
*/
const wordsAcquired = wordsWithQuery => {
if (!wordsWithQuery.length || (wordsWithQuery.length === 1 && query === wordsWithQuery[0].text) ||
(!this._suggestBox && hadSuggestBox)) {
this._hideSuggestBox();
this._onSuggestionsShownForTest([]);
return;
}
if (!this._suggestBox) {
this._suggestBox = new UI.SuggestBox.SuggestBox(this, 20);
if (this._config.anchorBehavior) {
this._suggestBox.setAnchorBehavior(this._config.anchorBehavior);
}
}
const oldQueryRange = this._queryRange;
this._queryRange = queryRange;
if (!oldQueryRange || queryRange.startLine !== oldQueryRange.startLine ||
queryRange.startColumn !== oldQueryRange.startColumn) {
this._updateAnchorBox();
}
this._anchorBox &&
this._suggestBox.updateSuggestions(
this._anchorBox, wordsWithQuery, true, !this._isCursorAtEndOfLine(), query);
if (this._suggestBox.visible()) {
this._tooltipGlassPane.hide();
}
this._onSuggestionsShownForTest(wordsWithQuery);
};
}
/**
* @param {string} hint
*/
_setHint(hint) {
if (this._queryRange === null) {
return;
}
const query = this._textEditor.text(this._queryRange);
if (!hint || !this._isCursorAtEndOfLine() || !hint.startsWith(query)) {
this._clearHint();
return;
}
const suffix = hint.substring(query.length).split('\n')[0];
this._hintElement.textContent = suffix.trimEndWithMaxLength(10000);
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor('to');
if (this._hintMarker) {
const position = this._hintMarker.position();
if (!position || !position.equal(TextUtils.TextRange.TextRange.createFromLocation(cursor.line, cursor.ch))) {
this._hintMarker.clear();
this._hintMarker = null;
}
}
if (!this._hintMarker) {
this._hintMarker = this._textEditor.addBookmark(
cursor.line, cursor.ch, /** @type {!HTMLElement} */ (this._hintElement),
TextEditorAutocompleteController.HintBookmark, true);
} else if (this._lastHintText !== hint) {
this._hintMarker.refresh();
}
this._lastHintText = hint;
}
_clearHint() {
if (!this._hintElement.textContent) {
return;
}
this._lastHintText = '';
this._hintElement.textContent = '';
if (this._hintMarker) {
this._hintMarker.refresh();
}
}
/**
* @param {!UI.SuggestBox.Suggestions} suggestions
*/
_onSuggestionsShownForTest(suggestions) {
}
_onSuggestionsHiddenForTest() {
}
clearAutocomplete() {
this._tooltipGlassPane.hide();
this._hideSuggestBox();
}
_hideSuggestBox() {
if (!this._suggestBox) {
return;
}
this._suggestBox.hide();
this._suggestBox = null;
this._queryRange = null;
this._anchorBox = null;
this._currentSuggestion = null;
this._textEditor.dispatchEventToListeners(UI.TextEditor.Events.SuggestionChanged);
this._clearHint();
this._onSuggestionsHiddenForTest();
}
/**
* @param {!KeyboardEvent} event
* @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) {
case UI.KeyboardShortcut.Keys.Tab.code:
this._suggestBox.acceptSuggestion();
this.clearAutocomplete();
return true;
case UI.KeyboardShortcut.Keys.End.code:
case UI.KeyboardShortcut.Keys.Right.code:
if (this._isCursorAtEndOfLine()) {
this._suggestBox.acceptSuggestion();
this.clearAutocomplete();
return true;
}
this.clearAutocomplete();
return false;
case UI.KeyboardShortcut.Keys.Left.code:
case UI.KeyboardShortcut.Keys.Home.code:
this.clearAutocomplete();
return false;
case UI.KeyboardShortcut.Keys.Esc.code:
this.clearAutocomplete();
return true;
}
return this._suggestBox.keyPressed(event);
}
/**
* @return {boolean}
*/
_isCursorAtEndOfLine() {
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor('to');
// @ts-ignore CodeMirror types are wrong.
return cursor.ch === this._codeMirror.getLine(cursor.line).length;
}
/**
* @override
* @param {?UI.SuggestBox.Suggestion} suggestion
* @param {boolean=} isIntermediateSuggestion
*/
applySuggestion(suggestion, isIntermediateSuggestion) {
const oldSuggestion = this._currentSuggestion;
this._currentSuggestion = suggestion;
this._setHint(suggestion ? suggestion.text : '');
if ((oldSuggestion ? oldSuggestion.text : '') !== (suggestion ? suggestion.text : '')) {
this._textEditor.dispatchEventToListeners(UI.TextEditor.Events.SuggestionChanged);
}
}
/**
* @override
*/
acceptSuggestion() {
if (this._currentSuggestion === null || this._queryRange === null) {
return;
}
// @ts-ignore CodeMirror types are wrong.
const selections = this._codeMirror.listSelections().slice();
const queryLength = this._queryRange.endColumn - this._queryRange.startColumn;
const suggestion = this._currentSuggestion.text;
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.operation(() => {
for (let i = selections.length - 1; i >= 0; --i) {
const start = selections[i].head;
const end = new CodeMirror.Pos(start.line, start.ch - queryLength);
// @ts-ignore CodeMirror types are wrong.
this._codeMirror.replaceRange(suggestion, start, end, '+autocomplete');
}
});
}
/**
* @return {string}
*/
textWithCurrentSuggestion() {
if (!this._queryRange || this._currentSuggestion === null) {
// @ts-ignore CodeMirror types are wrong.
return this._codeMirror.getValue();
}
// @ts-ignore CodeMirror types are wrong.
const selections = this._codeMirror.listSelections().slice();
let last = {line: 0, column: 0};
let text = '';
const queryLength = this._queryRange.endColumn - this._queryRange.startColumn;
for (const selection of selections) {
const range = new TextUtils.TextRange.TextRange(
last.line, last.column, selection.head.line, selection.head.ch - queryLength);
text += this._textEditor.text(range);
text += this._currentSuggestion.text;
last = {line: selection.head.line, column: selection.head.ch};
}
const range = new TextUtils.TextRange.TextRange(last.line, last.column, Infinity, Infinity);
text += this._textEditor.text(range);
return text;
}
_onScroll() {
this._tooltipGlassPane.hide();
if (!this._suggestBox) {
return;
}
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor();
// @ts-ignore CodeMirror types are wrong.
const scrollInfo = this._codeMirror.getScrollInfo();
// @ts-ignore CodeMirror types are wrong.
const topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, 'local');
// @ts-ignore CodeMirror types are wrong.
const bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local');
if (cursor.line < topmostLineNumber || cursor.line > bottomLine) {
this.clearAutocomplete();
} else {
this._updateAnchorBox();
this._anchorBox && this._suggestBox.setPosition(this._anchorBox);
}
}
async _updateTooltip() {
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor();
const tooltip = this._config.tooltipCallback ? await this._config.tooltipCallback(cursor.line, cursor.ch) : null;
// @ts-ignore CodeMirror types are wrong.
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 || this._queryRange === null) {
return;
}
// @ts-ignore CodeMirror types are wrong.
const cursor = this._codeMirror.getCursor();
let shouldCloseAutocomplete =
!(cursor.line === this._queryRange.startLine && this._queryRange.startColumn <= cursor.ch &&
cursor.ch <= this._queryRange.endColumn);
// Try not to hide autocomplete when user types in.
if (cursor.line === this._queryRange.startLine && cursor.ch === this._queryRange.endColumn + 1) {
// @ts-ignore CodeMirror types are wrong.
const line = this._codeMirror.getLine(cursor.line);
shouldCloseAutocomplete = this._config.isWordChar ? !this._config.isWordChar(line.charAt(cursor.ch - 1)) : false;
}
if (shouldCloseAutocomplete) {
this.clearAutocomplete();
}
this._onCursorActivityHandledForTest();
}
_onCursorActivityHandledForTest() {
}
_updateAnchorBox() {
if (this._queryRange === null) {
return;
}
const line = this._queryRange.startLine;
const column = this._queryRange.startColumn;
const metrics = this._textEditor.cursorPositionToCoordinates(line, column);
this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null;
}
}
TextEditorAutocompleteController.HintBookmark = Symbol('hint');