Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.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 { mapFindFirst } from '../../../../../base/common/arraysFind.js';
7
import { arrayEqualsC } from '../../../../../base/common/equals.js';
8
import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js';
9
import { Emitter } from '../../../../../base/common/event.js';
10
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, constObservable, derived, derivedHandleChanges, derivedOpts, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js';
12
import { firstNonWhitespaceIndex } from '../../../../../base/common/strings.js';
13
import { isDefined } from '../../../../../base/common/types.js';
14
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
15
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
16
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
17
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
18
import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js';
19
import { EditorOption } from '../../../../common/config/editorOptions.js';
20
import { CursorColumns } from '../../../../common/core/cursorColumns.js';
21
import { LineRange } from '../../../../common/core/ranges/lineRange.js';
22
import { Position } from '../../../../common/core/position.js';
23
import { Range } from '../../../../common/core/range.js';
24
import { Selection } from '../../../../common/core/selection.js';
25
import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js';
26
import { TextLength } from '../../../../common/core/text/textLength.js';
27
import { ScrollType } from '../../../../common/editorCommon.js';
28
import { InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js';
29
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
30
import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js';
31
import { TextModelText } from '../../../../common/model/textModelText.js';
32
import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';
33
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
34
import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';
35
import { SnippetController2 } from '../../../snippet/browser/snippetController2.js';
36
import { getEndPositionsAfterApplying, removeTextReplacementCommonSuffixPrefix } from '../utils.js';
37
import { AnimatedValue, easeOutCubic, ObservableAnimatedValue } from './animation.js';
38
import { computeGhostText } from './computeGhostText.js';
39
import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js';
40
import { InlineCompletionsSource } from './inlineCompletionsSource.js';
41
import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem } from './inlineSuggestionItem.js';
42
import { InlineCompletionContextWithoutUuid, InlineCompletionEditorType, InlineSuggestRequestInfo, InlineSuggestSku } from './provideInlineCompletions.js';
43
import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';
44
import { SuggestItemInfo } from './suggestWidgetAdapter.js';
45
import { TextModelEditSource, EditSources } from '../../../../common/textModelEditSource.js';
46
import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';
47
import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';
48
import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js';
49
import { TypingInterval } from './typingSpeed.js';
50
import { StringReplacement } from '../../../../common/core/edits/stringEdit.js';
51
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
52
import { URI } from '../../../../../base/common/uri.js';
53
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
54
import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js';
55
import { Schemas } from '../../../../../base/common/network.js';
56
57
export class InlineCompletionsModel extends Disposable {
58
private readonly _source;
59
private readonly _isActive = observableValue<boolean>(this, false);
60
private readonly _onlyRequestInlineEditsSignal = observableSignal(this);
61
private readonly _forceUpdateExplicitlySignal = observableSignal(this);
62
private readonly _noDelaySignal = observableSignal(this);
63
64
private readonly _fetchSpecificProviderSignal = observableSignal<InlineCompletionsProvider | undefined>(this);
65
66
// We use a semantic id to keep the same inline completion selected even if the provider reorders the completions.
67
private readonly _selectedInlineCompletionId = observableValue<string | undefined>(this, undefined);
68
public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1));
69
public readonly allPositions = derived(this, reader => this._positions.read(reader));
70
71
private readonly sku = observableValue<InlineSuggestSku | undefined>(this, undefined);
72
73
private _isAcceptingPartially = false;
74
private readonly _appearedInsideViewport = derived<boolean>(this, reader => {
75
const state = this.state.read(reader);
76
if (!state || !state.inlineSuggestion) {
77
return false;
78
}
79
80
return isSuggestionInViewport(this._editor, state.inlineSuggestion);
81
});
82
public get isAcceptingPartially() { return this._isAcceptingPartially; }
83
84
private readonly _onDidAccept = new Emitter<void>();
85
public readonly onDidAccept = this._onDidAccept.event;
86
87
private readonly _editorObs;
88
89
private readonly _typing: TypingInterval;
90
91
private readonly _suggestPreviewEnabled;
92
private readonly _suggestPreviewMode;
93
private readonly _inlineSuggestMode;
94
private readonly _suppressedInlineCompletionGroupIds;
95
private readonly _inlineEditsEnabled;
96
private readonly _inlineEditsShowCollapsedEnabled;
97
private readonly _triggerCommandOnProviderChange;
98
private readonly _minShowDelay;
99
private readonly _showOnSuggestConflict;
100
private readonly _suppressInSnippetMode;
101
private readonly _isInSnippetMode;
102
103
get editor() {
104
return this._editor;
105
}
106
107
constructor(
108
public readonly textModel: ITextModel,
109
private readonly _selectedSuggestItem: IObservable<SuggestItemInfo | undefined>,
110
public readonly _textModelVersionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,
111
private readonly _positions: IObservable<readonly Position[]>,
112
private readonly _debounceValue: IFeatureDebounceInformation,
113
private readonly _enabled: IObservable<boolean>,
114
private readonly _editor: ICodeEditor,
115
@IInstantiationService private readonly _instantiationService: IInstantiationService,
116
@ICommandService private readonly _commandService: ICommandService,
117
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
118
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
119
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
120
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
121
@IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService,
122
@IDefaultAccountService defaultAccountService: IDefaultAccountService,
123
) {
124
super();
125
this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition));
126
this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind);
127
128
this._editorObs = observableCodeEditor(this._editor);
129
130
const suggest = this._editorObs.getOption(EditorOption.suggest);
131
this._suggestPreviewEnabled = suggest.map(v => v.preview);
132
this._suggestPreviewMode = suggest.map(v => v.previewMode);
133
134
const inlineSuggest = this._editorObs.getOption(EditorOption.inlineSuggest);
135
this._inlineSuggestMode = inlineSuggest.map(v => v.mode);
136
this._suppressedInlineCompletionGroupIds = inlineSuggest.map(v => new Set(v.experimental.suppressInlineSuggestions.split(',')));
137
this._inlineEditsEnabled = inlineSuggest.map(v => !!v.edits.enabled);
138
this._inlineEditsShowCollapsedEnabled = inlineSuggest.map(s => s.edits.showCollapsed);
139
this._triggerCommandOnProviderChange = inlineSuggest.map(s => s.triggerCommandOnProviderChange);
140
this._minShowDelay = inlineSuggest.map(s => s.minShowDelay);
141
this._showOnSuggestConflict = inlineSuggest.map(s => s.experimental.showOnSuggestConflict);
142
this._suppressInSnippetMode = inlineSuggest.map(s => s.suppressInSnippetMode);
143
144
const snippetController = SnippetController2.get(this._editor);
145
this._isInSnippetMode = snippetController?.isInSnippetObservable ?? constObservable(false);
146
147
defaultAccountService.getDefaultAccount().then(createDisposableCb(account => this.sku.set(skuFromAccount(account), undefined), this._store));
148
this._register(defaultAccountService.onDidChangeDefaultAccount(account => this.sku.set(skuFromAccount(account), undefined)));
149
150
this._typing = this._register(new TypingInterval(this.textModel));
151
152
this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => {
153
if (isSnoozing) {
154
this.stop();
155
}
156
}));
157
158
{ // Determine editor type
159
const isNotebook = this.textModel.uri.scheme === Schemas.vscodeNotebookCell;
160
const [diffEditor] = this._codeEditorService.listDiffEditors()
161
.filter(d =>
162
d.getOriginalEditor().getId() === this._editor.getId() ||
163
d.getModifiedEditor().getId() === this._editor.getId());
164
165
this.isInDiffEditor = !!diffEditor;
166
this.editorType = isNotebook ? InlineCompletionEditorType.Notebook
167
: this.isInDiffEditor ? InlineCompletionEditorType.DiffEditor
168
: InlineCompletionEditorType.TextEditor;
169
}
170
171
this._register(recomputeInitiallyAndOnChange(this.state, (s) => {
172
if (s && s.inlineSuggestion) {
173
this._inlineCompletionsService.reportNewCompletion(s.inlineSuggestion.requestUuid);
174
}
175
}));
176
177
this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise));
178
179
this._register(autorun(reader => {
180
this._editorObs.versionId.read(reader);
181
this._inAcceptFlow.set(false, undefined);
182
}));
183
184
this._register(autorun(reader => {
185
const jumpToReset = this.state.map((s, reader) => !s || s.kind === 'inlineEdit' && !s.cursorAtInlineEdit.read(reader)).read(reader);
186
if (jumpToReset) {
187
this._jumpedToId.set(undefined, undefined);
188
}
189
}));
190
191
this._register(autorun(reader => {
192
const inlineSuggestion = this.state.map(s => s?.inlineSuggestion).read(reader);
193
if (inlineSuggestion) {
194
inlineSuggestion.addPerformanceMarker('activeSuggestion');
195
}
196
}));
197
198
const inlineEditSemanticId = this.inlineEditState.map(s => s?.inlineSuggestion.semanticId);
199
200
this._register(autorun(reader => {
201
const id = inlineEditSemanticId.read(reader);
202
if (id) {
203
this._editor.pushUndoStop();
204
this._lastShownInlineCompletionInfo = {
205
alternateTextModelVersionId: this.textModel.getAlternativeVersionId(),
206
inlineCompletion: this.state.get()!.inlineSuggestion!,
207
};
208
}
209
}));
210
211
// TODO: should use getAvailableProviders and update on _suppressedInlineCompletionGroupIds change
212
const inlineCompletionProviders = observableFromEvent(this._languageFeaturesService.inlineCompletionsProvider.onDidChange, () => this._languageFeaturesService.inlineCompletionsProvider.all(textModel));
213
mapObservableArrayCached(this, inlineCompletionProviders, (provider, store) => {
214
if (!provider.onDidChangeInlineCompletions) {
215
return;
216
}
217
218
store.add(provider.onDidChangeInlineCompletions(() => {
219
if (!this._enabled.get()) {
220
return;
221
}
222
223
// Only update the active editor
224
const activeEditor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor();
225
if (activeEditor !== this._editor) {
226
return;
227
}
228
229
if (this._triggerCommandOnProviderChange.get()) {
230
// TODO@hediet remove this and always do the else branch.
231
this.trigger(undefined, { onlyFetchInlineEdits: true });
232
return;
233
}
234
235
236
// If there is an active suggestion from a different provider, we ignore the update
237
const activeState = this.state.get();
238
if (activeState && (activeState.inlineSuggestion || activeState.edits) && activeState.inlineSuggestion?.source.provider !== provider) {
239
return;
240
}
241
242
transaction(tx => {
243
this._fetchSpecificProviderSignal.trigger(tx, provider);
244
this.trigger(tx);
245
});
246
247
}));
248
}).recomputeInitiallyAndOnChange(this._store);
249
250
this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store);
251
}
252
253
private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;
254
private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;
255
private readonly _didUndoInlineEdits = derivedHandleChanges({
256
owner: this,
257
changeTracker: {
258
createChangeSummary: () => ({ didUndo: false }),
259
handleChange: (ctx, changeSummary) => {
260
changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing;
261
return true;
262
}
263
}
264
}, (reader, changeSummary) => {
265
const versionId = this._textModelVersionId.read(reader);
266
if (versionId !== null
267
&& this._lastAcceptedInlineCompletionInfo
268
&& this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1
269
&& this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit
270
&& changeSummary.didUndo
271
) {
272
this._lastAcceptedInlineCompletionInfo = undefined;
273
return true;
274
}
275
return false;
276
});
277
278
public debugGetSelectedSuggestItem(): IObservable<SuggestItemInfo | undefined> {
279
return this._selectedSuggestItem;
280
}
281
282
public getIndentationInfo(reader: IReader) {
283
let startsWithIndentation = false;
284
let startsWithIndentationLessThanTabSize = true;
285
const ghostText = this?.primaryGhostText.read(reader);
286
if (!!this?._selectedSuggestItem && ghostText && ghostText.parts.length > 0) {
287
const { column, lines } = ghostText.parts[0];
288
289
const firstLine = lines[0].line;
290
291
const indentationEndColumn = this.textModel.getLineIndentColumn(ghostText.lineNumber);
292
const inIndentation = column <= indentationEndColumn;
293
294
if (inIndentation) {
295
let firstNonWsIdx = firstNonWhitespaceIndex(firstLine);
296
if (firstNonWsIdx === -1) {
297
firstNonWsIdx = firstLine.length - 1;
298
}
299
startsWithIndentation = firstNonWsIdx > 0;
300
301
const tabSize = this.textModel.getOptions().tabSize;
302
const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize);
303
startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize;
304
}
305
}
306
return {
307
startsWithIndentation,
308
startsWithIndentationLessThanTabSize,
309
};
310
}
311
312
private readonly _preserveCurrentCompletionReasons = new Set([
313
VersionIdChangeReason.Redo,
314
VersionIdChangeReason.Undo,
315
VersionIdChangeReason.AcceptWord,
316
]);
317
318
private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason {
319
if (e?.isUndoing) { return VersionIdChangeReason.Undo; }
320
if (e?.isRedoing) { return VersionIdChangeReason.Redo; }
321
if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; }
322
return VersionIdChangeReason.Other;
323
}
324
325
public readonly dontRefetchSignal = observableSignal(this);
326
327
private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({
328
owner: this,
329
changeTracker: {
330
createChangeSummary: () => ({
331
dontRefetch: false,
332
preserveCurrentCompletion: false,
333
inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic,
334
onlyRequestInlineEdits: false,
335
shouldDebounce: true,
336
provider: undefined as InlineCompletionsProvider | undefined,
337
textChange: false,
338
changeReason: '',
339
}),
340
handleChange: (ctx, changeSummary) => {
341
/** @description fetch inline completions */
342
if (ctx.didChange(this._textModelVersionId)) {
343
if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {
344
changeSummary.preserveCurrentCompletion = true;
345
}
346
const detailedReasons = ctx.change?.detailedReasons ?? [];
347
changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : '';
348
changeSummary.textChange = true;
349
} else if (ctx.didChange(this._forceUpdateExplicitlySignal)) {
350
changeSummary.preserveCurrentCompletion = true;
351
changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;
352
} else if (ctx.didChange(this.dontRefetchSignal)) {
353
changeSummary.dontRefetch = true;
354
} else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) {
355
changeSummary.onlyRequestInlineEdits = true;
356
} else if (ctx.didChange(this._fetchSpecificProviderSignal)) {
357
changeSummary.provider = ctx.change;
358
}
359
return true;
360
},
361
},
362
}, (reader, changeSummary) => {
363
this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation
364
this._noDelaySignal.read(reader);
365
this.dontRefetchSignal.read(reader);
366
this._onlyRequestInlineEditsSignal.read(reader);
367
this._forceUpdateExplicitlySignal.read(reader);
368
this._fetchSpecificProviderSignal.read(reader);
369
const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader))
370
&& (!this._inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit);
371
if (!shouldUpdate) {
372
this._source.cancelUpdate();
373
return undefined;
374
}
375
376
this._textModelVersionId.read(reader); // Refetch on text change
377
378
const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(undefined);
379
let suggestItem = this._selectedSuggestItem.read(reader);
380
if (this._shouldShowOnSuggestConflict.read(undefined)) {
381
suggestItem = undefined;
382
}
383
if (suggestWidgetInlineCompletions && !suggestItem) {
384
this._source.seedInlineCompletionsWithSuggestWidget();
385
}
386
387
if (changeSummary.dontRefetch) {
388
return Promise.resolve(true);
389
}
390
391
if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) {
392
transaction(tx => {
393
this._source.clear(tx);
394
});
395
return undefined;
396
}
397
398
let reason: string = '';
399
if (changeSummary.provider) {
400
reason += 'providerOnDidChange';
401
} else if (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit) {
402
reason += 'explicit';
403
}
404
if (changeSummary.changeReason) {
405
reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason;
406
}
407
408
const typingInterval = this._typing.getTypingInterval();
409
const requestInfo: InlineSuggestRequestInfo = {
410
editorType: this.editorType,
411
startTime: Date.now(),
412
languageId: this.textModel.getLanguageId(),
413
reason,
414
typingInterval: typingInterval.averageInterval,
415
typingIntervalCharacterCount: typingInterval.characterCount,
416
availableProviders: [],
417
sku: this.sku.read(undefined),
418
};
419
420
let context: InlineCompletionContextWithoutUuid = {
421
triggerKind: changeSummary.inlineCompletionTriggerKind,
422
selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(),
423
includeInlineCompletions: !changeSummary.onlyRequestInlineEdits,
424
includeInlineEdits: this._inlineEditsEnabled.read(reader),
425
requestIssuedDateTime: requestInfo.startTime,
426
earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)),
427
};
428
429
if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) {
430
if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) {
431
// When undoing back to a version where an inline edit/completion was shown,
432
// we want to show an inline edit (or completion) again if it was originally an inline edit (or completion).
433
context = {
434
...context,
435
includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,
436
includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,
437
};
438
}
439
}
440
441
const itemToPreserveCandidate = this.selectedInlineCompletion.read(undefined) ?? this._inlineCompletionItems.read(undefined)?.inlineEdit;
442
const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable
443
? itemToPreserveCandidate : undefined;
444
const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.read(undefined)?.inlineEdit?.semanticId);
445
446
const providers = changeSummary.provider
447
? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId?.toString() }
448
: { providers: this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel), label: undefined }; // TODO: should use inlineCompletionProviders
449
const availableProviders = this.getAvailableProviders(providers.providers);
450
requestInfo.availableProviders = availableProviders.map(p => p.providerId).filter(isDefined);
451
452
return this._source.fetch(availableProviders, providers.label, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, requestInfo);
453
});
454
455
// TODO: This is not an ideal implementation of excludesGroupIds, however as this is currently still behind proposed API
456
// and due to the time constraints, we are using a simplified approach
457
private getAvailableProviders(providers: InlineCompletionsProvider[]): InlineCompletionsProvider[] {
458
const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get();
459
const unsuppressedProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId)));
460
461
const excludedGroupIds = new Set<string>();
462
for (const provider of unsuppressedProviders) {
463
provider.excludesGroupIds?.forEach(p => excludedGroupIds.add(p));
464
}
465
466
const availableProviders: InlineCompletionsProvider[] = [];
467
for (const provider of unsuppressedProviders) {
468
if (provider.groupId && excludedGroupIds.has(provider.groupId)) {
469
continue;
470
}
471
availableProviders.push(provider);
472
}
473
474
return availableProviders;
475
}
476
477
public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean } = {}): Promise<void> {
478
subtransaction(tx, tx => {
479
if (options.onlyFetchInlineEdits) {
480
this._onlyRequestInlineEditsSignal.trigger(tx);
481
}
482
if (options.noDelay) {
483
this._noDelaySignal.trigger(tx);
484
}
485
this._isActive.set(true, tx);
486
487
if (options.explicit) {
488
this._inAcceptFlow.set(true, tx);
489
this._forceUpdateExplicitlySignal.trigger(tx);
490
}
491
if (options.provider) {
492
this._fetchSpecificProviderSignal.trigger(tx, options.provider);
493
}
494
});
495
await this._fetchInlineCompletionsPromise.get();
496
}
497
498
public async triggerExplicitly(tx?: ITransaction, onlyFetchInlineEdits: boolean = false): Promise<void> {
499
return this.trigger(tx, { onlyFetchInlineEdits, explicit: true });
500
}
501
502
public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void {
503
subtransaction(tx, tx => {
504
if (stopReason === 'explicitCancel') {
505
const inlineCompletion = this.state.get()?.inlineSuggestion;
506
if (inlineCompletion) {
507
inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected });
508
}
509
}
510
511
this._isActive.set(false, tx);
512
this._source.clear(tx);
513
});
514
}
515
516
private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => {
517
const c = this._source.inlineCompletions.read(reader);
518
if (!c) { return undefined; }
519
const cursorPosition = this.primaryPosition.read(reader);
520
let inlineEdit: InlineEditItem | undefined = undefined;
521
const visibleCompletions: InlineCompletionItem[] = [];
522
for (const completion of c.inlineCompletions) {
523
if (!completion.isInlineEdit) {
524
if (completion.isVisible(this.textModel, cursorPosition)) {
525
visibleCompletions.push(completion);
526
}
527
} else {
528
inlineEdit = completion;
529
}
530
}
531
532
if (visibleCompletions.length !== 0) {
533
// Don't show the inline edit if there is a visible completion
534
inlineEdit = undefined;
535
}
536
537
return {
538
inlineCompletions: visibleCompletions,
539
inlineEdit,
540
};
541
});
542
543
private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: arrayEqualsC() }, reader => {
544
const c = this._inlineCompletionItems.read(reader);
545
return c?.inlineCompletions ?? [];
546
});
547
548
public readonly selectedInlineCompletionIndex = derived<number>(this, (reader) => {
549
const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader);
550
const filteredCompletions = this._filteredInlineCompletionItems.read(reader);
551
const idx = this._selectedInlineCompletionId === undefined ? -1
552
: filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId);
553
if (idx === -1) {
554
// Reset the selection so that the selection does not jump back when it appears again
555
this._selectedInlineCompletionId.set(undefined, undefined);
556
return 0;
557
}
558
return idx;
559
});
560
561
public readonly selectedInlineCompletion = derived<InlineCompletionItem | undefined>(this, (reader) => {
562
const filteredCompletions = this._filteredInlineCompletionItems.read(reader);
563
const idx = this.selectedInlineCompletionIndex.read(reader);
564
return filteredCompletions[idx];
565
});
566
567
public readonly activeCommands = derivedOpts<InlineCompletionCommand[]>({ owner: this, equalsFn: arrayEqualsC() },
568
r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? []
569
);
570
571
public readonly lastTriggerKind: IObservable<InlineCompletionTriggerKind | undefined>;
572
573
public readonly inlineCompletionsCount = derived<number | undefined>(this, reader => {
574
if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) {
575
return this._filteredInlineCompletionItems.read(reader).length;
576
} else {
577
return undefined;
578
}
579
});
580
581
private readonly _hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0);
582
583
private readonly _shouldShowOnSuggestConflict = derived(this, reader => {
584
const showOnSuggestConflict = this._showOnSuggestConflict.read(reader);
585
if (showOnSuggestConflict !== 'never') {
586
const hasInlineCompletion = !!this.selectedInlineCompletion.read(reader);
587
if (hasInlineCompletion) {
588
const item = this._selectedSuggestItem.read(reader);
589
if (!item) {
590
return false;
591
}
592
if (showOnSuggestConflict === 'whenSuggestListIsIncomplete') {
593
return item.listIncomplete;
594
}
595
return true;
596
}
597
}
598
return false;
599
});
600
601
public readonly state = derivedOpts<{
602
kind: 'ghostText';
603
edits: readonly TextReplacement[];
604
primaryGhostText: GhostTextOrReplacement;
605
ghostTexts: readonly GhostTextOrReplacement[];
606
suggestItem: SuggestItemInfo | undefined;
607
inlineSuggestion: InlineCompletionItem | undefined;
608
} | {
609
kind: 'inlineEdit';
610
edits: readonly TextReplacement[];
611
inlineSuggestion: InlineEditItem;
612
cursorAtInlineEdit: IObservable<boolean>;
613
nextEditUri: URI | undefined;
614
} | undefined>({
615
owner: this,
616
equalsFn: (a, b) => {
617
if (!a || !b) { return a === b; }
618
619
if (a.kind === 'ghostText' && b.kind === 'ghostText') {
620
return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts)
621
&& a.inlineSuggestion === b.inlineSuggestion
622
&& a.suggestItem === b.suggestItem;
623
} else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') {
624
return a.inlineSuggestion === b.inlineSuggestion;
625
}
626
return false;
627
}
628
}, (reader) => {
629
const model = this.textModel;
630
631
if (this._suppressInSnippetMode.read(reader) && this._isInSnippetMode.read(reader)) {
632
return undefined;
633
}
634
635
const item = this._inlineCompletionItems.read(reader);
636
const inlineEditResult = item?.inlineEdit;
637
if (inlineEditResult) {
638
if (this._hasVisiblePeekWidgets.read(reader)) {
639
return undefined;
640
}
641
const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber));
642
const stringEdit = inlineEditResult.action?.kind === 'edit' ? inlineEditResult.action.stringEdit : undefined;
643
const replacements = stringEdit ? TextEdit.fromStringEdit(stringEdit, new TextModelText(this.textModel)).replacements : [];
644
645
const nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') &&
646
// eslint-disable-next-line local/code-no-any-casts
647
item.inlineEdit?.command.arguments?.length ? URI.from(<any>item.inlineEdit?.command.arguments[0]) : undefined;
648
return { kind: 'inlineEdit', inlineSuggestion: inlineEditResult, edits: replacements, cursorAtInlineEdit, nextEditUri };
649
}
650
651
const suggestItem = this._selectedSuggestItem.read(reader);
652
if (!this._shouldShowOnSuggestConflict.read(reader) && suggestItem) {
653
const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model);
654
const augmentation = this._computeAugmentation(suggestCompletionEdit, reader);
655
656
const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader);
657
if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; }
658
659
const fullEdit = augmentation?.edit ?? suggestCompletionEdit;
660
const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0;
661
662
const mode = this._suggestPreviewMode.read(reader);
663
const positions = this._positions.read(reader);
664
const allPotentialEdits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)];
665
const validEditsAndGhostTexts = allPotentialEdits
666
.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength) : undefined }))
667
.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);
668
const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);
669
const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);
670
const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []);
671
return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineSuggestion: augmentation?.completion, suggestItem };
672
} else {
673
if (!this._isActive.read(reader)) { return undefined; }
674
const inlineSuggestion = this.selectedInlineCompletion.read(reader);
675
if (!inlineSuggestion) { return undefined; }
676
677
const replacement = inlineSuggestion.getSingleTextEdit();
678
const mode = this._inlineSuggestMode.read(reader);
679
const positions = this._positions.read(reader);
680
const allPotentialEdits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)];
681
const validEditsAndGhostTexts = allPotentialEdits
682
.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], 0) : undefined }))
683
.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);
684
const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);
685
const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);
686
if (!ghostTexts[0]) { return undefined; }
687
return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineSuggestion, suggestItem: undefined };
688
}
689
});
690
691
public readonly status = derived(this, reader => {
692
if (this._source.loading.read(reader)) { return 'loading'; }
693
const s = this.state.read(reader);
694
if (s?.kind === 'ghostText') { return 'ghostText'; }
695
if (s?.kind === 'inlineEdit') { return 'inlineEdit'; }
696
return 'noSuggestion';
697
});
698
699
public readonly inlineCompletionState = derived(this, reader => {
700
const s = this.state.read(reader);
701
if (!s || s.kind !== 'ghostText') {
702
return undefined;
703
}
704
if (this._editorObs.inComposition.read(reader)) {
705
return undefined;
706
}
707
return s;
708
});
709
710
public readonly inlineEditState = derived(this, reader => {
711
const s = this.state.read(reader);
712
if (!s || s.kind !== 'inlineEdit') {
713
return undefined;
714
}
715
return s;
716
});
717
718
public readonly inlineEditAvailable = derived(this, reader => {
719
const s = this.inlineEditState.read(reader);
720
return !!s;
721
});
722
723
private _computeAugmentation(suggestCompletion: TextReplacement, reader: IReader | undefined) {
724
const model = this.textModel;
725
const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader);
726
const candidateInlineCompletions = suggestWidgetInlineCompletions
727
? suggestWidgetInlineCompletions.inlineCompletions.filter(c => !c.isInlineEdit)
728
: [this.selectedInlineCompletion.read(reader)].filter(isDefined);
729
730
const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => {
731
let r = completion.getSingleTextEdit();
732
r = singleTextRemoveCommonPrefix(
733
r,
734
model,
735
Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())
736
);
737
return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined;
738
});
739
740
return augmentedCompletion;
741
}
742
743
public readonly warning = derived(this, reader => {
744
return this.inlineCompletionState.read(reader)?.inlineSuggestion?.warning;
745
});
746
747
public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => {
748
const v = this.inlineCompletionState.read(reader);
749
if (!v) {
750
return undefined;
751
}
752
return v.ghostTexts;
753
});
754
755
public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => {
756
const v = this.inlineCompletionState.read(reader);
757
if (!v) {
758
return undefined;
759
}
760
return v?.primaryGhostText;
761
});
762
763
public readonly showCollapsed = derived<boolean>(this, reader => {
764
const state = this.state.read(reader);
765
if (!state || state.kind !== 'inlineEdit') {
766
return false;
767
}
768
769
if (state.inlineSuggestion.hint || state.inlineSuggestion.action?.kind === 'jumpTo') {
770
return false;
771
}
772
773
const isCurrentModelVersion = state.inlineSuggestion.updatedEditModelVersion === this._textModelVersionId.read(reader);
774
return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion)
775
&& this._jumpedToId.read(reader) !== state.inlineSuggestion.semanticId
776
&& !this._inAcceptFlow.read(reader);
777
});
778
779
private readonly _tabShouldIndent = derived(this, reader => {
780
if (this._inAcceptFlow.read(reader)) {
781
return false;
782
}
783
784
function isMultiLine(range: Range): boolean {
785
return range.startLineNumber !== range.endLineNumber;
786
}
787
788
function getNonIndentationRange(model: ITextModel, lineNumber: number): Range {
789
const columnStart = model.getLineIndentColumn(lineNumber);
790
const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber);
791
const columnEnd = Math.max(lastNonWsColumn, columnStart);
792
return new Range(lineNumber, columnStart, lineNumber, columnEnd);
793
}
794
795
const selections = this._editorObs.selections.read(reader);
796
return selections?.some(s => {
797
if (s.isEmpty()) {
798
return this.textModel.getLineLength(s.startLineNumber) === 0;
799
} else {
800
return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber));
801
}
802
});
803
});
804
805
public readonly tabShouldJumpToInlineEdit = derived(this, reader => {
806
if (this._tabShouldIndent.read(reader)) {
807
return false;
808
}
809
810
const s = this.inlineEditState.read(reader);
811
if (!s) {
812
return false;
813
}
814
815
816
if (s.inlineSuggestion.action?.kind === 'jumpTo') {
817
return true;
818
}
819
820
if (this.showCollapsed.read(reader)) {
821
return true;
822
}
823
824
if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {
825
return false;
826
}
827
828
return !s.cursorAtInlineEdit.read(reader);
829
});
830
831
public readonly tabShouldAcceptInlineEdit = derived(this, reader => {
832
const s = this.inlineEditState.read(reader);
833
if (!s) {
834
return false;
835
}
836
if (s.inlineSuggestion.action?.kind === 'jumpTo') {
837
return false;
838
}
839
if (this.showCollapsed.read(reader)) {
840
return false;
841
}
842
if (this._tabShouldIndent.read(reader)) {
843
return false;
844
}
845
if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {
846
return true;
847
}
848
if (s.inlineSuggestion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) {
849
return true;
850
}
851
if (this._jumpedToId.read(reader) === s.inlineSuggestion.semanticId) {
852
return true;
853
}
854
855
return s.cursorAtInlineEdit.read(reader);
856
});
857
858
public readonly isInDiffEditor;
859
860
public readonly editorType: InlineCompletionEditorType;
861
862
private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise<void> {
863
await this.triggerExplicitly();
864
865
const completions = this._filteredInlineCompletionItems.get() || [];
866
if (completions.length > 0) {
867
const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length;
868
this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined);
869
} else {
870
this._selectedInlineCompletionId.set(undefined, undefined);
871
}
872
}
873
874
public async next(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(1); }
875
876
public async previous(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(-1); }
877
878
private _getMetadata(completion: InlineSuggestionItem, languageId: string, type: 'word' | 'line' | undefined = undefined): TextModelEditSource {
879
if (type) {
880
return EditSources.inlineCompletionPartialAccept({
881
nes: completion.isInlineEdit,
882
requestUuid: completion.requestUuid,
883
providerId: completion.source.provider.providerId,
884
languageId,
885
type,
886
correlationId: completion.getSourceCompletion().correlationId,
887
});
888
} else {
889
return EditSources.inlineCompletionAccept({
890
nes: completion.isInlineEdit,
891
requestUuid: completion.requestUuid,
892
correlationId: completion.getSourceCompletion().correlationId,
893
providerId: completion.source.provider.providerId,
894
languageId
895
});
896
}
897
}
898
899
public async accept(editor: ICodeEditor = this._editor, alternativeAction: boolean = false): Promise<void> {
900
if (editor.getModel() !== this.textModel) {
901
throw new BugIndicatingError();
902
}
903
904
let completion: InlineSuggestionItem;
905
let isNextEditUri = false;
906
const state = this.state.get();
907
if (state?.kind === 'ghostText') {
908
if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {
909
return;
910
}
911
completion = state.inlineSuggestion;
912
} else if (state?.kind === 'inlineEdit') {
913
completion = state.inlineSuggestion;
914
isNextEditUri = !!state.nextEditUri;
915
} else {
916
return;
917
}
918
919
// Make sure the completion list will not be disposed before the text change is sent.
920
completion.addRef();
921
922
try {
923
let followUpTrigger = false;
924
editor.pushUndoStop();
925
if (isNextEditUri) {
926
// Do nothing
927
} else if (completion.action?.kind === 'edit') {
928
const action = completion.action;
929
if (alternativeAction && action.alternativeAction) {
930
followUpTrigger = true;
931
const altCommand = action.alternativeAction.command;
932
await this._commandService
933
.executeCommand(altCommand.id, ...(altCommand.arguments || []))
934
.then(undefined, onUnexpectedExternalError);
935
} else if (action.snippetInfo) {
936
const mainEdit = TextReplacement.delete(action.textReplacement.range);
937
const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));
938
const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]);
939
editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));
940
941
editor.setPosition(action.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept');
942
SnippetController2.get(editor)?.insert(action.snippetInfo.snippet, { undoStopBefore: false });
943
} else {
944
const edits = state.edits;
945
946
// The cursor should move to the end of the edit, not the end of the range provided by the extension
947
// Inline Edit diffs (human readable) the suggestion from the extension so it already removes common suffix/prefix
948
// Inline Completions does diff the suggestion so it may contain common suffix
949
let minimalEdits = edits;
950
if (state.kind === 'ghostText') {
951
minimalEdits = removeTextReplacementCommonSuffixPrefix(edits, this.textModel);
952
}
953
const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p));
954
955
const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));
956
const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]);
957
958
editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));
959
960
if (completion.hint === undefined) {
961
// do not move the cursor when the completion is displayed in a different location
962
editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept');
963
}
964
965
if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) {
966
const editRanges = edit.getNewRanges();
967
const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => {
968
this._store.delete(dec);
969
}));
970
}
971
}
972
}
973
974
this._onDidAccept.fire();
975
976
// Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset).
977
this.stop();
978
979
if (completion.command) {
980
await this._commandService
981
.executeCommand(completion.command.id, ...(completion.command.arguments || []))
982
.then(undefined, onUnexpectedExternalError);
983
}
984
985
// TODO: how can we make alternative actions to retrigger?
986
if (followUpTrigger) {
987
this.trigger(undefined);
988
}
989
990
completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction });
991
} finally {
992
completion.removeRef();
993
this._inAcceptFlow.set(true, undefined);
994
this._lastAcceptedInlineCompletionInfo = { textModelVersionIdAfter: this.textModel.getVersionId(), inlineCompletion: completion };
995
}
996
}
997
998
public async acceptNextWord(): Promise<void> {
999
await this._acceptNext(this._editor, 'word', (pos, text) => {
1000
const langId = this.textModel.getLanguageIdAtPosition(pos.lineNumber, pos.column);
1001
const config = this._languageConfigurationService.getLanguageConfiguration(langId);
1002
const wordRegExp = new RegExp(config.wordDefinition.source, config.wordDefinition.flags.replace('g', ''));
1003
1004
const m1 = text.match(wordRegExp);
1005
let acceptUntilIndexExclusive = 0;
1006
if (m1 && m1.index !== undefined) {
1007
if (m1.index === 0) {
1008
acceptUntilIndexExclusive = m1[0].length;
1009
} else {
1010
acceptUntilIndexExclusive = m1.index;
1011
}
1012
} else {
1013
acceptUntilIndexExclusive = text.length;
1014
}
1015
1016
const wsRegExp = /\s+/g;
1017
const m2 = wsRegExp.exec(text);
1018
if (m2 && m2.index !== undefined) {
1019
if (m2.index + m2[0].length < acceptUntilIndexExclusive) {
1020
acceptUntilIndexExclusive = m2.index + m2[0].length;
1021
}
1022
}
1023
return acceptUntilIndexExclusive;
1024
}, PartialAcceptTriggerKind.Word);
1025
}
1026
1027
public async acceptNextLine(): Promise<void> {
1028
await this._acceptNext(this._editor, 'line', (pos, text) => {
1029
const m = text.match(/\n/);
1030
if (m && m.index !== undefined) {
1031
return m.index + 1;
1032
}
1033
return text.length;
1034
}, PartialAcceptTriggerKind.Line);
1035
}
1036
1037
private async _acceptNext(editor: ICodeEditor, type: 'word' | 'line', getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise<void> {
1038
if (editor.getModel() !== this.textModel) {
1039
throw new BugIndicatingError();
1040
}
1041
1042
const state = this.inlineCompletionState.get();
1043
if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {
1044
return;
1045
}
1046
const ghostText = state.primaryGhostText;
1047
const completion = state.inlineSuggestion;
1048
1049
if (completion.snippetInfo) {
1050
// not in WYSIWYG mode, partial commit might change completion, thus it is not supported
1051
await this.accept(editor);
1052
return;
1053
}
1054
1055
const firstPart = ghostText.parts[0];
1056
const ghostTextPos = new Position(ghostText.lineNumber, firstPart.column);
1057
const ghostTextVal = firstPart.text;
1058
const acceptUntilIndexExclusive = getAcceptUntilIndex(ghostTextPos, ghostTextVal);
1059
if (acceptUntilIndexExclusive === ghostTextVal.length && ghostText.parts.length === 1) {
1060
this.accept(editor);
1061
return;
1062
}
1063
const partialGhostTextVal = ghostTextVal.substring(0, acceptUntilIndexExclusive);
1064
1065
const positions = this._positions.get();
1066
const cursorPosition = positions[0];
1067
1068
// Executing the edit might free the completion, so we have to hold a reference on it.
1069
completion.addRef();
1070
try {
1071
this._isAcceptingPartially = true;
1072
try {
1073
editor.pushUndoStop();
1074
const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos);
1075
const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal;
1076
const primaryEdit = new TextReplacement(replaceRange, newText);
1077
const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)].filter(isDefined);
1078
const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p));
1079
1080
editor.edit(TextEdit.fromParallelReplacementsUnsorted(edits), this._getMetadata(completion, type));
1081
editor.setSelections(selections, 'inlineCompletionPartialAccept');
1082
editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Smooth);
1083
} finally {
1084
this._isAcceptingPartially = false;
1085
}
1086
1087
const acceptedRange = Range.fromPositions(completion.editRange.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos));
1088
// This assumes that the inline completion and the model use the same EOL style.
1089
const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF);
1090
const acceptedLength = text.length;
1091
completion.reportPartialAccept(
1092
acceptedLength,
1093
{ kind, acceptedLength: acceptedLength },
1094
{ characters: acceptUntilIndexExclusive, ratio: acceptUntilIndexExclusive / ghostTextVal.length, count: 1 }
1095
);
1096
1097
} finally {
1098
completion.removeRef();
1099
}
1100
}
1101
1102
public handleSuggestAccepted(item: SuggestItemInfo) {
1103
const itemEdit = singleTextRemoveCommonPrefix(item.getSingleTextEdit(), this.textModel);
1104
const augmentedCompletion = this._computeAugmentation(itemEdit, undefined);
1105
if (!augmentedCompletion) { return; }
1106
1107
// This assumes that the inline completion and the model use the same EOL style.
1108
const alreadyAcceptedLength = this.textModel.getValueInRange(augmentedCompletion.completion.editRange, EndOfLinePreference.LF).length;
1109
const acceptedLength = alreadyAcceptedLength + itemEdit.text.length;
1110
1111
augmentedCompletion.completion.reportPartialAccept(itemEdit.text.length, {
1112
kind: PartialAcceptTriggerKind.Suggest,
1113
acceptedLength,
1114
}, {
1115
characters: itemEdit.text.length,
1116
count: 1,
1117
ratio: 1
1118
});
1119
}
1120
1121
public extractReproSample(): Repro {
1122
const value = this.textModel.getValue();
1123
const item = this.state.get()?.inlineSuggestion;
1124
return {
1125
documentValue: value,
1126
inlineCompletion: item?.getSourceCompletion(),
1127
};
1128
}
1129
1130
private readonly _jumpedToId = observableValue<undefined | string>(this, undefined);
1131
private readonly _inAcceptFlow = observableValue(this, false);
1132
public readonly inAcceptFlow: IObservable<boolean> = this._inAcceptFlow;
1133
1134
public jump(): void {
1135
const s = this.inlineEditState.get();
1136
if (!s) { return; }
1137
1138
const suggestion = s.inlineSuggestion;
1139
suggestion.addRef();
1140
try {
1141
transaction(tx => {
1142
if (suggestion.action?.kind === 'jumpTo') {
1143
this.stop(undefined, tx);
1144
suggestion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction: false });
1145
}
1146
1147
this._jumpedToId.set(s.inlineSuggestion.semanticId, tx);
1148
this.dontRefetchSignal.trigger(tx);
1149
const targetRange = s.inlineSuggestion.targetRange;
1150
const targetPosition = targetRange.getStartPosition();
1151
this._editor.setPosition(targetPosition, 'inlineCompletions.jump');
1152
1153
// TODO: consider using view information to reveal it
1154
const isSingleLineChange = targetRange.isSingleLine() && (s.inlineSuggestion.hint || (s.inlineSuggestion.action?.kind === 'edit' && !s.inlineSuggestion.action.textReplacement.text.includes('\n')));
1155
if (isSingleLineChange || s.inlineSuggestion.action?.kind === 'jumpTo') {
1156
this._editor.revealPosition(targetPosition, ScrollType.Smooth);
1157
} else {
1158
const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1);
1159
this._editor.revealRange(revealRange, ScrollType.Smooth);
1160
}
1161
1162
s.inlineSuggestion.identity.setJumpTo(tx);
1163
1164
this._editor.focus();
1165
});
1166
} finally {
1167
suggestion.removeRef();
1168
}
1169
}
1170
1171
public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, timeWhenShown: number): Promise<void> {
1172
await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData, this.textModel, timeWhenShown);
1173
}
1174
}
1175
1176
interface Repro {
1177
documentValue: string;
1178
inlineCompletion: InlineCompletion | undefined;
1179
}
1180
1181
export enum VersionIdChangeReason {
1182
Undo,
1183
Redo,
1184
AcceptWord,
1185
Other,
1186
}
1187
1188
export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryTextRepl: TextReplacement): (TextReplacement | undefined)[] {
1189
if (positions.length === 1) {
1190
// No secondary cursor positions
1191
return [];
1192
}
1193
const text = new TextModelText(textModel);
1194
const textTransformer = text.getTransformer();
1195
const primaryOffset = textTransformer.getOffset(positions[0]);
1196
const secondaryOffsets = positions.slice(1).map(pos => textTransformer.getOffset(pos));
1197
1198
primaryTextRepl = primaryTextRepl.removeCommonPrefixAndSuffix(text);
1199
const primaryStringRepl = textTransformer.getStringReplacement(primaryTextRepl);
1200
1201
const deltaFromOffsetToRangeStart = primaryStringRepl.replaceRange.start - primaryOffset;
1202
const primaryContextRange = primaryStringRepl.replaceRange.join(OffsetRange.emptyAt(primaryOffset));
1203
const primaryContextValue = text.getValueOfOffsetRange(primaryContextRange);
1204
1205
const replacements = secondaryOffsets.map(secondaryOffset => {
1206
const newRangeStart = secondaryOffset + deltaFromOffsetToRangeStart;
1207
const newRangeEnd = newRangeStart + primaryStringRepl.replaceRange.length;
1208
const range = new OffsetRange(newRangeStart, newRangeEnd);
1209
1210
const contextRange = range.join(OffsetRange.emptyAt(secondaryOffset));
1211
const contextValue = text.getValueOfOffsetRange(contextRange);
1212
if (contextValue !== primaryContextValue) {
1213
return undefined;
1214
}
1215
1216
const stringRepl = new StringReplacement(range, primaryStringRepl.newText);
1217
const repl = textTransformer.getTextReplacement(stringRepl);
1218
return repl;
1219
}).filter(isDefined);
1220
1221
return replacements;
1222
}
1223
1224
class FadeoutDecoration extends Disposable {
1225
constructor(
1226
editor: ICodeEditor,
1227
ranges: Range[],
1228
onDispose?: () => void,
1229
) {
1230
super();
1231
1232
if (onDispose) {
1233
this._register({ dispose: () => onDispose() });
1234
}
1235
1236
this._register(observableCodeEditor(editor).setDecorations(constObservable(ranges.map<IModelDeltaDecoration>(range => ({
1237
range: range,
1238
options: {
1239
description: 'animation',
1240
className: 'edits-fadeout-decoration',
1241
zIndex: 1,
1242
}
1243
})))));
1244
1245
const animation = new AnimatedValue(1, 0, 1000, easeOutCubic);
1246
const val = new ObservableAnimatedValue(animation);
1247
1248
this._register(autorun(reader => {
1249
const opacity = val.getValue(reader);
1250
editor.getContainerDomNode().style.setProperty('--animation-opacity', opacity.toString());
1251
if (animation.isFinished()) {
1252
this.dispose();
1253
}
1254
}));
1255
}
1256
}
1257
1258
export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem, reader: IReader | undefined = undefined): boolean {
1259
const targetRange = suggestion.targetRange;
1260
1261
// TODO make getVisibleRanges reactive!
1262
observableCodeEditor(editor).scrollTop.read(reader);
1263
const visibleRanges = editor.getVisibleRanges();
1264
1265
if (visibleRanges.length < 1) {
1266
return false;
1267
}
1268
1269
const viewportRange = new Range(
1270
visibleRanges[0].startLineNumber,
1271
visibleRanges[0].startColumn,
1272
visibleRanges[visibleRanges.length - 1].endLineNumber,
1273
visibleRanges[visibleRanges.length - 1].endColumn
1274
);
1275
return viewportRange.containsRange(targetRange);
1276
}
1277
1278
function skuFromAccount(account: IDefaultAccount | null): InlineSuggestSku | undefined {
1279
if (account?.access_type_sku && account?.copilot_plan) {
1280
return { type: account.access_type_sku, plan: account.copilot_plan };
1281
}
1282
return undefined;
1283
}
1284
1285
class DisposableCallback<T> {
1286
private _cb: ((e: T) => void) | undefined;
1287
1288
constructor(cb: (e: T) => void) {
1289
this._cb = cb;
1290
}
1291
1292
dispose(): void {
1293
this._cb = undefined;
1294
}
1295
1296
readonly handler = (val: T) => {
1297
return this._cb?.(val);
1298
};
1299
}
1300
1301
function createDisposableCb<T>(cb: (e: T) => void, store: DisposableStore): (e: T) => void {
1302
const dcb = new DisposableCallback(cb);
1303
store.add(dcb);
1304
return dcb.handler;
1305
}
1306
1307