mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-04 02:25:28 +08:00 
			
		
		
		
	Add selection orompt Search (#1289)
* Add selection prompt search as you type * Fix small bug * Simplify * Simplify * Remove spacebar as a selection prompt submit key * Trigger CI * Update src/Spectre.Console/Prompts/SelectionPrompt.cs Co-authored-by: Martin Costello <martin@martincostello.com> * Simplifty Mask method * Handle multi-selection prompt better * Update API naming * Address feedback * Add some tests * Remove whitespace * Improve search and highlighting * Add test case for previous issue * Add extra test case * Make prompt searchable --------- Co-authored-by: Martin Costello <martin@martincostello.com> Co-authored-by: Patrik Svensson <patrik@patriksvensson.se>
This commit is contained in:
		@@ -0,0 +1,15 @@
 | 
			
		||||
namespace Spectre.Console.Tests;
 | 
			
		||||
 | 
			
		||||
public static class ConsoleKeyExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static ConsoleKeyInfo ToConsoleKeyInfo(this ConsoleKey key)
 | 
			
		||||
    {
 | 
			
		||||
        var ch = (char)key;
 | 
			
		||||
        if (char.IsControl(ch))
 | 
			
		||||
        {
 | 
			
		||||
            ch = '\0';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new ConsoleKeyInfo(ch, key, false, false, false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								test/Spectre.Console.Tests/Unit/HighlightTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								test/Spectre.Console.Tests/Unit/HighlightTests.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
using Spectre.Console;
 | 
			
		||||
 | 
			
		||||
namespace Namespace;
 | 
			
		||||
 | 
			
		||||
public class HighlightTests
 | 
			
		||||
{
 | 
			
		||||
    private readonly Style _highlightStyle = new Style(foreground: Color.Default, background: Color.Yellow, Decoration.Bold);
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Return_Same_Value_When_SearchText_Is_Empty()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var value = "Sample text";
 | 
			
		||||
        var searchText = string.Empty;
 | 
			
		||||
        var highlightStyle = new Style();
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var result = value.Highlight(searchText, highlightStyle);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        result.ShouldBe(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Highlight_Matched_Text()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var value = "Sample text with test word";
 | 
			
		||||
        var searchText = "test";
 | 
			
		||||
        var highlightStyle = _highlightStyle;
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var result = value.Highlight(searchText, highlightStyle);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        result.ShouldBe("Sample text with [bold on yellow]test[/] word");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Not_Match_Text_Across_Tokens()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var value = "[red]Sample text[/] with test word";
 | 
			
		||||
        var searchText = "text with";
 | 
			
		||||
        var highlightStyle = _highlightStyle;
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var result = value.Highlight(searchText, highlightStyle);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        result.ShouldBe(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Highlight_Only_First_Matched_Text()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var value = "Sample text with test word";
 | 
			
		||||
        var searchText = "te";
 | 
			
		||||
        var highlightStyle = _highlightStyle;
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var result = value.Highlight(searchText, highlightStyle);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        result.ShouldBe("Sample [bold on yellow]te[/]xt with test word");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Not_Match_Text_Outside_Of_Text_Tokens()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var value = "[red]Sample text with test word[/]";
 | 
			
		||||
        var searchText = "red";
 | 
			
		||||
        var highlightStyle = _highlightStyle;
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var result = value.Highlight(searchText, highlightStyle);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        result.ShouldBe(value);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,14 +2,14 @@ namespace Spectre.Console.Tests.Unit;
 | 
			
		||||
 | 
			
		||||
public sealed class ListPromptStateTests
 | 
			
		||||
{
 | 
			
		||||
    private ListPromptState<string> CreateListPromptState(int count, int pageSize, bool shouldWrap)
 | 
			
		||||
        => new(Enumerable.Repeat(new ListPromptItem<string>(string.Empty), count).ToList(), pageSize, shouldWrap);
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Have_Start_Index_Zero()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, false);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, false, false);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        /* noop */
 | 
			
		||||
@@ -24,11 +24,11 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Increase_Index(bool wrap)
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, wrap);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, wrap, false);
 | 
			
		||||
        var index = state.Index;
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow);
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(index + 1);
 | 
			
		||||
@@ -40,10 +40,10 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Go_To_End(bool wrap)
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, wrap);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, wrap, false);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.End);
 | 
			
		||||
        state.Update(ConsoleKey.End.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(99);
 | 
			
		||||
@@ -53,11 +53,11 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Clamp_Index_If_No_Wrap()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, false);
 | 
			
		||||
        state.Update(ConsoleKey.End);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, false, false);
 | 
			
		||||
        state.Update(ConsoleKey.End.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow);
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(99);
 | 
			
		||||
@@ -67,11 +67,11 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Wrap_Index_If_Wrap()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, true);
 | 
			
		||||
        state.Update(ConsoleKey.End);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, true, false);
 | 
			
		||||
        state.Update(ConsoleKey.End.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow);
 | 
			
		||||
        state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(0);
 | 
			
		||||
