diff --git a/src/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console/Prompts/List/ListPrompt.cs index fa0d758..f30f4d3 100644 --- a/src/Spectre.Console/Prompts/List/ListPrompt.cs +++ b/src/Spectre.Console/Prompts/List/ListPrompt.cs @@ -15,7 +15,8 @@ internal sealed class ListPrompt public async Task> Show( ListPromptTree tree, CancellationToken cancellationToken, - int requestedPageSize = 15) + int requestedPageSize = 15, + bool wrapAround = false) { if (tree is null) { @@ -37,7 +38,7 @@ internal sealed class ListPrompt } var nodes = tree.Traverse().ToList(); - var state = new ListPromptState(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize)); + var state = new ListPromptState(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround); var hook = new ListPromptRenderHook(_console, () => BuildRenderable(state)); using (new RenderHookScope(_console, hook)) diff --git a/src/Spectre.Console/Prompts/List/ListPromptState.cs b/src/Spectre.Console/Prompts/List/ListPromptState.cs index a653449..a6b698d 100644 --- a/src/Spectre.Console/Prompts/List/ListPromptState.cs +++ b/src/Spectre.Console/Prompts/List/ListPromptState.cs @@ -6,15 +6,17 @@ internal sealed class ListPromptState public int Index { get; private set; } public int ItemCount => Items.Count; public int PageSize { get; } + public bool WrapAround { get; } public IReadOnlyList> Items { get; } public ListPromptItem Current => Items[Index]; - public ListPromptState(IReadOnlyList> items, int pageSize) + public ListPromptState(IReadOnlyList> items, int pageSize, bool wrapAround) { Index = 0; Items = items; PageSize = pageSize; + WrapAround = wrapAround; } public bool Update(ConsoleKey key) @@ -30,7 +32,9 @@ internal sealed class ListPromptState _ => Index, }; - index = index.Clamp(0, ItemCount - 1); + index = WrapAround + ? (ItemCount + (index % ItemCount)) % ItemCount + : index.Clamp(0, ItemCount - 1); if (index != Index) { Index = index; diff --git a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs index baccc19..5f68387 100644 --- a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs @@ -18,6 +18,12 @@ public sealed class MultiSelectionPrompt : IPrompt>, IListPromptStrat /// public int PageSize { get; set; } = 10; + /// + /// Gets or sets whether the selection should wrap around when reaching the edge. + /// Defaults to false. + /// + public bool WrapAround { get; set; } = false; + /// /// Gets or sets the highlight style of the selected choice. /// @@ -88,7 +94,7 @@ public sealed class MultiSelectionPrompt : IPrompt>, IListPromptStrat { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = await prompt.Show(Tree, cancellationToken, PageSize).ConfigureAwait(false); + var result = await prompt.Show(Tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false); if (Mode == SelectionMode.Leaf) { diff --git a/src/Spectre.Console/Prompts/MultiSelectionPromptExtensions.cs b/src/Spectre.Console/Prompts/MultiSelectionPromptExtensions.cs index c720043..a4aaecf 100644 --- a/src/Spectre.Console/Prompts/MultiSelectionPromptExtensions.cs +++ b/src/Spectre.Console/Prompts/MultiSelectionPromptExtensions.cs @@ -211,6 +211,25 @@ public static class MultiSelectionPromptExtensions return obj; } + /// + /// Sets whether the selection should wrap around when reaching its edges. + /// + /// The prompt result type. + /// The prompt. + /// Whether the selection should wrap around. + /// The same instance so that multiple calls can be chained. + public static MultiSelectionPrompt WrapAround(this MultiSelectionPrompt obj, bool shouldWrap = true) + where T : notnull + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.WrapAround = shouldWrap; + return obj; + } + /// /// Sets the highlight style of the selected choice. /// diff --git a/src/Spectre.Console/Prompts/SelectionPrompt.cs b/src/Spectre.Console/Prompts/SelectionPrompt.cs index cf17253..32edb20 100644 --- a/src/Spectre.Console/Prompts/SelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/SelectionPrompt.cs @@ -20,6 +20,12 @@ public sealed class SelectionPrompt : IPrompt, IListPromptStrategy /// public int PageSize { get; set; } = 10; + /// + /// Gets or sets whether the selection should wrap around when reaching the edge. + /// Defaults to false. + /// + public bool WrapAround { get; set; } = false; + /// /// Gets or sets the highlight style of the selected choice. /// @@ -78,7 +84,7 @@ public sealed class SelectionPrompt : IPrompt, IListPromptStrategy { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = await prompt.Show(_tree, cancellationToken, PageSize).ConfigureAwait(false); + var result = await prompt.Show(_tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false); // Return the selected item return result.Items[result.Index].Data; diff --git a/src/Spectre.Console/Prompts/SelectionPromptExtensions.cs b/src/Spectre.Console/Prompts/SelectionPromptExtensions.cs index 538ef32..1680b5e 100644 --- a/src/Spectre.Console/Prompts/SelectionPromptExtensions.cs +++ b/src/Spectre.Console/Prompts/SelectionPromptExtensions.cs @@ -163,6 +163,25 @@ public static class SelectionPromptExtensions return obj; } + /// + /// Sets whether the selection should wrap around when reaching its edges. + /// + /// The prompt result type. + /// The prompt. + /// Whether the selection should wrap around. + /// The same instance so that multiple calls can be chained. + public static SelectionPrompt WrapAround(this SelectionPrompt obj, bool shouldWrap = true) + where T : notnull + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.WrapAround = shouldWrap; + return obj; + } + /// /// Sets the highlight style of the selected choice. /// diff --git a/test/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs b/test/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs new file mode 100644 index 0000000..045c22f --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs @@ -0,0 +1,120 @@ +namespace Spectre.Console.Tests.Unit; + +public sealed class ListPromptStateTests +{ + private ListPromptState CreateListPromptState(int count, int pageSize, bool shouldWrap) + => new(Enumerable.Repeat(new ListPromptItem(string.Empty), count).ToList(), pageSize, shouldWrap); + + [Fact] + public void Should_Have_Start_Index_Zero() + { + // Given + var state = CreateListPromptState(100, 10, false); + + // When + /* noop */ + + // Then + state.Index.ShouldBe(0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_Increase_Index(bool wrap) + { + // Given + var state = CreateListPromptState(100, 10, wrap); + var index = state.Index; + + // When + state.Update(ConsoleKey.DownArrow); + + // Then + state.Index.ShouldBe(index + 1); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_Go_To_End(bool wrap) + { + // Given + var state = CreateListPromptState(100, 10, wrap); + + // When + state.Update(ConsoleKey.End); + + // Then + state.Index.ShouldBe(99); + } + + [Fact] + public void Should_Clamp_Index_If_No_Wrap() + { + // Given + var state = CreateListPromptState(100, 10, false); + state.Update(ConsoleKey.End); + + // When + state.Update(ConsoleKey.DownArrow); + + // Then + state.Index.ShouldBe(99); + } + + [Fact] + public void Should_Wrap_Index_If_Wrap() + { + // Given + var state = CreateListPromptState(100, 10, true); + state.Update(ConsoleKey.End); + + // When + state.Update(ConsoleKey.DownArrow); + + // Then + state.Index.ShouldBe(0); + } + + [Fact] + public void Should_Wrap_Index_If_Wrap_And_Down() + { + // Given + var state = CreateListPromptState(100, 10, true); + + // When + state.Update(ConsoleKey.UpArrow); + + // Then + state.Index.ShouldBe(99); + } + + [Fact] + public void Should_Wrap_Index_If_Wrap_And_Page_Up() + { + // Given + var state = CreateListPromptState(10, 100, true); + + // When + state.Update(ConsoleKey.PageUp); + + // Then + state.Index.ShouldBe(0); + } + + [Fact] + public void Should_Wrap_Index_If_Wrap_And_Offset_And_Page_Down() + { + // Given + var state = CreateListPromptState(10, 100, true); + state.Update(ConsoleKey.End); + state.Update(ConsoleKey.UpArrow); + + // When + state.Update(ConsoleKey.PageDown); + + // Then + state.Index.ShouldBe(8); + } +}