All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.text_editor.TextEditorAutocompleteController.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
// 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}
 * @unrestricted
 */
export class TextEditorAutocompleteController {
  /**
   * @param {!CodeMirrorTextEditor} textEditor
   * @param {!CodeMirror} 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();
    };
    this._codeMirror.on('changes', this._changes);
    this._lastHintText = '';
    /** @type {?UI.SuggestBox.SuggestBox} */
    this._suggestBox = null;
    /** @type {?UI.SuggestBox.Suggestion} */
    this._currentSuggestion = null;
    this._hintElement = createElementWithClass('span', '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 = createElementWithClass('div', 'autocomplete-tooltip');
    const shadowRoot = UI.Utils.createShadowRootWithCoreStyles(
        this._tooltipGlassPane.contentElement, 'text_editor/autocompleteTooltip.css');
    shadowRoot.appendChild(this._tooltipElement);
  }

  _initializeIfNeeded() {
    if (this._initialized) {
      return;
    }
    this._initialized = true;
    this._codeMirror.on('scroll', this._onScroll);
    this._codeMirror.on('cursorActivity', this._onCursorActivity);
    this._codeMirror.on('mousedown', this._mouseDown);
    this._codeMirror.on('blur', this._blur);
    if (this._config.isWordChar) {
      this._codeMirror.on('beforeChange', this._beforeChange);
      this._dictionary = new Common.TextDictionary.TextDictionary();
      this._addWordsFromText(this._codeMirror.getValue());
    }
  }

  dispose() {
    this._codeMirror.off('changes', this._changes);
    if (this._initialized) {
      this._codeMirror.off('scroll', this._onScroll);
      this._codeMirror.off('cursorActivity', this._onCursorActivity);
      this._codeMirror.off('mousedown', this._mouseDown);
      this._codeMirror.off('blur', this._blur);
    }
    if (this._dictionary) {
      this._codeMirror.off('beforeChange', this._beforeChange);
      this._dictionary.reset();
    }
  }

  /**
   * @param {!CodeMirror} codeMirror
   * @param {!CodeMirror.BeforeChangeObject} changeObject
   */
  _beforeChange(codeMirror, changeObject) {
    this._updatedLines = this._updatedLines || {};
    for (let i = changeObject.from.line; i <= changeObject.to.line; ++i) {
      if (this._updatedLines[i] === undefined) {
        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 (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.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.}
   */
  _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);
    }

    completions.sort((a, b) => this._dictionary.wordCount(b) - this._dictionary.wordCount(a) || a.length - b.length);
    return Promise.resolve(completions.map(item => ({text: item})));
  }

  /**
   * @param {!CodeMirror} codeMirror
   * @param {!Array.} changes
   */
  _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;

      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) {
          linesToUpdate[i] = this._codeMirror.getLine(i);
        }
      }
      for (const lineNumber in linesToUpdate) {
        this._addWordsFromText(linesToUpdate[lineNumber]);
      }
    }

    let singleCharInput = false;
    let singleCharDelete = false;
    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) {
      setImmediate(this.autocomplete.bind(this));
    } else {
      this.clearAutocomplete();
    }
  }

  _blur() {
    this.clearAutocomplete();
  }

  /**
   * @param {!TextUtils.TextRange.TextRange} mainSelection
   * @return {boolean}
   */
  _validateSelectionsContexts(mainSelection) {
    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();
    if (this._codeMirror.somethingSelected()) {
      this._hideSuggestBox();
      return;
    }

    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(wordsAcquired.bind(this));

    /**
     * @param {!UI.SuggestBox.Suggestions} wordsWithQuery
     * @this {TextEditorAutocompleteController}
     */
    function 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._suggestBox.updateSuggestions(this._anchorBox, wordsWithQuery, true, !this._isCursorAtEndOfLine(), query);
      if (this._suggestBox.visible) {
        this._tooltipGlassPane.hide();
      }
      this._onSuggestionsShownForTest(wordsWithQuery);
    }
  }

  /**
   * @param {string} hint
   */
  _setHint(hint) {
    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);
    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, 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() {
    const cursor = this._codeMirror.getCursor('to');
    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() {
    const selections = this._codeMirror.listSelections().slice();
    const queryLength = this._queryRange.endColumn - this._queryRange.startColumn;
    const suggestion = this._currentSuggestion.text;
    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);
        this._codeMirror.replaceRange(suggestion, start, end, '+autocomplete');
      }
    });
  }

  /**
   * @return {string}
   */
  textWithCurrentSuggestion() {
    if (!this._queryRange || this._currentSuggestion === null) {
      return this._codeMirror.getValue();
    }

    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;
    }
    const cursor = this._codeMirror.getCursor();
    const scrollInfo = this._codeMirror.getScrollInfo();
    const topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, 'local');
    const bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local');
    if (cursor.line < topmostLineNumber || cursor.line > bottomLine) {
      this.clearAutocomplete();
    } else {
      this._updateAnchorBox();
      this._suggestBox.setPosition(this._anchorBox);
    }
  }

  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();
    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) {
      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() {
    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');




© 2015 - 2024 Weber Informatics LLC | Privacy Policy