@@ -81,10 +81,10 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Wrap_Index_If_Wrap_And_Down()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(100, 10, true);
 | 
			
		||||
        var state = CreateListPromptState(100, 10, true, false);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.UpArrow);
 | 
			
		||||
        state.Update(ConsoleKey.UpArrow.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(99);
 | 
			
		||||
@@ -94,10 +94,10 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Wrap_Index_If_Wrap_And_Page_Up()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true);
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true, false);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.PageUp);
 | 
			
		||||
        state.Update(ConsoleKey.PageUp.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(0);
 | 
			
		||||
@@ -107,14 +107,41 @@ public sealed class ListPromptStateTests
 | 
			
		||||
    public void Should_Wrap_Index_If_Wrap_And_Offset_And_Page_Down()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true);
 | 
			
		||||
        state.Update(ConsoleKey.End);
 | 
			
		||||
        state.Update(ConsoleKey.UpArrow);
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true, false);
 | 
			
		||||
        state.Update(ConsoleKey.End.ToConsoleKeyInfo());
 | 
			
		||||
        state.Update(ConsoleKey.UpArrow.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.PageDown);
 | 
			
		||||
        state.Update(ConsoleKey.PageDown.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(8);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Jump_To_First_Matching_Item_When_Searching()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true, true);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.D3.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(3);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Jump_Back_To_First_Item_When_Clearing_Search_Term()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var state = CreateListPromptState(10, 100, true, true);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        state.Update(ConsoleKey.D3.ToConsoleKeyInfo());
 | 
			
		||||
        state.Update(ConsoleKey.Backspace.ToConsoleKeyInfo());
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        state.Index.ShouldBe(0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ namespace Spectre.Console.Tests.Unit;
 | 
			
		||||
 | 
			
		||||
public sealed class SelectionPromptTests
 | 
			
		||||
{
 | 
			
		||||
    private const string ESC = "\u001b";
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Not_Throw_When_Selecting_An_Item_With_Escaped_Markup()
 | 
			
		||||
    {
 | 
			
		||||
@@ -20,4 +22,67 @@ public sealed class SelectionPromptTests
 | 
			
		||||
        // Then
 | 
			
		||||
        console.Output.ShouldContain(@"[red]This text will never be red[/]");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Select_The_First_Leaf_Item()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var console = new TestConsole();
 | 
			
		||||
        console.Profile.Capabilities.Interactive = true;
 | 
			
		||||
        console.Input.PushKey(ConsoleKey.Enter);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var prompt = new SelectionPrompt<string>()
 | 
			
		||||
                .Title("Select one")
 | 
			
		||||
                .Mode(SelectionMode.Leaf)
 | 
			
		||||
                .AddChoiceGroup("Group one", "A", "B")
 | 
			
		||||
                .AddChoiceGroup("Group two", "C", "D");
 | 
			
		||||
        var selection = prompt.Show(console);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        selection.ShouldBe("A");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Select_The_Last_Leaf_Item_When_Wrapping_Around()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var console = new TestConsole();
 | 
			
		||||
        console.Profile.Capabilities.Interactive = true;
 | 
			
		||||
        console.Input.PushKey(ConsoleKey.UpArrow);
 | 
			
		||||
        console.Input.PushKey(ConsoleKey.Enter);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var prompt = new SelectionPrompt<string>()
 | 
			
		||||
            .Title("Select one")
 | 
			
		||||
            .Mode(SelectionMode.Leaf)
 | 
			
		||||
            .WrapAround()
 | 
			
		||||
            .AddChoiceGroup("Group one", "A", "B")
 | 
			
		||||
            .AddChoiceGroup("Group two", "C", "D");
 | 
			
		||||
        var selection = prompt.Show(console);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        selection.ShouldBe("D");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Should_Highlight_Search_Term()
 | 
			
		||||
    {
 | 
			
		||||
        // Given
 | 
			
		||||
        var console = new TestConsole();
 | 
			
		||||
        console.Profile.Capabilities.Interactive = true;
 | 
			
		||||
        console.EmitAnsiSequences();
 | 
			
		||||
        console.Input.PushText("1");
 | 
			
		||||
        console.Input.PushKey(ConsoleKey.Enter);
 | 
			
		||||
 | 
			
		||||
        // When
 | 
			
		||||
        var prompt = new SelectionPrompt<string>()
 | 
			
		||||
            .Title("Select one")
 | 
			
		||||
            .EnableSearch()
 | 
			
		||||
            .AddChoices("Item 1");
 | 
			
		||||
        prompt.Show(console);
 | 
			
		||||
 | 
			
		||||
        // Then
 | 
			
		||||
        console.Output.ShouldContain($"{ESC}[38;5;12m> Item {ESC}[0m{ESC}[1;38;5;12;48;5;11m1{ESC}[0m");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user