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
115 changes: 115 additions & 0 deletions UndoRedo.Test/UndoRedoStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,121 @@
Assert.HasCount(5, visualizations);
}

[TestMethod]
public void GetChangeVisualizations_MoreCommandsThanLimit_IncludesCurrentPosition()
{
// Arrange
UndoRedoService stack = CreateService();
for (int i = 0; i < 60; i++)
{
stack.Execute(new DelegateCommand($"c{i}", () => { }, () => { }));
}

// Act
List<ChangeVisualization> visualizations = [.. stack.GetChangeVisualizations(50)];

// Assert
Assert.HasCount(50, visualizations);
Assert.AreEqual(10, visualizations[0].Position, "The window should hold the most recent commands");
Assert.AreEqual("c10", visualizations[0].Command.Description);
Assert.AreEqual(59, visualizations[^1].Position, "The window should end at the current position");
Assert.AreEqual("c59", visualizations[^1].Command.Description);
Assert.IsTrue(visualizations.All(v => v.IsExecuted));
}

[TestMethod]
public void GetChangeVisualizations_AfterUndo_IncludesRedoableCommands()
{
// Arrange
UndoRedoService stack = CreateService();
for (int i = 0; i < 60; i++)
{
stack.Execute(new DelegateCommand($"c{i}", () => { }, () => { }));
}

for (int i = 0; i < 5; i++)
{
stack.Undo();
}

// Act
List<ChangeVisualization> visualizations = [.. stack.GetChangeVisualizations(50)];

// Assert
Assert.HasCount(50, visualizations);
ChangeVisualization current = visualizations.Single(v => v.Position == 54);
Assert.IsTrue(current.IsExecuted, "The current command should be marked executed");
List<ChangeVisualization> redoable = [.. visualizations.Where(v => v.Position > 54)];
Assert.HasCount(5, redoable, "The redoable commands should be in the window");
Assert.IsTrue(redoable.All(v => !v.IsExecuted), "Redoable commands should be marked not executed");
}

[TestMethod]
public void GetChangeVisualizations_CurrentPositionBeforeRecentWindow_IncludesCurrentPosition()
{
// Arrange
UndoRedoService stack = CreateService();
for (int i = 0; i < 60; i++)
{
stack.Execute(new DelegateCommand($"c{i}", () => { }, () => { }));
}

for (int i = 0; i < 40; i++)
{
stack.Undo();
}

// Act
List<ChangeVisualization> visualizations = [.. stack.GetChangeVisualizations(10)];

// Assert
Assert.HasCount(10, visualizations);
Assert.IsTrue(visualizations.Any(v => v.Position == 19), "The window should contain the current position");

Check warning on line 858 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDfOdZCobAzMXtwCR6n&open=AaDfOdZCobAzMXtwCR6n&pullRequest=98
Assert.IsTrue(visualizations.Any(v => v.Position > 19), "The window should include redoable commands");

Check warning on line 859 in UndoRedo.Test/UndoRedoStackTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_UndoRedo&issues=AaDfOdZCobAzMXtwCR6o&open=AaDfOdZCobAzMXtwCR6o&pullRequest=98
Assert.IsTrue(visualizations.All(v => v.Command.Description == $"c{v.Position}"), "Positions should be absolute stack indices");
Assert.IsTrue(visualizations.All(v => v.IsExecuted == (v.Position <= 19)));
}

[TestMethod]
public void GetChangeVisualizations_WindowedSaveBoundary_UsesAbsolutePosition()
{
// Arrange
UndoRedoService stack = CreateService();
for (int i = 0; i < 10; i++)
{
stack.Execute(new DelegateCommand($"c{i}", () => { }, () => { }));
if (i == 7)
{
stack.MarkAsSaved();
}
}

// Act
List<ChangeVisualization> visualizations = [.. stack.GetChangeVisualizations(5)];

// Assert
Assert.AreEqual(5, visualizations[0].Position);
List<ChangeVisualization> saved = [.. visualizations.Where(v => v.HasSaveBoundary)];
Assert.HasCount(1, saved);
Assert.AreEqual(7, saved[0].Position);
}

[TestMethod]
[DataRow(0)]
[DataRow(-3)]
public void GetChangeVisualizations_NonPositiveLimit_ReturnsEmpty(int maxItems)
{
// Arrange
UndoRedoService stack = CreateService();
stack.Execute(new DelegateCommand("c0", () => { }, () => { }));

// Act
List<ChangeVisualization> visualizations = [.. stack.GetChangeVisualizations(maxItems)];

// Assert
Assert.IsEmpty(visualizations);
}

[TestMethod]
public void Clear_WithSaveBoundariesAndCommands_ClearsEverything()
{
Expand Down
9 changes: 8 additions & 1 deletion UndoRedo/Contracts/IUndoRedoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,14 @@ public interface IUndoRedoService
/// <summary>
/// Gets change visualization data for the commands in the stack
/// </summary>
/// <param name="maxItems">Maximum number of items to return</param>
/// <remarks>
/// When the stack holds more than <paramref name="maxItems"/> commands, the result is the most
/// recent <paramref name="maxItems"/> commands. If the current position has been undone to before
/// that window, the window is centred on the current position instead, so it always contains the
/// current command along with the redoable commands after it. Each item's position is its
/// absolute index in the stack. The result is a snapshot taken at the time of the call.
/// </remarks>
/// <param name="maxItems">Maximum number of items to return; zero or less returns no items</param>
/// <returns>Visualization data for changes</returns>
public IEnumerable<ChangeVisualization> GetChangeVisualizations(int maxItems = 50);

Expand Down
34 changes: 26 additions & 8 deletions UndoRedo/Services/UndoRedoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,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 @@ -208,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 @@ -274,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 All @@ -292,14 +292,32 @@
int currentPosition = _stackManager.CurrentPosition;
IReadOnlyList<SaveBoundary> saveBoundaries = _saveBoundaryManager.SaveBoundaries;

return commands
.Take(Math.Min(commands.Count, maxItems))
.Select((cmd, index) => new ChangeVisualization(
cmd,
index,
index <= currentPosition,
saveBoundaries.Any(sb => sb.Position == index)
));
if (maxItems <= 0 || commands.Count == 0)
{
return [];
}

// Show the most recent commands, unless the current position has been undone back past
// them; then centre the window on it so both its history and its redo commands show.
int take = Math.Min(commands.Count, maxItems);
int start = commands.Count - take;
if (currentPosition < start)
{
start = Math.Max(0, currentPosition - (take / 2));
}

return [.. commands
.Skip(start)
.Take(take)
.Select((cmd, offset) =>
{
int index = start + offset;
return new ChangeVisualization(
cmd,
index,
index <= currentPosition,
saveBoundaries.Any(sb => sb.Position == index));
})];
}

/// <inheritdoc />
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,10 @@ if (lastSave != null)
```csharp
IEnumerable<ChangeVisualization> GetChangeVisualizations(int maxItems = 50);
```
Gets change visualization data for the commands in the stack.
Gets change visualization data for the commands in the stack. When there are more than `maxItems` commands, it returns the most recent ones. If the current position has been undone to before that window, the window is centred on the current position instead, so it always holds the current command and the redoable commands after it. `Position` is the absolute index in the stack.

**Parameters:**
- `maxItems`: Maximum number of items to return (default: 50)
- `maxItems`: Maximum number of items to return (default: 50). Zero or less returns no items

**Returns:** Visualization data for changes

Expand Down
Loading