Repository: spectreconsole/spectre.console (csharp)

Issue:
`SelectionPrompt` does not search on results of `UseConverter`
**Information**
 - OS: MacOS (also tested on Windows)
 - Version: 0.49.1
 - Terminal: IntelliJ Rider Built-in Terminal (using Bash)

**Describe the bug**
Selection prompts which use `UseConverter` do not jump to the search (they do not use the mapped text as the search index).

**To Reproduce**
- Create a selection prompt
- Use .UseConverter()
- Search
- Observe that the result you have searched for is not jumped to

You can also search for the underlying object and observe that it is jumped to.

Example unit test that currently fails (where `MyClass` is just a class with a name parameter):
```c#
    [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 myObject = new List<MyClass>{ new() { name = "Item 1" }, new() {name = "Item 2"} };

        // When
        var prompt = new SelectionPrompt<MyClass>()
            .Title("Select one")
            .EnableSearch()
            .UseConverter(o => o.name)
            .AddChoices(myObject);
        var selection = prompt.Show(console);

        // Then
        selection.name.ShouldBe("Item 2");
    }
```


**Expected behavior**
SelectionPrompt should search based on the results of `UseConverter` and not on the underlying object remapped by it. (see test)

**Screenshots**
If applicable, add screenshots to help explain your problem.


**Additional context**
Add any other context about the problem here.

---
Please upvote :+1: this issue if you are interested in it.

Pull request:
Fixes #1626

<!-- formalities. These are not optional. -->

- [X] I have read the [Contribution Guidelines](https://github.com/spectreconsole/spectre.console/blob/main/CONTRIBUTING.md)
- [X] I have commented on the issue above and discussed the intended changes
- [X] A maintainer has signed off on the changes and the issue was assigned to me
- [X] All newly added code is adequately covered by tests
- [X] All existing tests are still running without errors
- [X] The documentation was modified to reflect the changes _OR_ no documentation changes are required.

## Changes

`ListPromptState<T>` now takes converter into account.

---
Please upvote :+1: this pull request if you are interested in it.

Patch:
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<T> strategy)
 
     public async Task<ListPromptState<T>> Show(
         ListPromptTree<T> tree,
+        Func<T, string> converter,
         SelectionMode selectionMode,
         bool skipUnselectableItems,
         bool searchEnabled,
@@ -41,7 +42,7 @@ public async Task<ListPromptState<T>> Show(
         }
 
         var nodes = tree.Traverse().ToList();
-        var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
+        var state = new ListPromptState<T>(nodes, converter, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
         var hook = new ListPromptRenderHook<T>(_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<T>
     where T : notnull
 {
+    private readonly Func<T, string> _converter;
+
     public int Index { get; private set; }
     public int ItemCount => Items.Count;
     public int PageSize { get; }
@@ -16,8 +18,15 @@ internal sealed class ListPromptState<T>
     public ListPromptItem<T> Current => Items[Index];
     public string SearchText { get; private set; }
 
-    public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize, bool wrapAround, SelectionMode mode, bool skipUnselectableItems, bool searchEnabled)
+    public ListPromptState(
+        IReadOnlyList<ListPromptItem<T>> items,
+        Func<T, string> 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<List<T>> ShowAsync(IAnsiConsole console, CancellationToken can
     {
         // Create the list prompt
         var prompt = new ListPrompt<T>(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<T> ShowAsync(IAnsiConsole console, CancellationToken cancellat
     {
         // Create the list prompt
         var prompt = new ListPrompt<T>(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;


Test patch:
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<string> CreateListPromptState(int count, int pageSize, bool shouldWrap, bool searchEnabled)
-        => new(Enumerable.Range(0, count).Select(i => new ListPromptItem<string>(i.ToString())).ToList(), pageSize, shouldWrap, SelectionMode.Independent, true, searchEnabled);
+        => new(
+            Enumerable.Range(0, count).Select(i => new ListPromptItem<string>(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<CustomSelectionItem>
+        {
+            new(33, "Item 1"),
+            new(34, "Item 2"),
+        };
+
+        var prompt = new SelectionPrompt<CustomSelectionItem>()
+            .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));
+    }
 }