Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts
4798 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 { booleanComparator, compareBy, compareUndefinedSmallest, numberComparator } from '../../../../../base/common/arrays.js';
7
import { findLastMax } from '../../../../../base/common/arraysFind.js';
8
import { RunOnceScheduler } from '../../../../../base/common/async.js';
9
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
10
import { equalsIfDefined, thisEqualsC } from '../../../../../base/common/equals.js';
11
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
12
import { cloneAndChange } from '../../../../../base/common/objects.js';
13
import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, transaction } from '../../../../../base/common/observable.js';
14
// eslint-disable-next-line local/code-no-deep-import-of-internal
15
import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js';
16
import { isDefined, isObject } from '../../../../../base/common/types.js';
17
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
18
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
19
import { DataChannelForwardingTelemetryService, forwardToChannelIf, isCopilotLikeExtension } from '../../../../../platform/dataChannel/browser/forwardingTelemetryService.js';
20
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
21
import { ILogService } from '../../../../../platform/log/common/log.js';
22
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
23
import product from '../../../../../platform/product/common/product.js';
24
import { StringEdit } from '../../../../common/core/edits/stringEdit.js';
25
import { Position } from '../../../../common/core/position.js';
26
import { Range } from '../../../../common/core/range.js';
27
import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js';
28
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
29
import { ITextModel } from '../../../../common/model.js';
30
import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js';
31
import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';
32
import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';
33
import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js';
34
import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry } from '../telemetry.js';
35
import { wait } from '../utils.js';
36
import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js';
37
import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js';
38
import { RenameSymbolProcessor } from './renameSymbolProcessor.js';
39
40
export class InlineCompletionsSource extends Disposable {
41
private static _requestId = 0;
42
43
private readonly _updateOperation = this._register(new MutableDisposable<UpdateOperation>());
44
45
private readonly _loggingEnabled;
46
private readonly _sendRequestData;
47
48
private readonly _structuredFetchLogger;
49
50
private readonly _state = observableReducerSettable(this, {
51
initial: () => ({
52
inlineCompletions: InlineCompletionsState.createEmpty(),
53
suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),
54
}),
55
disposeFinal: (values) => {
56
values.inlineCompletions.dispose();
57
values.suggestWidgetInlineCompletions.dispose();
58
},
59
changeTracker: recordChangesLazy(() => ({ versionId: this._versionId })),
60
update: (reader, previousValue, changes) => {
61
const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined));
62
63
if (edit.isEmpty()) {
64
return previousValue;
65
}
66
try {
67
return {
68
inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel),
69
suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel),
70
};
71
} finally {
72
previousValue.inlineCompletions.dispose();
73
previousValue.suggestWidgetInlineCompletions.dispose();
74
}
75
}
76
});
77
78
public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions);
79
public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions);
80
81
private readonly _renameProcessor: RenameSymbolProcessor;
82
83
private _completionsEnabled: Record<string, boolean> | undefined = undefined;
84
85
constructor(
86
private readonly _textModel: ITextModel,
87
private readonly _versionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,
88
private readonly _debounceValue: IFeatureDebounceInformation,
89
private readonly _cursorPosition: IObservable<Position>,
90
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
91
@ILogService private readonly _logService: ILogService,
92
@IConfigurationService private readonly _configurationService: IConfigurationService,
93
@IInstantiationService private readonly _instantiationService: IInstantiationService,
94
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
95
) {
96
super();
97
this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store);
98
this._sendRequestData = observableConfigValue('editor.inlineSuggest.emptyResponseInformation', true, this._configurationService).recomputeInitiallyAndOnChange(this._store);
99
this._structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast<
100
{ kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry
101
| { kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry
102
>(),
103
'editor.inlineSuggest.logFetch.commandId'
104
));
105
106
this._renameProcessor = this._store.add(this._instantiationService.createInstance(RenameSymbolProcessor));
107
108
this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store);
109
110
const enablementSetting = product.defaultChatAgent?.completionsEnablementSetting ?? undefined;
111
if (enablementSetting) {
112
this._updateCompletionsEnablement(enablementSetting);
113
this._register(this._configurationService.onDidChangeConfiguration(e => {
114
if (e.affectsConfiguration(enablementSetting)) {
115
this._updateCompletionsEnablement(enablementSetting);
116
}
117
}));
118
}
119
120
this._state.recomputeInitiallyAndOnChange(this._store);
121
}
122
123
private _updateCompletionsEnablement(enalementSetting: string) {
124
const result = this._configurationService.getValue<Record<string, boolean>>(enalementSetting);
125
if (!isObject(result)) {
126
this._completionsEnabled = undefined;
127
} else {
128
this._completionsEnabled = result;
129
}
130
}
131
132
public readonly clearOperationOnTextModelChange = derived(this, reader => {
133
this._versionId.read(reader);
134
this._updateOperation.clear();
135
return undefined; // always constant
136
});
137
138
private _log(entry:
139
{ sourceId: string; kind: 'start'; requestId: number; context: unknown; provider: string | undefined } & IRecordableEditorLogEntry
140
| { sourceId: string; kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number; didAllProvidersReturn: boolean } & IRecordableLogEntry
141
) {
142
if (this._loggingEnabled.get()) {
143
this._logService.info(formatRecordableLogEntry(entry));
144
}
145
this._structuredFetchLogger.log(entry);
146
}
147
148
private readonly _loadingCount = observableValue(this, 0);
149
public readonly loading = this._loadingCount.map(this, v => v > 0);
150
151
public fetch(
152
providers: InlineCompletionsProvider[],
153
providersLabel: string | undefined,
154
context: InlineCompletionContextWithoutUuid,
155
activeInlineCompletion: InlineSuggestionIdentity | undefined,
156
withDebounce: boolean,
157
userJumpedToActiveCompletion: IObservable<boolean>,
158
requestInfo: InlineSuggestRequestInfo
159
): Promise<boolean> {
160
const position = this._cursorPosition.get();
161
const request = new UpdateRequest(position, context, this._textModel.getVersionId(), new Set(providers));
162
163
const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get();
164
165
if (this._updateOperation.value?.request.satisfies(request)) {
166
return this._updateOperation.value.promise;
167
} else if (target?.request?.satisfies(request)) {
168
return Promise.resolve(true);
169
}
170
171
const updateOngoing = !!this._updateOperation.value;
172
this._updateOperation.clear();
173
174
const source = new CancellationTokenSource();
175
176
const promise = (async () => {
177
const store = new DisposableStore();
178
179
this._loadingCount.set(this._loadingCount.get() + 1, undefined);
180
let didDecrease = false;
181
const decreaseLoadingCount = () => {
182
if (!didDecrease) {
183
didDecrease = true;
184
this._loadingCount.set(this._loadingCount.get() - 1, undefined);
185
}
186
};
187
const loadingReset = store.add(new RunOnceScheduler(() => decreaseLoadingCount(), 10 * 1000));
188
loadingReset.schedule();
189
190
const inlineSuggestionsProviders = providers.filter(p => p.providerId);
191
const requestResponseInfo = new RequestResponseData(context, requestInfo, inlineSuggestionsProviders);
192
193
194
try {
195
const recommendedDebounceValue = this._debounceValue.get(this._textModel);
196
const debounceValue = findLastMax(
197
providers.map(p => p.debounceDelayMs),
198
compareUndefinedSmallest(numberComparator)
199
) ?? recommendedDebounceValue;
200
201
// Debounce in any case if update is ongoing
202
const shouldDebounce = updateOngoing || (withDebounce && context.triggerKind === InlineCompletionTriggerKind.Automatic);
203
if (shouldDebounce) {
204
// This debounces the operation
205
await wait(debounceValue, source.token);
206
}
207
208
if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {
209
requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:beforeFetch');
210
return false;
211
}
212
213
const requestId = InlineCompletionsSource._requestId++;
214
if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {
215
this._log({
216
sourceId: 'InlineCompletions.fetch',
217
kind: 'start',
218
requestId,
219
modelUri: this._textModel.uri,
220
modelVersion: this._textModel.getVersionId(),
221
context: { triggerKind: context.triggerKind, suggestInfo: context.selectedSuggestionInfo ? true : undefined },
222
time: Date.now(),
223
provider: providersLabel,
224
});
225
}
226
227
const startTime = new Date();
228
const providerResult = provideInlineCompletions(providers, this._cursorPosition.get(), this._textModel, context, requestInfo, this._languageConfigurationService);
229
230
runWhenCancelled(source.token, () => providerResult.cancelAndDispose({ kind: 'tokenCancellation' }));
231
232
let shouldStopEarly = false;
233
let producedSuggestion = false;
234
235
const providerSuggestions: InlineSuggestionItem[] = [];
236
for await (const list of providerResult.lists) {
237
if (!list) {
238
continue;
239
}
240
list.addRef();
241
store.add(toDisposable(() => list.removeRef(list.inlineSuggestionsData.length === 0 ? { kind: 'empty' } : { kind: 'notTaken' })));
242
243
for (const item of list.inlineSuggestionsData) {
244
producedSuggestion = true;
245
if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) {
246
item.setNotShownReason('notInlineEditRequested');
247
continue;
248
}
249
if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) {
250
item.setNotShownReason('notInlineCompletionRequested');
251
continue;
252
}
253
254
item.addPerformanceMarker('providerReturned');
255
const i = InlineSuggestionItem.create(item, this._textModel);
256
item.addPerformanceMarker('itemCreated');
257
providerSuggestions.push(i);
258
// Stop after first visible inline completion
259
if (!i.isInlineEdit && !i.showInlineEditMenu && context.triggerKind === InlineCompletionTriggerKind.Automatic) {
260
if (i.isVisible(this._textModel, this._cursorPosition.get())) {
261
shouldStopEarly = true;
262
}
263
}
264
}
265
266
if (shouldStopEarly) {
267
break;
268
}
269
}
270
271
providerSuggestions.forEach(s => s.addPerformanceMarker('providersResolved'));
272
273
const suggestions: InlineSuggestionItem[] = await Promise.all(providerSuggestions.map(async s => {
274
return this._renameProcessor.proposeRenameRefactoring(this._textModel, s, context);
275
}));
276
277
suggestions.forEach(s => s.addPerformanceMarker('renameProcessed'));
278
279
providerResult.cancelAndDispose({ kind: 'lostRace' });
280
281
if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {
282
const didAllProvidersReturn = providerResult.didAllProvidersReturn;
283
let error: string | undefined = undefined;
284
if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {
285
error = 'canceled';
286
}
287
const result = suggestions.map(c => {
288
const comp = c.getSourceCompletion();
289
if (comp.doNotLog) {
290
return undefined;
291
}
292
const obj = {
293
insertText: comp.insertText,
294
range: comp.range,
295
additionalTextEdits: comp.additionalTextEdits,
296
uri: comp.uri,
297
command: comp.command,
298
gutterMenuLinkAction: comp.gutterMenuLinkAction,
299
shownCommand: comp.shownCommand,
300
completeBracketPairs: comp.completeBracketPairs,
301
isInlineEdit: comp.isInlineEdit,
302
showInlineEditMenu: comp.showInlineEditMenu,
303
showRange: comp.showRange,
304
warning: comp.warning,
305
hint: comp.hint,
306
supportsRename: comp.supportsRename,
307
correlationId: comp.correlationId,
308
jumpToPosition: comp.jumpToPosition,
309
};
310
return {
311
...(cloneAndChange(obj, v => {
312
if (Range.isIRange(v)) {
313
return Range.lift(v).toString();
314
}
315
if (Position.isIPosition(v)) {
316
return Position.lift(v).toString();
317
}
318
if (Command.is(v)) {
319
return { $commandId: v.id };
320
}
321
return v;
322
}) as object),
323
$providerId: c.source.provider.providerId?.toString(),
324
};
325
}).filter(result => result !== undefined);
326
327
this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now(), didAllProvidersReturn });
328
}
329
330
requestResponseInfo.setRequestUuid(providerResult.contextWithUuid.requestUuid);
331
if (producedSuggestion) {
332
requestResponseInfo.setHasProducedSuggestion();
333
if (suggestions.length > 0 && source.token.isCancellationRequested) {
334
suggestions.forEach(s => s.setNotShownReasonIfNotSet('canceled:whileAwaitingOtherProviders'));
335
}
336
} else {
337
if (source.token.isCancellationRequested) {
338
requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:whileFetching');
339
} else {
340
const completionsQuotaExceeded = this._contextKeyService.getContextKeyValue<boolean>('completionsQuotaExceeded');
341
requestResponseInfo.setNoSuggestionReasonIfNotSet(completionsQuotaExceeded ? 'completionsQuotaExceeded' : 'noSuggestion');
342
}
343
}
344
345
const remainingTimeToWait = context.earliestShownDateTime - Date.now();
346
if (remainingTimeToWait > 0) {
347
await wait(remainingTimeToWait, source.token);
348
}
349
350
suggestions.forEach(s => s.addPerformanceMarker('minShowDelayPassed'));
351
352
if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId
353
|| userJumpedToActiveCompletion.get() /* In the meantime the user showed interest for the active completion so dont hide it */) {
354
const notShownReason =
355
source.token.isCancellationRequested ? 'canceled:afterMinShowDelay' :
356
this._store.isDisposed ? 'canceled:disposed' :
357
this._textModel.getVersionId() !== request.versionId ? 'canceled:documentChanged' :
358
userJumpedToActiveCompletion.get() ? 'canceled:userJumped' :
359
'unknown';
360
suggestions.forEach(s => s.setNotShownReasonIfNotSet(notShownReason));
361
return false;
362
}
363
364
const endTime = new Date();
365
this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime());
366
367
const cursorPosition = this._cursorPosition.get();
368
this._updateOperation.clear();
369
transaction(tx => {
370
/** @description Update completions with provider result */
371
const v = this._state.get();
372
373
if (context.selectedSuggestionInfo) {
374
this._state.set({
375
inlineCompletions: InlineCompletionsState.createEmpty(),
376
suggestWidgetInlineCompletions: v.suggestWidgetInlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),
377
}, tx);
378
} else {
379
this._state.set({
380
inlineCompletions: v.inlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),
381
suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),
382
}, tx);
383
}
384
385
v.inlineCompletions.dispose();
386
v.suggestWidgetInlineCompletions.dispose();
387
});
388
} finally {
389
store.dispose();
390
decreaseLoadingCount();
391
this.sendInlineCompletionsRequestTelemetry(requestResponseInfo);
392
}
393
394
return true;
395
})();
396
397
const updateOperation = new UpdateOperation(request, source, promise);
398
this._updateOperation.value = updateOperation;
399
400
return promise;
401
}
402
403
public clear(tx: ITransaction): void {
404
this._updateOperation.clear();
405
const v = this._state.get();
406
this._state.set({
407
inlineCompletions: InlineCompletionsState.createEmpty(),
408
suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty()
409
}, tx);
410
v.inlineCompletions.dispose();
411
v.suggestWidgetInlineCompletions.dispose();
412
}
413
414
public seedInlineCompletionsWithSuggestWidget(): void {
415
const inlineCompletions = this.inlineCompletions.get();
416
const suggestWidgetInlineCompletions = this.suggestWidgetInlineCompletions.get();
417
if (!suggestWidgetInlineCompletions) {
418
return;
419
}
420
transaction(tx => {
421
/** @description Seed inline completions with (newer) suggest widget inline completions */
422
if (!inlineCompletions || (suggestWidgetInlineCompletions.request?.versionId ?? -1) > (inlineCompletions.request?.versionId ?? -1)) {
423
inlineCompletions?.dispose();
424
const s = this._state.get();
425
this._state.set({
426
inlineCompletions: suggestWidgetInlineCompletions.clone(),
427
suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),
428
}, tx);
429
s.inlineCompletions.dispose();
430
s.suggestWidgetInlineCompletions.dispose();
431
}
432
this.clearSuggestWidgetInlineCompletions(tx);
433
});
434
}
435
436
private sendInlineCompletionsRequestTelemetry(
437
requestResponseInfo: RequestResponseData
438
): void {
439
if (!this._sendRequestData.get() && !this._contextKeyService.getContextKeyValue<boolean>('isRunningUnificationExperiment')) {
440
return;
441
}
442
443
if (requestResponseInfo.requestUuid === undefined || requestResponseInfo.hasProducedSuggestion) {
444
return;
445
}
446
447
448
if (!isCompletionsEnabled(this._completionsEnabled, this._textModel.getLanguageId())) {
449
return;
450
}
451
452
if (!requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))) {
453
return;
454
}
455
456
const emptyEndOfLifeEvent: InlineCompletionEndOfLifeEvent = {
457
opportunityId: requestResponseInfo.requestUuid,
458
noSuggestionReason: requestResponseInfo.noSuggestionReason ?? 'unknown',
459
extensionId: 'vscode-core',
460
extensionVersion: '0.0.0',
461
groupId: 'empty',
462
shown: false,
463
skuPlan: requestResponseInfo.requestInfo.sku?.plan,
464
skuType: requestResponseInfo.requestInfo.sku?.type,
465
editorType: requestResponseInfo.requestInfo.editorType,
466
requestReason: requestResponseInfo.requestInfo.reason,
467
typingInterval: requestResponseInfo.requestInfo.typingInterval,
468
typingIntervalCharacterCount: requestResponseInfo.requestInfo.typingIntervalCharacterCount,
469
languageId: requestResponseInfo.requestInfo.languageId,
470
selectedSuggestionInfo: !!requestResponseInfo.context.selectedSuggestionInfo,
471
availableProviders: requestResponseInfo.providers.map(p => p.providerId?.toString()).filter(isDefined).join(','),
472
...forwardToChannelIf(requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))),
473
timeUntilProviderRequest: undefined,
474
timeUntilProviderResponse: undefined,
475
viewKind: undefined,
476
preceeded: undefined,
477
superseded: undefined,
478
reason: undefined,
479
acceptedAlternativeAction: undefined,
480
correlationId: undefined,
481
shownDuration: undefined,
482
shownDurationUncollapsed: undefined,
483
timeUntilShown: undefined,
484
partiallyAccepted: undefined,
485
partiallyAcceptedCountSinceOriginal: undefined,
486
partiallyAcceptedRatioSinceOriginal: undefined,
487
partiallyAcceptedCharactersSinceOriginal: undefined,
488
cursorColumnDistance: undefined,
489
cursorLineDistance: undefined,
490
lineCountOriginal: undefined,
491
lineCountModified: undefined,
492
characterCountOriginal: undefined,
493
characterCountModified: undefined,
494
disjointReplacements: undefined,
495
sameShapeReplacements: undefined,
496
longDistanceHintVisible: undefined,
497
longDistanceHintDistance: undefined,
498
notShownReason: undefined,
499
renameCreated: false,
500
renameDuration: undefined,
501
renameTimedOut: false,
502
renameDroppedOtherEdits: undefined,
503
renameDroppedRenameEdits: undefined,
504
performanceMarkers: undefined,
505
editKind: undefined,
506
};
507
508
const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService);
509
sendInlineCompletionsEndOfLifeTelemetry(dataChannel, emptyEndOfLifeEvent);
510
}
511
512
public clearSuggestWidgetInlineCompletions(tx: ITransaction): void {
513
if (this._updateOperation.value?.request.context.selectedSuggestionInfo) {
514
this._updateOperation.clear();
515
}
516
}
517
518
public cancelUpdate(): void {
519
this._updateOperation.clear();
520
}
521
}
522
523
class UpdateRequest {
524
constructor(
525
public readonly position: Position,
526
public readonly context: InlineCompletionContextWithoutUuid,
527
public readonly versionId: number,
528
public readonly providers: Set<InlineCompletionsProvider>,
529
) {
530
}
531
532
public satisfies(other: UpdateRequest): boolean {
533
return this.position.equals(other.position)
534
&& equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, thisEqualsC())
535
&& (other.context.triggerKind === InlineCompletionTriggerKind.Automatic
536
|| this.context.triggerKind === InlineCompletionTriggerKind.Explicit)
537
&& this.versionId === other.versionId
538
&& isSubset(other.providers, this.providers);
539
}
540
541
public get isExplicitRequest() {
542
return this.context.triggerKind === InlineCompletionTriggerKind.Explicit;
543
}
544
}
545
546
class RequestResponseData {
547
public requestUuid: string | undefined;
548
public noSuggestionReason: string | undefined;
549
public hasProducedSuggestion = false;
550
551
constructor(
552
public readonly context: InlineCompletionContextWithoutUuid,
553
public readonly requestInfo: InlineSuggestRequestInfo,
554
public readonly providers: InlineCompletionsProvider[],
555
) { }
556
557
setRequestUuid(uuid: string) {
558
this.requestUuid = uuid;
559
}
560
561
setNoSuggestionReasonIfNotSet(type: string) {
562
this.noSuggestionReason ??= type;
563
}
564
565
setHasProducedSuggestion() {
566
this.hasProducedSuggestion = true;
567
}
568
}
569
570
function isSubset<T>(set1: Set<T>, set2: Set<T>): boolean {
571
return [...set1].every(item => set2.has(item));
572
}
573
574
function isCompletionsEnabled(completionsEnablementObject: Record<string, boolean> | undefined, modeId: string = '*'): boolean {
575
if (completionsEnablementObject === undefined) {
576
return false; // default to disabled if setting is not available
577
}
578
579
if (typeof completionsEnablementObject[modeId] !== 'undefined') {
580
return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined
581
}
582
583
return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise
584
}
585
586
class UpdateOperation implements IDisposable {
587
constructor(
588
public readonly request: UpdateRequest,
589
public readonly cancellationTokenSource: CancellationTokenSource,
590
public readonly promise: Promise<boolean>,
591
) {
592
}
593
594
dispose() {
595
this.cancellationTokenSource.cancel();
596
}
597
}
598
599
class InlineCompletionsState extends Disposable {
600
public static createEmpty(): InlineCompletionsState {
601
return new InlineCompletionsState([], undefined);
602
}
603
604
constructor(
605
public readonly inlineCompletions: readonly InlineSuggestionItem[],
606
public readonly request: UpdateRequest | undefined,
607
) {
608
for (const inlineCompletion of inlineCompletions) {
609
inlineCompletion.addRef();
610
}
611
612
super();
613
614
this._register({
615
dispose: () => {
616
for (const inlineCompletion of this.inlineCompletions) {
617
inlineCompletion.removeRef();
618
}
619
}
620
});
621
}
622
623
private _findById(id: InlineSuggestionIdentity): InlineSuggestionItem | undefined {
624
return this.inlineCompletions.find(i => i.identity === id);
625
}
626
627
private _findByHash(hash: string): InlineSuggestionItem | undefined {
628
return this.inlineCompletions.find(i => i.hash === hash);
629
}
630
631
/**
632
* Applies the edit on the state.
633
*/
634
public createStateWithAppliedEdit(edit: StringEdit, textModel: ITextModel): InlineCompletionsState {
635
const newInlineCompletions = this.inlineCompletions.map(i => i.withEdit(edit, textModel)).filter(isDefined);
636
return new InlineCompletionsState(newInlineCompletions, this.request);
637
}
638
639
public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState {
640
let itemToPreserve: InlineSuggestionItem | undefined = undefined;
641
if (itemIdToPreserveAtTop) {
642
const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop);
643
if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) {
644
itemToPreserve = itemToPreserveCandidate;
645
646
const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash);
647
if (updatedItemToPreserve) {
648
updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions);
649
} else {
650
updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions];
651
}
652
}
653
}
654
655
const preferInlineCompletions = itemToPreserve
656
// itemToPreserve has precedence
657
? !itemToPreserve.isInlineEdit
658
// Otherwise: prefer inline completion if there is a visible one
659
: updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition));
660
661
let updatedItems: InlineSuggestionItem[] = [];
662
for (const i of updatedSuggestions) {
663
const oldItem = this._findByHash(i.hash);
664
let item;
665
if (oldItem && oldItem !== i) {
666
item = i.withIdentity(oldItem.identity);
667
i.setIsPreceeded(oldItem);
668
oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() });
669
} else {
670
item = i;
671
}
672
if (preferInlineCompletions !== item.isInlineEdit) {
673
updatedItems.push(item);
674
}
675
}
676
677
updatedItems.sort(compareBy(i => i.showInlineEditMenu, booleanComparator));
678
updatedItems = distinctByKey(updatedItems, i => i.semanticId);
679
680
return new InlineCompletionsState(updatedItems, request);
681
}
682
683
public clone(): InlineCompletionsState {
684
return new InlineCompletionsState(this.inlineCompletions, this.request);
685
}
686
}
687
688
/** Keeps the first item in case of duplicates. */
689
function distinctByKey<T>(items: T[], key: (item: T) => unknown): T[] {
690
const seen = new Set();
691
return items.filter(item => {
692
const k = key(item);
693
if (seen.has(k)) {
694
return false;
695
}
696
seen.add(k);
697
return true;
698
});
699
}
700
701
function moveToFront<T>(item: T, items: T[]): T[] {
702
const index = items.indexOf(item);
703
if (index > -1) {
704
return [item, ...items.slice(0, index), ...items.slice(index + 1)];
705
}
706
return items;
707
}
708
709