Files
vscode/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts
T
2025-12-04 20:20:41 +00:00

614 lines
29 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import * as vscode from 'vscode';
import { IAuthenticationService, IExperimentationService } from '../../../lib/node/chatLibMain';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEnvService } from '../../../platform/env/common/envService';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { JointCompletionsProviderStrategy } from '../../../platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions';
import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext';
import { ObservableGit } from '../../../platform/inlineEdits/common/observableGit';
import { shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils';
import { NesHistoryContextProvider } from '../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';
import { ILogService } from '../../../platform/log/common/logService';
import * as errors from '../../../util/common/errors';
import { isNotebookCell } from '../../../util/common/notebooks';
import { createTracer, ITracer } from '../../../util/common/tracing';
import { coalesce } from '../../../util/vs/base/common/arrays';
import { assertNever, softAssert } from '../../../util/vs/base/common/assert';
import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { autorun, derived, derivedDisposable, observableFromEvent } from '../../../util/vs/base/common/observable';
import { StopWatch } from '../../../util/vs/base/common/stopwatch';
import { URI } from '../../../util/vs/base/common/uri';
import { StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
import { Range } from '../../../util/vs/editor/common/core/range';
import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { IExtensionContribution } from '../../common/contributions';
import { registerUnificationCommands } from '../../completions-core/vscode-node/completionsServiceBridges';
import { CopilotInlineCompletionItemProvider } from '../../completions-core/vscode-node/extension/src/inlineCompletion';
import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService';
import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution';
import { unificationStateObservable } from '../../completions/vscode-node/completionsUnificationContribution';
import { TelemetrySender } from '../node/nextEditProviderTelemetry';
import { InlineEditDebugComponent, reportFeedbackCommandId } from './components/inlineEditDebugComponent';
import { LogContextRecorder } from './components/logContextRecorder';
import { DiagnosticsNextEditProvider } from './features/diagnosticsInlineEditProvider';
import { InlineCompletionProviderImpl, NesCompletionItem, NesCompletionList } from './inlineCompletionProvider';
import { InlineEditModel } from './inlineEditModel';
import { clearCacheCommandId, InlineEditProviderFeature, InlineEditProviderFeatureContribution, learnMoreCommandId, learnMoreLink, reportNotebookNESIssueCommandId } from './inlineEditProviderFeature';
import { InlineEditLogger } from './parts/inlineEditLogger';
import { VSCodeWorkspace } from './parts/vscodeWorkspace';
import { makeSettable } from './utils/observablesUtils';
export class JointCompletionsProviderContribution extends Disposable implements IExtensionContribution {
private readonly _inlineEditsProviderId = makeSettable(this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsProviderId, this._expService));
private readonly _hideInternalInterface = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsHideInternalInterface);
private readonly _enableDiagnosticsProvider = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.InlineEditsEnableDiagnosticsProvider, this._expService);
// FIXME@ulugbekna: re-enable when yieldTo is supported
// private readonly _yieldToCopilot = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsYieldToCopilot, this._expService);
private readonly _excludedProviders = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsExcludedProviders, this._expService).map(v => v ? v.split(',').map(v => v.trim()).filter(v => v !== '') : []);
private readonly _copilotToken = observableFromEvent(this, this._authenticationService.onDidAuthenticationChange, () => this._authenticationService.copilotToken);
public readonly inlineEditsEnabled = derived(this, (reader) => {
const copilotToken = this._copilotToken.read(reader);
if (copilotToken === undefined) {
return false;
}
if (copilotToken.isCompletionsQuotaExceeded) {
return false;
}
return true;
});
private readonly _internalActionsEnabled = derived(this, (reader) => {
return !!this._copilotToken.read(reader)?.isInternal && !this._hideInternalInterface.read(reader);
});
public readonly isInlineEditsLogFileEnabledObservable = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsLogContextRecorderEnabled);
private readonly _workspace = derivedDisposable(this, _reader => {
return this._instantiationService.createInstance(VSCodeWorkspace);
});
constructor(
@IVSCodeExtensionContext private readonly _vscodeExtensionContext: IVSCodeExtensionContext,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ICopilotInlineCompletionItemProviderService private readonly _copilotInlineCompletionItemProviderService: ICopilotInlineCompletionItemProviderService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IExperimentationService private readonly _expService: IExperimentationService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IEnvService private readonly _envService: IEnvService,
) {
super();
const useJointCompletionsProviderObs = _configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderEnabled, _expService);
this._register(autorun((reader) => { // FX
const useJointCompletionsProvider = useJointCompletionsProviderObs.read(reader);
if (!useJointCompletionsProvider) {
reader.store.add(_instantiationService.createInstance(InlineEditProviderFeatureContribution));
reader.store.add(_instantiationService.createInstance(CompletionsCoreContribution));
return;
}
const inlineEditFeature = _instantiationService.createInstance(InlineEditProviderFeature);
reader.store.add(inlineEditFeature.rolloutFeature());
inlineEditFeature.setContext();
const unificationState = unificationStateObservable(this);
reader.store.add(autorun((reader) => {
const unificationStateValue = unificationState.read(reader);
const excludes = this._excludedProviders.read(reader).slice();
let inlineEditProvider: InlineCompletionProviderImpl | undefined = undefined;
if (this.inlineEditsEnabled.read(reader)) {
const logger = reader.store.add(this._instantiationService.createInstance(InlineEditLogger));
const statelessProviderId = this._inlineEditsProviderId.read(reader);
const workspace = this._workspace.read(reader);
const git = reader.store.add(this._instantiationService.createInstance(ObservableGit));
const historyContextProvider = new NesHistoryContextProvider(workspace, git);
let diagnosticsProvider: DiagnosticsNextEditProvider | undefined = undefined;
if (this._enableDiagnosticsProvider.read(reader)) {
diagnosticsProvider = reader.store.add(this._instantiationService.createInstance(DiagnosticsNextEditProvider, workspace, git));
}
const model = reader.store.add(this._instantiationService.createInstance(InlineEditModel, statelessProviderId, workspace, historyContextProvider, diagnosticsProvider));
const recordingDirPath = join(this._vscodeExtensionContext.globalStorageUri.fsPath, 'logContextRecordings');
const isInlineEditLogFileEnabled = this.isInlineEditsLogFileEnabledObservable.read(reader);
let logContextRecorder: LogContextRecorder | undefined;
if (isInlineEditLogFileEnabled) {
logContextRecorder = reader.store.add(this._instantiationService.createInstance(LogContextRecorder, recordingDirPath, logger));
} else {
void LogContextRecorder.cleanupOldRecordings(recordingDirPath);
}
const inlineEditDebugComponent = reader.store.add(new InlineEditDebugComponent(this._internalActionsEnabled, this.inlineEditsEnabled, model.debugRecorder, this._inlineEditsProviderId));
const telemetrySender = reader.store.add(this._instantiationService.createInstance(TelemetrySender));
inlineEditProvider = this._instantiationService.createInstance(InlineCompletionProviderImpl, model, logger, logContextRecorder, inlineEditDebugComponent, telemetrySender);
reader.store.add(vscode.commands.registerCommand(learnMoreCommandId, () => {
this._envService.openExternal(URI.parse(learnMoreLink));
}));
reader.store.add(vscode.commands.registerCommand(clearCacheCommandId, () => {
model.nextEditProvider.clearCache();
}));
reader.store.add(vscode.commands.registerCommand(reportNotebookNESIssueCommandId, () => {
const activeNotebook = vscode.window.activeNotebookEditor;
const document = vscode.window.activeTextEditor?.document;
if (!activeNotebook || !document || !isNotebookCell(document.uri)) {
return;
}
const doc = model.workspace.getDocumentByTextDocument(document);
const selection = activeNotebook.selection;
if (!selection || !doc) {
return;
}
const logContext = new InlineEditRequestLogContext(doc.id.uri, document.version, undefined);
logContext.recordingBookmark = model.debugRecorder.createBookmark();
void vscode.commands.executeCommand(reportFeedbackCommandId, { logContext });
}));
}
let completionsProvider: CopilotInlineCompletionItemProvider | undefined;
{
const configEnabled = this._configurationService.getExperimentBasedConfigObservable<boolean>(ConfigKey.TeamInternal.InlineEditsEnableGhCompletionsProvider, this._expService).read(reader);
const extensionUnification = unificationStateValue?.extensionUnification ?? false;
// @ulugbekna: note that we don't want it if modelUnification is on
const modelUnification = unificationStateValue?.modelUnification ?? false;
if (!modelUnification || unificationStateValue?.codeUnification || extensionUnification || configEnabled || this._copilotToken.read(reader)?.isNoAuthUser) {
completionsProvider = this._copilotInlineCompletionItemProviderService.getOrCreateProvider() as CopilotInlineCompletionItemProvider;
}
void vscode.commands.executeCommand('setContext', 'github.copilot.extensionUnification.activated', extensionUnification);
if (extensionUnification && completionsProvider) {
const completionsInstaService = this._copilotInlineCompletionItemProviderService.getOrCreateInstantiationService();
reader.store.add(completionsInstaService.invokeFunction(registerUnificationCommands));
}
}
const singularProvider = reader.store.add(this._instantiationService.createInstance(JointCompletionsProvider, completionsProvider, inlineEditProvider));
if (unificationStateValue?.modelUnification) {
if (!excludes.includes('github.copilot')) {
excludes.push('github.copilot');
}
}
reader.store.add(vscode.languages.registerInlineCompletionItemProvider(
'*',
singularProvider,
{
displayName: inlineEditProvider?.displayName,
debounceDelayMs: 0, // set 0 debounce to ensure consistent delays/timings
groupId: 'nes',
excludes,
})
);
}));
}));
}
}
type SingularCompletionItem =
| ({ source: 'completions' } & vscode.InlineCompletionItem)
| ({ source: 'inlineEdits' } & NesCompletionItem)
;
type SingularCompletionList =
| ({ source: 'completions' } & vscode.InlineCompletionList)
| ({ source: 'inlineEdits' } & NesCompletionList)
;
function toCompletionsList(list: vscode.InlineCompletionList): SingularCompletionList {
return { ...list, items: list.items.map(item => ({ ...item, source: 'completions' })), source: 'completions' };
}
function toInlineEditsList(list: NesCompletionList): SingularCompletionList {
return { ...list, items: list.items.map(item => ({ ...item, source: 'inlineEdits' })), source: 'inlineEdits' };
}
type LastNesSuggestion = {
docUri: vscode.Uri;
docVersionId: number;
docWithNesEditApplied: StringText;
};
class JointCompletionsProvider extends Disposable implements vscode.InlineCompletionItemProvider {
public onDidChange?: vscode.Event<void> | undefined;
constructor(
private readonly _completionsProvider: CopilotInlineCompletionItemProvider | undefined,
private readonly _inlineEditProvider: InlineCompletionProviderImpl | undefined,
@IConfigurationService private readonly _configService: IConfigurationService,
@IExperimentationService private readonly _expService: IExperimentationService,
@ILogService private readonly _logService: ILogService,
) {
super();
this.onDidChange = _inlineEditProvider?.onDidChange;
softAssert(
_completionsProvider?.onDidChange === undefined,
'CompletionsProvider does not implement onDidChange'
);
}
public async provideInlineCompletionItems(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<SingularCompletionList | undefined> {
const tracer = createTracer(['JointCompletionsProvider', shortenOpportunityId(context.requestUuid), 'provideInlineCompletionItems'], (msg) => this._logService.trace(msg));
const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderStrategy, this._expService);
switch (strategy) {
case JointCompletionsProviderStrategy.Regular:
return this.provideInlineCompletionItemsRegular(document, position, context, token, tracer);
default:
assertNever(strategy);
}
}
private lastNesSuggestion: null | LastNesSuggestion = null;
private provideInlineCompletionItemsInvocationCount = 0;
private async provideInlineCompletionItemsRegular(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, tracer: ITracer): Promise<SingularCompletionList | undefined> {
const invocationId = ++this.provideInlineCompletionItemsInvocationCount;
const completionsCts = new CancellationTokenSource(token);
const nesCts = new CancellationTokenSource(token);
let saveLastNesSuggestion: null | LastNesSuggestion = null;
try {
const docSnapshot = new StringText(document.getText());
const docVersionId = document.version;
if (this.lastNesSuggestion && this.lastNesSuggestion.docUri.toString() !== document.uri.toString()) {
tracer.trace('last NES suggestion is not for the current document, ignoring');
this.lastNesSuggestion = null;
}
const list = await this._provideInlineCompletionItemsRegular({ document, docSnapshot }, position, this.lastNesSuggestion, context, tracer, { coreToken: token, completionsCts, nesCts });
if (token.isCancellationRequested) {
return list;
}
if (!list || list.source !== 'inlineEdits' || list.items.length === 0) {
return list;
}
const firstItem = list.items[0];
if (!firstItem.range || typeof firstItem.insertText !== 'string') {
return list;
}
if (firstItem.uri && firstItem.uri.toString() !== document.uri.toString()) {
tracer.trace(`The NES suggestion is for a different document (${firstItem.uri.toString()} vs ${document.uri.toString()}), not saving as last NES suggestion`);
return list;
}
const applied = applyTextEdit(docSnapshot, firstItem.range, firstItem.insertText);
saveLastNesSuggestion = {
docUri: document.uri,
docVersionId,
docWithNesEditApplied: new StringText(applied),
};
return list;
} finally {
// Only save the last NES suggestion if this is the latest invocation
if (invocationId === this.provideInlineCompletionItemsInvocationCount) {
this.lastNesSuggestion = saveLastNesSuggestion;
if (this.lastNesSuggestion) {
tracer.trace(`Set the last NES suggestion for document ${this.lastNesSuggestion.docUri.toString()}`);
} else {
tracer.trace(`Cleared the last NES suggestion`);
}
} else {
tracer.trace(`Not setting the last NES suggestion because a newer invocation exists`);
}
completionsCts.dispose();
nesCts.dispose();
}
}
private async _provideInlineCompletionItemsRegular(
{ document, docSnapshot }: { document: vscode.TextDocument; docSnapshot: StringText },
position: vscode.Position,
lastNesSuggestion: null | LastNesSuggestion,
context: vscode.InlineCompletionContext,
tracer: ITracer,
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
): Promise<SingularCompletionList | undefined> {
const sw = new StopWatch();
if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {
tracer.returns('neither completions nor NES provider available');
return undefined;
}
tracer.trace('requesting completions and/or NES');
if (!lastNesSuggestion) {
// prefer completions unless there are none
tracer.trace(`no last NES suggestion to consider`);
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
const nesP = this._invokeNESProvider(tracer, document, position, context, tokens, sw);
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
}
tracer.trace(`last NES suggestion is for the current document, checking if it agrees with the current suggestion`);
const nesP = this._invokeNESProvider(tracer, document, position, context, tokens, sw);
if (!nesP) {
tracer.trace(`no NES provider`);
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
}
const NES_CACHE_WAIT_MS = 10;
// scoping the variables
{
tracer.trace(`giving the NES provider ${NES_CACHE_WAIT_MS}ms to return what it has in its cache`);
const fastNesResult = await raceCancellation(
raceTimeout(
nesP,
NES_CACHE_WAIT_MS
),
tokens.coreToken
);
// got nes quickly
if (fastNesResult && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (fastNesResult.items as NesCompletionItem[]).at(0))) {
tracer.trace('last NES suggestion agrees with the current suggestion, using NES');
const list: SingularCompletionList = toInlineEditsList(fastNesResult);
tracer.returns(`returning NES result in ${sw.elapsed()}ms`);
return list;
}
if (tokens.coreToken.isCancellationRequested) {
tracer.trace(`suggestions request was cancelled`);
void setEndOfLifeReason(this._completionsProvider, undefined, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
tokens.completionsCts.cancel();
tokens.nesCts.cancel();
return undefined;
}
}
tracer.trace(`the NES provider did not return in ${NES_CACHE_WAIT_MS}ms so we are triggering the completions provider too`);
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
const suggestionsList = await raceCancellation(
Promise.race(coalesce([
completionsP?.then(res => ({ type: 'completions' as const, res })),
nesP?.then(res => ({ type: 'nes' as const, res })),
])),
tokens.coreToken
);
// got cancelled
if (suggestionsList === undefined) {
tracer.trace(`suggestions request was cancelled`);
void setEndOfLifeReason(this._completionsProvider, completionsP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
tokens.completionsCts.cancel();
tokens.nesCts.cancel();
return undefined;
}
// got NES first
if (suggestionsList.type === 'nes' && suggestionsList.res && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (suggestionsList.res.items as NesCompletionItem[]).at(0))) {
tracer.trace('last NES suggestion agrees with the current suggestion, using NES');
return this._returnNES(suggestionsList.res, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, tracer, tokens);
}
tracer.trace('falling back to the default because completions came first or NES disagreed');
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
}
private _invokeNESProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) {
let nesP: Promise<NesCompletionList | undefined> | undefined;
if (this._inlineEditProvider) {
tracer.trace(`- requesting NES provideInlineCompletionItems`);
nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, context, tokens.nesCts.token);
nesP.then((nesR) => {
tracer.trace(`got NES response in ${sw.elapsed()}ms -- ${nesR === undefined ? 'undefined' : `with ${nesR.items.length} items`}`);
}).catch((e) => {
tracer.trace(`NES provider errored after ${sw.elapsed()}ms -- ${errors.toString(errors.fromUnknown(e))}`);
});
} else {
tracer.trace(`- no NES provider`);
nesP = undefined;
}
return nesP;
}
private _invokeCompletionsProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) {
let completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined;
if (this._completionsProvider) {
tracer.trace(`- requesting completions provideInlineCompletionItems`);
completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, tokens.completionsCts.token);
completionsP.then((completionsR) => {
tracer.trace(`got completions response in ${sw.elapsed()}ms -- ${completionsR === undefined ? 'undefined' : `with ${completionsR.items.length} items`}`);
}).catch((e) => {
tracer.trace(`completions provider errored after ${sw.elapsed()}ms -- ${errors.toString(errors.fromUnknown(e))}`);
});
} else {
tracer.trace(`- no completions provider`);
completionsP = undefined;
}
return completionsP;
}
private async _returnCompletionsOrOtherwiseNES(
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
nesP: Promise<NesCompletionList | undefined> | undefined,
sw: StopWatch,
tracer: ITracer,
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
) {
tracer.trace(`waiting for completions and/or NES responses`);
const completionsR = completionsP ? await completionsP : undefined;
tracer.trace(`completions response received`);
if (completionsR && completionsR.items.length > 0) {
tracer.trace(`using completions response, cancelling NES provider`);
return this._returnCompletions(completionsR, { kind: vscode.InlineCompletionsDisposeReasonKind.LostRace }, nesP, sw, tracer, tokens);
}
const nesR = nesP ? await nesP : undefined;
tracer.trace(`NES response received`);
if (nesR && nesR.items.length > 0) {
tracer.trace(`using NES response`);
return this._returnNES(nesR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, tracer, tokens);
}
// return empty completions
return this._returnCompletions(completionsR ?? { items: [] }, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, nesP, sw, tracer, tokens);
}
private _returnCompletions(
completionsR: vscode.InlineCompletionList,
nesDisposeReason: vscode.InlineCompletionsDisposeReason,
nesP: Promise<NesCompletionList | undefined> | undefined,
sw: StopWatch,
tracer: ITracer,
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
) {
void setEndOfLifeReason(this._inlineEditProvider, nesP, nesDisposeReason);
tokens.nesCts.cancel(); // cancel NES request if still pending
const list: SingularCompletionList = toCompletionsList(completionsR);
tracer.returns(`use completions response in ${sw.elapsed()}ms`);
return list;
}
private _returnNES(
nesR: NesCompletionList,
completionsDisposeReason: vscode.InlineCompletionsDisposeReason,
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
sw: StopWatch,
tracer: ITracer,
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
) {
void setEndOfLifeReason(this._completionsProvider, completionsP, completionsDisposeReason);
tokens.completionsCts.cancel(); // cancel completions request if still pending
const list: SingularCompletionList = toInlineEditsList(nesR);
tracer.returns(`returning NES result in ${sw.elapsed()}ms`);
return list;
}
private doesNesSuggestionAgree(doc: StringText, docWithNesEditApplied: StringText, nesEdit: NesCompletionItem | undefined): boolean {
if (nesEdit === undefined || nesEdit.range === undefined || typeof nesEdit.insertText !== 'string') {
return false;
}
const applied = applyTextEdit(doc, nesEdit.range, nesEdit.insertText);
return applied === docWithNesEditApplied.getValue();
}
public handleDidShowCompletionItem?(completionItem: SingularCompletionItem, updatedInsertText: string): void {
switch (completionItem.source) {
case 'completions':
this._completionsProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);
break;
case 'inlineEdits':
this._inlineEditProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);
break;
default:
assertNever(completionItem);
}
}
public handleDidPartiallyAcceptCompletionItem?(completionItem: SingularCompletionItem, acceptedLength: number & vscode.PartialAcceptInfo): void {
switch (completionItem.source) {
case 'completions':
this._completionsProvider?.handleDidPartiallyAcceptCompletionItem?.(completionItem, acceptedLength);
break;
case 'inlineEdits':
softAssert(this._inlineEditProvider?.handleDidPartiallyAcceptCompletionItem === undefined, 'InlineEditProvider does not implement handleDidPartiallyAcceptCompletionItem');
break;
default:
assertNever(completionItem);
}
}
public handleEndOfLifetime?(completionItem: SingularCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {
switch (completionItem.source) {
case 'completions':
this._completionsProvider?.handleEndOfLifetime?.(completionItem, reason);
break;
case 'inlineEdits':
this._inlineEditProvider?.handleEndOfLifetime?.(completionItem, reason);
break;
default:
assertNever(completionItem);
}
}
public handleListEndOfLifetime?(list: SingularCompletionList, reason: vscode.InlineCompletionsDisposeReason): void {
switch (list.source) {
case 'completions':
softAssert(this._completionsProvider?.handleListEndOfLifetime === undefined, 'CompletionsProvider does not implement handleListEndOfLifetime');
break;
case 'inlineEdits':
this._inlineEditProvider?.handleListEndOfLifetime?.(list, reason);
break;
default:
assertNever(list);
}
}
// neither provider implements this deprecated method
public handleDidRejectCompletionItem = undefined;
}
function applyTextEdit(doc: StringText, range: vscode.Range, insertText: string): string {
const rangeOneBased = new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);
const offsetRange = doc.getTransformer().getOffsetRange(rangeOneBased);
const edit = new StringReplacement(offsetRange, insertText);
const bigEdit = edit.toEdit();
return bigEdit.apply(doc.getValue());
}
async function setEndOfLifeReason(provider: vscode.InlineCompletionItemProvider | undefined, promise: Promise<vscode.InlineCompletionList | undefined> | undefined, reason: vscode.InlineCompletionsDisposeReason) {
if (promise === undefined) {
return;
}
const result = await promise;
if (result === undefined) {
return;
}
for (const item of result.items) {
provider?.handleEndOfLifetime?.(item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false });
}
provider?.handleListEndOfLifetime?.(result, reason);
}