Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/suggest/browser/suggest.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 { CancellationError, isCancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js';
8
import { FuzzyScore } from '../../../../base/common/filters.js';
9
import { DisposableStore, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
10
import { StopWatch } from '../../../../base/common/stopwatch.js';
11
import { assertType } from '../../../../base/common/types.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import { ICodeEditor } from '../../../browser/editorBrowser.js';
14
import { IPosition, Position } from '../../../common/core/position.js';
15
import { Range } from '../../../common/core/range.js';
16
import { IEditorContribution } from '../../../common/editorCommon.js';
17
import { ITextModel } from '../../../common/model.js';
18
import * as languages from '../../../common/languages.js';
19
import { ITextModelService } from '../../../common/services/resolverService.js';
20
import { SnippetParser } from '../../snippet/browser/snippetParser.js';
21
import { localize } from '../../../../nls.js';
22
import { MenuId } from '../../../../platform/actions/common/actions.js';
23
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
24
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
25
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
26
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
27
import { historyNavigationVisible } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
28
import { InternalQuickSuggestionsOptions, QuickSuggestionsValue } from '../../../common/config/editorOptions.js';
29
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
30
import { StandardTokenType } from '../../../common/encodedTokenAttributes.js';
31
32
export const Context = {
33
Visible: historyNavigationVisible,
34
HasFocusedSuggestion: new RawContextKey<boolean>('suggestWidgetHasFocusedSuggestion', false, localize('suggestWidgetHasSelection', "Whether any suggestion is focused")),
35
DetailsVisible: new RawContextKey<boolean>('suggestWidgetDetailsVisible', false, localize('suggestWidgetDetailsVisible', "Whether suggestion details are visible")),
36
DetailsFocused: new RawContextKey<boolean>('suggestWidgetDetailsFocused', false, localize('suggestWidgetDetailsFocused', "Whether the details pane of the suggest widget has focus")),
37
MultipleSuggestions: new RawContextKey<boolean>('suggestWidgetMultipleSuggestions', false, localize('suggestWidgetMultipleSuggestions', "Whether there are multiple suggestions to pick from")),
38
MakesTextEdit: new RawContextKey<boolean>('suggestionMakesTextEdit', true, localize('suggestionMakesTextEdit', "Whether inserting the current suggestion yields in a change or has everything already been typed")),
39
AcceptSuggestionsOnEnter: new RawContextKey<boolean>('acceptSuggestionOnEnter', true, localize('acceptSuggestionOnEnter', "Whether suggestions are inserted when pressing Enter")),
40
HasInsertAndReplaceRange: new RawContextKey<boolean>('suggestionHasInsertAndReplaceRange', false, localize('suggestionHasInsertAndReplaceRange', "Whether the current suggestion has insert and replace behaviour")),
41
InsertMode: new RawContextKey<'insert' | 'replace'>('suggestionInsertMode', undefined, { type: 'string', description: localize('suggestionInsertMode', "Whether the default behaviour is to insert or replace") }),
42
CanResolve: new RawContextKey<boolean>('suggestionCanResolve', false, localize('suggestionCanResolve', "Whether the current suggestion supports to resolve further details")),
43
};
44
45
export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar');
46
47
export class CompletionItem {
48
49
_brand!: 'ISuggestionItem';
50
51
//
52
readonly editStart: IPosition;
53
readonly editInsertEnd: IPosition;
54
readonly editReplaceEnd: IPosition;
55
56
//
57
readonly textLabel: string;
58
59
// perf
60
readonly labelLow: string;
61
readonly sortTextLow?: string;
62
readonly filterTextLow?: string;
63
64
// validation
65
readonly isInvalid: boolean = false;
66
67
// sorting, filtering
68
score: FuzzyScore = FuzzyScore.Default;
69
distance: number = 0;
70
idx?: number;
71
word?: string;
72
73
// instrumentation
74
readonly extensionId?: ExtensionIdentifier;
75
76
// resolving
77
private _resolveDuration?: number;
78
private _resolveCache?: Promise<void>;
79
80
constructor(
81
readonly position: IPosition,
82
readonly completion: languages.CompletionItem,
83
readonly container: languages.CompletionList,
84
readonly provider: languages.CompletionItemProvider,
85
) {
86
this.textLabel = typeof completion.label === 'string'
87
? completion.label
88
: completion.label?.label;
89
90
// ensure lower-variants (perf)
91
this.labelLow = this.textLabel.toLowerCase();
92
93
// validate label
94
this.isInvalid = !this.textLabel;
95
96
this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
97
this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
98
99
this.extensionId = completion.extensionId;
100
101
// normalize ranges
102
if (Range.isIRange(completion.range)) {
103
this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn);
104
this.editInsertEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
105
this.editReplaceEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
106
107
// validate range
108
this.isInvalid = this.isInvalid
109
|| Range.spansMultipleLines(completion.range) || completion.range.startLineNumber !== position.lineNumber;
110
111
} else {
112
this.editStart = new Position(completion.range.insert.startLineNumber, completion.range.insert.startColumn);
113
this.editInsertEnd = new Position(completion.range.insert.endLineNumber, completion.range.insert.endColumn);
114
this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
115
116
// validate ranges
117
this.isInvalid = this.isInvalid
118
|| Range.spansMultipleLines(completion.range.insert) || Range.spansMultipleLines(completion.range.replace)
119
|| completion.range.insert.startLineNumber !== position.lineNumber || completion.range.replace.startLineNumber !== position.lineNumber
120
|| completion.range.insert.startColumn !== completion.range.replace.startColumn;
121
}
122
123
// create the suggestion resolver
124
if (typeof provider.resolveCompletionItem !== 'function') {
125
this._resolveCache = Promise.resolve();
126
this._resolveDuration = 0;
127
}
128
}
129
130
// ---- resolving
131
132
get isResolved(): boolean {
133
return this._resolveDuration !== undefined;
134
}
135
136
get resolveDuration(): number {
137
return this._resolveDuration !== undefined ? this._resolveDuration : -1;
138
}
139
140
async resolve(token: CancellationToken) {
141
if (!this._resolveCache) {
142
const sub = token.onCancellationRequested(() => {
143
this._resolveCache = undefined;
144
this._resolveDuration = undefined;
145
});
146
const sw = new StopWatch(true);
147
this._resolveCache = Promise.resolve(this.provider.resolveCompletionItem!(this.completion, token)).then(value => {
148
Object.assign(this.completion, value);
149
this._resolveDuration = sw.elapsed();
150
}, err => {
151
if (isCancellationError(err)) {
152
// the IPC queue will reject the request with the
153
// cancellation error -> reset cached
154
this._resolveCache = undefined;
155
this._resolveDuration = undefined;
156
}
157
}).finally(() => {
158
sub.dispose();
159
});
160
}
161
return this._resolveCache;
162
}
163
}
164
165
export const enum SnippetSortOrder {
166
Top, Inline, Bottom
167
}
168
169
export class CompletionOptions {
170
171
static readonly default = new CompletionOptions();
172
173
constructor(
174
readonly snippetSortOrder = SnippetSortOrder.Bottom,
175
readonly kindFilter = new Set<languages.CompletionItemKind>(),
176
readonly providerFilter = new Set<languages.CompletionItemProvider>(),
177
readonly providerItemsToReuse: ReadonlyMap<languages.CompletionItemProvider, CompletionItem[]> = new Map<languages.CompletionItemProvider, CompletionItem[]>(),
178
readonly showDeprecated = true
179
) { }
180
}
181
182
let _snippetSuggestSupport: languages.CompletionItemProvider | undefined;
183
184
export function getSnippetSuggestSupport(): languages.CompletionItemProvider | undefined {
185
return _snippetSuggestSupport;
186
}
187
188
export function setSnippetSuggestSupport(support: languages.CompletionItemProvider | undefined): languages.CompletionItemProvider | undefined {
189
const old = _snippetSuggestSupport;
190
_snippetSuggestSupport = support;
191
return old;
192
}
193
194
export interface CompletionDurationEntry {
195
readonly providerName: string;
196
readonly elapsedProvider: number;
197
readonly elapsedOverall: number;
198
}
199
200
export interface CompletionDurations {
201
readonly entries: readonly CompletionDurationEntry[];
202
readonly elapsed: number;
203
}
204
205
export class CompletionItemModel {
206
constructor(
207
readonly items: CompletionItem[],
208
readonly needsClipboard: boolean,
209
readonly durations: CompletionDurations,
210
readonly disposable: IDisposable,
211
) { }
212
}
213
214
export async function provideSuggestionItems(
215
registry: LanguageFeatureRegistry<languages.CompletionItemProvider>,
216
model: ITextModel,
217
position: Position,
218
options: CompletionOptions = CompletionOptions.default,
219
context: languages.CompletionContext = { triggerKind: languages.CompletionTriggerKind.Invoke },
220
token: CancellationToken = CancellationToken.None
221
): Promise<CompletionItemModel> {
222
223
const sw = new StopWatch();
224
position = position.clone();
225
226
const word = model.getWordAtPosition(position);
227
const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position);
228
const defaultRange = { replace: defaultReplaceRange, insert: defaultReplaceRange.setEndPosition(position.lineNumber, position.column) };
229
230
const result: CompletionItem[] = [];
231
const disposables = new DisposableStore();
232
const durations: CompletionDurationEntry[] = [];
233
let needsClipboard = false;
234
235
const onCompletionList = (provider: languages.CompletionItemProvider, container: languages.CompletionList | null | undefined, sw: StopWatch): boolean => {
236
let didAddResult = false;
237
if (!container) {
238
return didAddResult;
239
}
240
for (const suggestion of container.suggestions) {
241
if (!options.kindFilter.has(suggestion.kind)) {
242
// skip if not showing deprecated suggestions
243
if (!options.showDeprecated && suggestion?.tags?.includes(languages.CompletionItemTag.Deprecated)) {
244
continue;
245
}
246
// fill in default range when missing
247
if (!suggestion.range) {
248
suggestion.range = defaultRange;
249
}
250
// fill in default sortText when missing
251
if (!suggestion.sortText) {
252
suggestion.sortText = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.label;
253
}
254
if (!needsClipboard && suggestion.insertTextRules && suggestion.insertTextRules & languages.CompletionItemInsertTextRule.InsertAsSnippet) {
255
needsClipboard = SnippetParser.guessNeedsClipboard(suggestion.insertText);
256
}
257
result.push(new CompletionItem(position, suggestion, container, provider));
258
didAddResult = true;
259
}
260
}
261
if (isDisposable(container)) {
262
disposables.add(container);
263
}
264
durations.push({
265
providerName: provider._debugDisplayName ?? 'unknown_provider', elapsedProvider: container.duration ?? -1, elapsedOverall: sw.elapsed()
266
});
267
return didAddResult;
268
};
269
270
// ask for snippets in parallel to asking "real" providers. Only do something if configured to
271
// do so - no snippet filter, no special-providers-only request
272
const snippetCompletions = (async () => {
273
if (!_snippetSuggestSupport || options.kindFilter.has(languages.CompletionItemKind.Snippet)) {
274
return;
275
}
276
// we have items from a previous session that we can reuse
277
const reuseItems = options.providerItemsToReuse.get(_snippetSuggestSupport);
278
if (reuseItems) {
279
reuseItems.forEach(item => result.push(item));
280
return;
281
}
282
if (options.providerFilter.size > 0 && !options.providerFilter.has(_snippetSuggestSupport)) {
283
return;
284
}
285
const sw = new StopWatch();
286
const list = await _snippetSuggestSupport.provideCompletionItems(model, position, context, token);
287
onCompletionList(_snippetSuggestSupport, list, sw);
288
})();
289
290
// add suggestions from contributed providers - providers are ordered in groups of
291
// equal score and once a group produces a result the process stops
292
// get provider groups, always add snippet suggestion provider
293
for (const providerGroup of registry.orderedGroups(model)) {
294
295
// for each support in the group ask for suggestions
296
let didAddResult = false;
297
await Promise.all(providerGroup.map(async provider => {
298
// we have items from a previous session that we can reuse
299
if (options.providerItemsToReuse.has(provider)) {
300
const items = options.providerItemsToReuse.get(provider)!;
301
items.forEach(item => result.push(item));
302
didAddResult = didAddResult || items.length > 0;
303
return;
304
}
305
// check if this provider is filtered out
306
if (options.providerFilter.size > 0 && !options.providerFilter.has(provider)) {
307
return;
308
}
309
try {
310
const sw = new StopWatch();
311
const list = await provider.provideCompletionItems(model, position, context, token);
312
didAddResult = onCompletionList(provider, list, sw) || didAddResult;
313
} catch (err) {
314
onUnexpectedExternalError(err);
315
}
316
}));
317
318
if (didAddResult || token.isCancellationRequested) {
319
break;
320
}
321
}
322
323
await snippetCompletions;
324
325
if (token.isCancellationRequested) {
326
disposables.dispose();
327
return Promise.reject(new CancellationError());
328
}
329
330
return new CompletionItemModel(
331
result.sort(getSuggestionComparator(options.snippetSortOrder)),
332
needsClipboard,
333
{ entries: durations, elapsed: sw.elapsed() },
334
disposables,
335
);
336
}
337
338
339
function defaultComparator(a: CompletionItem, b: CompletionItem): number {
340
// check with 'sortText'
341
if (a.sortTextLow && b.sortTextLow) {
342
if (a.sortTextLow < b.sortTextLow) {
343
return -1;
344
} else if (a.sortTextLow > b.sortTextLow) {
345
return 1;
346
}
347
}
348
// check with 'label'
349
if (a.textLabel < b.textLabel) {
350
return -1;
351
} else if (a.textLabel > b.textLabel) {
352
return 1;
353
}
354
// check with 'type'
355
return a.completion.kind - b.completion.kind;
356
}
357
358
function snippetUpComparator(a: CompletionItem, b: CompletionItem): number {
359
if (a.completion.kind !== b.completion.kind) {
360
if (a.completion.kind === languages.CompletionItemKind.Snippet) {
361
return -1;
362
} else if (b.completion.kind === languages.CompletionItemKind.Snippet) {
363
return 1;
364
}
365
}
366
return defaultComparator(a, b);
367
}
368
369
function snippetDownComparator(a: CompletionItem, b: CompletionItem): number {
370
if (a.completion.kind !== b.completion.kind) {
371
if (a.completion.kind === languages.CompletionItemKind.Snippet) {
372
return 1;
373
} else if (b.completion.kind === languages.CompletionItemKind.Snippet) {
374
return -1;
375
}
376
}
377
return defaultComparator(a, b);
378
}
379
380
interface Comparator<T> { (a: T, b: T): number }
381
const _snippetComparators = new Map<SnippetSortOrder, Comparator<CompletionItem>>();
382
_snippetComparators.set(SnippetSortOrder.Top, snippetUpComparator);
383
_snippetComparators.set(SnippetSortOrder.Bottom, snippetDownComparator);
384
_snippetComparators.set(SnippetSortOrder.Inline, defaultComparator);
385
386
export function getSuggestionComparator(snippetConfig: SnippetSortOrder): (a: CompletionItem, b: CompletionItem) => number {
387
return _snippetComparators.get(snippetConfig)!;
388
}
389
390
CommandsRegistry.registerCommand('_executeCompletionItemProvider', async (accessor, ...args: [URI, IPosition, string?, number?]) => {
391
const [uri, position, triggerCharacter, maxItemsToResolve] = args;
392
assertType(URI.isUri(uri));
393
assertType(Position.isIPosition(position));
394
assertType(typeof triggerCharacter === 'string' || !triggerCharacter);
395
assertType(typeof maxItemsToResolve === 'number' || !maxItemsToResolve);
396
397
const { completionProvider } = accessor.get(ILanguageFeaturesService);
398
const ref = await accessor.get(ITextModelService).createModelReference(uri);
399
try {
400
401
const result: languages.CompletionList = {
402
incomplete: false,
403
suggestions: []
404
};
405
406
const resolving: Promise<unknown>[] = [];
407
const actualPosition = ref.object.textEditorModel.validatePosition(position);
408
const completions = await provideSuggestionItems(completionProvider, ref.object.textEditorModel, actualPosition, undefined, { triggerCharacter: triggerCharacter ?? undefined, triggerKind: triggerCharacter ? languages.CompletionTriggerKind.TriggerCharacter : languages.CompletionTriggerKind.Invoke });
409
for (const item of completions.items) {
410
if (resolving.length < (maxItemsToResolve ?? 0)) {
411
resolving.push(item.resolve(CancellationToken.None));
412
}
413
result.incomplete = result.incomplete || item.container.incomplete;
414
result.suggestions.push(item.completion);
415
}
416
417
try {
418
await Promise.all(resolving);
419
return result;
420
} finally {
421
setTimeout(() => completions.disposable.dispose(), 100);
422
}
423
424
} finally {
425
ref.dispose();
426
}
427
428
});
429
430
interface SuggestController extends IEditorContribution {
431
triggerSuggest(onlyFrom?: Set<languages.CompletionItemProvider>, auto?: boolean, noFilter?: boolean): void;
432
}
433
434
export function showSimpleSuggestions(editor: ICodeEditor, provider: languages.CompletionItemProvider) {
435
editor.getContribution<SuggestController>('editor.contrib.suggestController')?.triggerSuggest(
436
new Set<languages.CompletionItemProvider>().add(provider), undefined, true
437
);
438
}
439
440
export interface ISuggestItemPreselector {
441
/**
442
* The preselector with highest priority is asked first.
443
*/
444
readonly priority: number;
445
446
/**
447
* Is called to preselect a suggest item.
448
* When -1 is returned, item preselectors with lower priority are asked.
449
*/
450
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number | -1;
451
}
452
453
454
export abstract class QuickSuggestionsOptions {
455
456
static isAllOff(config: InternalQuickSuggestionsOptions): boolean {
457
return config.other === 'off' && config.comments === 'off' && config.strings === 'off';
458
}
459
460
static isAllOn(config: InternalQuickSuggestionsOptions): boolean {
461
return config.other === 'on' && config.comments === 'on' && config.strings === 'on';
462
}
463
464
static valueFor(config: InternalQuickSuggestionsOptions, tokenType: StandardTokenType): QuickSuggestionsValue {
465
switch (tokenType) {
466
case StandardTokenType.Comment: return config.comments;
467
case StandardTokenType.String: return config.strings;
468
default: return config.other;
469
}
470
}
471
}
472
473