Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts
4797 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 { CancellationToken } from '../../../../base/common/cancellation.js';6import { FuzzyScore } from '../../../../base/common/filters.js';7import { Iterable } from '../../../../base/common/iterator.js';8import { Disposable, RefCountedDisposable } from '../../../../base/common/lifecycle.js';9import { ICodeEditor } from '../../../browser/editorBrowser.js';10import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';11import { EditorOption } from '../../../common/config/editorOptions.js';12import { ISingleEditOperation } from '../../../common/core/editOperation.js';13import { IPosition, Position } from '../../../common/core/position.js';14import { IRange, Range } from '../../../common/core/range.js';15import { IWordAtPosition } from '../../../common/core/wordHelper.js';16import { registerEditorFeature } from '../../../common/editorFeatures.js';17import { Command, CompletionItemInsertTextRule, CompletionItemProvider, CompletionTriggerKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../common/languages.js';18import { ITextModel } from '../../../common/model.js';19import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';20import { CompletionModel, LineContext } from './completionModel.js';21import { CompletionItem, CompletionItemModel, CompletionOptions, provideSuggestionItems, QuickSuggestionsOptions } from './suggest.js';22import { ISuggestMemoryService } from './suggestMemory.js';23import { SuggestModel } from './suggestModel.js';24import { WordDistance } from './wordDistance.js';25import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';2627class SuggestInlineCompletion implements InlineCompletion {28readonly doNotLog = true;2930constructor(31readonly range: IRange,32readonly insertText: string | { snippet: string },33readonly filterText: string,34readonly additionalTextEdits: ISingleEditOperation[] | undefined,35readonly command: Command | undefined,36readonly gutterMenuLinkAction: Command | undefined,37readonly completion: CompletionItem,38) { }39}4041class InlineCompletionResults extends RefCountedDisposable implements InlineCompletions<SuggestInlineCompletion> {4243constructor(44readonly model: ITextModel,45readonly line: number,46readonly word: IWordAtPosition,47readonly completionModel: CompletionModel,48completions: CompletionItemModel,49@ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService,50) {51super(completions.disposable);52}5354canBeReused(model: ITextModel, line: number, word: IWordAtPosition) {55return this.model === model // same model56&& this.line === line57&& this.word.word.length > 058&& this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word59&& this.completionModel.getIncompleteProvider().size === 0; // no incomplete results60}6162get items(): SuggestInlineCompletion[] {63const result: SuggestInlineCompletion[] = [];6465// Split items by preselected index. This ensures the memory-selected item shows first and that better/worst66// ranked items are before/after67const { items } = this.completionModel;68const selectedIndex = this._suggestMemoryService.select(this.model, { lineNumber: this.line, column: this.word.endColumn + this.completionModel.lineContext.characterCountDelta }, items);69const first = Iterable.slice(items, selectedIndex);70const second = Iterable.slice(items, 0, selectedIndex);7172let resolveCount = 5;7374for (const item of Iterable.concat(first, second)) {7576if (item.score === FuzzyScore.Default) {77// skip items that have no overlap78continue;79}8081const range = new Range(82item.editStart.lineNumber, item.editStart.column,83item.editInsertEnd.lineNumber, item.editInsertEnd.column + this.completionModel.lineContext.characterCountDelta // end PLUS character delta84);85const insertText = item.completion.insertTextRules && (item.completion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)86? { snippet: item.completion.insertText }87: item.completion.insertText;8889result.push(new SuggestInlineCompletion(90range,91insertText,92item.filterTextLow ?? item.labelLow,93item.completion.additionalTextEdits,94item.completion.command,95item.completion.action,96item97));9899// resolve the first N suggestions eagerly100if (resolveCount-- >= 0) {101item.resolve(CancellationToken.None);102}103}104return result;105}106}107108109export class SuggestInlineCompletions extends Disposable implements InlineCompletionsProvider<InlineCompletionResults> {110111private _lastResult?: InlineCompletionResults;112113constructor(114@ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService,115@IClipboardService private readonly _clipboardService: IClipboardService,116@ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService,117@ICodeEditorService private readonly _editorService: ICodeEditorService,118) {119super();120this._store.add(_languageFeatureService.inlineCompletionsProvider.register('*', this));121}122123async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletionResults | undefined> {124125if (context.selectedSuggestionInfo) {126return;127}128129let editor: ICodeEditor | undefined;130for (const candidate of this._editorService.listCodeEditors()) {131if (candidate.getModel() === model) {132editor = candidate;133break;134}135}136137if (!editor) {138return;139}140141const config = editor.getOption(EditorOption.quickSuggestions);142if (QuickSuggestionsOptions.isAllOff(config)) {143// quick suggest is off (for this model/language)144return;145}146147model.tokenization.tokenizeIfCheap(position.lineNumber);148const lineTokens = model.tokenization.getLineTokens(position.lineNumber);149const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(position.column - 1 - 1, 0)));150if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'inline') {151// quick suggest is off (for this token)152return undefined;153}154155// We consider non-empty leading words and trigger characters. The latter only156// when no word is being typed (word characters superseed trigger characters)157let wordInfo = model.getWordAtPosition(position);158let triggerCharacterInfo: { ch: string; providers: Set<CompletionItemProvider> } | undefined;159160if (!wordInfo?.word) {161triggerCharacterInfo = this._getTriggerCharacterInfo(model, position);162}163164if (!wordInfo?.word && !triggerCharacterInfo) {165// not at word, not a trigger character166return;167}168169// ensure that we have word information and that we are at the end of a word170// otherwise we stop because we don't want to do quick suggestions inside words171if (!wordInfo) {172wordInfo = model.getWordUntilPosition(position);173}174if (wordInfo.endColumn !== position.column) {175return;176}177178let result: InlineCompletionResults;179const leadingLineContents = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column));180if (!triggerCharacterInfo && this._lastResult?.canBeReused(model, position.lineNumber, wordInfo)) {181// reuse a previous result iff possible, only a refilter is needed182// TODO@jrieken this can be improved further and only incomplete results can be updated183// console.log(`REUSE with ${wordInfo.word}`);184const newLineContext = new LineContext(leadingLineContents, position.column - this._lastResult.word.endColumn);185this._lastResult.completionModel.lineContext = newLineContext;186this._lastResult.acquire();187result = this._lastResult;188189} else {190// refesh model is required191const completions = await provideSuggestionItems(192this._languageFeatureService.completionProvider,193model, position,194new CompletionOptions(undefined, SuggestModel.createSuggestFilter(editor).itemKind, triggerCharacterInfo?.providers),195triggerCharacterInfo && { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: triggerCharacterInfo.ch },196token197);198199let clipboardText: string | undefined;200if (completions.needsClipboard) {201clipboardText = await this._clipboardService.readText();202}203204const completionModel = new CompletionModel(205completions.items,206position.column,207new LineContext(leadingLineContents, 0),208WordDistance.None,209editor.getOption(EditorOption.suggest),210editor.getOption(EditorOption.snippetSuggestions),211{ boostFullMatch: false, firstMatchCanBeWeak: false },212clipboardText213);214result = new InlineCompletionResults(model, position.lineNumber, wordInfo, completionModel, completions, this._suggestMemoryService);215}216217this._lastResult = result;218return result;219}220221handleItemDidShow(_completions: InlineCompletionResults, item: SuggestInlineCompletion): void {222item.completion.resolve(CancellationToken.None);223}224225disposeInlineCompletions(result: InlineCompletionResults): void {226result.release();227}228229private _getTriggerCharacterInfo(model: ITextModel, position: IPosition) {230const ch = model.getValueInRange(Range.fromPositions({ lineNumber: position.lineNumber, column: position.column - 1 }, position));231const providers = new Set<CompletionItemProvider>();232for (const provider of this._languageFeatureService.completionProvider.all(model)) {233if (provider.triggerCharacters?.includes(ch)) {234providers.add(provider);235}236}237if (providers.size === 0) {238return undefined;239}240return { providers, ch };241}242}243244245registerEditorFeature(SuggestInlineCompletions);246247248