Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts
13399 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 { join } from 'path';
7
import * as vscode from 'vscode';
8
import { InlineCompletionModelInfo, InlineCompletionProviderOption } from 'vscode';
9
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
10
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
11
import { IEnvService } from '../../../platform/env/common/envService';
12
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
13
import { JointCompletionsProviderStrategy, JointCompletionsProviderTriggerChangeStrategy } from '../../../platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions';
14
import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext';
15
import { ObservableGit } from '../../../platform/inlineEdits/common/observableGit';
16
import { checkIfCursorAtEndOfLine, shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils';
17
import { NesHistoryContextProvider } from '../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';
18
import { ILogger, ILogService } from '../../../platform/log/common/logService';
19
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
20
import { ErrorUtils } from '../../../util/common/errors';
21
import { isNotebookCell } from '../../../util/common/notebooks';
22
import { coalesce } from '../../../util/vs/base/common/arrays';
23
import { assertNever, softAssert } from '../../../util/vs/base/common/assert';
24
import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async';
25
import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
26
import { Disposable } from '../../../util/vs/base/common/lifecycle';
27
import { autorun, derived, derivedDisposable, observableFromEvent } from '../../../util/vs/base/common/observable';
28
import { StopWatch } from '../../../util/vs/base/common/stopwatch';
29
import { URI } from '../../../util/vs/base/common/uri';
30
import { StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
31
import { Range } from '../../../util/vs/editor/common/core/range';
32
import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
33
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
34
import { IExtensionContribution } from '../../common/contributions';
35
import { registerUnificationCommands } from '../../completions-core/vscode-node/completionsServiceBridges';
36
import { GhostTextCompletionItem, GhostTextCompletionList } from '../../completions-core/vscode-node/extension/src/ghostText/ghostTextProvider';
37
import { CopilotInlineCompletionItemProvider } from '../../completions-core/vscode-node/extension/src/vscodeInlineCompletionItemProvider';
38
import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService';
39
import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution';
40
import { unificationStateObservable } from '../../completions/vscode-node/completionsUnificationContribution';
41
import { NesChangeHint } from '../common/nesTriggerHint';
42
import { NESInlineCompletionContext } from '../node/nextEditProvider';
43
import { TelemetrySender } from '../node/nextEditProviderTelemetry';
44
import { ExpectedEditCaptureController } from './components/expectedEditCaptureController';
45
import { InlineEditDebugComponent, reportFeedbackCommandId } from './components/inlineEditDebugComponent';
46
import { LogContextRecorder } from './components/logContextRecorder';
47
import { DiagnosticsNextEditProvider } from './features/diagnosticsInlineEditProvider';
48
import { InlineCompletionProviderImpl, NesCompletionItem, NesCompletionList } from './inlineCompletionProvider';
49
import { InlineEditModel } from './inlineEditModel';
50
import { captureExpectedAbortCommandId, captureExpectedConfirmCommandId, captureExpectedStartCommandId, captureExpectedSubmitCommandId, clearCacheCommandId, InlineEditProviderFeature, InlineEditProviderFeatureContribution, learnMoreCommandId, learnMoreLink, reportNotebookNESIssueCommandId } from './inlineEditProviderFeature';
51
import { InlineEditLogger } from './parts/inlineEditLogger';
52
import { VSCodeWorkspace } from './parts/vscodeWorkspace';
53
import { makeSettable } from './utils/observablesUtils';
54
55
export class JointCompletionsProviderContribution extends Disposable implements IExtensionContribution {
56
57
private static NES_GROUP_ID = 'nes';
58
private static COMPLETIONS_GROUP_ID = 'completions';
59
60
private readonly _inlineEditsProviderId = makeSettable(this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsProviderId, this._expService));
61
62
private readonly _hideInternalInterface = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsHideInternalInterface);
63
private readonly _enableDiagnosticsProvider = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.InlineEditsEnableDiagnosticsProvider, this._expService);
64
// FIXME@ulugbekna: re-enable when yieldTo is supported
65
// private readonly _yieldToCopilot = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsYieldToCopilot, this._expService);
66
private readonly _excludedProviders = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsExcludedProviders, this._expService).map(v => v ? v.split(',').map(v => v.trim()).filter(v => v !== '') : []);
67
private readonly _copilotToken = observableFromEvent(this, this._authenticationService.onDidAuthenticationChange, () => this._authenticationService.copilotToken);
68
69
public readonly inlineEditsEnabled = derived(this, (reader) => {
70
const copilotToken = this._copilotToken.read(reader);
71
if (copilotToken === undefined) {
72
return false;
73
}
74
if (copilotToken.isCompletionsQuotaExceeded) {
75
return false;
76
}
77
return true;
78
});
79
80
private readonly _internalActionsEnabled = derived(this, (reader) => {
81
return !!this._copilotToken.read(reader)?.isInternal && !this._hideInternalInterface.read(reader);
82
});
83
84
public readonly isInlineEditsLogFileEnabledObservable = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsLogContextRecorderEnabled);
85
86
private readonly _workspace = derivedDisposable(this, _reader => {
87
return this._instantiationService.createInstance(VSCodeWorkspace);
88
});
89
90
91
constructor(
92
@IVSCodeExtensionContext private readonly _vscodeExtensionContext: IVSCodeExtensionContext,
93
@IInstantiationService private readonly _instantiationService: IInstantiationService,
94
@ICopilotInlineCompletionItemProviderService private readonly _copilotInlineCompletionItemProviderService: ICopilotInlineCompletionItemProviderService,
95
@IConfigurationService private readonly _configurationService: IConfigurationService,
96
@IExperimentationService private readonly _expService: IExperimentationService,
97
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
98
@IEnvService private readonly _envService: IEnvService,
99
) {
100
super();
101
102
const useJointCompletionsProviderObs = _configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderEnabled, _expService);
103
104
this._register(autorun((reader) => { // FX
105
const useJointCompletionsProvider = useJointCompletionsProviderObs.read(reader);
106
if (!useJointCompletionsProvider) {
107
reader.store.add(_instantiationService.createInstance(InlineEditProviderFeatureContribution));
108
reader.store.add(_instantiationService.createInstance(CompletionsCoreContribution));
109
return;
110
}
111
112
const inlineEditFeature = _instantiationService.createInstance(InlineEditProviderFeature);
113
inlineEditFeature.setContext();
114
115
const unificationState = unificationStateObservable(this);
116
117
reader.store.add(autorun((reader) => {
118
const unificationStateValue = unificationState.read(reader);
119
120
const excludes = this._excludedProviders.read(reader).slice();
121
122
let inlineEditProvider: InlineCompletionProviderImpl | undefined = undefined;
123
if (!excludes.includes(JointCompletionsProviderContribution.NES_GROUP_ID) && this.inlineEditsEnabled.read(reader)) {
124
const logger = reader.store.add(this._instantiationService.createInstance(InlineEditLogger));
125
126
const statelessProviderId = this._inlineEditsProviderId.read(reader);
127
128
const workspace = this._workspace.read(reader);
129
const git = reader.store.add(this._instantiationService.createInstance(ObservableGit));
130
const historyContextProvider = new NesHistoryContextProvider(workspace, git);
131
132
let diagnosticsProvider: DiagnosticsNextEditProvider | undefined = undefined;
133
if (this._enableDiagnosticsProvider.read(reader)) {
134
diagnosticsProvider = reader.store.add(this._instantiationService.createInstance(DiagnosticsNextEditProvider, workspace, git));
135
}
136
137
const model = reader.store.add(this._instantiationService.createInstance(InlineEditModel, statelessProviderId, workspace, historyContextProvider, diagnosticsProvider));
138
139
const recordingDirPath = join(this._vscodeExtensionContext.globalStorageUri.fsPath, 'logContextRecordings');
140
141
const isInlineEditLogFileEnabled = this.isInlineEditsLogFileEnabledObservable.read(reader);
142
143
let logContextRecorder: LogContextRecorder | undefined;
144
if (isInlineEditLogFileEnabled) {
145
logContextRecorder = reader.store.add(this._instantiationService.createInstance(LogContextRecorder, recordingDirPath, logger));
146
} else {
147
void LogContextRecorder.cleanupOldRecordings(recordingDirPath);
148
}
149
150
const inlineEditDebugComponent = reader.store.add(new InlineEditDebugComponent(this._internalActionsEnabled, this.inlineEditsEnabled, model.debugRecorder, this._inlineEditsProviderId));
151
152
const telemetrySender = reader.store.add(this._instantiationService.createInstance(TelemetrySender, workspace));
153
154
// Create the expected edit capture controller
155
const expectedEditCaptureController = reader.store.add(this._instantiationService.createInstance(
156
ExpectedEditCaptureController,
157
model.debugRecorder
158
));
159
160
inlineEditProvider = this._instantiationService.createInstance(InlineCompletionProviderImpl, model, logger, logContextRecorder, inlineEditDebugComponent, telemetrySender, expectedEditCaptureController);
161
162
reader.store.add(vscode.commands.registerCommand(learnMoreCommandId, () => {
163
this._envService.openExternal(URI.parse(learnMoreLink));
164
}));
165
166
reader.store.add(vscode.commands.registerCommand(clearCacheCommandId, () => {
167
model.nextEditProvider.clearCache();
168
}));
169
170
reader.store.add(vscode.commands.registerCommand(reportNotebookNESIssueCommandId, () => {
171
const activeNotebook = vscode.window.activeNotebookEditor;
172
const document = vscode.window.activeTextEditor?.document;
173
if (!activeNotebook || !document || !isNotebookCell(document.uri)) {
174
return;
175
}
176
const doc = model.workspace.getDocumentByTextDocument(document);
177
const selection = activeNotebook.selection;
178
if (!selection || !doc) {
179
return;
180
}
181
182
const logContext = new InlineEditRequestLogContext(doc.id.uri, document.version, undefined);
183
logContext.recordingBookmark = model.debugRecorder.createBookmark();
184
void vscode.commands.executeCommand(reportFeedbackCommandId, { logContext });
185
}));
186
187
// Register expected edit capture commands
188
reader.store.add(vscode.commands.registerCommand(captureExpectedStartCommandId, () => {
189
void expectedEditCaptureController.startCapture('manual');
190
}));
191
192
reader.store.add(vscode.commands.registerCommand(captureExpectedConfirmCommandId, () => {
193
void expectedEditCaptureController.confirmCapture();
194
}));
195
196
reader.store.add(vscode.commands.registerCommand(captureExpectedAbortCommandId, () => {
197
void expectedEditCaptureController.abortCapture();
198
}));
199
200
reader.store.add(vscode.commands.registerCommand(captureExpectedSubmitCommandId, () => {
201
void expectedEditCaptureController.submitCaptures();
202
}));
203
}
204
205
let completionsProvider: CopilotInlineCompletionItemProvider | undefined;
206
{
207
const configEnabled = this._configurationService.getExperimentBasedConfigObservable<boolean>(ConfigKey.TeamInternal.InlineEditsEnableGhCompletionsProvider, this._expService).read(reader);
208
const extensionUnification = unificationStateValue?.extensionUnification ?? false;
209
210
// respect excludes if NES is enabled
211
const isExcluded = excludes.includes(JointCompletionsProviderContribution.COMPLETIONS_GROUP_ID) && this.inlineEditsEnabled.read(reader);
212
213
// @ulugbekna: note that we don't want it if modelUnification is on
214
const modelUnification = unificationStateValue?.modelUnification ?? false;
215
if (
216
(!modelUnification || unificationStateValue?.codeUnification || extensionUnification || configEnabled || this._copilotToken.read(reader)?.isNoAuthUser) &&
217
!isExcluded
218
) {
219
completionsProvider = this._copilotInlineCompletionItemProviderService.getOrCreateProvider() as CopilotInlineCompletionItemProvider;
220
}
221
222
void vscode.commands.executeCommand('setContext', 'github.copilot.extensionUnification.activated', extensionUnification);
223
224
if (extensionUnification && completionsProvider) {
225
const completionsInstaService = this._copilotInlineCompletionItemProviderService.getOrCreateInstantiationService();
226
reader.store.add(completionsInstaService.invokeFunction(registerUnificationCommands));
227
}
228
}
229
230
const singularProvider = reader.store.add(this._instantiationService.createInstance(JointCompletionsProvider, completionsProvider, inlineEditProvider));
231
232
if (unificationStateValue?.modelUnification) {
233
if (!excludes.includes('github.copilot')) {
234
excludes.push('github.copilot');
235
}
236
}
237
238
reader.store.add(vscode.languages.registerInlineCompletionItemProvider(
239
'*',
240
singularProvider,
241
{
242
displayName: inlineEditProvider?.displayName,
243
debounceDelayMs: 0, // set 0 debounce to ensure consistent delays/timings
244
groupId: 'nes',
245
excludes,
246
})
247
);
248
249
}));
250
}));
251
}
252
}
253
254
type SingularCompletionItem =
255
| ({ source: 'completions' } & GhostTextCompletionItem)
256
| ({ source: 'inlineEdits' } & NesCompletionItem)
257
;
258
259
type SingularCompletionList =
260
| ({ source: 'completions' } & GhostTextCompletionList)
261
| ({ source: 'inlineEdits' } & NesCompletionList)
262
;
263
264
function toCompletionsList(list: GhostTextCompletionList): SingularCompletionList {
265
return { ...list, items: list.items.map(item => ({ ...item, source: 'completions' })), source: 'completions' };
266
}
267
268
function toInlineEditsList(list: NesCompletionList): SingularCompletionList {
269
return { ...list, items: list.items.map(item => ({ ...item, source: 'inlineEdits' })), source: 'inlineEdits' };
270
}
271
272
type LastNesSuggestion = {
273
docUri: vscode.Uri;
274
docVersionId: number;
275
docWithNesEditApplied: StringText;
276
completionItem: NesCompletionItem;
277
};
278
279
class JointCompletionsProvider extends Disposable implements vscode.InlineCompletionItemProvider {
280
281
private readonly _onDidChangeEmitter: vscode.EventEmitter<NesChangeHint> | undefined;
282
public readonly onDidChange?: vscode.Event<NesChangeHint> | undefined;
283
284
private _requestsInFlight = new Set<CancellationToken>();
285
private _completionsRequestsInFlight = new Set<CancellationToken>();
286
287
private get _isRequestInFlight(): boolean {
288
return this._requestsInFlight.size > 0;
289
}
290
291
private get _isCompletionsRequestInFlight(): boolean {
292
return this._completionsRequestsInFlight.size > 0;
293
}
294
295
private _logger: ILogger;
296
297
//#region Model picker
298
public readonly onDidChangeModelInfo = this._inlineEditProvider?.onDidChangeModelInfo;
299
public readonly setCurrentModelId = this._inlineEditProvider?.setCurrentModelId?.bind(this._inlineEditProvider);
300
public get modelInfo(): InlineCompletionModelInfo | undefined {
301
return this._inlineEditProvider?.modelInfo;
302
}
303
//#endregion
304
305
//#region Provider options
306
public readonly onDidChangeProviderOptions = this._inlineEditProvider?.onDidChangeProviderOptions;
307
public readonly setProviderOptionValue = this._inlineEditProvider?.setProviderOptionValue?.bind(this._inlineEditProvider);
308
public get providerOptions(): readonly InlineCompletionProviderOption[] | undefined {
309
return this._inlineEditProvider?.providerOptions;
310
}
311
//#endregion
312
313
constructor(
314
private readonly _completionsProvider: CopilotInlineCompletionItemProvider | undefined,
315
private readonly _inlineEditProvider: InlineCompletionProviderImpl | undefined,
316
@IConfigurationService private readonly _configService: IConfigurationService,
317
@IExperimentationService private readonly _expService: IExperimentationService,
318
@ILogService logService: ILogService,
319
) {
320
super();
321
322
this._logger = logService.createSubLogger(['NES', 'JointCompletionsProvider']);
323
324
// Only set up the onDidChange emitter if the inlineEditProvider has one to channel
325
if (this._inlineEditProvider?.onDidChange) {
326
this._onDidChangeEmitter = this._register(new vscode.EventEmitter<NesChangeHint>());
327
this.onDidChange = this._onDidChangeEmitter.event;
328
329
this._register(this._inlineEditProvider.onDidChange((changeHint) => {
330
const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderTriggerChangeStrategy, this._expService);
331
switch (strategy) {
332
case JointCompletionsProviderTriggerChangeStrategy.AlwaysTrigger:
333
break;
334
case JointCompletionsProviderTriggerChangeStrategy.NoTriggerOnRequestInFlight:
335
if (this._isRequestInFlight) {
336
this._logger.trace('Skipping onDidChange event firing because request is in flight');
337
return;
338
}
339
break;
340
case JointCompletionsProviderTriggerChangeStrategy.NoTriggerOnCompletionsRequestInFlight:
341
if (this._isCompletionsRequestInFlight) {
342
this._logger.trace('Skipping onDidChange event firing because completions request is in flight');
343
return;
344
}
345
break;
346
default:
347
assertNever(strategy);
348
}
349
this._logger.trace('Firing onDidChange event');
350
this._onDidChangeEmitter!.fire(changeHint);
351
}));
352
}
353
354
softAssert(
355
_completionsProvider?.onDidChange === undefined,
356
'CompletionsProvider does not implement onDidChange'
357
);
358
}
359
360
public async provideInlineCompletionItems(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<SingularCompletionList | undefined> {
361
const logger = this._logger.createSubLogger([shortenOpportunityId(context.requestUuid), 'provideInlineCompletionItems']);
362
363
const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderStrategy, this._expService);
364
365
switch (strategy) {
366
case JointCompletionsProviderStrategy.Regular:
367
return this.provideInlineCompletionItemsRegular(document, position, context, token, logger);
368
case JointCompletionsProviderStrategy.CursorEndOfLine:
369
return this.provideInlineCompletionItemsCursorEndOfLine(document, position, context, token, logger);
370
default:
371
assertNever(strategy);
372
}
373
}
374
375
private async provideInlineCompletionItemsCursorEndOfLine(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, logger: ILogger): Promise<SingularCompletionList | undefined> {
376
const sw = new StopWatch();
377
378
this._requestsInFlight.add(token);
379
const disp = token.onCancellationRequested(() => {
380
this._requestsInFlight.delete(token);
381
});
382
try {
383
if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {
384
logger.trace('Return: neither completions nor NES provider available');
385
return undefined;
386
387
} else if (this._completionsProvider === undefined && this._inlineEditProvider !== undefined) {
388
logger.trace('only NES provider is available, invoking it');
389
const r = await this._invokeNESProvider(logger, document, position, false, context, token, sw);
390
return r ? toInlineEditsList(r) : undefined;
391
392
} else if (this._completionsProvider !== undefined && this._inlineEditProvider === undefined) {
393
logger.trace('only completions provider is available, invoking it');
394
const r = await this._invokeCompletionsProvider(logger, document, position, context, token, sw);
395
return r ? toCompletionsList(r) : undefined;
396
} else {
397
398
const cursorLine = document.lineAt(position.line).text;
399
const isCursorAtEndOfLine = checkIfCursorAtEndOfLine(cursorLine, position.character);
400
401
if (isCursorAtEndOfLine) {
402
logger.trace('cursor is at end of line, invoking ghost-text provider only');
403
const r = await this._invokeCompletionsProvider(logger, document, position, context, token, sw);
404
return r ? toCompletionsList(r) : undefined;
405
}
406
407
const r = await this._invokeNESProvider(logger, document, position, false, context, token, sw);
408
return r ? toInlineEditsList(r) : undefined;
409
}
410
} finally {
411
if (!token.isCancellationRequested) {
412
this._logger.trace('request in flight: false -- due to provider finishing');
413
this._requestsInFlight.delete(token);
414
}
415
disp.dispose();
416
}
417
}
418
419
private lastNesSuggestion: null | LastNesSuggestion = null;
420
private provideInlineCompletionItemsInvocationCount = 0;
421
422
private async provideInlineCompletionItemsRegular(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, logger: ILogger): Promise<SingularCompletionList | undefined> {
423
424
const invocationId = ++this.provideInlineCompletionItemsInvocationCount;
425
const completionsCts = new CancellationTokenSource(token);
426
const nesCts = new CancellationTokenSource(token);
427
428
this._requestsInFlight.add(token);
429
const disp1 = token.onCancellationRequested(() => {
430
logger.trace(`invocation #${invocationId}: request in flight: false -- due to cancellation`);
431
this._requestsInFlight.delete(token);
432
});
433
logger.trace(`invocation #${invocationId} started; request in flight: true`);
434
435
let saveLastNesSuggestion: null | LastNesSuggestion = null;
436
try {
437
const docSnapshot = new StringText(document.getText());
438
const docVersionId = document.version;
439
440
if (this.lastNesSuggestion && this.lastNesSuggestion.docUri.toString() !== document.uri.toString()) {
441
logger.trace('last NES suggestion is not for the current document, ignoring');
442
this.lastNesSuggestion = null;
443
}
444
445
const list = await this._provideInlineCompletionItemsRegular({ document, docSnapshot }, position, this.lastNesSuggestion, context, logger, { coreToken: token, completionsCts, nesCts });
446
447
if (token.isCancellationRequested) {
448
return list;
449
}
450
451
if (!list || list.source !== 'inlineEdits' || list.items.length === 0) {
452
return list;
453
}
454
455
const firstItem = (list.items as NesCompletionItem[])[0];
456
if (!firstItem.range || typeof firstItem.insertText !== 'string') {
457
return list;
458
}
459
460
if (firstItem.uri && firstItem.uri.toString() !== document.uri.toString()) {
461
logger.trace(`The NES suggestion is for a different document (${firstItem.uri.toString()} vs ${document.uri.toString()}), not saving as last NES suggestion`);
462
return list;
463
}
464
465
const applied = applyTextEdit(docSnapshot, firstItem.range, firstItem.insertText);
466
saveLastNesSuggestion = {
467
docUri: document.uri,
468
docVersionId,
469
docWithNesEditApplied: new StringText(applied),
470
completionItem: firstItem,
471
};
472
473
return list;
474
} finally {
475
if (!token.isCancellationRequested) {
476
logger.trace(`invocation #${invocationId}: request in flight: false -- due to provider finishing`);
477
this._requestsInFlight.delete(token);
478
}
479
disp1.dispose();
480
481
// Only save the last NES suggestion if this is the latest invocation
482
if (invocationId === this.provideInlineCompletionItemsInvocationCount) {
483
this.lastNesSuggestion = saveLastNesSuggestion;
484
if (this.lastNesSuggestion) {
485
logger.trace(`Set the last NES suggestion for document ${this.lastNesSuggestion.docUri.toString()}`);
486
} else {
487
logger.trace(`Cleared the last NES suggestion`);
488
}
489
} else {
490
logger.trace(`Not setting the last NES suggestion because a newer invocation exists`);
491
}
492
493
completionsCts.dispose();
494
nesCts.dispose();
495
}
496
}
497
498
private async _provideInlineCompletionItemsRegular(
499
{ document, docSnapshot }: { document: vscode.TextDocument; docSnapshot: StringText },
500
position: vscode.Position,
501
lastNesSuggestion: null | LastNesSuggestion,
502
context: vscode.InlineCompletionContext,
503
logger: ILogger,
504
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
505
): Promise<SingularCompletionList | undefined> {
506
507
const sw = new StopWatch();
508
509
if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {
510
logger.trace('Return: neither completions nor NES provider available');
511
return undefined;
512
}
513
514
logger.trace('requesting completions and/or NES');
515
516
// we don't want to trigger completions on selection change events
517
const isTriggeredDueToSelectionChange = context && (context as NESInlineCompletionContext).changeHint !== undefined;
518
519
if (!lastNesSuggestion || !lastNesSuggestion.completionItem.wasShown) {
520
// prefer completions unless there are none
521
logger.trace(`defaulting to yielding to completions; last NES suggestion is ${lastNesSuggestion ? 'not shown' : 'not available'}`);
522
const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);
523
const nesP = this._invokeNESProvider(logger, document, position, true, context, tokens.nesCts.token, sw);
524
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);
525
}
526
527
logger.trace(`last NES suggestion is for the current document, checking if it agrees with the current suggestion`);
528
529
const enforceCacheDelay = (lastNesSuggestion.docVersionId !== document.version);
530
const nesP = this._invokeNESProvider(logger, document, position, enforceCacheDelay, context, tokens.nesCts.token, sw);
531
if (!nesP) {
532
logger.trace(`no NES provider`);
533
const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);
534
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);
535
}
536
537
const NES_CACHE_WAIT_MS = 10;
538
// scoping the variables
539
{
540
logger.trace(`giving the NES provider ${NES_CACHE_WAIT_MS}ms to return what it has in its cache`);
541
const fastNesResult = await raceCancellation(
542
raceTimeout(
543
nesP,
544
NES_CACHE_WAIT_MS
545
),
546
tokens.coreToken
547
);
548
549
// got nes quickly
550
if (fastNesResult && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (fastNesResult.items as NesCompletionItem[]).at(0))) {
551
logger.trace('last NES suggestion agrees with the current suggestion, using NES');
552
const list: SingularCompletionList = toInlineEditsList(fastNesResult);
553
logger.trace(`Return: returning NES result in ${sw.elapsed()}ms`);
554
return list;
555
}
556
557
if (tokens.coreToken.isCancellationRequested) {
558
logger.trace(`suggestions request was cancelled`);
559
void setEndOfLifeReason(this._completionsProvider, undefined, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
560
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
561
tokens.completionsCts.cancel();
562
tokens.nesCts.cancel();
563
return undefined;
564
}
565
}
566
567
logger.trace(`the NES provider did not return in ${NES_CACHE_WAIT_MS}ms so we are triggering the completions provider too`);
568
const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);
569
570
const suggestionsList = await raceCancellation(
571
Promise.race(coalesce([
572
completionsP?.then(res => ({ type: 'completions' as const, res })),
573
nesP?.then(res => ({ type: 'nes' as const, res })),
574
])),
575
tokens.coreToken
576
);
577
578
// got cancelled
579
if (suggestionsList === undefined) {
580
logger.trace(`suggestions request was cancelled`);
581
void setEndOfLifeReason(this._completionsProvider, completionsP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
582
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
583
tokens.completionsCts.cancel();
584
tokens.nesCts.cancel();
585
return undefined;
586
}
587
588
// got NES first
589
if (suggestionsList.type === 'nes' && suggestionsList.res && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (suggestionsList.res.items as NesCompletionItem[]).at(0))) {
590
logger.trace('last NES suggestion agrees with the current suggestion, using NES');
591
return this._returnNES(suggestionsList.res, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, logger, tokens);
592
}
593
594
logger.trace('falling back to the default because completions came first or NES disagreed');
595
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);
596
}
597
598
private _invokeNESProvider(logger: ILogger, document: vscode.TextDocument, position: vscode.Position, enforceCacheDelay: boolean, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) {
599
const changeHint = context.changeHint === undefined || NesChangeHint.is(context.changeHint) ? context.changeHint as NesChangeHint | undefined : undefined;
600
const nesContext: NESInlineCompletionContext = { ...context, enforceCacheDelay, changeHint };
601
let nesP: Promise<NesCompletionList | undefined> | undefined;
602
if (this._inlineEditProvider) {
603
logger.trace(`- requesting NES provideInlineCompletionItems`);
604
nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, nesContext, ct);
605
nesP.then((nesR) => {
606
logger.trace(`got NES response in ${sw.elapsed()}ms -- ${nesR === undefined ? 'undefined' : `with ${nesR.items.length} items`}`);
607
}).catch((e) => {
608
logger.trace(`NES provider errored after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);
609
});
610
} else {
611
logger.trace(`- no NES provider`);
612
nesP = undefined;
613
}
614
return nesP;
615
}
616
617
private _invokeCompletionsProvider(logger: ILogger, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) {
618
let completionsP: Promise<GhostTextCompletionList | undefined> | undefined;
619
if (this._completionsProvider) {
620
this._completionsRequestsInFlight.add(ct);
621
const disp = ct.onCancellationRequested(() => this._completionsRequestsInFlight.delete(ct));
622
const cleanup = () => {
623
this._completionsRequestsInFlight.delete(ct);
624
disp.dispose();
625
};
626
try { // in case the provider throws synchronously
627
logger.trace(`- requesting completions provideInlineCompletionItems`);
628
completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, ct);
629
completionsP.then((completionsR) => {
630
logger.trace(`got completions response in ${sw.elapsed()}ms -- ${completionsR === undefined ? 'undefined' : `with ${completionsR.items.length} items`}`);
631
}).catch((e) => {
632
logger.trace(`completions provider errored after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);
633
}).finally(() => {
634
cleanup();
635
});
636
} catch (e) {
637
cleanup();
638
logger.trace(`completions provider threw synchronously after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);
639
throw e;
640
}
641
} else {
642
logger.trace(`- no completions provider`);
643
completionsP = undefined;
644
}
645
return completionsP;
646
}
647
648
private async _returnCompletionsOrOtherwiseNES(
649
completionsP: Promise<GhostTextCompletionList | undefined> | undefined,
650
nesP: Promise<NesCompletionList | undefined> | undefined,
651
docSnapshot: StringText,
652
sw: StopWatch,
653
logger: ILogger,
654
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
655
): Promise<SingularCompletionList | undefined> {
656
logger.trace(`waiting for completions and/or NES responses`);
657
658
const completionsR = completionsP ? await completionsP : undefined;
659
logger.trace(`completions response received`);
660
661
if (completionsR && completionsR.items.length > 0) {
662
const filteredCompletionR = JointCompletionsProvider.retainOnlyMeaningfulEdits(docSnapshot, completionsR);
663
if (filteredCompletionR.items.length === 0) {
664
logger.trace(`all completions edits are no-op, ignoring completions response`);
665
} else {
666
logger.trace(`using completions response, cancelling NES provider`);
667
return this._returnCompletions(filteredCompletionR, { kind: vscode.InlineCompletionsDisposeReasonKind.LostRace }, nesP, sw, logger, tokens);
668
}
669
}
670
671
const nesR = nesP ? await nesP : undefined;
672
logger.trace(`NES response received`);
673
674
if (nesR && nesR.items.length > 0) {
675
const filteredNesR = JointCompletionsProvider.retainOnlyMeaningfulEdits(docSnapshot, nesR);
676
if (filteredNesR.items.length === 0) {
677
logger.trace(`all NES edits are no-op, ignoring NES response`);
678
} else {
679
logger.trace(`using NES response`);
680
return this._returnNES(filteredNesR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, logger, tokens);
681
}
682
}
683
684
// return empty completions
685
return this._returnCompletions(completionsR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, nesP, sw, logger, tokens);
686
}
687
688
private _returnCompletions(
689
completionsR: GhostTextCompletionList | undefined,
690
nesDisposeReason: vscode.InlineCompletionsDisposeReason,
691
nesP: Promise<NesCompletionList | undefined> | undefined,
692
sw: StopWatch,
693
logger: ILogger,
694
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
695
): SingularCompletionList | undefined {
696
void setEndOfLifeReason(this._inlineEditProvider, nesP, nesDisposeReason);
697
tokens.nesCts.cancel(); // cancel NES request if still pending
698
if (completionsR === undefined) {
699
logger.trace(`Return: no completions to return in ${sw.elapsed()}ms`);
700
return undefined;
701
}
702
const list: SingularCompletionList = toCompletionsList(completionsR);
703
logger.trace(`Return: use completions response in ${sw.elapsed()}ms`);
704
return list;
705
}
706
707
private _returnNES(
708
nesR: NesCompletionList,
709
completionsDisposeReason: vscode.InlineCompletionsDisposeReason,
710
completionsP: Promise<GhostTextCompletionList | undefined> | undefined,
711
sw: StopWatch,
712
logger: ILogger,
713
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
714
): SingularCompletionList {
715
void setEndOfLifeReason(this._completionsProvider, completionsP, completionsDisposeReason);
716
tokens.completionsCts.cancel(); // cancel completions request if still pending
717
const list: SingularCompletionList = toInlineEditsList(nesR);
718
logger.trace(`Return: returning NES result in ${sw.elapsed()}ms`);
719
return list;
720
}
721
722
private doesNesSuggestionAgree(doc: StringText, docWithNesEditApplied: StringText, nesEdit: NesCompletionItem | undefined): boolean {
723
if (nesEdit === undefined || nesEdit.range === undefined || typeof nesEdit.insertText !== 'string') {
724
return false;
725
}
726
const applied = applyTextEdit(doc, nesEdit.range, nesEdit.insertText);
727
return applied === docWithNesEditApplied.getValue();
728
}
729
730
private static retainOnlyMeaningfulEdits<T extends vscode.InlineCompletionList>(docSnapshot: StringText, list: T): T {
731
// meaningful = not noop
732
function isMeaningfulEdit(item: T['items'][number]): boolean {
733
if (item.range === undefined || // must be a completion with a side-effect, eg a command invocation or something
734
typeof item.insertText !== 'string' // shouldn't happen
735
) {
736
return true;
737
}
738
const originalSnippet = docSnapshot.getValueOfRange(new Range(
739
item.range.start.line + 1,
740
item.range.start.character + 1,
741
item.range.end.line + 1,
742
item.range.end.character + 1,
743
));
744
return originalSnippet !== item.insertText;
745
}
746
const filteredEdits = list.items.filter(isMeaningfulEdit);
747
if (filteredEdits.length === list.items.length) {
748
return list;
749
}
750
return { ...list, items: filteredEdits };
751
}
752
753
public handleDidShowCompletionItem?(completionItem: SingularCompletionItem, updatedInsertText: string): void {
754
switch (completionItem.source) {
755
case 'completions':
756
this._completionsProvider?.handleDidShowCompletionItem?.(completionItem);
757
break;
758
case 'inlineEdits':
759
this._inlineEditProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);
760
break;
761
default:
762
assertNever(completionItem);
763
}
764
}
765
766
public handleDidPartiallyAcceptCompletionItem?(completionItem: SingularCompletionItem, acceptedLength: number & vscode.PartialAcceptInfo): void {
767
switch (completionItem.source) {
768
case 'completions':
769
this._completionsProvider?.handleDidPartiallyAcceptCompletionItem?.(completionItem, acceptedLength);
770
break;
771
case 'inlineEdits':
772
softAssert(this._inlineEditProvider?.handleDidPartiallyAcceptCompletionItem === undefined, 'InlineEditProvider does not implement handleDidPartiallyAcceptCompletionItem');
773
break;
774
default:
775
assertNever(completionItem);
776
}
777
}
778
779
public handleEndOfLifetime?(completionItem: SingularCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {
780
switch (completionItem.source) {
781
case 'completions':
782
this._completionsProvider?.handleEndOfLifetime?.(completionItem, reason);
783
break;
784
case 'inlineEdits':
785
this._inlineEditProvider?.handleEndOfLifetime?.(completionItem, reason);
786
break;
787
default:
788
assertNever(completionItem);
789
}
790
}
791
792
public handleListEndOfLifetime?(list: SingularCompletionList, reason: vscode.InlineCompletionsDisposeReason): void {
793
switch (list.source) {
794
case 'completions':
795
this._completionsProvider?.handleListEndOfLifetime?.(list, reason);
796
break;
797
case 'inlineEdits':
798
this._inlineEditProvider?.handleListEndOfLifetime?.(list, reason);
799
break;
800
default:
801
assertNever(list);
802
}
803
}
804
805
// neither provider implements this deprecated method
806
public handleDidRejectCompletionItem = undefined;
807
}
808
809
function applyTextEdit(doc: StringText, range: vscode.Range, insertText: string): string {
810
const rangeOneBased = new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);
811
const offsetRange = doc.getTransformer().getOffsetRange(rangeOneBased);
812
const edit = new StringReplacement(offsetRange, insertText);
813
const bigEdit = edit.toEdit();
814
return bigEdit.apply(doc.getValue());
815
}
816
817
async function setEndOfLifeReason(provider: vscode.InlineCompletionItemProvider | undefined, promise: Promise<vscode.InlineCompletionList | undefined> | undefined, reason: vscode.InlineCompletionsDisposeReason) {
818
if (promise === undefined) {
819
return;
820
}
821
const result = await promise;
822
if (result === undefined) {
823
return;
824
}
825
for (const item of result.items) {
826
provider?.handleEndOfLifetime?.(item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false });
827
}
828
provider?.handleListEndOfLifetime?.(result, reason);
829
}
830
831