Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { join } from 'path';6import * as vscode from 'vscode';7import { InlineCompletionModelInfo, InlineCompletionProviderOption } from 'vscode';8import { IAuthenticationService } from '../../../platform/authentication/common/authentication';9import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';10import { IEnvService } from '../../../platform/env/common/envService';11import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';12import { JointCompletionsProviderStrategy, JointCompletionsProviderTriggerChangeStrategy } from '../../../platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions';13import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext';14import { ObservableGit } from '../../../platform/inlineEdits/common/observableGit';15import { checkIfCursorAtEndOfLine, shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils';16import { NesHistoryContextProvider } from '../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';17import { ILogger, ILogService } from '../../../platform/log/common/logService';18import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';19import { ErrorUtils } from '../../../util/common/errors';20import { isNotebookCell } from '../../../util/common/notebooks';21import { coalesce } from '../../../util/vs/base/common/arrays';22import { assertNever, softAssert } from '../../../util/vs/base/common/assert';23import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async';24import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation';25import { Disposable } from '../../../util/vs/base/common/lifecycle';26import { autorun, derived, derivedDisposable, observableFromEvent } from '../../../util/vs/base/common/observable';27import { StopWatch } from '../../../util/vs/base/common/stopwatch';28import { URI } from '../../../util/vs/base/common/uri';29import { StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';30import { Range } from '../../../util/vs/editor/common/core/range';31import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';32import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';33import { IExtensionContribution } from '../../common/contributions';34import { registerUnificationCommands } from '../../completions-core/vscode-node/completionsServiceBridges';35import { GhostTextCompletionItem, GhostTextCompletionList } from '../../completions-core/vscode-node/extension/src/ghostText/ghostTextProvider';36import { CopilotInlineCompletionItemProvider } from '../../completions-core/vscode-node/extension/src/vscodeInlineCompletionItemProvider';37import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService';38import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution';39import { unificationStateObservable } from '../../completions/vscode-node/completionsUnificationContribution';40import { NesChangeHint } from '../common/nesTriggerHint';41import { NESInlineCompletionContext } from '../node/nextEditProvider';42import { TelemetrySender } from '../node/nextEditProviderTelemetry';43import { ExpectedEditCaptureController } from './components/expectedEditCaptureController';44import { InlineEditDebugComponent, reportFeedbackCommandId } from './components/inlineEditDebugComponent';45import { LogContextRecorder } from './components/logContextRecorder';46import { DiagnosticsNextEditProvider } from './features/diagnosticsInlineEditProvider';47import { InlineCompletionProviderImpl, NesCompletionItem, NesCompletionList } from './inlineCompletionProvider';48import { InlineEditModel } from './inlineEditModel';49import { captureExpectedAbortCommandId, captureExpectedConfirmCommandId, captureExpectedStartCommandId, captureExpectedSubmitCommandId, clearCacheCommandId, InlineEditProviderFeature, InlineEditProviderFeatureContribution, learnMoreCommandId, learnMoreLink, reportNotebookNESIssueCommandId } from './inlineEditProviderFeature';50import { InlineEditLogger } from './parts/inlineEditLogger';51import { VSCodeWorkspace } from './parts/vscodeWorkspace';52import { makeSettable } from './utils/observablesUtils';5354export class JointCompletionsProviderContribution extends Disposable implements IExtensionContribution {5556private static NES_GROUP_ID = 'nes';57private static COMPLETIONS_GROUP_ID = 'completions';5859private readonly _inlineEditsProviderId = makeSettable(this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsProviderId, this._expService));6061private readonly _hideInternalInterface = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsHideInternalInterface);62private readonly _enableDiagnosticsProvider = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.InlineEditsEnableDiagnosticsProvider, this._expService);63// FIXME@ulugbekna: re-enable when yieldTo is supported64// private readonly _yieldToCopilot = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsYieldToCopilot, this._expService);65private readonly _excludedProviders = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsExcludedProviders, this._expService).map(v => v ? v.split(',').map(v => v.trim()).filter(v => v !== '') : []);66private readonly _copilotToken = observableFromEvent(this, this._authenticationService.onDidAuthenticationChange, () => this._authenticationService.copilotToken);6768public readonly inlineEditsEnabled = derived(this, (reader) => {69const copilotToken = this._copilotToken.read(reader);70if (copilotToken === undefined) {71return false;72}73if (copilotToken.isCompletionsQuotaExceeded) {74return false;75}76return true;77});7879private readonly _internalActionsEnabled = derived(this, (reader) => {80return !!this._copilotToken.read(reader)?.isInternal && !this._hideInternalInterface.read(reader);81});8283public readonly isInlineEditsLogFileEnabledObservable = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsLogContextRecorderEnabled);8485private readonly _workspace = derivedDisposable(this, _reader => {86return this._instantiationService.createInstance(VSCodeWorkspace);87});888990constructor(91@IVSCodeExtensionContext private readonly _vscodeExtensionContext: IVSCodeExtensionContext,92@IInstantiationService private readonly _instantiationService: IInstantiationService,93@ICopilotInlineCompletionItemProviderService private readonly _copilotInlineCompletionItemProviderService: ICopilotInlineCompletionItemProviderService,94@IConfigurationService private readonly _configurationService: IConfigurationService,95@IExperimentationService private readonly _expService: IExperimentationService,96@IAuthenticationService private readonly _authenticationService: IAuthenticationService,97@IEnvService private readonly _envService: IEnvService,98) {99super();100101const useJointCompletionsProviderObs = _configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderEnabled, _expService);102103this._register(autorun((reader) => { // FX104const useJointCompletionsProvider = useJointCompletionsProviderObs.read(reader);105if (!useJointCompletionsProvider) {106reader.store.add(_instantiationService.createInstance(InlineEditProviderFeatureContribution));107reader.store.add(_instantiationService.createInstance(CompletionsCoreContribution));108return;109}110111const inlineEditFeature = _instantiationService.createInstance(InlineEditProviderFeature);112inlineEditFeature.setContext();113114const unificationState = unificationStateObservable(this);115116reader.store.add(autorun((reader) => {117const unificationStateValue = unificationState.read(reader);118119const excludes = this._excludedProviders.read(reader).slice();120121let inlineEditProvider: InlineCompletionProviderImpl | undefined = undefined;122if (!excludes.includes(JointCompletionsProviderContribution.NES_GROUP_ID) && this.inlineEditsEnabled.read(reader)) {123const logger = reader.store.add(this._instantiationService.createInstance(InlineEditLogger));124125const statelessProviderId = this._inlineEditsProviderId.read(reader);126127const workspace = this._workspace.read(reader);128const git = reader.store.add(this._instantiationService.createInstance(ObservableGit));129const historyContextProvider = new NesHistoryContextProvider(workspace, git);130131let diagnosticsProvider: DiagnosticsNextEditProvider | undefined = undefined;132if (this._enableDiagnosticsProvider.read(reader)) {133diagnosticsProvider = reader.store.add(this._instantiationService.createInstance(DiagnosticsNextEditProvider, workspace, git));134}135136const model = reader.store.add(this._instantiationService.createInstance(InlineEditModel, statelessProviderId, workspace, historyContextProvider, diagnosticsProvider));137138const recordingDirPath = join(this._vscodeExtensionContext.globalStorageUri.fsPath, 'logContextRecordings');139140const isInlineEditLogFileEnabled = this.isInlineEditsLogFileEnabledObservable.read(reader);141142let logContextRecorder: LogContextRecorder | undefined;143if (isInlineEditLogFileEnabled) {144logContextRecorder = reader.store.add(this._instantiationService.createInstance(LogContextRecorder, recordingDirPath, logger));145} else {146void LogContextRecorder.cleanupOldRecordings(recordingDirPath);147}148149const inlineEditDebugComponent = reader.store.add(new InlineEditDebugComponent(this._internalActionsEnabled, this.inlineEditsEnabled, model.debugRecorder, this._inlineEditsProviderId));150151const telemetrySender = reader.store.add(this._instantiationService.createInstance(TelemetrySender, workspace));152153// Create the expected edit capture controller154const expectedEditCaptureController = reader.store.add(this._instantiationService.createInstance(155ExpectedEditCaptureController,156model.debugRecorder157));158159inlineEditProvider = this._instantiationService.createInstance(InlineCompletionProviderImpl, model, logger, logContextRecorder, inlineEditDebugComponent, telemetrySender, expectedEditCaptureController);160161reader.store.add(vscode.commands.registerCommand(learnMoreCommandId, () => {162this._envService.openExternal(URI.parse(learnMoreLink));163}));164165reader.store.add(vscode.commands.registerCommand(clearCacheCommandId, () => {166model.nextEditProvider.clearCache();167}));168169reader.store.add(vscode.commands.registerCommand(reportNotebookNESIssueCommandId, () => {170const activeNotebook = vscode.window.activeNotebookEditor;171const document = vscode.window.activeTextEditor?.document;172if (!activeNotebook || !document || !isNotebookCell(document.uri)) {173return;174}175const doc = model.workspace.getDocumentByTextDocument(document);176const selection = activeNotebook.selection;177if (!selection || !doc) {178return;179}180181const logContext = new InlineEditRequestLogContext(doc.id.uri, document.version, undefined);182logContext.recordingBookmark = model.debugRecorder.createBookmark();183void vscode.commands.executeCommand(reportFeedbackCommandId, { logContext });184}));185186// Register expected edit capture commands187reader.store.add(vscode.commands.registerCommand(captureExpectedStartCommandId, () => {188void expectedEditCaptureController.startCapture('manual');189}));190191reader.store.add(vscode.commands.registerCommand(captureExpectedConfirmCommandId, () => {192void expectedEditCaptureController.confirmCapture();193}));194195reader.store.add(vscode.commands.registerCommand(captureExpectedAbortCommandId, () => {196void expectedEditCaptureController.abortCapture();197}));198199reader.store.add(vscode.commands.registerCommand(captureExpectedSubmitCommandId, () => {200void expectedEditCaptureController.submitCaptures();201}));202}203204let completionsProvider: CopilotInlineCompletionItemProvider | undefined;205{206const configEnabled = this._configurationService.getExperimentBasedConfigObservable<boolean>(ConfigKey.TeamInternal.InlineEditsEnableGhCompletionsProvider, this._expService).read(reader);207const extensionUnification = unificationStateValue?.extensionUnification ?? false;208209// respect excludes if NES is enabled210const isExcluded = excludes.includes(JointCompletionsProviderContribution.COMPLETIONS_GROUP_ID) && this.inlineEditsEnabled.read(reader);211212// @ulugbekna: note that we don't want it if modelUnification is on213const modelUnification = unificationStateValue?.modelUnification ?? false;214if (215(!modelUnification || unificationStateValue?.codeUnification || extensionUnification || configEnabled || this._copilotToken.read(reader)?.isNoAuthUser) &&216!isExcluded217) {218completionsProvider = this._copilotInlineCompletionItemProviderService.getOrCreateProvider() as CopilotInlineCompletionItemProvider;219}220221void vscode.commands.executeCommand('setContext', 'github.copilot.extensionUnification.activated', extensionUnification);222223if (extensionUnification && completionsProvider) {224const completionsInstaService = this._copilotInlineCompletionItemProviderService.getOrCreateInstantiationService();225reader.store.add(completionsInstaService.invokeFunction(registerUnificationCommands));226}227}228229const singularProvider = reader.store.add(this._instantiationService.createInstance(JointCompletionsProvider, completionsProvider, inlineEditProvider));230231if (unificationStateValue?.modelUnification) {232if (!excludes.includes('github.copilot')) {233excludes.push('github.copilot');234}235}236237reader.store.add(vscode.languages.registerInlineCompletionItemProvider(238'*',239singularProvider,240{241displayName: inlineEditProvider?.displayName,242debounceDelayMs: 0, // set 0 debounce to ensure consistent delays/timings243groupId: 'nes',244excludes,245})246);247248}));249}));250}251}252253type SingularCompletionItem =254| ({ source: 'completions' } & GhostTextCompletionItem)255| ({ source: 'inlineEdits' } & NesCompletionItem)256;257258type SingularCompletionList =259| ({ source: 'completions' } & GhostTextCompletionList)260| ({ source: 'inlineEdits' } & NesCompletionList)261;262263function toCompletionsList(list: GhostTextCompletionList): SingularCompletionList {264return { ...list, items: list.items.map(item => ({ ...item, source: 'completions' })), source: 'completions' };265}266267function toInlineEditsList(list: NesCompletionList): SingularCompletionList {268return { ...list, items: list.items.map(item => ({ ...item, source: 'inlineEdits' })), source: 'inlineEdits' };269}270271type LastNesSuggestion = {272docUri: vscode.Uri;273docVersionId: number;274docWithNesEditApplied: StringText;275completionItem: NesCompletionItem;276};277278class JointCompletionsProvider extends Disposable implements vscode.InlineCompletionItemProvider {279280private readonly _onDidChangeEmitter: vscode.EventEmitter<NesChangeHint> | undefined;281public readonly onDidChange?: vscode.Event<NesChangeHint> | undefined;282283private _requestsInFlight = new Set<CancellationToken>();284private _completionsRequestsInFlight = new Set<CancellationToken>();285286private get _isRequestInFlight(): boolean {287return this._requestsInFlight.size > 0;288}289290private get _isCompletionsRequestInFlight(): boolean {291return this._completionsRequestsInFlight.size > 0;292}293294private _logger: ILogger;295296//#region Model picker297public readonly onDidChangeModelInfo = this._inlineEditProvider?.onDidChangeModelInfo;298public readonly setCurrentModelId = this._inlineEditProvider?.setCurrentModelId?.bind(this._inlineEditProvider);299public get modelInfo(): InlineCompletionModelInfo | undefined {300return this._inlineEditProvider?.modelInfo;301}302//#endregion303304//#region Provider options305public readonly onDidChangeProviderOptions = this._inlineEditProvider?.onDidChangeProviderOptions;306public readonly setProviderOptionValue = this._inlineEditProvider?.setProviderOptionValue?.bind(this._inlineEditProvider);307public get providerOptions(): readonly InlineCompletionProviderOption[] | undefined {308return this._inlineEditProvider?.providerOptions;309}310//#endregion311312constructor(313private readonly _completionsProvider: CopilotInlineCompletionItemProvider | undefined,314private readonly _inlineEditProvider: InlineCompletionProviderImpl | undefined,315@IConfigurationService private readonly _configService: IConfigurationService,316@IExperimentationService private readonly _expService: IExperimentationService,317@ILogService logService: ILogService,318) {319super();320321this._logger = logService.createSubLogger(['NES', 'JointCompletionsProvider']);322323// Only set up the onDidChange emitter if the inlineEditProvider has one to channel324if (this._inlineEditProvider?.onDidChange) {325this._onDidChangeEmitter = this._register(new vscode.EventEmitter<NesChangeHint>());326this.onDidChange = this._onDidChangeEmitter.event;327328this._register(this._inlineEditProvider.onDidChange((changeHint) => {329const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderTriggerChangeStrategy, this._expService);330switch (strategy) {331case JointCompletionsProviderTriggerChangeStrategy.AlwaysTrigger:332break;333case JointCompletionsProviderTriggerChangeStrategy.NoTriggerOnRequestInFlight:334if (this._isRequestInFlight) {335this._logger.trace('Skipping onDidChange event firing because request is in flight');336return;337}338break;339case JointCompletionsProviderTriggerChangeStrategy.NoTriggerOnCompletionsRequestInFlight:340if (this._isCompletionsRequestInFlight) {341this._logger.trace('Skipping onDidChange event firing because completions request is in flight');342return;343}344break;345default:346assertNever(strategy);347}348this._logger.trace('Firing onDidChange event');349this._onDidChangeEmitter!.fire(changeHint);350}));351}352353softAssert(354_completionsProvider?.onDidChange === undefined,355'CompletionsProvider does not implement onDidChange'356);357}358359public async provideInlineCompletionItems(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<SingularCompletionList | undefined> {360const logger = this._logger.createSubLogger([shortenOpportunityId(context.requestUuid), 'provideInlineCompletionItems']);361362const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderStrategy, this._expService);363364switch (strategy) {365case JointCompletionsProviderStrategy.Regular:366return this.provideInlineCompletionItemsRegular(document, position, context, token, logger);367case JointCompletionsProviderStrategy.CursorEndOfLine:368return this.provideInlineCompletionItemsCursorEndOfLine(document, position, context, token, logger);369default:370assertNever(strategy);371}372}373374private async provideInlineCompletionItemsCursorEndOfLine(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, logger: ILogger): Promise<SingularCompletionList | undefined> {375const sw = new StopWatch();376377this._requestsInFlight.add(token);378const disp = token.onCancellationRequested(() => {379this._requestsInFlight.delete(token);380});381try {382if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {383logger.trace('Return: neither completions nor NES provider available');384return undefined;385386} else if (this._completionsProvider === undefined && this._inlineEditProvider !== undefined) {387logger.trace('only NES provider is available, invoking it');388const r = await this._invokeNESProvider(logger, document, position, false, context, token, sw);389return r ? toInlineEditsList(r) : undefined;390391} else if (this._completionsProvider !== undefined && this._inlineEditProvider === undefined) {392logger.trace('only completions provider is available, invoking it');393const r = await this._invokeCompletionsProvider(logger, document, position, context, token, sw);394return r ? toCompletionsList(r) : undefined;395} else {396397const cursorLine = document.lineAt(position.line).text;398const isCursorAtEndOfLine = checkIfCursorAtEndOfLine(cursorLine, position.character);399400if (isCursorAtEndOfLine) {401logger.trace('cursor is at end of line, invoking ghost-text provider only');402const r = await this._invokeCompletionsProvider(logger, document, position, context, token, sw);403return r ? toCompletionsList(r) : undefined;404}405406const r = await this._invokeNESProvider(logger, document, position, false, context, token, sw);407return r ? toInlineEditsList(r) : undefined;408}409} finally {410if (!token.isCancellationRequested) {411this._logger.trace('request in flight: false -- due to provider finishing');412this._requestsInFlight.delete(token);413}414disp.dispose();415}416}417418private lastNesSuggestion: null | LastNesSuggestion = null;419private provideInlineCompletionItemsInvocationCount = 0;420421private async provideInlineCompletionItemsRegular(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, logger: ILogger): Promise<SingularCompletionList | undefined> {422423const invocationId = ++this.provideInlineCompletionItemsInvocationCount;424const completionsCts = new CancellationTokenSource(token);425const nesCts = new CancellationTokenSource(token);426427this._requestsInFlight.add(token);428const disp1 = token.onCancellationRequested(() => {429logger.trace(`invocation #${invocationId}: request in flight: false -- due to cancellation`);430this._requestsInFlight.delete(token);431});432logger.trace(`invocation #${invocationId} started; request in flight: true`);433434let saveLastNesSuggestion: null | LastNesSuggestion = null;435try {436const docSnapshot = new StringText(document.getText());437const docVersionId = document.version;438439if (this.lastNesSuggestion && this.lastNesSuggestion.docUri.toString() !== document.uri.toString()) {440logger.trace('last NES suggestion is not for the current document, ignoring');441this.lastNesSuggestion = null;442}443444const list = await this._provideInlineCompletionItemsRegular({ document, docSnapshot }, position, this.lastNesSuggestion, context, logger, { coreToken: token, completionsCts, nesCts });445446if (token.isCancellationRequested) {447return list;448}449450if (!list || list.source !== 'inlineEdits' || list.items.length === 0) {451return list;452}453454const firstItem = (list.items as NesCompletionItem[])[0];455if (!firstItem.range || typeof firstItem.insertText !== 'string') {456return list;457}458459if (firstItem.uri && firstItem.uri.toString() !== document.uri.toString()) {460logger.trace(`The NES suggestion is for a different document (${firstItem.uri.toString()} vs ${document.uri.toString()}), not saving as last NES suggestion`);461return list;462}463464const applied = applyTextEdit(docSnapshot, firstItem.range, firstItem.insertText);465saveLastNesSuggestion = {466docUri: document.uri,467docVersionId,468docWithNesEditApplied: new StringText(applied),469completionItem: firstItem,470};471472return list;473} finally {474if (!token.isCancellationRequested) {475logger.trace(`invocation #${invocationId}: request in flight: false -- due to provider finishing`);476this._requestsInFlight.delete(token);477}478disp1.dispose();479480// Only save the last NES suggestion if this is the latest invocation481if (invocationId === this.provideInlineCompletionItemsInvocationCount) {482this.lastNesSuggestion = saveLastNesSuggestion;483if (this.lastNesSuggestion) {484logger.trace(`Set the last NES suggestion for document ${this.lastNesSuggestion.docUri.toString()}`);485} else {486logger.trace(`Cleared the last NES suggestion`);487}488} else {489logger.trace(`Not setting the last NES suggestion because a newer invocation exists`);490}491492completionsCts.dispose();493nesCts.dispose();494}495}496497private async _provideInlineCompletionItemsRegular(498{ document, docSnapshot }: { document: vscode.TextDocument; docSnapshot: StringText },499position: vscode.Position,500lastNesSuggestion: null | LastNesSuggestion,501context: vscode.InlineCompletionContext,502logger: ILogger,503tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },504): Promise<SingularCompletionList | undefined> {505506const sw = new StopWatch();507508if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {509logger.trace('Return: neither completions nor NES provider available');510return undefined;511}512513logger.trace('requesting completions and/or NES');514515// we don't want to trigger completions on selection change events516const isTriggeredDueToSelectionChange = context && (context as NESInlineCompletionContext).changeHint !== undefined;517518if (!lastNesSuggestion || !lastNesSuggestion.completionItem.wasShown) {519// prefer completions unless there are none520logger.trace(`defaulting to yielding to completions; last NES suggestion is ${lastNesSuggestion ? 'not shown' : 'not available'}`);521const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);522const nesP = this._invokeNESProvider(logger, document, position, true, context, tokens.nesCts.token, sw);523return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);524}525526logger.trace(`last NES suggestion is for the current document, checking if it agrees with the current suggestion`);527528const enforceCacheDelay = (lastNesSuggestion.docVersionId !== document.version);529const nesP = this._invokeNESProvider(logger, document, position, enforceCacheDelay, context, tokens.nesCts.token, sw);530if (!nesP) {531logger.trace(`no NES provider`);532const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);533return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);534}535536const NES_CACHE_WAIT_MS = 10;537// scoping the variables538{539logger.trace(`giving the NES provider ${NES_CACHE_WAIT_MS}ms to return what it has in its cache`);540const fastNesResult = await raceCancellation(541raceTimeout(542nesP,543NES_CACHE_WAIT_MS544),545tokens.coreToken546);547548// got nes quickly549if (fastNesResult && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (fastNesResult.items as NesCompletionItem[]).at(0))) {550logger.trace('last NES suggestion agrees with the current suggestion, using NES');551const list: SingularCompletionList = toInlineEditsList(fastNesResult);552logger.trace(`Return: returning NES result in ${sw.elapsed()}ms`);553return list;554}555556if (tokens.coreToken.isCancellationRequested) {557logger.trace(`suggestions request was cancelled`);558void setEndOfLifeReason(this._completionsProvider, undefined, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });559void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });560tokens.completionsCts.cancel();561tokens.nesCts.cancel();562return undefined;563}564}565566logger.trace(`the NES provider did not return in ${NES_CACHE_WAIT_MS}ms so we are triggering the completions provider too`);567const completionsP = isTriggeredDueToSelectionChange ? undefined : this._invokeCompletionsProvider(logger, document, position, context, tokens.completionsCts.token, sw);568569const suggestionsList = await raceCancellation(570Promise.race(coalesce([571completionsP?.then(res => ({ type: 'completions' as const, res })),572nesP?.then(res => ({ type: 'nes' as const, res })),573])),574tokens.coreToken575);576577// got cancelled578if (suggestionsList === undefined) {579logger.trace(`suggestions request was cancelled`);580void setEndOfLifeReason(this._completionsProvider, completionsP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });581void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });582tokens.completionsCts.cancel();583tokens.nesCts.cancel();584return undefined;585}586587// got NES first588if (suggestionsList.type === 'nes' && suggestionsList.res && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (suggestionsList.res.items as NesCompletionItem[]).at(0))) {589logger.trace('last NES suggestion agrees with the current suggestion, using NES');590return this._returnNES(suggestionsList.res, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, logger, tokens);591}592593logger.trace('falling back to the default because completions came first or NES disagreed');594return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, logger, tokens);595}596597private _invokeNESProvider(logger: ILogger, document: vscode.TextDocument, position: vscode.Position, enforceCacheDelay: boolean, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) {598const changeHint = context.changeHint === undefined || NesChangeHint.is(context.changeHint) ? context.changeHint as NesChangeHint | undefined : undefined;599const nesContext: NESInlineCompletionContext = { ...context, enforceCacheDelay, changeHint };600let nesP: Promise<NesCompletionList | undefined> | undefined;601if (this._inlineEditProvider) {602logger.trace(`- requesting NES provideInlineCompletionItems`);603nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, nesContext, ct);604nesP.then((nesR) => {605logger.trace(`got NES response in ${sw.elapsed()}ms -- ${nesR === undefined ? 'undefined' : `with ${nesR.items.length} items`}`);606}).catch((e) => {607logger.trace(`NES provider errored after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);608});609} else {610logger.trace(`- no NES provider`);611nesP = undefined;612}613return nesP;614}615616private _invokeCompletionsProvider(logger: ILogger, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) {617let completionsP: Promise<GhostTextCompletionList | undefined> | undefined;618if (this._completionsProvider) {619this._completionsRequestsInFlight.add(ct);620const disp = ct.onCancellationRequested(() => this._completionsRequestsInFlight.delete(ct));621const cleanup = () => {622this._completionsRequestsInFlight.delete(ct);623disp.dispose();624};625try { // in case the provider throws synchronously626logger.trace(`- requesting completions provideInlineCompletionItems`);627completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, ct);628completionsP.then((completionsR) => {629logger.trace(`got completions response in ${sw.elapsed()}ms -- ${completionsR === undefined ? 'undefined' : `with ${completionsR.items.length} items`}`);630}).catch((e) => {631logger.trace(`completions provider errored after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);632}).finally(() => {633cleanup();634});635} catch (e) {636cleanup();637logger.trace(`completions provider threw synchronously after ${sw.elapsed()}ms -- ${ErrorUtils.toString(ErrorUtils.fromUnknown(e))}`);638throw e;639}640} else {641logger.trace(`- no completions provider`);642completionsP = undefined;643}644return completionsP;645}646647private async _returnCompletionsOrOtherwiseNES(648completionsP: Promise<GhostTextCompletionList | undefined> | undefined,649nesP: Promise<NesCompletionList | undefined> | undefined,650docSnapshot: StringText,651sw: StopWatch,652logger: ILogger,653tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },654): Promise<SingularCompletionList | undefined> {655logger.trace(`waiting for completions and/or NES responses`);656657const completionsR = completionsP ? await completionsP : undefined;658logger.trace(`completions response received`);659660if (completionsR && completionsR.items.length > 0) {661const filteredCompletionR = JointCompletionsProvider.retainOnlyMeaningfulEdits(docSnapshot, completionsR);662if (filteredCompletionR.items.length === 0) {663logger.trace(`all completions edits are no-op, ignoring completions response`);664} else {665logger.trace(`using completions response, cancelling NES provider`);666return this._returnCompletions(filteredCompletionR, { kind: vscode.InlineCompletionsDisposeReasonKind.LostRace }, nesP, sw, logger, tokens);667}668}669670const nesR = nesP ? await nesP : undefined;671logger.trace(`NES response received`);672673if (nesR && nesR.items.length > 0) {674const filteredNesR = JointCompletionsProvider.retainOnlyMeaningfulEdits(docSnapshot, nesR);675if (filteredNesR.items.length === 0) {676logger.trace(`all NES edits are no-op, ignoring NES response`);677} else {678logger.trace(`using NES response`);679return this._returnNES(filteredNesR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, logger, tokens);680}681}682683// return empty completions684return this._returnCompletions(completionsR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, nesP, sw, logger, tokens);685}686687private _returnCompletions(688completionsR: GhostTextCompletionList | undefined,689nesDisposeReason: vscode.InlineCompletionsDisposeReason,690nesP: Promise<NesCompletionList | undefined> | undefined,691sw: StopWatch,692logger: ILogger,693tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },694): SingularCompletionList | undefined {695void setEndOfLifeReason(this._inlineEditProvider, nesP, nesDisposeReason);696tokens.nesCts.cancel(); // cancel NES request if still pending697if (completionsR === undefined) {698logger.trace(`Return: no completions to return in ${sw.elapsed()}ms`);699return undefined;700}701const list: SingularCompletionList = toCompletionsList(completionsR);702logger.trace(`Return: use completions response in ${sw.elapsed()}ms`);703return list;704}705706private _returnNES(707nesR: NesCompletionList,708completionsDisposeReason: vscode.InlineCompletionsDisposeReason,709completionsP: Promise<GhostTextCompletionList | undefined> | undefined,710sw: StopWatch,711logger: ILogger,712tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },713): SingularCompletionList {714void setEndOfLifeReason(this._completionsProvider, completionsP, completionsDisposeReason);715tokens.completionsCts.cancel(); // cancel completions request if still pending716const list: SingularCompletionList = toInlineEditsList(nesR);717logger.trace(`Return: returning NES result in ${sw.elapsed()}ms`);718return list;719}720721private doesNesSuggestionAgree(doc: StringText, docWithNesEditApplied: StringText, nesEdit: NesCompletionItem | undefined): boolean {722if (nesEdit === undefined || nesEdit.range === undefined || typeof nesEdit.insertText !== 'string') {723return false;724}725const applied = applyTextEdit(doc, nesEdit.range, nesEdit.insertText);726return applied === docWithNesEditApplied.getValue();727}728729private static retainOnlyMeaningfulEdits<T extends vscode.InlineCompletionList>(docSnapshot: StringText, list: T): T {730// meaningful = not noop731function isMeaningfulEdit(item: T['items'][number]): boolean {732if (item.range === undefined || // must be a completion with a side-effect, eg a command invocation or something733typeof item.insertText !== 'string' // shouldn't happen734) {735return true;736}737const originalSnippet = docSnapshot.getValueOfRange(new Range(738item.range.start.line + 1,739item.range.start.character + 1,740item.range.end.line + 1,741item.range.end.character + 1,742));743return originalSnippet !== item.insertText;744}745const filteredEdits = list.items.filter(isMeaningfulEdit);746if (filteredEdits.length === list.items.length) {747return list;748}749return { ...list, items: filteredEdits };750}751752public handleDidShowCompletionItem?(completionItem: SingularCompletionItem, updatedInsertText: string): void {753switch (completionItem.source) {754case 'completions':755this._completionsProvider?.handleDidShowCompletionItem?.(completionItem);756break;757case 'inlineEdits':758this._inlineEditProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);759break;760default:761assertNever(completionItem);762}763}764765public handleDidPartiallyAcceptCompletionItem?(completionItem: SingularCompletionItem, acceptedLength: number & vscode.PartialAcceptInfo): void {766switch (completionItem.source) {767case 'completions':768this._completionsProvider?.handleDidPartiallyAcceptCompletionItem?.(completionItem, acceptedLength);769break;770case 'inlineEdits':771softAssert(this._inlineEditProvider?.handleDidPartiallyAcceptCompletionItem === undefined, 'InlineEditProvider does not implement handleDidPartiallyAcceptCompletionItem');772break;773default:774assertNever(completionItem);775}776}777778public handleEndOfLifetime?(completionItem: SingularCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {779switch (completionItem.source) {780case 'completions':781this._completionsProvider?.handleEndOfLifetime?.(completionItem, reason);782break;783case 'inlineEdits':784this._inlineEditProvider?.handleEndOfLifetime?.(completionItem, reason);785break;786default:787assertNever(completionItem);788}789}790791public handleListEndOfLifetime?(list: SingularCompletionList, reason: vscode.InlineCompletionsDisposeReason): void {792switch (list.source) {793case 'completions':794this._completionsProvider?.handleListEndOfLifetime?.(list, reason);795break;796case 'inlineEdits':797this._inlineEditProvider?.handleListEndOfLifetime?.(list, reason);798break;799default:800assertNever(list);801}802}803804// neither provider implements this deprecated method805public handleDidRejectCompletionItem = undefined;806}807808function applyTextEdit(doc: StringText, range: vscode.Range, insertText: string): string {809const rangeOneBased = new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);810const offsetRange = doc.getTransformer().getOffsetRange(rangeOneBased);811const edit = new StringReplacement(offsetRange, insertText);812const bigEdit = edit.toEdit();813return bigEdit.apply(doc.getValue());814}815816async function setEndOfLifeReason(provider: vscode.InlineCompletionItemProvider | undefined, promise: Promise<vscode.InlineCompletionList | undefined> | undefined, reason: vscode.InlineCompletionsDisposeReason) {817if (promise === undefined) {818return;819}820const result = await promise;821if (result === undefined) {822return;823}824for (const item of result.items) {825provider?.handleEndOfLifetime?.(item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false });826}827provider?.handleListEndOfLifetime?.(result, reason);828}829830831