Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts
4797 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { FuzzyScore } from '../../../../base/common/filters.js';
8
import { Iterable } from '../../../../base/common/iterator.js';
9
import { Disposable, RefCountedDisposable } from '../../../../base/common/lifecycle.js';
10
import { ICodeEditor } from '../../../browser/editorBrowser.js';
11
import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';
12
import { EditorOption } from '../../../common/config/editorOptions.js';
13
import { ISingleEditOperation } from '../../../common/core/editOperation.js';
14
import { IPosition, Position } from '../../../common/core/position.js';
15
import { IRange, Range } from '../../../common/core/range.js';
16
import { IWordAtPosition } from '../../../common/core/wordHelper.js';
17
import { registerEditorFeature } from '../../../common/editorFeatures.js';
18
import { Command, CompletionItemInsertTextRule, CompletionItemProvider, CompletionTriggerKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../common/languages.js';
19
import { ITextModel } from '../../../common/model.js';
20
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
21
import { CompletionModel, LineContext } from './completionModel.js';
22
import { CompletionItem, CompletionItemModel, CompletionOptions, provideSuggestionItems, QuickSuggestionsOptions } from './suggest.js';
23
import { ISuggestMemoryService } from './suggestMemory.js';
24
import { SuggestModel } from './suggestModel.js';
25
import { WordDistance } from './wordDistance.js';
26
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
27
28
class SuggestInlineCompletion implements InlineCompletion {
29
readonly doNotLog = true;
30
31
constructor(
32
readonly range: IRange,
33
readonly insertText: string | { snippet: string },
34
readonly filterText: string,
35
readonly additionalTextEdits: ISingleEditOperation[] | undefined,
36
readonly command: Command | undefined,
37
readonly gutterMenuLinkAction: Command | undefined,
38
readonly completion: CompletionItem,
39
) { }
40
}
41
42
class InlineCompletionResults extends RefCountedDisposable implements InlineCompletions<SuggestInlineCompletion> {
43
44
constructor(
45
readonly model: ITextModel,
46
readonly line: number,
47
readonly word: IWordAtPosition,
48
readonly completionModel: CompletionModel,
49
completions: CompletionItemModel,
50
@ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService,
51
) {
52
super(completions.disposable);
53
}
54
55
canBeReused(model: ITextModel, line: number, word: IWordAtPosition) {
56
return this.model === model // same model
57
&& this.line === line
58
&& this.word.word.length > 0
59
&& this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word
60
&& this.completionModel.getIncompleteProvider().size === 0; // no incomplete results
61
}
62
63
get items(): SuggestInlineCompletion[] {
64
const result: SuggestInlineCompletion[] = [];
65
66
// Split items by preselected index. This ensures the memory-selected item shows first and that better/worst
67
// ranked items are before/after
68
const { items } = this.completionModel;
69
const selectedIndex = this._suggestMemoryService.select(this.model, { lineNumber: this.line, column: this.word.endColumn + this.completionModel.lineContext.characterCountDelta }, items);
70
const first = Iterable.slice(items, selectedIndex);
71
const second = Iterable.slice(items, 0, selectedIndex);
72
73
let resolveCount = 5;
74
75
for (const item of Iterable.concat(first, second)) {
76
77
if (item.score === FuzzyScore.Default) {
78
// skip items that have no overlap
79
continue;
80
}
81
82
const range = new Range(
83
item.editStart.lineNumber, item.editStart.column,
84
item.editInsertEnd.lineNumber, item.editInsertEnd.column + this.completionModel.lineContext.characterCountDelta // end PLUS character delta
85
);
86
const insertText = item.completion.insertTextRules && (item.completion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)
87
? { snippet: item.completion.insertText }
88
: item.completion.insertText;
89
90
result.push(new SuggestInlineCompletion(
91
range,
92
insertText,
93
item.filterTextLow ?? item.labelLow,
94
item.completion.additionalTextEdits,
95
item.completion.command,
96
item.completion.action,
97
item
98
));
99
100
// resolve the first N suggestions eagerly
101
if (resolveCount-- >= 0) {
102
item.resolve(CancellationToken.None);
103
}
104
}
105
return result;
106
}
107
}
108
109
110
export class SuggestInlineCompletions extends Disposable implements InlineCompletionsProvider<InlineCompletionResults> {
111
112
private _lastResult?: InlineCompletionResults;
113
114
constructor(
115
@ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService,
116
@IClipboardService private readonly _clipboardService: IClipboardService,
117
@ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService,
118
@ICodeEditorService private readonly _editorService: ICodeEditorService,
119
) {
120
super();
121
this._store.add(_languageFeatureService.inlineCompletionsProvider.register('*', this));
122
}
123
124
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletionResults | undefined> {
125
126
if (context.selectedSuggestionInfo) {
127
return;
128
}
129
130
let editor: ICodeEditor | undefined;
131
for (const candidate of this._editorService.listCodeEditors()) {
132
if (candidate.getModel() === model) {
133
editor = candidate;
134
break;
135
}
136
}
137
138
if (!editor) {
139
return;
140
}
141
142
const config = editor.getOption(EditorOption.quickSuggestions);
143
if (QuickSuggestionsOptions.isAllOff(config)) {
144
// quick suggest is off (for this model/language)
145
return;
146
}
147
148
model.tokenization.tokenizeIfCheap(position.lineNumber);
149
const lineTokens = model.tokenization.getLineTokens(position.lineNumber);
150
const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(position.column - 1 - 1, 0)));
151
if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'inline') {
152
// quick suggest is off (for this token)
153
return undefined;
154
}
155
156
// We consider non-empty leading words and trigger characters. The latter only
157
// when no word is being typed (word characters superseed trigger characters)
158
let wordInfo = model.getWordAtPosition(position);
159
let triggerCharacterInfo: { ch: string; providers: Set<CompletionItemProvider> } | undefined;
160
161
if (!wordInfo?.word) {
162
triggerCharacterInfo = this._getTriggerCharacterInfo(model, position);
163
}
164
165
if (!wordInfo?.word && !triggerCharacterInfo) {
166
// not at word, not a trigger character
167
return;
168
}
169
170
// ensure that we have word information and that we are at the end of a word
171
// otherwise we stop because we don't want to do quick suggestions inside words
172
if (!wordInfo) {
173
wordInfo = model.getWordUntilPosition(position);
174
}
175
if (wordInfo.endColumn !== position.column) {
176
return;
177
}
178
179
let result: InlineCompletionResults;
180
const leadingLineContents = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column));
181
if (!triggerCharacterInfo && this._lastResult?.canBeReused(model, position.lineNumber, wordInfo)) {
182
// reuse a previous result iff possible, only a refilter is needed
183
// TODO@jrieken this can be improved further and only incomplete results can be updated
184
// console.log(`REUSE with ${wordInfo.word}`);
185
const newLineContext = new LineContext(leadingLineContents, position.column - this._lastResult.word.endColumn);
186
this._lastResult.completionModel.lineContext = newLineContext;
187
this._lastResult.acquire();
188
result = this._lastResult;
189
190
} else {
191
// refesh model is required
192
const completions = await provideSuggestionItems(
193
this._languageFeatureService.completionProvider,
194
model, position,
195
new CompletionOptions(undefined, SuggestModel.createSuggestFilter(editor).itemKind, triggerCharacterInfo?.providers),
196
triggerCharacterInfo && { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: triggerCharacterInfo.ch },
197
token
198
);
199
200
let clipboardText: string | undefined;
201
if (completions.needsClipboard) {
202
clipboardText = await this._clipboardService.readText();
203
}
204
205
const completionModel = new CompletionModel(
206
completions.items,
207
position.column,
208
new LineContext(leadingLineContents, 0),
209
WordDistance.None,
210
editor.getOption(EditorOption.suggest),
211
editor.getOption(EditorOption.snippetSuggestions),
212
{ boostFullMatch: false, firstMatchCanBeWeak: false },
213
clipboardText
214
);
215
result = new InlineCompletionResults(model, position.lineNumber, wordInfo, completionModel, completions, this._suggestMemoryService);
216
}
217
218
this._lastResult = result;
219
return result;
220
}
221
222
handleItemDidShow(_completions: InlineCompletionResults, item: SuggestInlineCompletion): void {
223
item.completion.resolve(CancellationToken.None);
224
}
225
226
disposeInlineCompletions(result: InlineCompletionResults): void {
227
result.release();
228
}
229
230
private _getTriggerCharacterInfo(model: ITextModel, position: IPosition) {
231
const ch = model.getValueInRange(Range.fromPositions({ lineNumber: position.lineNumber, column: position.column - 1 }, position));
232
const providers = new Set<CompletionItemProvider>();
233
for (const provider of this._languageFeatureService.completionProvider.all(model)) {
234
if (provider.triggerCharacters?.includes(ch)) {
235
providers.add(provider);
236
}
237
}
238
if (providers.size === 0) {
239
return undefined;
240
}
241
return { providers, ch };
242
}
243
}
244
245
246
registerEditorFeature(SuggestInlineCompletions);
247
248