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