diff --git a/src/Spectre.Console.Rx/Spectre.Console/Extensions/StringExtensions.cs b/src/Spectre.Console.Rx/Spectre.Console/Extensions/StringExtensions.cs
index 0c18188..7bd36e9 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Extensions/StringExtensions.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Extensions/StringExtensions.cs
@@ -84,19 +84,106 @@ public static string Mask(this string value, char? mask)
throw new ArgumentNullException(nameof(value));
}
- var output = string.Empty;
-
if (mask is null)
{
- return output;
+ return string.Empty;
+ }
+
+ return new string(mask.Value, value.Length);
+ }
+
+ ///
+ /// Highlights the first text match in provided value.
+ ///
+ /// Input value.
+ /// Text to search for.
+ /// The style to apply to the matched text.
+ /// Markup of input with the first matched text highlighted.
+ internal static string Highlight(this string value, string searchText, Style? highlightStyle)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (searchText is null)
+ {
+ throw new ArgumentNullException(nameof(searchText));
+ }
+
+ if (highlightStyle is null)
+ {
+ throw new ArgumentNullException(nameof(highlightStyle));
+ }
+
+ if (searchText.Length == 0)
+ {
+ return value;
}
- foreach (var c in value)
+ var foundSearchPattern = false;
+ var builder = new StringBuilder();
+ using var tokenizer = new MarkupTokenizer(value);
+ while (tokenizer.MoveNext())
{
- output += mask;
+ var token = tokenizer.Current!;
+
+ switch (token.Kind)
+ {
+ case MarkupTokenKind.Text:
+ {
+ var tokenValue = token.Value;
+ if (tokenValue.Length == 0)
+ {
+ break;
+ }
+
+ if (foundSearchPattern)
+ {
+ builder.Append(tokenValue);
+ break;
+ }
+
+ var index = tokenValue.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
+ if (index == -1)
+ {
+ builder.Append(tokenValue);
+ break;
+ }
+
+ foundSearchPattern = true;
+ var before = tokenValue.Substring(0, index);
+ var match = tokenValue.Substring(index, searchText.Length);
+ var after = tokenValue.Substring(index + searchText.Length);
+
+ builder
+ .Append(before)
+ .AppendWithStyle(highlightStyle, match)
+ .Append(after);
+
+ break;
+ }
+
+ case MarkupTokenKind.Open:
+ {
+ builder.Append("[" + token.Value + "]");
+ break;
+ }
+
+ case MarkupTokenKind.Close:
+ {
+ builder.Append("[/]");
+ break;
+ }
+
+ default:
+ {
+ throw new InvalidOperationException("Unknown markup token kind.");
+ }
+ }
}
- return output;
+ return builder.ToString();
}
internal static string CapitalizeFirstLetter(this string? text, CultureInfo? culture = null)
@@ -202,6 +289,11 @@ internal static string Repeat(this string text, int count)
return string.Concat(Enumerable.Repeat(text, count));
}
+#if NETSTANDARD2_0
+ internal static bool Contains(this string target, string value, StringComparison comparisonType) =>
+ target.IndexOf(value, comparisonType) != -1;
+#endif
+
internal static string ReplaceExact(this string text, string oldValue, string? newValue) =>
#if NETSTANDARD2_0
text.Replace(oldValue, newValue);
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs b/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs
index a3858c0..cc92041 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs
@@ -31,12 +31,11 @@ public static IEnumerable Repeat(this IEnumerable source, int count)
}
public static int IndexOf(this IEnumerable source, T item)
- where T : class
{
var index = 0;
foreach (var candidate in source)
{
- if (candidate == item)
+ if (Equals(candidate, item))
{
return index;
}
@@ -89,9 +88,9 @@ public static void ForEach(this IEnumerable source, Action action)
throw new ArgumentNullException(nameof(source));
}
- return DoEnumeration();
+ return EnumerateValues();
- IEnumerable<(int Index, bool First, bool Last, T Item)> DoEnumeration()
+ IEnumerable<(int Index, bool First, bool Last, T Item)> EnumerateValues()
{
var first = true;
var last = !source.MoveNext();
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/IListPromptStrategy.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/IListPromptStrategy.cs
index c2c04aa..7fab8f7 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/IListPromptStrategy.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/IListPromptStrategy.cs
@@ -34,6 +34,8 @@ internal interface IListPromptStrategy
/// Whether or not the list is scrollable.
/// The cursor index.
/// The visible items.
+ /// A value indicating whether or not the prompt should skip unselectable items.
+ /// The search text.
/// A representing the items.
- public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items);
-}
\ No newline at end of file
+ public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items, bool skipUnselectableItems, string searchText);
+}
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPrompt.cs
index 2575c0c..a33bbfa 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPrompt.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPrompt.cs
@@ -3,17 +3,26 @@
namespace Spectre.Console.Rx;
-internal sealed class ListPrompt(IAnsiConsole console, IListPromptStrategy strategy)
+internal sealed class ListPrompt
where T : notnull
{
- private readonly IAnsiConsole _console = console ?? throw new ArgumentNullException(nameof(console));
- private readonly IListPromptStrategy _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
+ private readonly IAnsiConsole _console;
+ private readonly IListPromptStrategy _strategy;
+
+ public ListPrompt(IAnsiConsole console, IListPromptStrategy strategy)
+ {
+ _console = console ?? throw new ArgumentNullException(nameof(console));
+ _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
+ }
public async Task> Show(
ListPromptTree tree,
- CancellationToken cancellationToken,
- int requestedPageSize = 15,
- bool wrapAround = false)
+ SelectionMode selectionMode,
+ bool skipUnselectableItems,
+ bool searchEnabled,
+ int requestedPageSize,
+ bool wrapAround,
+ CancellationToken cancellationToken = default)
{
if (tree is null)
{
@@ -35,7 +44,7 @@ public async Task> Show(
}
var nodes = tree.Traverse().ToList();
- var state = new ListPromptState(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround);
+ var state = new ListPromptState(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
var hook = new ListPromptRenderHook(_console, () => BuildRenderable(state));
using (new RenderHookScope(_console, hook))
@@ -45,6 +54,7 @@ public async Task> Show(
while (true)
{
+ cancellationToken.ThrowIfCancellationRequested();
var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
@@ -58,7 +68,7 @@ public async Task> Show(
break;
}
- if (state.Update(key.Key) || result == ListPromptInputResult.Refresh)
+ if (state.Update(key) || result == ListPromptInputResult.Refresh)
{
hook.Refresh();
}
@@ -107,6 +117,8 @@ private IRenderable BuildRenderable(ListPromptState state)
scrollable,
cursorIndex,
state.Items.Skip(skip).Take(take)
- .Select((node, index) => (index, node)));
+ .Select((node, index) => (index, node)),
+ state.SkipUnselectableItems,
+ state.SearchText);
}
}
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptConstants.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptConstants.cs
index c9b4dec..679119f 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptConstants.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptConstants.cs
@@ -11,4 +11,5 @@ internal static class ListPromptConstants
public const string GroupSelectedCheckbox = "[[[grey]X[/]]]";
public const string InstructionsMarkup = "[grey](Press to select, to accept)[/]";
public const string MoreChoicesMarkup = "[grey](Move up and down to reveal more choices)[/]";
+ public const string SearchPlaceholderMarkup = "[grey](Type to search)[/]";
}
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptState.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptState.cs
index a8487f9..7a2b654 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptState.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPromptState.cs
@@ -1,42 +1,175 @@
// Copyright (c) Chris Pulman. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Collections.Generic;
+
namespace Spectre.Console.Rx;
-internal sealed class ListPromptState(IReadOnlyList> items, int pageSize, bool wrapAround)
+internal sealed class ListPromptState
where T : notnull
{
+ private readonly IReadOnlyList? _leafIndexes;
+
+ public ListPromptState(IReadOnlyList> items, int pageSize, bool wrapAround, SelectionMode mode, bool skipUnselectableItems, bool searchEnabled)
+ {
+ Items = items;
+ PageSize = pageSize;
+ WrapAround = wrapAround;
+ Mode = mode;
+ SkipUnselectableItems = skipUnselectableItems;
+ SearchEnabled = searchEnabled;
+ SearchText = string.Empty;
+
+ if (SkipUnselectableItems && mode == SelectionMode.Leaf)
+ {
+ _leafIndexes =
+ Items
+ .Select((item, index) => new { item, index })
+ .Where(x => !x.item.IsGroup)
+ .Select(x => x.index)
+ .ToList()
+ .AsReadOnly();
+
+ Index = _leafIndexes[0];
+ }
+ else
+ {
+ Index = 0;
+ }
+ }
+
public int Index { get; private set; }
public int ItemCount => Items.Count;
- public int PageSize { get; } = pageSize;
+ public int PageSize { get; }
+
+ public bool WrapAround { get; }
+
+ public SelectionMode Mode { get; }
- public bool WrapAround { get; } = wrapAround;
+ public bool SkipUnselectableItems { get; }
- public IReadOnlyList> Items { get; } = items;
+ public bool SearchEnabled { get; }
+
+ public IReadOnlyList> Items { get; }
public ListPromptItem Current => Items[Index];
- public bool Update(ConsoleKey key)
+ public string SearchText { get; private set; }
+
+ public bool Update(ConsoleKeyInfo keyInfo)
{
- var index = key switch
+ var index = Index;
+ if (SkipUnselectableItems && Mode == SelectionMode.Leaf)
+ {
+ Debug.Assert(_leafIndexes != null, nameof(_leafIndexes) + " != null");
+ var currentLeafIndex = _leafIndexes.IndexOf(index);
+ switch (keyInfo.Key)
+ {
+ case ConsoleKey.UpArrow:
+ if (currentLeafIndex > 0)
+ {
+ index = _leafIndexes[currentLeafIndex - 1];
+ }
+ else if (WrapAround)
+ {
+ index = _leafIndexes[_leafIndexes.Count - 1];
+ }
+
+ break;
+
+ case ConsoleKey.DownArrow:
+ if (currentLeafIndex < _leafIndexes.Count - 1)
+ {
+ index = _leafIndexes[currentLeafIndex + 1];
+ }
+ else if (WrapAround)
+ {
+ index = _leafIndexes[0];
+ }
+
+ break;
+
+ case ConsoleKey.Home:
+ index = _leafIndexes[0];
+ break;
+
+ case ConsoleKey.End:
+ index = _leafIndexes[_leafIndexes.Count - 1];
+ break;
+
+ case ConsoleKey.PageUp:
+ index = Math.Max(currentLeafIndex - PageSize, 0);
+ if (index < _leafIndexes.Count)
+ {
+ index = _leafIndexes[index];
+ }
+
+ break;
+
+ case ConsoleKey.PageDown:
+ index = Math.Min(currentLeafIndex + PageSize, _leafIndexes.Count - 1);
+ if (index < _leafIndexes.Count)
+ {
+ index = _leafIndexes[index];
+ }
+
+ break;
+ }
+ }
+ else
{
- ConsoleKey.UpArrow => Index - 1,
- ConsoleKey.DownArrow => Index + 1,
- ConsoleKey.Home => 0,
- ConsoleKey.End => ItemCount - 1,
- ConsoleKey.PageUp => Index - PageSize,
- ConsoleKey.PageDown => Index + PageSize,
- _ => Index,
- };
+ index = keyInfo.Key switch
+ {
+ ConsoleKey.UpArrow => Index - 1,
+ ConsoleKey.DownArrow => Index + 1,
+ ConsoleKey.Home => 0,
+ ConsoleKey.End => ItemCount - 1,
+ ConsoleKey.PageUp => Index - PageSize,
+ ConsoleKey.PageDown => Index + PageSize,
+ _ => Index,
+ };
+ }
+
+ var search = SearchText;
+
+ if (SearchEnabled)
+ {
+ // If is text input, append to search filter
+ 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));
+ if (item != null)
+ {
+ index = Items.IndexOf(item);
+ }
+ }
+
+ if (keyInfo.Key == ConsoleKey.Backspace)
+ {
+ if (search.Length > 0)
+ {
+ 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));
+ if (item != null)
+ {
+ index = Items.IndexOf(item);
+ }
+ }
+ }
index = WrapAround
? (ItemCount + (index % ItemCount)) % ItemCount
: index.Clamp(0, ItemCount - 1);
- if (index != Index)
+
+ if (index != Index || SearchText != search)
{
Index = index;
+ SearchText = search;
return true;
}
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/MultiSelectionPrompt.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/MultiSelectionPrompt.cs
index edd21bf..0c1eb5a 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/MultiSelectionPrompt.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/MultiSelectionPrompt.cs
@@ -7,12 +7,21 @@ namespace Spectre.Console.Rx;
/// Represents a multi selection list prompt.
///
/// The prompt result type.
-///
-/// Initializes a new instance of the class.
-///
-public sealed class MultiSelectionPrompt(IEqualityComparer? comparer = null) : IPrompt>, IListPromptStrategy
+public sealed class MultiSelectionPrompt : IPrompt>, IListPromptStrategy
where T : notnull
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The implementation to use when comparing items,
+ /// or null to use the default for the type of the item.
+ ///
+ public MultiSelectionPrompt(IEqualityComparer? comparer = null)
+ {
+ Tree = new ListPromptTree(comparer ?? EqualityComparer.Default);
+ }
+
///
/// Gets or sets the title.
///
@@ -63,7 +72,7 @@ public sealed class MultiSelectionPrompt(IEqualityComparer? comparer = nul
///
public SelectionMode Mode { get; set; } = SelectionMode.Leaf;
- internal ListPromptTree Tree { get; } = new ListPromptTree(comparer ?? EqualityComparer.Default);
+ internal ListPromptTree Tree { get; }
///
/// Adds a choice.
@@ -85,7 +94,7 @@ public async Task> ShowAsync(IAnsiConsole console, CancellationToken can
{
// Create the list prompt
var prompt = new ListPrompt(console, this);
- var result = await prompt.Show(Tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
+ var result = await prompt.Show(Tree, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
if (Mode == SelectionMode.Leaf)
{
@@ -108,7 +117,12 @@ public async Task> ShowAsync(IAnsiConsole console, CancellationToken can
/// The parent items, or an empty list, if the given item has no parents.
public IEnumerable GetParents(T item)
{
- var promptItem = Tree.Find(item) ?? throw new ArgumentOutOfRangeException(nameof(item), "Item not found in tree.");
+ var promptItem = Tree.Find(item);
+ if (promptItem == null)
+ {
+ throw new ArgumentOutOfRangeException(nameof(item), "Item not found in tree.");
+ }
+
var parents = new List>();
while (promptItem.Parent != null)
{
@@ -205,7 +219,7 @@ int IListPromptStrategy.CalculatePageSize(IAnsiConsole console, int totalItem
}
///
- IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items)
+ IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items, bool skipUnselectableItems, string searchText)
{
var list = new List();
var highlightStyle = HighlightStyle ?? Color.Blue;
@@ -223,22 +237,22 @@ IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable,
grid.AddEmptyRow();
}
- foreach (var (index, node) in items)
+ foreach (var item in items)
{
- var current = index == cursorIndex;
+ var current = item.Index == cursorIndex;
var style = current ? highlightStyle : Style.Plain;
- var indent = new string(' ', node.Depth * 2);
- var prompt = index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length);
+ var indent = new string(' ', item.Node.Depth * 2);
+ var prompt = item.Index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length);
- var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(node.Data) ?? node.Data.ToString() ?? "?";
+ var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(item.Node.Data) ?? item.Node.Data.ToString() ?? "?";
if (current)
{
text = text.RemoveMarkup().EscapeMarkup();
}
- var checkbox = node.IsSelected
- ? (node.IsGroup && Mode == SelectionMode.Leaf
+ var checkbox = item.Node.IsSelected
+ ? (item.Node.IsGroup && Mode == SelectionMode.Leaf
? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox)
: ListPromptConstants.Checkbox;
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPrompt.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPrompt.cs
index 6a60fcc..46cb925 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPrompt.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPrompt.cs
@@ -4,9 +4,11 @@
namespace Spectre.Console.Rx;
///
-/// Represents a single list prompt.
+/// SelectionPrompt.
///
-/// The prompt result type.
+/// The type.
+///
+///
public sealed class SelectionPrompt : IPrompt, IListPromptStrategy
where T : notnull
{
@@ -44,6 +46,16 @@ public sealed class SelectionPrompt : IPrompt, IListPromptStrategy
///
public Style? DisabledStyle { get; set; }
+ ///
+ /// Gets or sets the style of highlighted search matches.
+ ///
+ public Style? SearchHighlightStyle { get; set; }
+
+ ///
+ /// Gets or sets the text that will be displayed when no search text has been entered.
+ ///
+ public string? SearchPlaceholderText { get; set; }
+
///
/// Gets or sets the converter to get the display string for a choice. By default
/// the corresponding is used.
@@ -61,6 +73,11 @@ public sealed class SelectionPrompt : IPrompt, IListPromptStrategy
///
public SelectionMode Mode { get; set; } = SelectionMode.Leaf;
+ ///
+ /// Gets or sets a value indicating whether or not search is enabled.
+ ///
+ public bool SearchEnabled { get; set; }
+
///
/// Adds a choice.
///
@@ -81,7 +98,7 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat
{
// Create the list prompt
var prompt = new ListPrompt(console, this);
- var result = await prompt.Show(_tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
+ var result = await prompt.Show(_tree, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
// Return the selected item
return result.Items[result.Index].Data;
@@ -115,11 +132,20 @@ int IListPromptStrategy.CalculatePageSize(IAnsiConsole console, int totalItem
extra += 2;
}
- // Scrolling?
- if (totalItemCount > requestedPageSize)
+ var scrollable = totalItemCount > requestedPageSize;
+ if (SearchEnabled || scrollable)
{
- // The scrolling instructions takes up two rows
- extra += 2;
+ extra++;
+ }
+
+ if (SearchEnabled)
+ {
+ extra++;
+ }
+
+ if (scrollable)
+ {
+ extra++;
}
if (requestedPageSize > console.Profile.Height - extra)
@@ -131,11 +157,12 @@ int IListPromptStrategy.CalculatePageSize(IAnsiConsole console, int totalItem
}
///
- IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items)
+ IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items, bool skipUnselectableItems, string searchText)
{
var list = new List();
var disabledStyle = DisabledStyle ?? Color.Grey;
var highlightStyle = HighlightStyle ?? Color.Blue;
+ var searchHighlightStyle = SearchHighlightStyle ?? new Style(foreground: Color.Default, background: Color.Yellow, Decoration.Bold);
if (Title != null)
{
@@ -150,31 +177,47 @@ IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable,
grid.AddEmptyRow();
}
- foreach (var (index, node) in items)
+ foreach (var item in items)
{
- var current = index == cursorIndex;
- var prompt = index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length);
- var style = node.IsGroup && Mode == SelectionMode.Leaf
+ var current = item.Index == cursorIndex;
+ var prompt = item.Index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length);
+ var style = item.Node.IsGroup && Mode == SelectionMode.Leaf
? disabledStyle
: current ? highlightStyle : Style.Plain;
- var indent = new string(' ', node.Depth * 2);
+ var indent = new string(' ', item.Node.Depth * 2);
- var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(node.Data) ?? node.Data.ToString() ?? "?";
+ var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(item.Node.Data) ?? item.Node.Data.ToString() ?? "?";
if (current)
{
text = text.RemoveMarkup().EscapeMarkup();
}
+ if (searchText.Length > 0 && !(item.Node.IsGroup && Mode == SelectionMode.Leaf))
+ {
+ text = text.Highlight(searchText, searchHighlightStyle);
+ }
+
grid.AddRow(new Markup(indent + prompt + " " + text, style));
}
list.Add(grid);
+ if (SearchEnabled || scrollable)
+ {
+ // Add padding
+ list.Add(Text.Empty);
+ }
+
+ if (SearchEnabled)
+ {
+ list.Add(new Markup(
+ searchText.Length > 0 ? searchText.EscapeMarkup() : SearchPlaceholderText ?? ListPromptConstants.SearchPlaceholderMarkup));
+ }
+
if (scrollable)
{
// (Move up and down to reveal more choices)
- list.Add(Text.Empty);
list.Add(new Markup(MoreChoicesText ?? ListPromptConstants.MoreChoicesMarkup));
}
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPromptExtensions.cs b/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPromptExtensions.cs
index 482f17c..6210493 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPromptExtensions.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Prompts/SelectionPromptExtensions.cs
@@ -205,6 +205,61 @@ public static SelectionPrompt WrapAround(this SelectionPrompt obj, bool
return obj;
}
+ ///
+ /// Enables search for the prompt.
+ ///
+ /// The prompt result type.
+ /// The prompt.
+ /// The same instance so that multiple calls can be chained.
+ public static SelectionPrompt EnableSearch(this SelectionPrompt obj)
+ where T : notnull
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.SearchEnabled = true;
+ return obj;
+ }
+
+ ///
+ /// Disables search for the prompt.
+ ///
+ /// The prompt result type.
+ /// The prompt.
+ /// The same instance so that multiple calls can be chained.
+ public static SelectionPrompt DisableSearch(this SelectionPrompt obj)
+ where T : notnull
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.SearchEnabled = false;
+ return obj;
+ }
+
+ ///
+ /// Sets the text that will be displayed when no search text has been entered.
+ ///
+ /// The prompt result type.
+ /// The prompt.
+ /// The text to display.
+ /// The same instance so that multiple calls can be chained.
+ public static SelectionPrompt SearchPlaceholderText(this SelectionPrompt obj, string? text)
+ where T : notnull
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.SearchPlaceholderText = text;
+ return obj;
+ }
+
///
/// Sets the highlight style of the selected choice.
///
diff --git a/src/Spectre.Console.Rx/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console.Rx/Spectre.Console/Widgets/Paragraph.cs
index a57a17c..4e46813 100644
--- a/src/Spectre.Console.Rx/Spectre.Console/Widgets/Paragraph.cs
+++ b/src/Spectre.Console.Rx/Spectre.Console/Widgets/Paragraph.cs
@@ -7,7 +7,6 @@ namespace Spectre.Console.Rx;
/// A paragraph of text where different parts
/// of the paragraph can have individual styling.
///
-[DebuggerDisplay("{_text,nq}")]
public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
{
private readonly List _lines;