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