@@ -14,7 +14,7 @@ import { SpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsEnable
1414import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext' ;
1515import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit' ;
1616import { MutableObservableWorkspace } from '../../../../platform/inlineEdits/common/observableWorkspace' ;
17- import { EditStreamingWithTelemetry , IStatelessNextEditProvider , NoNextEditReason , StatelessNextEditRequest , StatelessNextEditTelemetryBuilder , WithStatelessProviderTelemetry } from '../../../../platform/inlineEdits/common/statelessNextEditProvider' ;
17+ import { EditStreamingWithTelemetry , IStatelessNextEditProvider , NoNextEditReason , RequestEditWindow , RequestEditWindowWithCursorJump , StatelessNextEditRequest , StatelessNextEditTelemetryBuilder , WithStatelessProviderTelemetry } from '../../../../platform/inlineEdits/common/statelessNextEditProvider' ;
1818import { NesHistoryContextProvider } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider' ;
1919import { NesXtabHistoryTracker } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker' ;
2020import { ILogger , ILogService , LogServiceImpl } from '../../../../platform/log/common/logService' ;
@@ -72,6 +72,12 @@ class TestStatelessNextEditProvider implements IStatelessNextEditProvider {
7272 public readonly calls : ICallRecord [ ] = [ ] ;
7373 private readonly _callDeferreds : DeferredPromise < void > [ ] = [ ] ;
7474
75+ /**
76+ * When set, each `provideNextEdit` call will assign this to `request.requestEditWindow`
77+ * (mirroring how the real XtabProvider sets the edit window early in its execution).
78+ */
79+ public editWindow : RequestEditWindow | RequestEditWindowWithCursorJump | undefined ;
80+
7581 public enqueueBehavior ( behavior : ProviderBehavior ) : void {
7682 this . _behaviors . push ( behavior ) ;
7783 }
@@ -100,6 +106,12 @@ class TestStatelessNextEditProvider implements IStatelessNextEditProvider {
100106 throw new Error ( 'Missing provider behavior' ) ;
101107 }
102108
109+ if ( this . editWindow ) {
110+ request . requestEditWindow = this . editWindow ;
111+ }
112+
113+ const streamedEditWindow = this . editWindow ?. window ;
114+ const streamedOriginalWindow = this . editWindow instanceof RequestEditWindowWithCursorJump ? this . editWindow . originalWindow : undefined ;
103115 const telemetryBuilder = new StatelessNextEditTelemetryBuilder ( request . headerRequestId ) ;
104116 const activeDocId = request . getActiveDocument ( ) . id ;
105117 const cancellationRequested = new DeferredPromise < void > ( ) ;
@@ -128,24 +140,24 @@ class TestStatelessNextEditProvider implements IStatelessNextEditProvider {
128140 }
129141
130142 if ( behavior . kind === 'yieldEditThenWaitThenYieldEditsThenNoSuggestions' ) {
131- yield new WithStatelessProviderTelemetry ( { edit : behavior . firstEdit , isFromCursorJump : false , targetDocument : activeDocId } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
143+ yield new WithStatelessProviderTelemetry ( { edit : behavior . firstEdit , isFromCursorJump : false , targetDocument : activeDocId , window : streamedEditWindow , originalWindow : streamedOriginalWindow } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
132144 await Promise . race ( [ behavior . continueSignal . p , cancellationRequested . p ] ) ;
133145 if ( ! call . wasCancelled ) {
134146 for ( const edit of behavior . remainingEdits ) {
135- yield new WithStatelessProviderTelemetry ( { edit, isFromCursorJump : false , targetDocument : activeDocId } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
147+ yield new WithStatelessProviderTelemetry ( { edit, isFromCursorJump : false , targetDocument : activeDocId , window : streamedEditWindow , originalWindow : streamedOriginalWindow } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
136148 }
137149 }
138- const noSuggestions = new NoNextEditReason . NoSuggestions ( request . documentBeforeEdits , undefined ) ;
150+ const noSuggestions = new NoNextEditReason . NoSuggestions ( request . documentBeforeEdits , streamedEditWindow ) ;
139151 return new WithStatelessProviderTelemetry ( noSuggestions , telemetryBuilder . build ( Result . error ( noSuggestions ) ) ) ;
140152 }
141153
142- yield new WithStatelessProviderTelemetry ( { edit : behavior . edit , isFromCursorJump : false , targetDocument : activeDocId } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
154+ yield new WithStatelessProviderTelemetry ( { edit : behavior . edit , isFromCursorJump : false , targetDocument : activeDocId , window : streamedEditWindow , originalWindow : streamedOriginalWindow } , telemetryBuilder . build ( Result . ok ( undefined ) ) ) ;
143155
144156 if ( behavior . kind === 'yieldEditThenWait' ) {
145157 await Promise . race ( [ behavior . continueSignal . p , cancellationRequested . p ] ) ;
146158 }
147159
148- const noSuggestions = new NoNextEditReason . NoSuggestions ( request . documentBeforeEdits , undefined ) ;
160+ const noSuggestions = new NoNextEditReason . NoSuggestions ( request . documentBeforeEdits , streamedEditWindow ) ;
149161 return new WithStatelessProviderTelemetry ( noSuggestions , telemetryBuilder . build ( Result . error ( noSuggestions ) ) ) ;
150162 } finally {
151163 cancellationDisposable . dispose ( ) ;
@@ -1117,4 +1129,186 @@ describe('NextEditProvider speculative requests', () => {
11171129 await statelessProvider . calls [ 1 ] . completed . p ;
11181130 } ) ;
11191131 } ) ;
1132+
1133+ describe ( 'edit window cursor check for request reuse' , ( ) => {
1134+ beforeEach ( async ( ) => {
1135+ await configService . setConfig ( ConfigKey . TeamInternal . InlineEditsCheckEditWindowOnReuse , true ) ;
1136+ } ) ;
1137+
1138+ it ( 'does not reuse in-flight request when cursor moves outside edit window' , async ( ) => {
1139+ const statelessProvider = new TestStatelessNextEditProvider ( ) ;
1140+ // Edit window covers offsets 0–20 of the document
1141+ statelessProvider . editWindow = new RequestEditWindow ( new OffsetRange ( 0 , 20 ) ) ;
1142+ const continueSignal1 = new DeferredPromise < void > ( ) ;
1143+ const continueSignal2 = new DeferredPromise < void > ( ) ;
1144+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenWait' , edit : lineReplacement ( 1 , 'const value = 2;' ) , continueSignal : continueSignal1 } ) ;
1145+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenWait' , edit : lineReplacement ( 1 , 'const value = 3;' ) , continueSignal : continueSignal2 } ) ;
1146+ const { nextEditProvider, workspace } = createProviderAndWorkspace ( statelessProvider ) ;
1147+
1148+ const doc = workspace . addDocument ( {
1149+ id : DocumentId . create ( URI . file ( '/test/ew-outside.ts' ) . toString ( ) ) ,
1150+ initialValue : 'const value = 1;\nconsole.log(value);\nconst other = 3;\n' ,
1151+ } ) ;
1152+ doc . setSelection ( [ new OffsetRange ( 0 , 0 ) ] , undefined ) ;
1153+
1154+ // First request — yields first edit, stream still running in background
1155+ const firstSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1156+ assert ( firstSuggestion . result ?. edit ) ;
1157+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1158+
1159+ // Move cursor far outside the edit window (offset 40)
1160+ doc . setSelection ( [ new OffsetRange ( 40 , 40 ) ] , undefined ) ;
1161+
1162+ // Second request — should NOT reuse the in-flight request because cursor is outside edit window
1163+ // The first request's stream is still running, but cursor is outside its edit window, so a new request is made
1164+ const secondSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1165+ assert ( secondSuggestion . result ?. edit ) ;
1166+
1167+ // Two separate provider calls were made
1168+ expect ( statelessProvider . calls . length ) . toBe ( 2 ) ;
1169+
1170+ // Clean up
1171+ continueSignal1 . complete ( ) ;
1172+ continueSignal2 . complete ( ) ;
1173+ await statelessProvider . calls [ 0 ] . completed . p ;
1174+ await statelessProvider . calls [ 1 ] . completed . p ;
1175+ } ) ;
1176+
1177+ it ( 'reuses in-flight request when cursor stays within edit window' , async ( ) => {
1178+ const statelessProvider = new TestStatelessNextEditProvider ( ) ;
1179+ // Edit window covers offsets 0–50 (whole document)
1180+ statelessProvider . editWindow = new RequestEditWindow ( new OffsetRange ( 0 , 50 ) ) ;
1181+ const continueSignal = new DeferredPromise < void > ( ) ;
1182+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenWait' , edit : lineReplacement ( 1 , 'const value = 2;' ) , continueSignal } ) ;
1183+ const { nextEditProvider, workspace } = createProviderAndWorkspace ( statelessProvider ) ;
1184+
1185+ const doc = workspace . addDocument ( {
1186+ id : DocumentId . create ( URI . file ( '/test/ew-inside.ts' ) . toString ( ) ) ,
1187+ initialValue : 'const value = 1;\nconsole.log(value);\n' ,
1188+ } ) ;
1189+ doc . setSelection ( [ new OffsetRange ( 0 , 0 ) ] , undefined ) ;
1190+
1191+ // First request — yields first edit, stream still running
1192+ const firstSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1193+ assert ( firstSuggestion . result ?. edit ) ;
1194+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1195+
1196+ // Move cursor but still within the edit window (offset 10)
1197+ doc . setSelection ( [ new OffsetRange ( 10 , 10 ) ] , undefined ) ;
1198+
1199+ // Second request — should reuse the in-flight request
1200+ const secondSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1201+ assert ( secondSuggestion . result ?. edit ) ;
1202+
1203+ // Only one provider call was made (reused)
1204+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1205+
1206+ // Clean up
1207+ continueSignal . complete ( ) ;
1208+ await statelessProvider . calls [ 0 ] . completed . p ;
1209+ } ) ;
1210+
1211+ it ( 'reuses in-flight request when editWindow is undefined (graceful fallback)' , async ( ) => {
1212+ const statelessProvider = new TestStatelessNextEditProvider ( ) ;
1213+ // No editWindow set — should allow reuse
1214+ const continueSignal = new DeferredPromise < void > ( ) ;
1215+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenWait' , edit : lineReplacement ( 1 , 'const value = 2;' ) , continueSignal } ) ;
1216+ const { nextEditProvider, workspace } = createProviderAndWorkspace ( statelessProvider ) ;
1217+
1218+ const doc = workspace . addDocument ( {
1219+ id : DocumentId . create ( URI . file ( '/test/ew-undefined.ts' ) . toString ( ) ) ,
1220+ initialValue : 'const value = 1;\nconsole.log(value);\nconst other = 3;\n' ,
1221+ } ) ;
1222+ doc . setSelection ( [ new OffsetRange ( 0 , 0 ) ] , undefined ) ;
1223+
1224+ // First request — yields first edit, stream still running
1225+ const firstSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1226+ assert ( firstSuggestion . result ?. edit ) ;
1227+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1228+
1229+ // Move cursor far away — but editWindow is undefined so reuse is allowed
1230+ doc . setSelection ( [ new OffsetRange ( 40 , 40 ) ] , undefined ) ;
1231+
1232+ // Second request — should reuse (no edit window to check)
1233+ const secondSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1234+ assert ( secondSuggestion . result ?. edit ) ;
1235+
1236+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1237+
1238+ // Clean up
1239+ continueSignal . complete ( ) ;
1240+ await statelessProvider . calls [ 0 ] . completed . p ;
1241+ } ) ;
1242+
1243+ it ( 'does not reuse speculative request when cursor moves outside edit window' , async ( ) => {
1244+ await configService . setConfig ( ConfigKey . TeamInternal . InlineEditsSpeculativeRequests , SpeculativeRequestsEnablement . On ) ;
1245+
1246+ const statelessProvider = new TestStatelessNextEditProvider ( ) ;
1247+ // Edit window covers offsets 0–20
1248+ statelessProvider . editWindow = new RequestEditWindow ( new OffsetRange ( 0 , 20 ) ) ;
1249+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenNoSuggestions' , edit : lineReplacement ( 1 , 'const value = 2;' ) } ) ;
1250+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenNoSuggestions' , edit : lineReplacement ( 2 , 'console.log(value + 1);' ) } ) ;
1251+ // Third behavior for the new request that will be needed since speculative won't be reused
1252+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenNoSuggestions' , edit : lineReplacement ( 2 , 'console.log(value + 1);' ) } ) ;
1253+ const { nextEditProvider, workspace } = createProviderAndWorkspace ( statelessProvider ) ;
1254+
1255+ const doc = workspace . addDocument ( {
1256+ id : DocumentId . create ( URI . file ( '/test/ew-spec-outside.ts' ) . toString ( ) ) ,
1257+ initialValue : 'const value = 1;\nconsole.log(value);\nconst other = 3;\n' ,
1258+ } ) ;
1259+ doc . setSelection ( [ new OffsetRange ( 0 , 0 ) ] , undefined ) ;
1260+
1261+ const firstSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1262+ assert ( firstSuggestion . result ?. edit ) ;
1263+ nextEditProvider . handleShown ( firstSuggestion ) ;
1264+ await statelessProvider . waitForCall ( 2 ) ;
1265+ await statelessProvider . calls [ 1 ] . completed . p ;
1266+
1267+ // Accept and apply the edit
1268+ nextEditProvider . handleAcceptance ( doc . id , firstSuggestion ) ;
1269+ doc . applyEdit ( firstSuggestion . result . edit . toEdit ( ) ) ;
1270+
1271+ // Move cursor outside the speculative request's edit window
1272+ doc . setSelection ( [ new OffsetRange ( 40 , 40 ) ] , undefined ) ;
1273+
1274+ // This should NOT reuse the speculative request (cursor is outside)
1275+ await getNextEdit ( nextEditProvider , doc . id ) ;
1276+
1277+ // Three calls: original, speculative, and a new one (speculative was not reused)
1278+ expect ( statelessProvider . calls . length ) . toBe ( 3 ) ;
1279+ } ) ;
1280+
1281+ it ( 'reuses in-flight request when cursor is within originalWindow of cursor jump edit window' , async ( ) => {
1282+ const statelessProvider = new TestStatelessNextEditProvider ( ) ;
1283+ // Cursor jump: new window is at 30–50, original window is at 0–20
1284+ statelessProvider . editWindow = new RequestEditWindowWithCursorJump ( new OffsetRange ( 30 , 50 ) , new OffsetRange ( 0 , 20 ) ) ;
1285+ const continueSignal = new DeferredPromise < void > ( ) ;
1286+ statelessProvider . enqueueBehavior ( { kind : 'yieldEditThenWait' , edit : lineReplacement ( 1 , 'const value = 2;' ) , continueSignal } ) ;
1287+ const { nextEditProvider, workspace } = createProviderAndWorkspace ( statelessProvider ) ;
1288+
1289+ const doc = workspace . addDocument ( {
1290+ id : DocumentId . create ( URI . file ( '/test/ew-cursorjump.ts' ) . toString ( ) ) ,
1291+ initialValue : 'const value = 1;\nconsole.log(value);\nconst other = 3;\nconst extra = 4;\n' ,
1292+ } ) ;
1293+ doc . setSelection ( [ new OffsetRange ( 0 , 0 ) ] , undefined ) ;
1294+
1295+ // First request — yields first edit, stream still running
1296+ const firstSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1297+ assert ( firstSuggestion . result ?. edit ) ;
1298+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1299+
1300+ // Move cursor to offset 10 — inside originalWindow (0–20) but outside jump target (30–50)
1301+ doc . setSelection ( [ new OffsetRange ( 10 , 10 ) ] , undefined ) ;
1302+
1303+ // Second request — should reuse because cursor is in originalWindow
1304+ const secondSuggestion = await getNextEdit ( nextEditProvider , doc . id ) ;
1305+ assert ( secondSuggestion . result ?. edit ) ;
1306+
1307+ expect ( statelessProvider . calls . length ) . toBe ( 1 ) ;
1308+
1309+ // Clean up
1310+ continueSignal . complete ( ) ;
1311+ await statelessProvider . calls [ 0 ] . completed . p ;
1312+ } ) ;
1313+ } ) ;
11201314} ) ;
0 commit comments