Skip to content

Commit

Permalink
Fix search bug in prompt related to custom item types
Browse files Browse the repository at this point in the history
Closes #1626
  • Loading branch information
patriksvensson committed Sep 2, 2024
1 parent 753894d commit fd69ad0
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 7 deletions.
3 changes: 2 additions & 1 deletion src/Spectre.Console/Prompts/List/ListPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down
22 changes: 19 additions & 3 deletions src/Spectre.Console/Prompts/List/ListPromptState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/Spectre.Console/Prompts/MultiSelectionPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Spectre.Console/Prompts/SelectionPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
41 changes: 41 additions & 0 deletions src/Tests/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

0 comments on commit fd69ad0

Please sign in to comment.