You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After RestoreFromState, SaveBoundary objects from GetCurrentState() or SaveBoundaryCreated no longer resolve: UndoToSaveBoundaryAsync returns false and GetCommandsToUndo returns nothing #123
UndoToSaveBoundaryAsync and GetCommandsToUndo map the SaveBoundary they are given to a live boundary through FindLiveSaveBoundary (UndoRedo/Services/UndoRedoService.cs:278-279). The match is by the internal Identity object (SaveBoundary.IsSameSavePointAs, UndoRedo/Models/SaveBoundary.cs:62,67). #88 added this so a boundary a caller already holds still works after the stack is trimmed. AdjustPositions keeps Identity because it builds the copy with the internal SaveBoundary(original, position) constructor (SaveBoundary.cs:37-42).
RestoreFromState rebuilds every boundary without its identity:
built-in manager: SaveBoundaryManager.RestoreSaveBoundary (UndoRedo/Services/SaveBoundaryManager.cs:46-50) calls new SaveBoundary(saveBoundary.Position, saveBoundary.Description, saveBoundary.Timestamp), which gets a fresh Identity
custom manager: CreateSaveBoundary (UndoRedoService.cs:441), which also makes a new identity
So once a state is restored, no SaveBoundary object from before the restore resolves any more. That includes the objects inside the very UndoRedoStackState that was restored, and the ones handed out by SaveBoundaryCreated. UndoToSaveBoundaryAsync quietly returns false and GetCommandsToUndo returns an empty sequence, even though the same save point is still in service.SaveBoundaries at the same position.
Failure scenario (reproduced on net10.0 against HEAD)
varsvc=newUndoRedoService(newStackManager(),newSaveBoundaryManager(),newCommandMerger());SaveBoundary?fromEvent=null;svc.SaveBoundaryCreated+=(_,e)=>fromEvent=e.SaveBoundary;intv=0;svc.Execute(newDelegateCommand("a",()=>v++,()=>v--));svc.MarkAsSaved("s");// boundary at 0svc.Execute(newDelegateCommand("b",()=>v++,()=>v--));svc.Execute(newDelegateCommand("c",()=>v++,()=>v--));varstate=svc.GetCurrentState();svc.GetCommandsToUndo(fromEvent!).Count();// 2svc.RestoreFromState(state);// true, same historysvc.GetCommandsToUndo(fromEvent!).Count();// 0 (expected 2)svc.GetCommandsToUndo(state.SaveBoundaries[0]).Count();// 0 (expected 2)awaitsvc.UndoToSaveBoundaryAsync(state.SaveBoundaries[0]);// false, position stays 2 (expected true, position 0)svc.GetCommandsToUndo(svc.SaveBoundaries[0]).Count();// 2 (only a freshly read boundary works)
Output of the probe:
before restore: toUndo=2
restore=True
after restore event boundary: toUndo=0
after restore state boundary: toUndo=0
UndoToSaveBoundaryAsync(state boundary)=False pos=2 v=3
live boundary: toUndo=2
A real-world case is an editor that keeps one history per open document and switches between them with GetCurrentState() / RestoreFromState(), while its UI keeps a "revert to save" list filled from SaveBoundaryCreated. After switching back to a document, every entry in that list does nothing. The call does not throw, and it returns the same false as "that save point is gone".
This is separate from #118, which covers restoring from the live Commands/SaveBoundaries views, and from #91, where timestamps are now kept. PR #122 touches RestoreFromState but does not change how boundaries are rebuilt.
Suggested fix
In SaveBoundaryManager.RestoreSaveBoundary, keep the incoming boundary's identity (and timestamp) by using the existing internal copy constructor:
Boundaries that come back from JSON already have a fresh identity from deserialization, so nothing changes on the LoadStateAsync path. Optionally, document on IUndoRedoService.RestoreFromState that a custom ISaveBoundaryManager (the CreateSaveBoundary fallback) does not keep boundary identity.
Acceptance criteria
After RestoreFromState(service.GetCurrentState()), UndoToSaveBoundaryAsync and GetCommandsToUndo accept the boundaries in that state and the ones from earlier SaveBoundaryCreated events, and act as they did before the restore.
What's wrong
UndoToSaveBoundaryAsyncandGetCommandsToUndomap theSaveBoundarythey are given to a live boundary throughFindLiveSaveBoundary(UndoRedo/Services/UndoRedoService.cs:278-279). The match is by the internalIdentityobject (SaveBoundary.IsSameSavePointAs,UndoRedo/Models/SaveBoundary.cs:62,67). #88 added this so a boundary a caller already holds still works after the stack is trimmed.AdjustPositionskeepsIdentitybecause it builds the copy with the internalSaveBoundary(original, position)constructor (SaveBoundary.cs:37-42).RestoreFromStaterebuilds every boundary without its identity:SaveBoundaryManager.RestoreSaveBoundary(UndoRedo/Services/SaveBoundaryManager.cs:46-50) callsnew SaveBoundary(saveBoundary.Position, saveBoundary.Description, saveBoundary.Timestamp), which gets a freshIdentityCreateSaveBoundary(UndoRedoService.cs:441), which also makes a new identitySo once a state is restored, no
SaveBoundaryobject from before the restore resolves any more. That includes the objects inside the veryUndoRedoStackStatethat was restored, and the ones handed out bySaveBoundaryCreated.UndoToSaveBoundaryAsyncquietly returnsfalseandGetCommandsToUndoreturns an empty sequence, even though the same save point is still inservice.SaveBoundariesat the same position.Failure scenario (reproduced on net10.0 against HEAD)
Output of the probe:
A real-world case is an editor that keeps one history per open document and switches between them with
GetCurrentState()/RestoreFromState(), while its UI keeps a "revert to save" list filled fromSaveBoundaryCreated. After switching back to a document, every entry in that list does nothing. The call does not throw, and it returns the samefalseas "that save point is gone".This is separate from #118, which covers restoring from the live
Commands/SaveBoundariesviews, and from #91, where timestamps are now kept. PR #122 touchesRestoreFromStatebut does not change how boundaries are rebuilt.Suggested fix
In
SaveBoundaryManager.RestoreSaveBoundary, keep the incoming boundary's identity (and timestamp) by using the existing internal copy constructor:Boundaries that come back from JSON already have a fresh identity from deserialization, so nothing changes on the
LoadStateAsyncpath. Optionally, document onIUndoRedoService.RestoreFromStatethat a customISaveBoundaryManager(theCreateSaveBoundaryfallback) does not keep boundary identity.Acceptance criteria
RestoreFromState(service.GetCurrentState()),UndoToSaveBoundaryAsyncandGetCommandsToUndoaccept the boundaries in that state and the ones from earlierSaveBoundaryCreatedevents, and act as they did before the restore.