Skip to content

Resolve a held save boundary to its live position before undoing to it - #101

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/undoredo-88-stale-save-boundary
Sep 28, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/undoredo-88-stale-save-boundary

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #88

What was wrong

When MaxStackSize trimmed the stack, SaveBoundaryManager.AdjustPositions replaced each SaveBoundary with a new object at the adjusted position. A caller could already be holding a boundary from SaveBoundaryCreated, GetLastSaveBoundary(), or an earlier read of SaveBoundaries. That boundary kept its old position, and UndoToSaveBoundaryAsync and GetCommandsToUndo used it without checking. In the issue's repro, "revert to last save" left the document at value 2, an unsaved intermediate state, when the saved value was 1.

Change

I took the stable-id option from the issue:

  • SaveBoundary gains an internal identity and an internal copy constructor. AdjustPositions uses the copy constructor, so the boundary at the new position is still the same save point. The public shape of SaveBoundary is unchanged. The copy also keeps the original Timestamp; before, a trim reset it to the time of the trim.
  • UndoRedoService.UndoToSaveBoundaryAsync and GetCommandsToUndo first resolve their argument against the live boundaries. A boundary held across a trim resolves to its current position. A boundary whose save point no longer exists, because a branch or Clear() removed it, makes the call return false or an empty sequence, and nothing is undone.
  • Boundaries are still replaced rather than mutated, so a GetCurrentState() snapshot taken before a trim keeps the positions it was taken with.
  • docs/api-reference.md now documents both behaviours.

The related IsRestorable boundary-range check from the issue was already added in #97.

One behaviour change: the service now rejects a boundary it doesn't know about, including one built with new SaveBoundary(...). Before, it would undo to whatever position that boundary named. The existing test that passes a hand-built boundary already expected false for its own reason, so it still passes.

Tests

  • UndoToSaveBoundary_BoundaryHeldAcrossTrim_UndoesToSavedState is the issue's repro. It expects value 1, position -1 and no unsaved changes.
  • UndoToSaveBoundary_BoundaryRemovedByBranching_ReturnsFalse checks that nothing is undone and GetCommandsToUndo is empty.
  • Both tests fail with the library change reverted and pass with it. The full suite passes locally: 92 of 92.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TX4nouJvSXNs7eSvDP13qU


Generated by Claude Code

… to it [patch]

Trimming replaced each SaveBoundary with a new object at the adjusted
position, so a boundary a caller already held (from SaveBoundaryCreated,
GetLastSaveBoundary or an earlier SaveBoundaries read) kept its old
position. UndoToSaveBoundaryAsync and GetCommandsToUndo used that stale
position as given and landed on an unsaved intermediate state.

Adjusted boundaries now share an internal identity with the boundary they
replace, and the service resolves its argument against the live
boundaries. A boundary held across a trim undoes to its save point; one
removed by branching or clearing is rejected.

Fixes #88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TX4nouJvSXNs7eSvDP13qU
…ale-save-boundary

# Conflicts:
#	UndoRedo/Services/UndoRedoService.cs
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UndoToSaveBoundaryAsync undoes to the wrong position when given a SaveBoundary obtained before the stack was trimmed

1 participant