Skip to content

Commit e5b30f1

Browse files
Fix search bug in prompt related to custom item types
Closes #1626
1 parent 753894d commit e5b30f1

File tree

6 files changed

+70
-7
lines changed

6 files changed

+70
-7
lines changed

src/Spectre.Console/Prompts/List/ListPrompt.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public ListPrompt(IAnsiConsole console, IListPromptStrategy<T> strategy)
1414

1515
public async Task<ListPromptState<T>> Show(
1616
ListPromptTree<T> tree,
17+
Func<T, string>? converter,
1718
SelectionMode selectionMode,
1819
bool skipUnselectableItems,
1920
bool searchEnabled,
@@ -41,7 +42,7 @@ public async Task<ListPromptState<T>> Show(
4142
}
4243

4344
var nodes = tree.Traverse().ToList();
44-
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
45+
var state = new ListPromptState<T>(nodes, converter, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
4546
var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state));
4647

4748
using (new RenderHookScope(_console, hook))

src/Spectre.Console/Prompts/List/ListPromptState.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ namespace Spectre.Console;
33
internal sealed class ListPromptState<T>
44
where T : notnull
55
{
6+
private readonly Func<T, string> _converter;
7+
68
public int Index { get; private set; }
79
public int ItemCount => Items.Count;
810
public int PageSize { get; }
@@ -16,8 +18,15 @@ internal sealed class ListPromptState<T>
1618
public ListPromptItem<T> Current => Items[Index];
1719
public string SearchText { get; private set; }
1820

19-
public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize, bool wrapAround, SelectionMode mode, bool skipUnselectableItems, bool searchEnabled)
21+
public ListPromptState(
22+
IReadOnlyList<ListPromptItem<T>> items,
23+
Func<T, string> converter,
24+
int pageSize, bool wrapAround,
25+
SelectionMode mode,
26+
bool skipUnselectableItems,
27+
bool searchEnabled)
2028
{
29+
_converter = converter ?? throw new ArgumentNullException(nameof(converter));
2130
Items = items;
2231
PageSize = pageSize;
2332
WrapAround = wrapAround;
@@ -126,7 +135,11 @@ public bool Update(ConsoleKeyInfo keyInfo)
126135
if (!char.IsControl(keyInfo.KeyChar))
127136
{
128137
search = SearchText + keyInfo.KeyChar;
129-
var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf));
138+
139+
var item = Items.FirstOrDefault(x =>
140+
_converter.Invoke(x.Data).Contains(search, StringComparison.OrdinalIgnoreCase)
141+
&& (!x.IsGroup || Mode != SelectionMode.Leaf));
142+
130143
if (item != null)
131144
{
132145
index = Items.IndexOf(item);
@@ -140,7 +153,10 @@ public bool Update(ConsoleKeyInfo keyInfo)
140153
search = search.Substring(0, search.Length - 1);
141154
}
142155

143-
var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf));
156+
var item = Items.FirstOrDefault(x =>
157+
_converter.Invoke(x.Data).Contains(search, StringComparison.OrdinalIgnoreCase) &&
158+
(!x.IsGroup || Mode != SelectionMode.Leaf));
159+
144160
if (item != null)
145161
{
146162
index = Items.IndexOf(item);

src/Spectre.Console/Prompts/MultiSelectionPrompt.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ public async Task<List<T>> ShowAsync(IAnsiConsole console, CancellationToken can
9494
{
9595
// Create the list prompt
9696
var prompt = new ListPrompt<T>(console, this);
97-
var result = await prompt.Show(Tree, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
97+
var converter = Converter ?? TypeConverterHelper.ConvertToString;
98+
var result = await prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
9899

99100
if (Mode == SelectionMode.Leaf)
100101
{

src/Spectre.Console/Prompts/SelectionPrompt.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellat
9999
{
100100
// Create the list prompt
101101
var prompt = new ListPrompt<T>(console, this);
102-
var result = await prompt.Show(_tree, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
102+
var converter = Converter ?? TypeConverterHelper.ConvertToString;
103+
var result = await prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
103104

104105
// Return the selected item
105106
return result.Items[result.Index].Data;

src/Tests/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ namespace Spectre.Console.Tests.Unit;
33
public sealed class ListPromptStateTests
44
{
55
private ListPromptState<string> CreateListPromptState(int count, int pageSize, bool shouldWrap, bool searchEnabled)
6-
=> new(Enumerable.Range(0, count).Select(i => new ListPromptItem<string>(i.ToString())).ToList(), pageSize, shouldWrap, SelectionMode.Independent, true, searchEnabled);
6+
=> new(
7+
Enumerable.Range(0, count).Select(i => new ListPromptItem<string>(i.ToString())).ToList(),
8+
text => text,
9+
pageSize, shouldWrap, SelectionMode.Independent, true, searchEnabled);
710

811
[Fact]
912
public void Should_Have_Start_Index_Zero()

src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,4 +410,45 @@ public Task Uses_the_specified_choices_style()
410410
// Then
411411
return Verifier.Verify(console.Output);
412412
}
413+
414+
[Fact]
415+
public void Should_Search_In_Remapped_Result()
416+
{
417+
// Given
418+
var console = new TestConsole();
419+
console.Profile.Capabilities.Interactive = true;
420+
console.EmitAnsiSequences();
421+
console.Input.PushText("2");
422+
console.Input.PushKey(ConsoleKey.Enter);
423+
424+
var choices = new List<CustomSelectionItem>
425+
{
426+
new(33, "Item 1"),
427+
new(34, "Item 2"),
428+
};
429+
430+
var prompt = new SelectionPrompt<CustomSelectionItem>()
431+
.Title("Select one")
432+
.EnableSearch()
433+
.UseConverter(o => o.Name)
434+
.AddChoices(choices);
435+
436+
// When
437+
var selection = prompt.Show(console);
438+
439+
// Then
440+
selection.ShouldBe(choices[1]);
441+
}
442+
}
443+
444+
file sealed class CustomSelectionItem
445+
{
446+
public int Value { get; }
447+
public string Name { get; }
448+
449+
public CustomSelectionItem(int value, string name)
450+
{
451+
Value = value;
452+
Name = name ?? throw new ArgumentNullException(nameof(name));
453+
}
413454
}

0 commit comments

Comments
 (0)