From 0ed62f64dde5f379da0eb0a2783f48fc1a486669 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Thu, 11 Dec 2025 13:01:47 +0100 Subject: [PATCH 1/2] nes: joint: support invoking one of ghost-text/NES based on if cursor is at the end of the line (#2534) --- .../jointInlineCompletionProvider.ts | 72 +++++++++++++++---- .../jointCompletionsProviderOptions.ts | 1 + .../inlineEdits/common/utils/utils.ts | 6 ++ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts index 6ebc58f2dd..ec0a6cbb34 100644 --- a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts +++ b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts @@ -13,7 +13,7 @@ import { IVSCodeExtensionContext } from '../../../platform/extContext/common/ext import { JointCompletionsProviderStrategy, JointCompletionsProviderTriggerChangeStrategy } 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 { checkIfCursorAtEndOfLine, 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'; @@ -317,11 +317,57 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple switch (strategy) { case JointCompletionsProviderStrategy.Regular: return this.provideInlineCompletionItemsRegular(document, position, context, token, tracer); + case JointCompletionsProviderStrategy.CursorEndOfLine: + return this.provideInlineCompletionItemsCursorEndOfLine(document, position, context, token, tracer); default: assertNever(strategy); } } + private async provideInlineCompletionItemsCursorEndOfLine(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, tracer: ITracer): Promise { + const sw = new StopWatch(); + + this._requestsInFlight.add(token); + const disp = token.onCancellationRequested(() => { + this._requestsInFlight.delete(token); + }); + try { + if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) { + tracer.returns('neither completions nor NES provider available'); + return undefined; + + } else if (this._completionsProvider === undefined && this._inlineEditProvider !== undefined) { + tracer.trace('only NES provider is available, invoking it'); + const r = await this._invokeNESProvider(tracer, document, position, false, context, token, sw); + return r ? toInlineEditsList(r) : undefined; + + } else if (this._completionsProvider !== undefined && this._inlineEditProvider === undefined) { + tracer.trace('only completions provider is available, invoking it'); + const r = await this._invokeCompletionsProvider(tracer, document, position, context, token, sw); + return r ? toCompletionsList(r) : undefined; + } else { + + const cursorLine = document.lineAt(position.line).text; + const isCursorAtEndOfLine = checkIfCursorAtEndOfLine(cursorLine, position.character); + + if (isCursorAtEndOfLine) { + tracer.trace('cursor is at end of line, invoking ghost-text provider only'); + const r = await this._invokeCompletionsProvider(tracer, document, position, context, token, sw); + return r ? toCompletionsList(r) : undefined; + } + + const r = await this._invokeNESProvider(tracer, document, position, false, context, token, sw); + return r ? toInlineEditsList(r) : undefined; + } + } finally { + if (!token.isCancellationRequested) { + this._tracer.trace('request in flight: false -- due to provider finishing'); + this._requestsInFlight.delete(token); + } + disp.dispose(); + } + } + private lastNesSuggestion: null | LastNesSuggestion = null; private provideInlineCompletionItemsInvocationCount = 0; @@ -421,18 +467,18 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple 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, true, context, tokens, sw); + const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens.completionsCts.token, sw); + const nesP = this._invokeNESProvider(tracer, document, position, true, context, tokens.nesCts.token, sw); return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, tracer, tokens); } tracer.trace(`last NES suggestion is for the current document, checking if it agrees with the current suggestion`); const enforceCacheDelay = (lastNesSuggestion.docVersionId !== document.version); - const nesP = this._invokeNESProvider(tracer, document, position, enforceCacheDelay, context, tokens, sw); + const nesP = this._invokeNESProvider(tracer, document, position, enforceCacheDelay, context, tokens.nesCts.token, sw); if (!nesP) { tracer.trace(`no NES provider`); - const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw); + const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens.completionsCts.token, sw); return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, tracer, tokens); } @@ -467,7 +513,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple } 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 completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens.completionsCts.token, sw); const suggestionsList = await raceCancellation( Promise.race(coalesce([ @@ -497,12 +543,12 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, tracer, tokens); } - private _invokeNESProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, enforceCacheDelay: boolean, 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, ct: CancellationToken, 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, nesContext, tokens.nesCts.token); + nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, nesContext, ct); nesP.then((nesR) => { tracer.trace(`got NES response in ${sw.elapsed()}ms -- ${nesR === undefined ? 'undefined' : `with ${nesR.items.length} items`}`); }).catch((e) => { @@ -515,18 +561,18 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple return nesP; } - private _invokeCompletionsProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) { + private _invokeCompletionsProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) { let completionsP: Promise | undefined; if (this._completionsProvider) { - this._completionsRequestsInFlight.add(tokens.completionsCts.token); - const disp = tokens.completionsCts.token.onCancellationRequested(() => this._completionsRequestsInFlight.delete(tokens.completionsCts.token)); + this._completionsRequestsInFlight.add(ct); + const disp = ct.onCancellationRequested(() => this._completionsRequestsInFlight.delete(ct)); const cleanup = () => { - this._completionsRequestsInFlight.delete(tokens.completionsCts.token); + this._completionsRequestsInFlight.delete(ct); disp.dispose(); }; try { // in case the provider throws synchronously tracer.trace(`- requesting completions provideInlineCompletionItems`); - completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, tokens.completionsCts.token); + completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, ct); completionsP.then((completionsR) => { tracer.trace(`got completions response in ${sw.elapsed()}ms -- ${completionsR === undefined ? 'undefined' : `with ${completionsR.items.length} items`}`); }).catch((e) => { diff --git a/src/platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions.ts b/src/platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions.ts index 6f32eaa9a6..9e99ac3f4f 100644 --- a/src/platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions.ts +++ b/src/platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions.ts @@ -5,6 +5,7 @@ export enum JointCompletionsProviderStrategy { Regular = 'regular', + CursorEndOfLine = 'cursorEndOfLine', } export enum JointCompletionsProviderTriggerChangeStrategy { diff --git a/src/platform/inlineEdits/common/utils/utils.ts b/src/platform/inlineEdits/common/utils/utils.ts index 9fa2fe9720..1e9340186f 100644 --- a/src/platform/inlineEdits/common/utils/utils.ts +++ b/src/platform/inlineEdits/common/utils/utils.ts @@ -59,3 +59,9 @@ export function shortenOpportunityId(opportunityId: string): string { // example: `icr-1234abcd5678efgh` -> `1234`, where we strip the `icr-` prefix and take the first 4 characters return opportunityId.substring(4, 8); } + +export function checkIfCursorAtEndOfLine(lineWithCursor: string, cursorOffsetZeroBased: number): boolean { + // check if there's any non-whitespace character after the cursor in the line + const isCursorAtEndOfLine = lineWithCursor.substring(cursorOffsetZeroBased).match(/^\s*$/) !== null; + return isCursorAtEndOfLine; +} From 877daeac1acdd1a2d2e495f893c25e6c3dd7eea7 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Thu, 11 Dec 2025 16:11:00 +0100 Subject: [PATCH 2/2] nes: joint: don't prefer last NES suggestion if it wasn't shown anyway (#2537) --- .../vscode-node/jointInlineCompletionProvider.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts index ec0a6cbb34..a4cdbe5696 100644 --- a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts +++ b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts @@ -239,6 +239,7 @@ type LastNesSuggestion = { docUri: vscode.Uri; docVersionId: number; docWithNesEditApplied: StringText; + completionItem: NesCompletionItem; }; class JointCompletionsProvider extends Disposable implements vscode.InlineCompletionItemProvider { @@ -404,7 +405,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple return list; } - const firstItem = list.items[0]; + const firstItem = (list.items as NesCompletionItem[])[0]; if (!firstItem.range || typeof firstItem.insertText !== 'string') { return list; } @@ -419,6 +420,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple docUri: document.uri, docVersionId, docWithNesEditApplied: new StringText(applied), + completionItem: firstItem, }; return list; @@ -464,9 +466,9 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple tracer.trace('requesting completions and/or NES'); - if (!lastNesSuggestion) { + if (!lastNesSuggestion || !lastNesSuggestion.completionItem.wasShown) { // prefer completions unless there are none - tracer.trace(`no last NES suggestion to consider`); + tracer.trace(`defaulting to yielding to completions; last NES suggestion is ${lastNesSuggestion ? 'not shown' : 'not available'}`); const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens.completionsCts.token, sw); const nesP = this._invokeNESProvider(tracer, document, position, true, context, tokens.nesCts.token, sw); return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, docSnapshot, sw, tracer, tokens);