66
77
88import { equals as arraysEqual , binarySearch2 } from '../../../../../base/common/arrays.js' ;
9+ import { equals as objectsEqual } from '../../../../../base/common/objects.js' ;
910import { findLast } from '../../../../../base/common/arraysFind.js' ;
11+ import { Iterable } from '../../../../../base/common/iterator.js' ;
1012import { DisposableStore } from '../../../../../base/common/lifecycle.js' ;
1113import { ResourceMap } from '../../../../../base/common/map.js' ;
1214import { derived , derivedOpts , IObservable , ITransaction , ObservablePromise , observableValue , transaction } from '../../../../../base/common/observable.js' ;
@@ -46,6 +48,22 @@ export class ChatEditingTimeline {
4648 public readonly canUndo : IObservable < boolean > ;
4749 public readonly canRedo : IObservable < boolean > ;
4850
51+ public readonly requestDisablement = derivedOpts < IChatRequestDisablement [ ] > ( { equalsFn : ( a , b ) => arraysEqual ( a , b , objectsEqual ) } , reader => {
52+ const history = this . _linearHistory . read ( reader ) ;
53+ const index = this . _linearHistoryIndex . read ( reader ) ;
54+ const undoRequests : IChatRequestDisablement [ ] = [ ] ;
55+ for ( const entry of history ) {
56+ if ( ! entry . requestId ) {
57+ // ignored
58+ } else if ( entry . startIndex >= index ) {
59+ undoRequests . push ( { requestId : entry . requestId } ) ;
60+ } else if ( entry . startIndex + entry . stops . length > index ) {
61+ undoRequests . push ( { requestId : entry . requestId , afterUndoStop : entry . stops [ ( index - 1 ) - entry . startIndex ] . stopId } ) ;
62+ }
63+ }
64+ return undoRequests ;
65+ } ) ;
66+
4967 constructor (
5068 @IEditorWorkerService private readonly _editorWorkerService : IEditorWorkerService ,
5169 @IInstantiationService private readonly _instantiationService : IInstantiationService ,
@@ -76,7 +94,7 @@ export class ChatEditingTimeline {
7694 * Get the snapshot and history index for restoring, given requestId and stopId.
7795 * If requestId is undefined, returns undefined (pending snapshot is managed by session).
7896 */
79- public getSnapshotForRestore ( requestId : string | undefined , stopId : string | undefined ) : { stop : IChatEditingSessionStop ; historyIndex : number ; apply ( ) : void } | undefined {
97+ public getSnapshotForRestore ( requestId : string | undefined , stopId : string | undefined ) : { stop : IChatEditingSessionStop ; apply ( ) : void } | undefined {
8098 if ( requestId === undefined ) {
8199 return undefined ;
82100 }
@@ -85,7 +103,13 @@ export class ChatEditingTimeline {
85103 return undefined ;
86104 }
87105
88- return { stop : stopRef . stop , historyIndex : stopRef . historyIndex , apply : ( ) => this . _linearHistoryIndex . set ( stopRef . historyIndex + 1 , undefined ) } ;
106+ // When rolling back to the first snapshot taken for a request, mark the
107+ // entire request as undone.
108+ const toIndex = stopRef . stop . stopId === undefined ? stopRef . historyIndex : stopRef . historyIndex + 1 ;
109+ return {
110+ stop : stopRef . stop ,
111+ apply : ( ) => this . _linearHistoryIndex . set ( toIndex , undefined )
112+ } ;
89113 }
90114
91115 /**
@@ -119,22 +143,21 @@ export class ChatEditingTimeline {
119143 return ;
120144 }
121145
122- const snap = history [ snapIndex ] ;
146+ const snap = { ... history [ snapIndex ] } ;
123147 let stopIndex = snap . stops . findIndex ( ( s ) => s . stopId === undoStop ) ;
124148 if ( stopIndex === - 1 ) {
125149 return ;
126150 }
127151
152+ let linearHistoryIndexIncr = 0 ;
128153 if ( next ) {
129154 if ( stopIndex === snap . stops . length - 1 ) {
130- const postEdit = new ResourceMap ( snap . postEdit || ChatEditingTimeline . createEmptySnapshot ( undefined ) . entries ) ;
131- if ( ! snap . postEdit || ! entry . equalsSnapshot ( postEdit . get ( entry . modifiedURI ) as ISnapshotEntry | undefined ) ) {
132- postEdit . set ( entry . modifiedURI , entry . createSnapshot ( requestId , ChatEditingTimeline . POST_EDIT_STOP_ID ) ) ;
133- const newHistory = history . slice ( ) ;
134- newHistory [ snapIndex ] = { ...snap , postEdit } ;
135- this . _linearHistory . set ( newHistory , tx ) ;
155+ if ( snap . stops [ stopIndex ] . stopId === ChatEditingTimeline . POST_EDIT_STOP_ID ) {
156+ throw new Error ( 'cannot duplicate post-edit stop' ) ;
136157 }
137- return ;
158+
159+ snap . stops = snap . stops . concat ( ChatEditingTimeline . createEmptySnapshot ( ChatEditingTimeline . POST_EDIT_STOP_ID ) ) ;
160+ linearHistoryIndexIncr ++ ;
138161 }
139162 stopIndex ++ ;
140163 }
@@ -149,10 +172,15 @@ export class ChatEditingTimeline {
149172
150173 const newStop = snap . stops . slice ( ) ;
151174 newStop [ stopIndex ] = { ...stop , entries : newMap } ;
175+ snap . stops = newStop ;
152176
153177 const newHistory = history . slice ( ) ;
154- newHistory [ snapIndex ] = { ...snap , stops : newStop } ;
178+ newHistory [ snapIndex ] = snap ;
179+
155180 this . _linearHistory . set ( newHistory , tx ) ;
181+ if ( linearHistoryIndexIncr ) {
182+ this . _linearHistoryIndex . set ( this . _linearHistoryIndex . get ( ) + linearHistoryIndexIncr , tx ) ;
183+ }
156184 }
157185
158186 /**
@@ -161,23 +189,40 @@ export class ChatEditingTimeline {
161189 * pushed into the history.
162190 */
163191 public getUndoSnapshot ( ) : { stop : IChatEditingSessionStop ; apply ( ) : void } | undefined {
164- const idx = this . _linearHistoryIndex . get ( ) - 2 ;
165- const entry = this . getHistoryEntryByLinearIndex ( idx ) ;
166- if ( entry ) {
167- return { stop : entry . stop , apply : ( ) => this . _linearHistoryIndex . set ( idx + 1 , undefined ) } ;
168- }
169- return undefined ;
192+ return this . getUndoRedoSnapshot ( - 1 ) ;
170193 }
171194
172195 /**
173196 * Get the redo snapshot (next in history), or undefined if at end.
174197 */
175198 public getRedoSnapshot ( ) : { stop : IChatEditingSessionStop ; apply ( ) : void } | undefined {
176- const idx = this . _linearHistoryIndex . get ( ) ;
177- const entry = this . getHistoryEntryByLinearIndex ( idx ) ;
199+ return this . getUndoRedoSnapshot ( 1 ) ;
200+ }
201+
202+ private getUndoRedoSnapshot ( direction : number ) {
203+ let idx = this . _linearHistoryIndex . get ( ) - 1 ;
204+ const max = getMaxHistoryIndex ( this . _linearHistory . get ( ) ) ;
205+ const startEntry = this . getHistoryEntryByLinearIndex ( idx ) ;
206+ let entry = startEntry ;
207+ if ( ! startEntry ) {
208+ return undefined ;
209+ }
210+
211+ do {
212+ idx += direction ;
213+ entry = this . getHistoryEntryByLinearIndex ( idx ) ;
214+ } while (
215+ idx + direction < max &&
216+ idx + direction >= 0 &&
217+ entry &&
218+ ! ( direction === - 1 && entry . entry . requestId !== startEntry . entry . requestId ) &&
219+ ! stopProvidesNewData ( startEntry . stop , entry . stop )
220+ ) ;
221+
178222 if ( entry ) {
179223 return { stop : entry . stop , apply : ( ) => this . _linearHistoryIndex . set ( idx + 1 , undefined ) } ;
180224 }
225+
181226 return undefined ;
182227 }
183228
@@ -221,23 +266,27 @@ export class ChatEditingTimeline {
221266 if ( entry . startIndex >= linearHistoryPtr ) {
222267 break ;
223268 } else if ( linearHistoryPtr - entry . startIndex < entry . stops . length ) {
224- newLinearHistory . push ( { requestId : entry . requestId , stops : entry . stops . slice ( 0 , linearHistoryPtr - entry . startIndex ) , startIndex : entry . startIndex , postEdit : undefined } ) ;
269+ newLinearHistory . push ( { requestId : entry . requestId , stops : entry . stops . slice ( 0 , linearHistoryPtr - entry . startIndex ) , startIndex : entry . startIndex } ) ;
225270 } else {
226271 newLinearHistory . push ( entry ) ;
227272 }
228273 }
229274
230275 const lastEntry = newLinearHistory . at ( - 1 ) ;
231276 if ( requestId && lastEntry ?. requestId === requestId ) {
232- if ( lastEntry . postEdit && undoStop ) {
277+ const hadPostEditStop = lastEntry . stops . at ( - 1 ) ?. stopId === ChatEditingTimeline . POST_EDIT_STOP_ID && undoStop ;
278+ if ( hadPostEditStop ) {
233279 const rebaseUri = ( uri : URI ) => uri . with ( { query : uri . query . replace ( ChatEditingTimeline . POST_EDIT_STOP_ID , undoStop ) } ) ;
234- for ( const [ uri , prev ] of lastEntry . postEdit . entries ( ) ) {
280+ for ( const [ uri , prev ] of lastEntry . stops . at ( - 1 ) ! . entries ) {
235281 snapshot . entries . set ( uri , { ...prev , snapshotUri : rebaseUri ( prev . snapshotUri ) , resource : rebaseUri ( prev . resource ) } ) ;
236282 }
237283 }
238- newLinearHistory [ newLinearHistory . length - 1 ] = { ...lastEntry , stops : [ ...lastEntry . stops , snapshot ] , postEdit : undefined } ;
284+ newLinearHistory [ newLinearHistory . length - 1 ] = {
285+ ...lastEntry ,
286+ stops : [ ...hadPostEditStop ? lastEntry . stops . slice ( 0 , - 1 ) : lastEntry . stops , snapshot ]
287+ } ;
239288 } else {
240- newLinearHistory . push ( { requestId, startIndex : lastEntry ? lastEntry . startIndex + lastEntry . stops . length : 0 , stops : [ snapshot ] , postEdit : undefined } ) ;
289+ newLinearHistory . push ( { requestId, startIndex : lastEntry ? lastEntry . startIndex + lastEntry . stops . length : 0 , stops : [ snapshot ] } ) ;
241290 }
242291
243292 transaction ( ( tx ) => {
@@ -247,25 +296,6 @@ export class ChatEditingTimeline {
247296 } ) ;
248297 }
249298
250- /**
251- * Gets chat disablement entries for the current timeline state.
252- */
253- public getRequestDisablement ( ) {
254- const history = this . _linearHistory . get ( ) ;
255- const index = this . _linearHistoryIndex . get ( ) ;
256- const undoRequests : IChatRequestDisablement [ ] = [ ] ;
257- for ( const entry of history ) {
258- if ( ! entry . requestId ) {
259- // ignored
260- } else if ( entry . startIndex >= index ) {
261- undoRequests . push ( { requestId : entry . requestId } ) ;
262- } else if ( entry . startIndex + entry . stops . length > index ) {
263- undoRequests . push ( { requestId : entry . requestId , afterUndoStop : entry . stops [ index - entry . startIndex ] . stopId } ) ;
264- }
265- }
266- return undoRequests ;
267- }
268-
269299 /**
270300 * Gets diff for text entries between stops.
271301 * @param entriesContent Observable that observes either snapshot entry
@@ -390,6 +420,10 @@ export class ChatEditingTimeline {
390420 }
391421}
392422
423+ function stopProvidesNewData ( origin : IChatEditingSessionStop , target : IChatEditingSessionStop ) {
424+ return Iterable . some ( target . entries , ( [ uri , e ] ) => origin . entries . get ( uri ) ?. current !== e . current ) ;
425+ }
426+
393427function getMaxHistoryIndex ( history : readonly IChatEditingSessionSnapshot [ ] ) {
394428 const lastHistory = history . at ( - 1 ) ;
395429 return lastHistory ? lastHistory . startIndex + lastHistory . stops . length : 0 ;
@@ -415,14 +449,11 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi
415449 const nextStop = stopIndex < snapshot . stops . length - 1
416450 ? snapshot . stops [ stopIndex + 1 ]
417451 : undefined ;
418- const next = nextStop ?. entries || snapshot . postEdit ;
419-
420-
421- if ( ! next ) {
452+ if ( ! nextStop ) {
422453 return undefined ;
423454 }
424455
425- return { current, currentStopId : currentStop . stopId , next, nextStopId : nextStop ? .stopId || ChatEditingTimeline . POST_EDIT_STOP_ID } ;
456+ return { current, currentStopId : currentStop . stopId , next : nextStop . entries , nextStopId : nextStop . stopId } ;
426457}
427458
428459function getFirstAndLastStop ( uri : URI , history : readonly IChatEditingSessionSnapshot [ ] ) {
@@ -439,12 +470,6 @@ function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnap
439470 let lastStopWithUriId : string | undefined ;
440471 for ( let i = history . length - 1 ; i >= 0 ; i -- ) {
441472 const snapshot = history [ i ] ;
442- if ( snapshot . postEdit ?. has ( uri ) ) {
443- lastStopWithUri = snapshot . postEdit ;
444- lastStopWithUriId = ChatEditingTimeline . POST_EDIT_STOP_ID ;
445- break ;
446- }
447-
448473 const stop = findLast ( snapshot . stops , s => s . entries . has ( uri ) ) ;
449474 if ( stop ) {
450475 lastStopWithUri = stop . entries ;
0 commit comments