Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/Spectre.Console.Tests/Extensions/ConsoleKeyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ public static class ConsoleKeyExtensions
{
public static ConsoleKeyInfo ToConsoleKeyInfo(this ConsoleKey key)
{
var ch = (char)key;
if (char.IsControl(ch))
var ch = key.GetKeyChar();

return new ConsoleKeyInfo(ch, key, false, false, false);
}

private static char GetKeyChar(this ConsoleKey key)
{
if (key is ConsoleKey.UpArrow
or ConsoleKey.DownArrow
or ConsoleKey.LeftArrow
or ConsoleKey.RightArrow
or ConsoleKey.Home
or ConsoleKey.End
or ConsoleKey.PageUp
or ConsoleKey.PageDown
or ConsoleKey.Insert
or ConsoleKey.Delete)
{
ch = '\0';
return '\0';
}

return new ConsoleKeyInfo(ch, key, false, false, false);
var ch = (char)key;
return char.IsControl(ch) ? '\0' : ch;
}
}
112 changes: 112 additions & 0 deletions src/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,116 @@ public void Should_Jump_Back_To_First_Item_When_Clearing_Search_Term()
// Then
state.Index.ShouldBe(0);
}

[Fact]
public void Should_Cycle_To_Next_Match_On_Tab()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: true);

// When
state.Update(ConsoleKey.D1.ToConsoleKeyInfo());
var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeTrue();
state.Index.ShouldBe(10);

state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());
state.Index.ShouldBe(11);
}

[Fact]
public void Should_Wrap_To_First_Match_On_Tab()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: true);

// When
state.Update(ConsoleKey.D1.ToConsoleKeyInfo());

state.Update(ConsoleKey.End.ToConsoleKeyInfo());
state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
state.Index.ShouldBe(1);
}

[Fact]
public void Should_Not_Cycle_When_Search_Disabled()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: false);
var start = state.Index;

// When
var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeFalse();
state.Index.ShouldBe(start);
}

[Fact]
public void Should_Not_Cycle_When_Search_Empty()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: true);
var start = state.Index;

// When
var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeFalse();
state.Index.ShouldBe(start);
}

[Fact]
public void Should_Not_Move_When_No_Matches_For_Search()
{
// Given
var state = CreateListPromptState(10, 10, shouldWrap: true, searchEnabled: true);
state.Update(ConsoleKey.End.ToConsoleKeyInfo());
var indexBefore = state.Index;

// When
state.Update(ConsoleKey.X.ToConsoleKeyInfo());

var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeFalse();
state.Index.ShouldBe(indexBefore);
}

[Fact]
public void Should_Return_True_When_Tab_Changes_Index()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: true);

// When
state.Update(ConsoleKey.D1.ToConsoleKeyInfo());
var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeTrue();
state.Index.ShouldBe(10);
}

[Fact]
public void Should_Return_False_When_Tab_Noop()
{
// Given
var state = CreateListPromptState(20, 10, shouldWrap: true, searchEnabled: true);
var start = state.Index;

// When
var moved = state.Update(ConsoleKey.Tab.ToConsoleKeyInfo());

// Then
moved.ShouldBeFalse();
state.Index.ShouldBe(start);
}
}
25 changes: 25 additions & 0 deletions src/Spectre.Console/Prompts/List/ListPromptState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,31 @@ public bool Update(ConsoleKeyInfo keyInfo)
index = Items.IndexOf(item);
}
}

if (keyInfo.Key == ConsoleKey.Tab && !string.IsNullOrEmpty(SearchText))
{
var matches = Items.Select((item, i) => new { Item = item, Index = i })
.Where(x =>
_converter.Invoke(x.Item.Data).Contains(search, StringComparison.OrdinalIgnoreCase) &&
(!x.Item.IsGroup || Mode != SelectionMode.Leaf))
.Select(x => x.Index)
.ToList();

if (matches.Count > 0)
{
var matchIndex = matches.IndexOf(Index);

if (matchIndex == -1)
{
index = matches[0];
}
else
{
var nextMatchIndex = (matchIndex + 1) % matches.Count;
index = matches[nextMatchIndex];
}
}
}
}

index = WrapAround
Expand Down