Skip to content

HasUnsavedChanges is false at an older save point after a newer save, so "save changes?" prompts are skipped while disk holds different content #112

Description

@matt-edmondson

What's wrong

SaveBoundaryManager.HasUnsavedChanges (UndoRedo/Services/SaveBoundaryManager.cs, ~line 23) returns false whenever the current position matches any save boundary:

return !_saveBoundaries.Any(boundary => boundary.Position == currentPosition);

MarkAsSaved adds a new boundary each time and never retires the earlier ones. After a second save, the first save point still counts as clean, even though the file on disk now holds the second save.

Failure scenario

int value = 0;
UndoRedoService s = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger());
s.Execute(new DelegateCommand("A", () => value = 1, () => value = 0));
s.MarkAsSaved("save 1");   // disk: 1
s.Execute(new DelegateCommand("B", () => value = 2, () => value = 1));
s.MarkAsSaved("save 2");   // disk: 2
s.Undo();                  // document: 1
Assert.IsTrue(s.HasUnsavedChanges); // fails: HasUnsavedChanges == false

An app that asks "Save changes?" only when HasUnsavedChanges is true, which is the pattern in docs/api-reference.md, closes without asking. The undo is lost, and the next time the file is opened it contains the change the user reverted. UndoToSaveBoundaryAsync(olderBoundary) fails the same way. Both cases were confirmed with a temporary test against main (71a1527).

The class's own reasoning says otherwise. The comment on _initialStateIsClean says the initial state stops being clean "once anything is saved, since the saved state replaces it". The same logic applies to earlier save points, but the code never applies it.

Note for triage: the existing test SaveBoundaries_MultipleUndoRedoOperations_MaintainsCorrectState (UndoRedo.Test/UndoRedoStackTests.cs:532) asserts the current behavior. That test and the documented contract ("unsaved changes since the last save boundary") disagree, and a maintainer needs to decide which one is right. This is related to #83 but separate from it: #83 is only about position -1.

Suggested fix

  • Track the most recent save boundary, for example the identity of the last boundary CreateSaveBoundary made.
  • HasUnsavedChanges(position) returns false only when that boundary still exists and Position == position.
  • If that boundary is removed (by branch cleanup or by trimming), every position is dirty. Do not fall back to an older boundary.
  • Keep the older boundaries for UndoToSaveBoundaryAsync and the visualization.
  • Restore the latest boundary in RestoreFromState. The last one in the list works, because boundaries are appended in order.
  • Update the existing test and add a regression test for save, edit, save, undo.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions