Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts
4798 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { compareBy, numberComparator } from '../../../../../base/common/arrays.js';6import { findFirstMax } from '../../../../../base/common/arraysFind.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { Disposable } from '../../../../../base/common/lifecycle.js';9import { ICodeEditor } from '../../../../browser/editorBrowser.js';10import { Position } from '../../../../common/core/position.js';11import { Range } from '../../../../common/core/range.js';12import { TextReplacement } from '../../../../common/core/edits/textEdit.js';13import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from '../../../../common/languages.js';14import { ITextModel } from '../../../../common/model.js';15import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';16import { SnippetParser } from '../../../snippet/browser/snippetParser.js';17import { SnippetSession } from '../../../snippet/browser/snippetSession.js';18import { CompletionItem } from '../../../suggest/browser/suggest.js';19import { SuggestController } from '../../../suggest/browser/suggestController.js';20import { ObservableCodeEditor } from '../../../../browser/observableCodeEditor.js';21import { observableFromEvent } from '../../../../../base/common/observable.js';2223export class SuggestWidgetAdaptor extends Disposable {24private isSuggestWidgetVisible: boolean = false;25private isShiftKeyPressed = false;26private _isActive = false;27private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined;28public get selectedItem(): SuggestItemInfo | undefined {29return this._currentSuggestItemInfo;30}31private _onDidSelectedItemChange = this._register(new Emitter<void>());32public readonly onDidSelectedItemChange: Event<void> = this._onDidSelectedItemChange.event;3334constructor(35private readonly editor: ICodeEditor,36private readonly suggestControllerPreselector: () => TextReplacement | undefined,37private readonly onWillAccept: (item: SuggestItemInfo) => void,38) {39super();4041// See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab42this._register(editor.onKeyDown(e => {43if (e.shiftKey && !this.isShiftKeyPressed) {44this.isShiftKeyPressed = true;45this.update(this._isActive);46}47}));48this._register(editor.onKeyUp(e => {49if (e.shiftKey && this.isShiftKeyPressed) {50this.isShiftKeyPressed = false;51this.update(this._isActive);52}53}));5455const suggestController = SuggestController.get(this.editor);56if (suggestController) {57this._register(suggestController.registerSelector({58priority: 100,59select: (model, pos, suggestItems) => {60const textModel = this.editor.getModel();61if (!textModel) {62// Should not happen63return -1;64}6566const i = this.suggestControllerPreselector();67const itemToPreselect = i ? singleTextRemoveCommonPrefix(i, textModel) : undefined;68if (!itemToPreselect) {69return -1;70}71const position = Position.lift(pos);7273const candidates = suggestItems74.map((suggestItem, index) => {75const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed);76const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.getSingleTextEdit(), textModel);77const valid = singleTextEditAugments(itemToPreselect, suggestItemTextEdit);78return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem };79})80.filter(item => item && item.valid && item.prefixLength > 0);8182const result = findFirstMax(83candidates,84compareBy(s => s.prefixLength, numberComparator)85);86return result ? result.index : -1;87}88}));8990let isBoundToSuggestWidget = false;91const bindToSuggestWidget = () => {92if (isBoundToSuggestWidget) {93return;94}95isBoundToSuggestWidget = true;9697this._register(suggestController.widget.value.onDidShow(() => {98this.isSuggestWidgetVisible = true;99this.update(true);100}));101this._register(suggestController.widget.value.onDidHide(() => {102this.isSuggestWidgetVisible = false;103this.update(false);104}));105this._register(suggestController.widget.value.onDidFocus(() => {106this.isSuggestWidgetVisible = true;107this.update(true);108}));109};110111this._register(Event.once(suggestController.model.onDidTrigger)(e => {112bindToSuggestWidget();113}));114115this._register(suggestController.onWillInsertSuggestItem(e => {116const position = this.editor.getPosition();117const model = this.editor.getModel();118if (!position || !model) { return undefined; }119120const suggestItemInfo = SuggestItemInfo.fromSuggestion(121suggestController,122model,123position,124e.item,125this.isShiftKeyPressed126);127128this.onWillAccept(suggestItemInfo);129}));130}131this.update(this._isActive);132}133134private update(newActive: boolean): void {135const newInlineCompletion = this.getSuggestItemInfo();136137if (this._isActive !== newActive || !suggestItemInfoEquals(this._currentSuggestItemInfo, newInlineCompletion)) {138this._isActive = newActive;139this._currentSuggestItemInfo = newInlineCompletion;140141this._onDidSelectedItemChange.fire();142}143}144145private getSuggestItemInfo(): SuggestItemInfo | undefined {146const suggestController = SuggestController.get(this.editor);147if (!suggestController || !this.isSuggestWidgetVisible) {148return undefined;149}150151const focusedItem = suggestController.widget.value.getFocusedItem();152const position = this.editor.getPosition();153const model = this.editor.getModel();154155if (!focusedItem || !position || !model) {156return undefined;157}158159return SuggestItemInfo.fromSuggestion(160suggestController,161model,162position,163focusedItem.item,164this.isShiftKeyPressed165);166}167168public stopForceRenderingAbove(): void {169const suggestController = SuggestController.get(this.editor);170suggestController?.stopForceRenderingAbove();171}172173public forceRenderingAbove(): void {174const suggestController = SuggestController.get(this.editor);175suggestController?.forceRenderingAbove();176}177}178179export class SuggestItemInfo {180public static fromSuggestion(suggestController: SuggestController, model: ITextModel, position: Position, item: CompletionItem, toggleMode: boolean): SuggestItemInfo {181let { insertText } = item.completion;182let isSnippetText = false;183if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {184const snippet = new SnippetParser().parse(insertText);185186if (snippet.children.length < 100) {187// Adjust whitespace is expensive.188SnippetSession.adjustWhitespace(model, position, true, snippet);189}190191insertText = snippet.toString();192isSnippetText = true;193}194195const info = suggestController.getOverwriteInfo(item, toggleMode);196197return new SuggestItemInfo(198Range.fromPositions(199position.delta(0, -info.overwriteBefore),200position.delta(0, Math.max(info.overwriteAfter, 0))201),202insertText,203item.completion.kind,204isSnippetText,205item.container.incomplete ?? false,206);207}208209private constructor(210public readonly range: Range,211public readonly insertText: string,212public readonly completionItemKind: CompletionItemKind,213public readonly isSnippetText: boolean,214public readonly listIncomplete: boolean,215) { }216217public equals(other: SuggestItemInfo): boolean {218return this.range.equalsRange(other.range)219&& this.insertText === other.insertText220&& this.completionItemKind === other.completionItemKind221&& this.isSnippetText === other.isSnippetText;222}223224public toSelectedSuggestionInfo(): SelectedSuggestionInfo {225return new SelectedSuggestionInfo(this.range, this.insertText, this.completionItemKind, this.isSnippetText);226}227228public getSingleTextEdit(): TextReplacement {229return new TextReplacement(this.range, this.insertText);230}231}232233function suggestItemInfoEquals(a: SuggestItemInfo | undefined, b: SuggestItemInfo | undefined): boolean {234if (a === b) {235return true;236}237if (!a || !b) {238return false;239}240return a.equals(b);241}242243export class ObservableSuggestWidgetAdapter extends Disposable {244private readonly _suggestWidgetAdaptor;245246public readonly selectedItem;247248constructor(249private readonly _editorObs: ObservableCodeEditor,250251private readonly _handleSuggestAccepted: (item: SuggestItemInfo) => void,252private readonly _suggestControllerPreselector: () => TextReplacement | undefined,253) {254super();255this._suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor(256this._editorObs.editor,257() => {258this._editorObs.forceUpdate();259return this._suggestControllerPreselector();260},261(item) => this._editorObs.forceUpdate(_tx => {262/** @description InlineCompletionsController.handleSuggestAccepted */263this._handleSuggestAccepted(item);264})265));266this.selectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => {267this._editorObs.forceUpdate(_tx => cb(undefined));268}), () => this._suggestWidgetAdaptor.selectedItem);269}270271public stopForceRenderingAbove(): void {272this._suggestWidgetAdaptor.stopForceRenderingAbove();273}274275public forceRenderingAbove(): void {276this._suggestWidgetAdaptor.forceRenderingAbove();277}278}279280281