55
66import type * as vscode from 'vscode' ;
77import { ConfigKey , IConfigurationService } from '../../../platform/configuration/common/configurationService' ;
8+ import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService' ;
89import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId' ;
910import { RootedEdit } from '../../../platform/inlineEdits/common/dataTypes/edit' ;
1011import { RootedLineEdit } from '../../../platform/inlineEdits/common/dataTypes/rootedLineEdit' ;
@@ -20,9 +21,9 @@ import { IExperimentationService } from '../../../platform/telemetry/common/null
2021import { Result } from '../../../util/common/result' ;
2122import { createTracer , ITracer } from '../../../util/common/tracing' ;
2223import { assert } from '../../../util/vs/base/common/assert' ;
23- import { DeferredPromise , timeout , TimeoutTimer } from '../../../util/vs/base/common/async' ;
24+ import { DeferredPromise , TimeoutTimer } from '../../../util/vs/base/common/async' ;
2425import { CachedFunction } from '../../../util/vs/base/common/cache' ;
25- import { CancellationToken } from '../../../util/vs/base/common/cancellation' ;
26+ import { CancellationToken , CancellationTokenSource } from '../../../util/vs/base/common/cancellation' ;
2627import { BugIndicatingError } from '../../../util/vs/base/common/errors' ;
2728import { Disposable , DisposableStore , IDisposable , toDisposable } from '../../../util/vs/base/common/lifecycle' ;
2829import { LRUCache } from '../../../util/vs/base/common/map' ;
@@ -38,7 +39,7 @@ import { RejectionCollector } from '../common/rejectionCollector';
3839import { DebugRecorder } from './debugRecorder' ;
3940import { INesConfigs } from './nesConfigs' ;
4041import { CachedOrRebasedEdit , NextEditCache } from './nextEditCache' ;
41- import { LlmNESTelemetryBuilder } from './nextEditProviderTelemetry' ;
42+ import { LlmNESTelemetryBuilder , NextEditProviderTelemetryBuilder } from './nextEditProviderTelemetry' ;
4243import { INextEditResult , NextEditResult } from './nextEditResult' ;
4344
4445export interface INextEditProvider < T extends INextEditResult , TTelemetry , TData = void > extends IDisposable {
@@ -66,6 +67,10 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
6667 private readonly _nextEditCache : NextEditCache ;
6768 private readonly _recentlyShownCache = new RecentlyShownCache ( ) ;
6869
70+ // Track active prefetch requests to avoid duplicates
71+ private readonly _activePrefetches = new Map < string , CancellationTokenSource > ( ) ;
72+ private readonly _prefetchChainDepth = 3 ; // Maximum depth for prediction chain
73+
6974 private _pendingStatelessNextEditRequest : StatelessNextEditRequest < CachedOrRebasedEdit > | null = null ;
7075
7176 private _lastShownTime = 0 ;
@@ -92,6 +97,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
9297 @ISnippyService private readonly _snippyService : ISnippyService ,
9398 @ILogService private readonly _logService : ILogService ,
9499 @IExperimentationService private readonly _expService : IExperimentationService ,
100+ @IGitExtensionService private readonly _gitExtensionService : IGitExtensionService ,
95101 ) {
96102 super ( ) ;
97103
@@ -117,6 +123,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
117123 }
118124
119125 public async getNextEdit ( docId : DocumentId , context : vscode . InlineCompletionContext , logContext : InlineEditRequestLogContext , cancellationToken : CancellationToken , telemetryBuilder : LlmNESTelemetryBuilder ) : Promise < NextEditResult > {
126+ const startTime = Date . now ( ) ;
120127 const tracer = this . _tracer . sub ( 'getNextEdit' ) ;
121128
122129 this . _lastTriggerTime = Date . now ( ) ;
@@ -141,6 +148,9 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
141148 telemetryBuilder . setNESConfigs ( { ...nesConfigs } ) ;
142149 logContext . addCodeblockToLog ( JSON . stringify ( nesConfigs , null , '\t' ) ) ;
143150
151+ // The existing cache lookup will now find our prefetched results!
152+
153+
144154 const recentlyShownCachedEdit = this . _recentlyShownCache . get ( docId , documentAtInvocationTime ) ;
145155 const cachedEdit = this . _nextEditCache . lookupNextEdit ( docId , documentAtInvocationTime , doc . selection . get ( ) , nesConfigs ) ;
146156
@@ -158,8 +168,6 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
158168 let req : NextEditFetchRequest ;
159169 let targetDocumentId = docId ;
160170
161- const cacheDelay = this . _configService . getExperimentBasedConfig ( ConfigKey . Internal . InlineEditsCacheDelay , this . _expService ) ;
162-
163171 if ( recentlyShownCachedEdit ) {
164172 tracer . trace ( 'using recently shown cached edit' ) ;
165173 edit = recentlyShownCachedEdit [ 0 ] ;
@@ -173,10 +181,12 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
173181 // back-date the recording bookmark of the cached edit to the bookmark of the original request.
174182 logContext . recordingBookmark = req . log . recordingBookmark ;
175183
176- await timeout ( cacheDelay ) ;
184+ // No artificial delay for cache hits
177185
178186 } else if ( cachedEdit ) {
187+ const timeElapsed = Date . now ( ) - startTime ;
179188 tracer . trace ( 'using cached edit' ) ;
189+ this . _logService . logger . info ( `[NextEditProvider] CACHE HIT (prefetched) - ${ timeElapsed } ms, subsequentN=${ cachedEdit . subsequentN } ` ) ;
180190 edit = cachedEdit . rebasedEdit || cachedEdit . edit ;
181191 req = cachedEdit . source ;
182192 logContext . setIsCachedResult ( cachedEdit . source . log ) ;
@@ -187,7 +197,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
187197 // back-date the recording bookmark of the cached edit to the bookmark of the original request.
188198 logContext . recordingBookmark = req . log . recordingBookmark ;
189199
190- await timeout ( cacheDelay ) ;
200+ // No artificial delay for cache hits
191201
192202 } else {
193203 tracer . trace ( 'fetching next edit' ) ;
@@ -305,6 +315,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
305315 private async fetchNextEdit ( req : NextEditFetchRequest , doc : IObservableDocument , nesConfigs : INesConfigs , telemetryBuilder : LlmNESTelemetryBuilder , cancellationToken : CancellationToken ) : Promise < Result < CachedOrRebasedEdit , NoNextEditReason > > {
306316 const curDocId = doc . id ;
307317 const tracer = this . _tracer . sub ( 'fetchNextEdit' ) ;
318+
308319 const historyContext = this . _historyContextProvider . getHistoryContext ( curDocId ) ;
309320
310321 if ( ! historyContext ) {
@@ -630,11 +641,144 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
630641
631642 public handleShown ( suggestion : NextEditResult ) {
632643 this . _lastShownTime = Date . now ( ) ;
644+
645+ // Trigger optimistic prefetching as soon as the edit is shown
646+ // This assumes the user will accept it and pre-fetches the next edits
647+ if ( suggestion . result ?. edit ) {
648+ const docId = suggestion . result . targetDocumentId ||
649+ DocumentId . create ( suggestion . source . log . filePath ) ;
650+ const doc = this . _workspace . getDocument ( docId ) ;
651+ if ( ! doc ) {
652+ return ;
653+ }
654+
655+ // Calculate what the document state would be AFTER accepting this edit
656+ const baseDocState = suggestion . result . documentBeforeEdits ;
657+ const stringEdit = StringEdit . create ( [ suggestion . result . edit ] ) ;
658+ const futureDocState = stringEdit . applyOnText ( baseDocState ) ;
659+
660+ // Start the prefetch chain
661+ this . _startPrefetchChain ( docId , futureDocState , 0 ) ;
662+ }
663+ }
664+
665+ private async _startPrefetchChain ( docId : DocumentId , documentState : StringText , depth : number ) : Promise < void > {
666+ if ( depth >= this . _prefetchChainDepth ) {
667+ return ;
668+ }
669+
670+ // Create a unique key for this prefetch based on document state
671+ const prefetchKey = `${ docId } :${ documentState . value . length } :${ depth } ` ;
672+
673+ // Cancel any existing prefetch for this state
674+ const existingPrefetch = this . _activePrefetches . get ( prefetchKey ) ;
675+ if ( existingPrefetch ) {
676+ existingPrefetch . cancel ( ) ;
677+ this . _activePrefetches . delete ( prefetchKey ) ;
678+ }
679+
680+ const cancellationSource = new CancellationTokenSource ( ) ;
681+ this . _activePrefetches . set ( prefetchKey , cancellationSource ) ;
682+
683+ try {
684+
685+ // Create a synthetic document that represents the future state
686+ const syntheticDoc = this . _workspace . getDocument ( docId ) ;
687+ if ( ! syntheticDoc || cancellationSource . token . isCancellationRequested ) {
688+ return ;
689+ }
690+
691+ // Perform the prefetch - this will populate the cache
692+ const result = await this . _performPrefetch ( docId , documentState , cancellationSource . token ) ;
693+
694+ if ( result && result . result ?. edit && ! cancellationSource . token . isCancellationRequested ) {
695+ // Apply the edit to get the next document state
696+ const nextEdit = StringEdit . create ( [ result . result . edit ] ) ;
697+ const nextDocumentState = nextEdit . applyOnText ( documentState ) ;
698+
699+ // Check if the document actually changed
700+ if ( nextDocumentState . value === documentState . value ) {
701+ // Edit made no changes, no point in continuing the chain
702+ return ;
703+ }
704+
705+ // Continue the chain only if the document changed
706+ await this . _startPrefetchChain ( docId , nextDocumentState , depth + 1 ) ;
707+ }
708+ } catch ( err ) {
709+ this . _logService . logger . trace ( `[Prefetch] Error at depth ${ depth } : ${ err } ` ) ;
710+ } finally {
711+ this . _activePrefetches . delete ( prefetchKey ) ;
712+ }
713+ }
714+
715+ private async _performPrefetch ( docId : DocumentId , futureDocumentState : StringText , cancellationToken : CancellationToken ) : Promise < NextEditResult | undefined > {
716+ const doc = this . _workspace . getDocument ( docId ) ;
717+ if ( ! doc ) {
718+ return undefined ;
719+ }
720+
721+ // Create synthetic context and telemetry for the prefetch
722+ const syntheticContext : vscode . InlineCompletionContext = {
723+ triggerKind : 0 as vscode . InlineCompletionTriggerKind ,
724+ selectedCompletionInfo : undefined ,
725+ requestUuid : generateUuid ( )
726+ } ;
727+
728+ const logContext = new InlineEditRequestLogContext ( docId . toUri ( ) . toString ( ) , 0 , syntheticContext ) ;
729+ const req = new NextEditFetchRequest ( logContext , Date . now ( ) ) ;
730+
731+ // Create a proper telemetry builder for prefetch
732+ const telemetryBuilder = new NextEditProviderTelemetryBuilder ( this . _gitExtensionService , this . ID , doc ) ;
733+ telemetryBuilder . nesBuilder . setHeaderRequestId ( req . headerRequestId ) ;
734+ telemetryBuilder . nesBuilder . setIsFromCache ( ) ; // Mark as prefetched
735+
736+ const nesConfigs : INesConfigs = {
737+ isAsyncCompletions : this . _configService . getExperimentBasedConfig ( ConfigKey . Internal . InlineEditsAsyncCompletions , this . _expService ) ,
738+ isRevisedCacheStrategy : this . _configService . getExperimentBasedConfig ( ConfigKey . Internal . InlineEditsRevisedCacheStrategy , this . _expService ) ,
739+ isCacheTracksRejections : this . _configService . getExperimentBasedConfig ( ConfigKey . Internal . InlineEditsCacheTracksRejections , this . _expService ) ,
740+ isRecentlyShownCacheEnabled : this . _configService . getExperimentBasedConfig ( ConfigKey . Internal . InlineEditsRecentlyShownCacheEnabled , this . _expService ) ,
741+ } ;
742+
743+ // Create a synthetic document wrapper that returns our future state
744+ const syntheticDocWrapper = {
745+ ...doc ,
746+ value : {
747+ get : ( ) => futureDocumentState
748+ }
749+ } ;
750+
751+ try {
752+ // This will fetch the edit and populate the cache via pushEdit
753+ const result = await this . fetchNextEdit ( req , syntheticDocWrapper as IObservableDocument , nesConfigs , telemetryBuilder . nesBuilder , cancellationToken ) ;
754+
755+ if ( result . isOk ( ) && result . val . edit ) {
756+ const nextEditResult = new NextEditResult (
757+ logContext . requestId ,
758+ req ,
759+ {
760+ edit : result . val . edit ,
761+ documentBeforeEdits : futureDocumentState ,
762+ showRangePreference : this . _statelessNextEditProvider . showNextEditPreference ?? ShowNextEditPreference . AroundEdit ,
763+ targetDocumentId : docId
764+ }
765+ ) ;
766+ return nextEditResult ;
767+ }
768+ } catch ( err ) {
769+ this . _logService . logger . trace ( `[Prefetch] Error during fetch: ${ err } ` ) ;
770+ } finally {
771+ telemetryBuilder . dispose ( ) ;
772+ }
773+
774+ return undefined ;
633775 }
634776
635777 public handleAcceptance ( docId : DocumentId , suggestion : NextEditResult ) {
636778 this . runSnippy ( docId , suggestion ) ;
637779 this . _statelessNextEditProvider . handleAcceptance ?.( ) ;
780+
781+ // Optimistic fetching already triggered in handleShown
638782 }
639783
640784 public handleRejection ( docId : DocumentId , suggestion : NextEditResult ) {
@@ -656,7 +800,19 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
656800 this . _statelessNextEditProvider . handleRejection ?.( ) ;
657801 }
658802
659- public handleIgnored ( docId : DocumentId , suggestion : NextEditResult , supersededBy : INextEditResult | undefined ) : void { }
803+ public handleIgnored ( _docId : DocumentId , _suggestion : NextEditResult , _supersededBy : INextEditResult | undefined ) : void {
804+ // No-op for now
805+ }
806+
807+ public override dispose ( ) : void {
808+ // Cancel all active prefetches
809+ for ( const [ , cancellationSource ] of this . _activePrefetches ) {
810+ cancellationSource . cancel ( ) ;
811+ }
812+ this . _activePrefetches . clear ( ) ;
813+
814+ super . dispose ( ) ;
815+ }
660816
661817 private async runSnippy ( docId : DocumentId , suggestion : NextEditResult ) {
662818 if ( suggestion . result === undefined ) {
@@ -718,3 +874,4 @@ class RecentlyShownCache {
718874 return docId . uri + ';' + documentContent . value ;
719875 }
720876}
877+
0 commit comments