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