Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions UndoRedo.Test/UndoRedoStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,55 @@ public void CommandMerging_AfterUndo_CleansInvalidSaveBoundaries()
Assert.IsFalse(stack.CanRedo, "Redo should be unavailable after forward commands are cleared");
}

[TestMethod]
public void CommandMerging_AfterMarkAsSaved_KeepsTheSaveBoundary()
{
// Arrange
UndoRedoOptions options = UndoRedoOptions.Create(autoMerge: true);
UndoRedoService stack = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger(), options);
List<char> value = [];

stack.Execute(new TestInsertMergeCommand(value, 0, "a"));
stack.MarkAsSaved();

// Act
stack.Execute(new TestInsertMergeCommand(value, 1, "b"));

// Assert
Assert.AreEqual("ab", new string([.. value]));
Assert.AreEqual(2, stack.CommandCount, "An edit after a save should not merge into the saved command");
Assert.HasCount(1, stack.SaveBoundaries, "The save boundary should survive the next edit");
Assert.IsTrue(stack.HasUnsavedChanges, "The edit after the save should be unsaved");

stack.Undo();
Assert.AreEqual("a", new string([.. value]), "Undo should return to the saved text");
Assert.IsFalse(stack.HasUnsavedChanges, "Undo back to the save should leave the stack clean");
}

[TestMethod]
public void CommandMerging_AfterMarkAsSaved_MergesLaterEditsWithEachOther()
{
// Arrange
UndoRedoOptions options = UndoRedoOptions.Create(autoMerge: true);
UndoRedoService stack = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger(), options);
List<char> value = [];

stack.Execute(new TestInsertMergeCommand(value, 0, "a"));
stack.MarkAsSaved();

// Act
stack.Execute(new TestInsertMergeCommand(value, 1, "b"));
stack.Execute(new TestInsertMergeCommand(value, 2, "c"));

// Assert
Assert.AreEqual("abc", new string([.. value]));
Assert.AreEqual(2, stack.CommandCount, "Edits with no save between them should still merge");

stack.Undo();
Assert.AreEqual("a", new string([.. value]), "Undo should revert the merged post-save edits together");
Assert.IsFalse(stack.HasUnsavedChanges);
}

[TestMethod]
public void CompositeCommand_NestedComposites_HandlesCorrectly()
{
Expand Down
8 changes: 6 additions & 2 deletions UndoRedo/Services/UndoRedoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@
/// <param name="navigationProvider">The navigation provider</param>
public void SetNavigationProvider(INavigationProvider? navigationProvider) => _navigationProvider = navigationProvider;

private bool IsAtSaveBoundary(int position) => _saveBoundaryManager.SaveBoundaries.Any(boundary => boundary.Position == position);

/// <inheritdoc />
public void Execute(ICommand command)
{
Ensure.NotNull(command);

// Try to merge with the last command if auto-merge is enabled
if (_options.AutoMergeCommands && _stackManager.CanUndo)
// Try to merge with the last command if auto-merge is enabled. Never merge into a command that
// ends at a save boundary: that would fold the new edit into the saved state and delete the
// boundary, so Undo could never return to what was saved.
if (_options.AutoMergeCommands && _stackManager.CanUndo && !IsAtSaveBoundary(_stackManager.CurrentPosition))
{
ICommand? lastCommand = _stackManager.GetCurrentCommand();
if (lastCommand != null && _commandMerger.CanMerge(lastCommand, command))
Expand Down Expand Up @@ -168,7 +172,7 @@

try
{
await _navigationProvider.NavigateToAsync(command.NavigationContext!, cts.Token).ConfigureAwait(false);

Check warning on line 175 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 175 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 175 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 175 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.
}
catch (OperationCanceledException)
{
Expand Down Expand Up @@ -204,7 +208,7 @@

try
{
await _navigationProvider.NavigateToAsync(command.NavigationContext!, cts.Token).ConfigureAwait(false);

Check warning on line 211 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 211 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 211 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 211 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.
}
catch (OperationCanceledException)
{
Expand Down Expand Up @@ -270,7 +274,7 @@

try
{
await _navigationProvider.NavigateToAsync(lastCommand.NavigationContext!, cts.Token).ConfigureAwait(false);

Check warning on line 277 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 277 in UndoRedo/Services/UndoRedoService.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this null-forgiving operator; the compiler already knows this expression is not null here.
}
catch (OperationCanceledException)
{
Expand Down
Loading