From fd69ad0b0100b4f94a42270377c51a554ea56699 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Tue, 3 Sep 2024 00:37:00 +0200 Subject: [PATCH] Fix search bug in prompt related to custom item types Closes #1626 --- .../Prompts/List/ListPrompt.cs | 3 +- .../Prompts/List/ListPromptState.cs | 22 ++++++++-- .../Prompts/MultiSelectionPrompt.cs | 3 +- .../Prompts/SelectionPrompt.cs | 3 +- .../Unit/Prompts/ListPromptStateTests.cs | 5 ++- .../Unit/Prompts/TextPromptTests.cs | 41 +++++++++++++++++++ 6 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console/Prompts/List/ListPrompt.cs index 7f6948c1a..c66850256 100644 --- a/src/Spectre.Console/Prompts/List/ListPrompt.cs +++ b/src/Spectre.Console/Prompts/List/ListPrompt.cs @@ -14,6 +14,7 @@ public ListPrompt(IAnsiConsole console, IListPromptStrategy strategy) public async Task> Show( ListPromptTree tree, + Func converter, SelectionMode selectionMode, bool skipUnselectableItems, bool searchEnabled, @@ -41,7 +42,7 @@ public async Task> Show( } var nodes = tree.Traverse().ToList(); - var state = new ListPromptState(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled); + var state = new ListPromptState(nodes, converter, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled); 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 177b97edb..fdbe3c803 100644 --- a/src/Spectre.Console/Prompts/List/ListPromptState.cs +++ b/src/Spectre.Console/Prompts/List/ListPromptState.cs @@ -3,6 +3,8 @@ namespace Spectre.Console; internal sealed class ListPromptState where T : notnull { + private readonly Func _converter; + public int Index { get; private set; } public int ItemCount => Items.Count; public int PageSize { get; } @@ -16,8 +18,15 @@ internal sealed class ListPromptState public ListPromptItem Current => Items[Index]; public string SearchText { get; private set; } - public ListPromptState(IReadOnlyList> items, int pageSize, bool wrapAround, SelectionMode mode, bool skipUnselectableItems, bool searchEnabled) + public ListPromptState( + IReadOnlyList> items, + Func converter, + int pageSize, bool wrapAround, + SelectionMode mode, + bool skipUnselectableItems, + bool searchEnabled) { + _converter = converter ?? throw new ArgumentNullException(nameof(converter)); Items = items; PageSize = pageSize; WrapAround = wrapAround; @@ -126,7 +135,11 @@ public bool Update(ConsoleKeyInfo keyInfo) if (!char.IsControl(keyInfo.KeyChar)) { search = SearchText + keyInfo.KeyChar; - var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf)); + + var item = Items.FirstOrDefault(x => + _converter.Invoke(x.Data).Contains(search, StringComparison.OrdinalIgnoreCase) + && (!x.IsGroup || Mode != SelectionMode.Leaf)); + if (item != null) { index = Items.IndexOf(item); @@ -140,7 +153,10 @@ public bool Update(ConsoleKeyInfo keyInfo) search = search.Substring(0, search.Length - 1); } - var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf)); + var item = Items.FirstOrDefault(x => + _converter.Invoke(x.Data).Contains(search, StringComparison.OrdinalIgnoreCase) && + (!x.IsGroup || Mode != SelectionMode.Leaf)); + if (item != null) { index = Items.IndexOf(item); diff --git a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs index 0a023773c..8c63e2e4b 100644 --- a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs @@ -94,7 +94,8 @@ public async Task> ShowAsync(IAnsiConsole console, CancellationToken can { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = await prompt.Show(Tree, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var result = await prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); if (Mode == SelectionMode.Leaf) { diff --git a/src/Spectre.Console/Prompts/SelectionPrompt.cs b/src/Spectre.Console/Prompts/SelectionPrompt.cs index e6ed46b87..c9f55690e 100644 --- a/src/Spectre.Console/Prompts/SelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/SelectionPrompt.cs @@ -99,7 +99,8 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = await prompt.Show(_tree, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var result = await prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); // Return the selected item return result.Items[result.Index].Data; diff --git a/src/Tests/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs index cda4f3755..2a45b66bc 100644 --- a/src/Tests/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs +++ b/src/Tests/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs @@ -3,7 +3,10 @@ namespace Spectre.Console.Tests.Unit; public sealed class ListPromptStateTests { private ListPromptState CreateListPromptState(int count, int pageSize, bool shouldWrap, bool searchEnabled) - => new(Enumerable.Range(0, count).Select(i => new ListPromptItem(i.ToString())).ToList(), pageSize, shouldWrap, SelectionMode.Independent, true, searchEnabled); + => new( + Enumerable.Range(0, count).Select(i => new ListPromptItem(i.ToString())).ToList(), + text => text, + pageSize, shouldWrap, SelectionMode.Independent, true, searchEnabled); [Fact] public void Should_Have_Start_Index_Zero() diff --git a/src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs index c358b9be3..4d95dc59a 100644 --- a/src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -410,4 +410,45 @@ public Task Uses_the_specified_choices_style() // Then return Verifier.Verify(console.Output); } + + [Fact] + public void Should_Search_In_Remapped_Result() + { + // Given + var console = new TestConsole(); + console.Profile.Capabilities.Interactive = true; + console.EmitAnsiSequences(); + console.Input.PushText("2"); + console.Input.PushKey(ConsoleKey.Enter); + + var choices = new List + { + new(33, "Item 1"), + new(34, "Item 2"), + }; + + var prompt = new SelectionPrompt() + .Title("Select one") + .EnableSearch() + .UseConverter(o => o.Name) + .AddChoices(choices); + + // When + var selection = prompt.Show(console); + + // Then + selection.ShouldBe(choices[1]); + } +} + +file sealed class CustomSelectionItem +{ + public int Value { get; } + public string Name { get; } + + public CustomSelectionItem(int value, string name) + { + Value = value; + Name = name ?? throw new ArgumentNullException(nameof(name)); + } }