Skip to content

Commit

Permalink
Merge pull request #57 from ChrisPulman/UpdatePrompts
Browse files Browse the repository at this point in the history
Update Prompts
  • Loading branch information
ChrisPulman committed Mar 6, 2024
2 parents 6445a0a + fa6604a commit 003445b
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 67 deletions.
104 changes: 98 additions & 6 deletions src/Spectre.Console.Rx/Spectre.Console/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Highlights the first text match in provided value.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="searchText">Text to search for.</param>
/// <param name="highlightStyle">The style to apply to the matched text.</param>
/// <returns>Markup of input with the first matched text highlighted.</returns>
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)
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source, int count)
}

public static int IndexOf<T>(this IEnumerable<T> source, T item)
where T : class
{
var index = 0;
foreach (var candidate in source)
{
if (candidate == item)
if (Equals(candidate, item))
{
return index;
}
Expand Down Expand Up @@ -89,9 +88,9 @@ public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate<
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ internal interface IListPromptStrategy<T>
/// <param name="scrollable">Whether or not the list is scrollable.</param>
/// <param name="cursorIndex">The cursor index.</param>
/// <param name="items">The visible items.</param>
/// <param name="skipUnselectableItems">A value indicating whether or not the prompt should skip unselectable items.</param>
/// <param name="searchText">The search text.</param>
/// <returns>A <see cref="IRenderable"/> representing the items.</returns>
public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items);
}
public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items, bool skipUnselectableItems, string searchText);
}
30 changes: 21 additions & 9 deletions src/Spectre.Console.Rx/Spectre.Console/Prompts/List/ListPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@

namespace Spectre.Console.Rx;

internal sealed class ListPrompt<T>(IAnsiConsole console, IListPromptStrategy<T> strategy)
internal sealed class ListPrompt<T>
where T : notnull
{
private readonly IAnsiConsole _console = console ?? throw new ArgumentNullException(nameof(console));
private readonly IListPromptStrategy<T> _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
private readonly IAnsiConsole _console;
private readonly IListPromptStrategy<T> _strategy;

public ListPrompt(IAnsiConsole console, IListPromptStrategy<T> strategy)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
}

public async Task<ListPromptState<T>> Show(
ListPromptTree<T> 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)
{
Expand All @@ -35,7 +44,7 @@ internal sealed class ListPrompt<T>(IAnsiConsole console, IListPromptStrategy<T>
}

var nodes = tree.Traverse().ToList();
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround);
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state));

using (new RenderHookScope(_console, hook))
Expand All @@ -45,6 +54,7 @@ internal sealed class ListPrompt<T>(IAnsiConsole console, IListPromptStrategy<T>

while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
Expand All @@ -58,7 +68,7 @@ internal sealed class ListPrompt<T>(IAnsiConsole console, IListPromptStrategy<T>
break;
}

if (state.Update(key.Key) || result == ListPromptInputResult.Refresh)
if (state.Update(key) || result == ListPromptInputResult.Refresh)
{
hook.Refresh();
}
Expand Down Expand Up @@ -107,6 +117,8 @@ private IRenderable BuildRenderable(ListPromptState<T> state)
scrollable,
cursorIndex,
state.Items.Skip(skip).Take(take)
.Select((node, index) => (index, node)));
.Select((node, index) => (index, node)),
state.SkipUnselectableItems,
state.SearchText);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ internal static class ListPromptConstants
public const string GroupSelectedCheckbox = "[[[grey]X[/]]]";
public const string InstructionsMarkup = "[grey](Press <space> to select, <enter> to accept)[/]";
public const string MoreChoicesMarkup = "[grey](Move up and down to reveal more choices)[/]";
public const string SearchPlaceholderMarkup = "[grey](Type to search)[/]";
}
Loading

0 comments on commit 003445b

Please sign in to comment.