From aee2abe2e10bc67856b364e01f11ea9324e76dda Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Thu, 4 Dec 2025 23:13:16 +0100 Subject: [PATCH] nes: joint: don't enforce cache delay if document hasn't changed (#2409) --- .../inlineEdits/node/nextEditProvider.ts | 19 ++++++++++++++----- .../test/node/nextEditProviderCaching.spec.ts | 13 ++++++------- .../features/diagnosticsInlineEditProvider.ts | 6 +++--- .../vscode-node/inlineCompletionProvider.ts | 9 +++++---- .../jointInlineCompletionProvider.ts | 11 +++++++---- .../copilot/src/lib/node/chatLibMain.ts | 5 +++-- .../simulation/inlineEdit/inlineEditTester.ts | 5 ++--- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts index effede86b61..5c3e82b09d5 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts @@ -42,9 +42,13 @@ import { CachedOrRebasedEdit, NextEditCache } from './nextEditCache'; import { LlmNESTelemetryBuilder } from './nextEditProviderTelemetry'; import { INextEditResult, NextEditResult } from './nextEditResult'; +export interface NESInlineCompletionContext extends vscode.InlineCompletionContext { + enforceCacheDelay: boolean; +} + export interface INextEditProvider extends IDisposable { readonly ID: string; - getNextEdit(docId: DocumentId, context: vscode.InlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, telemetryBuilder: TTelemetry, data?: TData): Promise; + getNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, telemetryBuilder: TTelemetry, data?: TData): Promise; handleShown(suggestion: T): void; handleAcceptance(docId: DocumentId, suggestion: T): void; handleRejection(docId: DocumentId, suggestion: T): void; @@ -121,7 +125,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider 0) { await timeout(delay); if (cancellationToken.isCancellationRequested) { @@ -702,7 +706,12 @@ export class NextEditProvider extends Disposable implements INextEditProvider { @@ -110,7 +109,7 @@ describe('NextEditProvider Caching', () => { doc.applyEdit(StringEdit.insert(11, '3D')); - const context: InlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200 }; + const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false }; const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context); const cancellationToken = CancellationToken.None; const tb1 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc); diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.ts index 2321f2d4c69..c41e02379e8 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.ts @@ -18,7 +18,7 @@ import { BugIndicatingError } from '../../../../util/vs/base/common/errors'; import { Disposable } from '../../../../util/vs/base/common/lifecycle'; import { StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; -import { INextEditProvider } from '../../node/nextEditProvider'; +import { INextEditProvider, NESInlineCompletionContext } from '../../node/nextEditProvider'; import { DiagnosticsTelemetryBuilder } from '../../node/nextEditProviderTelemetry'; import { INextEditDisplayLocation, INextEditResult } from '../../node/nextEditResult'; import { VSCodeWorkspace } from '../parts/vscodeWorkspace'; @@ -66,7 +66,7 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit this._diagnosticsCompletionHandler = this._register(instantiationService.createInstance(DiagnosticsCompletionProcessor, workspace, git)); } - async getNextEdit(docId: DocumentId, context: vscode.InlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise { + async getNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise { this._lastTriggerTime = Date.now(); if (cancellationToken.isCancellationRequested) { @@ -82,7 +82,7 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit return this._createNextEditResult(diagnosticEditResult, logContext, tb); } - async runUntilNextEdit(docId: DocumentId, context: vscode.InlineCompletionContext, logContext: InlineEditRequestLogContext, delayStart: number, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise { + async runUntilNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, delayStart: number, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise { try { await timeout(delayStart); if (cancellationToken.isCancellationRequested) { diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts index d238cbde8ab..b5652bc179c 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts @@ -36,6 +36,7 @@ import { StringEdit } from '../../../util/vs/editor/common/core/edits/stringEdit import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { LineCheck } from '../../inlineChat/vscode-node/naturalLanguageHint'; import { createCorrelationId } from '../common/correlationId'; +import { NESInlineCompletionContext } from '../node/nextEditProvider'; import { NextEditProviderTelemetryBuilder, TelemetrySender } from '../node/nextEditProviderTelemetry'; import { INextEditResult, NextEditResult } from '../node/nextEditResult'; import { InlineCompletionCommand, InlineEditDebugComponent } from './components/inlineEditDebugComponent'; @@ -174,19 +175,19 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo public async provideInlineCompletionItems( document: TextDocument, position: Position, - context: InlineCompletionContext, + context: InlineCompletionContext | NESInlineCompletionContext, token: CancellationToken ): Promise { const label = `NES | ${basename(document.uri.fsPath)} (v${document.version})`; const capturingToken = new CapturingToken(label, undefined, true, true); - - return this._requestLogger.captureInvocation(capturingToken, () => this._provideInlineCompletionItems(document, position, context, token)); + const nesContext: NESInlineCompletionContext = { enforceCacheDelay: true, ...context }; + return this._requestLogger.captureInvocation(capturingToken, () => this._provideInlineCompletionItems(document, position, nesContext, token)); } private async _provideInlineCompletionItems( document: TextDocument, position: Position, - context: InlineCompletionContext, + context: NESInlineCompletionContext, token: CancellationToken ): Promise { const tracer = this._tracer.sub(['provideInlineCompletionItems', shortenOpportunityId(context.requestUuid)]); diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts index 825402a2169..1e206a7b19c 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts @@ -36,6 +36,7 @@ import { CopilotInlineCompletionItemProvider } from '../../completions-core/vsco import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService'; import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution'; import { unificationStateObservable } from '../../completions/vscode-node/completionsUnificationContribution'; +import { NESInlineCompletionContext } from '../node/nextEditProvider'; import { TelemetrySender } from '../node/nextEditProviderTelemetry'; import { InlineEditDebugComponent, reportFeedbackCommandId } from './components/inlineEditDebugComponent'; import { LogContextRecorder } from './components/logContextRecorder'; @@ -359,13 +360,14 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple // 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); + const nesP = this._invokeNESProvider(tracer, document, position, true, 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); + const enforceCacheDelay = (lastNesSuggestion.docVersionId !== document.version); + const nesP = this._invokeNESProvider(tracer, document, position, enforceCacheDelay, context, tokens, sw); if (!nesP) { tracer.trace(`no NES provider`); const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw); @@ -433,11 +435,12 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple 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) { + private _invokeNESProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, enforceCacheDelay: boolean, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) { + const nesContext: NESInlineCompletionContext = { ...context, enforceCacheDelay }; let nesP: Promise | undefined; if (this._inlineEditProvider) { tracer.trace(`- requesting NES provideInlineCompletionItems`); - nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, context, tokens.nesCts.token); + nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, nesContext, 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) => { diff --git a/extensions/copilot/src/lib/node/chatLibMain.ts b/extensions/copilot/src/lib/node/chatLibMain.ts index 53726346e18..471d17b06b1 100644 --- a/extensions/copilot/src/lib/node/chatLibMain.ts +++ b/extensions/copilot/src/lib/node/chatLibMain.ts @@ -49,7 +49,7 @@ import { ICompletionsPromiseQueueService, PromiseQueue } from '../../extension/c import { ICompletionsRuntimeModeService, RuntimeMode } from '../../extension/completions-core/vscode-node/lib/src/util/runtimeMode'; import { DocumentContext, WorkspaceFolder } from '../../extension/completions-core/vscode-node/types/src'; import { DebugRecorder } from '../../extension/inlineEdits/node/debugRecorder'; -import { INextEditProvider, NextEditProvider } from '../../extension/inlineEdits/node/nextEditProvider'; +import { INextEditProvider, NESInlineCompletionContext, NextEditProvider } from '../../extension/inlineEdits/node/nextEditProvider'; import { LlmNESTelemetryBuilder, NextEditProviderTelemetryBuilder, TelemetrySender } from '../../extension/inlineEdits/node/nextEditProviderTelemetry'; import { INextEditResult } from '../../extension/inlineEdits/node/nextEditResult'; import { ChatMLFetcherImpl } from '../../extension/prompt/node/chatMLFetcher'; @@ -277,12 +277,13 @@ class NESProvider extends Disposable implements INESProvider { const docId = DocumentId.create(documentUri.toString()); // Create minimal required context objects - const context: vscode.InlineCompletionContext = { + const context: NESInlineCompletionContext = { triggerKind: 1, // Invoke selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, + enforceCacheDelay: true, }; // Create log context diff --git a/extensions/copilot/test/simulation/inlineEdit/inlineEditTester.ts b/extensions/copilot/test/simulation/inlineEdit/inlineEditTester.ts index 9659ee86ffb..96e0edadc4b 100644 --- a/extensions/copilot/test/simulation/inlineEdit/inlineEditTester.ts +++ b/extensions/copilot/test/simulation/inlineEdit/inlineEditTester.ts @@ -5,12 +5,11 @@ import { rename } from 'fs/promises'; import { basename, dirname, join } from 'path'; -import type { InlineCompletionContext } from 'vscode'; import { VisualizationTestRun } from '../../../src/extension/inlineChat/node/rendererVisualization'; import { IRecordingInformation, ObservableWorkspaceRecordingReplayer } from '../../../src/extension/inlineEdits/common/observableWorkspaceRecordingReplayer'; import { createNextEditProvider } from '../../../src/extension/inlineEdits/node/createNextEditProvider'; import { DebugRecorder } from '../../../src/extension/inlineEdits/node/debugRecorder'; -import { NextEditProvider } from '../../../src/extension/inlineEdits/node/nextEditProvider'; +import { NESInlineCompletionContext, NextEditProvider } from '../../../src/extension/inlineEdits/node/nextEditProvider'; import { NextEditProviderTelemetryBuilder } from '../../../src/extension/inlineEdits/node/nextEditProviderTelemetry'; import { NextEditResult } from '../../../src/extension/inlineEdits/node/nextEditResult'; import { ConfigKey, IConfigurationService } from '../../../src/platform/configuration/common/configurationService'; @@ -164,7 +163,7 @@ export class InlineEditTester { const historyContext = historyContextProvider.getHistoryContext(docId)!; const activeDocument = historyContext.getMostRecentDocument(); // TODO - const context: InlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200 }; + const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false }; const logContext = new InlineEditRequestLogContext(activeDocument.docId.toString(), 1, context); const telemetryBuilder = new NextEditProviderTelemetryBuilder(gitExtensionService, notebookService, workspaceService, nextEditProvider.ID, workspace.getDocument(activeDocument.docId)!);