From 1cf30f62fc844c84ad9966284ffcd224e2a26053 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Mon, 21 Dec 2020 03:21:02 +0100 Subject: [PATCH] Add autocomplete for text prompt Closes #166 --- ...d_Auto_Complete_To_Best_Match.verified.txt | 1 + ..._Pressing_Tab_On_Empty_String.verified.txt | 1 + ..._When_Pressing_Tab_On_A_Match.verified.txt | 1 + .../Tools/TestableConsoleInput.cs | 4 + src/Spectre.Console.Tests/Unit/PanelTests.cs | 2 - src/Spectre.Console.Tests/Unit/PromptTests.cs | 77 ++++++++++++++-- .../Extensions/AnsiConsoleExtensions.Input.cs | 65 ++++++++++++-- .../Internal/TypeConverterHelper.cs | 55 ++++++++++++ src/Spectre.Console/Spectre.Console.csproj | 5 +- .../{ => Widgets}/Figlet/FigletCharacter.cs | 0 .../{ => Widgets}/Figlet/FigletFont.cs | 5 +- .../{ => Widgets}/Figlet/FigletFontParser.cs | 0 .../{ => Widgets}/Figlet/FigletHeader.cs | 0 .../{ => Widgets}/Figlet/FigletText.cs | 0 .../{ => Widgets}/Figlet/Fonts/Standard.flf | 0 .../Progress/Columns/PercentageColumn.cs | 0 .../Progress/Columns/ProgressBarColumn.cs | 0 .../Progress/Columns/RemainingTimeColumn.cs | 0 .../Progress/Columns/SpinnerColumn.cs | 0 .../Progress/Columns/TaskDescriptionColumn.cs | 0 .../{ => Widgets}/Progress/Progress.cs | 0 .../{ => Widgets}/Progress/ProgressColumn.cs | 0 .../{ => Widgets}/Progress/ProgressContext.cs | 0 .../Progress/ProgressRefreshThread.cs | 0 .../Progress/ProgressRenderer.cs | 0 .../{ => Widgets}/Progress/ProgressSample.cs | 0 .../{ => Widgets}/Progress/ProgressTask.cs | 0 .../Progress/ProgressTaskSettings.cs | 0 .../Progress/ProgressTaskState.cs | 0 .../Renderers/DefaultProgressRenderer.cs | 0 .../Renderers/FallbackProgressRenderer.cs | 0 .../Renderers/StatusFallbackRenderer.cs | 0 .../Progress/Spinner.Generated.cs | 0 .../{ => Widgets}/Progress/Spinner.cs | 0 .../{ => Widgets}/Progress/Status.cs | 0 .../{ => Widgets}/Progress/StatusContext.cs | 0 .../Widgets/Prompt/DefaultPromptValue.cs | 12 +++ .../{ => Widgets/Prompt}/IPrompt.cs | 0 .../{ => Widgets/Prompt}/TextPrompt.cs | 88 ++----------------- .../Prompt}/TextPromptExtensions.cs | 2 +- .../{ => Widgets/Prompt}/ValidationResult.cs | 4 +- 41 files changed, 218 insertions(+), 104 deletions(-) create mode 100644 src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Best_Match.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match.verified.txt create mode 100644 src/Spectre.Console/Internal/TypeConverterHelper.cs rename src/Spectre.Console/{ => Widgets}/Figlet/FigletCharacter.cs (100%) rename src/Spectre.Console/{ => Widgets}/Figlet/FigletFont.cs (94%) rename src/Spectre.Console/{ => Widgets}/Figlet/FigletFontParser.cs (100%) rename src/Spectre.Console/{ => Widgets}/Figlet/FigletHeader.cs (100%) rename src/Spectre.Console/{ => Widgets}/Figlet/FigletText.cs (100%) rename src/Spectre.Console/{ => Widgets}/Figlet/Fonts/Standard.flf (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Columns/PercentageColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Columns/ProgressBarColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Columns/RemainingTimeColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Columns/SpinnerColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Columns/TaskDescriptionColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Progress.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressColumn.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressContext.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressRefreshThread.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressRenderer.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressSample.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressTask.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressTaskSettings.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/ProgressTaskState.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Renderers/DefaultProgressRenderer.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Renderers/FallbackProgressRenderer.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Renderers/StatusFallbackRenderer.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Spinner.Generated.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Spinner.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/Status.cs (100%) rename src/Spectre.Console/{ => Widgets}/Progress/StatusContext.cs (100%) create mode 100644 src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs rename src/Spectre.Console/{ => Widgets/Prompt}/IPrompt.cs (100%) rename src/Spectre.Console/{ => Widgets/Prompt}/TextPrompt.cs (67%) rename src/Spectre.Console/{Extensions => Widgets/Prompt}/TextPromptExtensions.cs (99%) rename src/Spectre.Console/{ => Widgets/Prompt}/ValidationResult.cs (94%) diff --git a/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Best_Match.verified.txt b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Best_Match.verified.txt new file mode 100644 index 0000000..4986b8a --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Best_Match.verified.txt @@ -0,0 +1 @@ +Favorite fruit? [Banana/Bandana/Orange]: Band    Bandana diff --git a/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String.verified.txt b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String.verified.txt new file mode 100644 index 0000000..675f10f --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String.verified.txt @@ -0,0 +1 @@ +Favorite fruit? [Banana/Orange] (Banana): Banana diff --git a/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match.verified.txt b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match.verified.txt new file mode 100644 index 0000000..39f152e --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match.verified.txt @@ -0,0 +1 @@ +Favorite fruit? [Apple/Banana/Orange]: Apple     Banana diff --git a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs b/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs index cd16bdd..0764be4 100644 --- a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs +++ b/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs @@ -23,7 +23,11 @@ namespace Spectre.Console.Tests { PushCharacter(character); } + } + public void PushTextWithEnter(string input) + { + PushText(input); PushKey(ConsoleKey.Enter); } diff --git a/src/Spectre.Console.Tests/Unit/PanelTests.cs b/src/Spectre.Console.Tests/Unit/PanelTests.cs index ef89490..361f9a9 100644 --- a/src/Spectre.Console.Tests/Unit/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/PanelTests.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; -using Shouldly; using Spectre.Console.Rendering; using VerifyXunit; using Xunit; diff --git a/src/Spectre.Console.Tests/Unit/PromptTests.cs b/src/Spectre.Console.Tests/Unit/PromptTests.cs index 4a5ec8f..acf1790 100644 --- a/src/Spectre.Console.Tests/Unit/PromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/PromptTests.cs @@ -13,8 +13,8 @@ namespace Spectre.Console.Tests.Unit { // Given var console = new PlainConsole(); - console.Input.PushText("ninety-nine"); - console.Input.PushText("99"); + console.Input.PushTextWithEnter("ninety-nine"); + console.Input.PushTextWithEnter("99"); // When console.Prompt(new TextPrompt("Age?")); @@ -46,8 +46,8 @@ namespace Spectre.Console.Tests.Unit { // Given var console = new PlainConsole(); - console.Input.PushText("Apple"); - console.Input.PushText("Banana"); + console.Input.PushTextWithEnter("Apple"); + console.Input.PushTextWithEnter("Banana"); // When console.Prompt( @@ -65,7 +65,7 @@ namespace Spectre.Console.Tests.Unit { // Given var console = new PlainConsole(); - console.Input.PushText("Orange"); + console.Input.PushTextWithEnter("Orange"); // When console.Prompt( @@ -78,15 +78,74 @@ namespace Spectre.Console.Tests.Unit return Verifier.Verify(console.Output); } + [Fact] + public Task Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String() + { + // Given + var console = new PlainConsole(); + console.Input.PushKey(ConsoleKey.Tab); + console.Input.PushKey(ConsoleKey.Enter); + + // When + console.Prompt( + new TextPrompt("Favorite fruit?") + .AddChoice("Banana") + .AddChoice("Orange") + .DefaultValue("Banana")); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + public Task Should_Auto_Complete_To_Best_Match() + { + // Given + var console = new PlainConsole(); + console.Input.PushText("Band"); + console.Input.PushKey(ConsoleKey.Tab); + console.Input.PushKey(ConsoleKey.Enter); + + // When + console.Prompt( + new TextPrompt("Favorite fruit?") + .AddChoice("Banana") + .AddChoice("Bandana") + .AddChoice("Orange")); + + // Then + return Verifier.Verify(console.Output); + } + + [Fact] + public Task Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match() + { + // Given + var console = new PlainConsole(); + console.Input.PushText("Apple"); + console.Input.PushKey(ConsoleKey.Tab); + console.Input.PushKey(ConsoleKey.Enter); + + // When + console.Prompt( + new TextPrompt("Favorite fruit?") + .AddChoice("Apple") + .AddChoice("Banana") + .AddChoice("Orange")); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] public Task Should_Return_Error_If_Custom_Validation_Fails() { // Given var console = new PlainConsole(); - console.Input.PushText("22"); - console.Input.PushText("102"); - console.Input.PushText("ABC"); - console.Input.PushText("99"); + console.Input.PushTextWithEnter("22"); + console.Input.PushTextWithEnter("102"); + console.Input.PushTextWithEnter("ABC"); + console.Input.PushTextWithEnter("99"); // When console.Prompt( diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index d83e367..19ef45e 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; namespace Spectre.Console { @@ -7,7 +10,7 @@ namespace Spectre.Console /// public static partial class AnsiConsoleExtensions { - internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret) + internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable? items = null) { if (console is null) { @@ -15,35 +18,83 @@ namespace Spectre.Console } style ??= Style.Plain; + var text = string.Empty; + + var autocomplete = new List(items ?? Enumerable.Empty()); - var result = string.Empty; while (true) { var key = console.Input.ReadKey(true); if (key.Key == ConsoleKey.Enter) { - return result; + return text; + } + + if (key.Key == ConsoleKey.Tab && autocomplete.Count > 0) + { + var replace = AutoComplete(autocomplete, text); + if (!string.IsNullOrEmpty(replace)) + { + // Render the suggestion + console.Write("\b \b".Repeat(text.Length), style); + console.Write(replace); + text = replace; + continue; + } } if (key.Key == ConsoleKey.Backspace) { - if (result.Length > 0) + if (text.Length > 0) { - result = result.Substring(0, result.Length - 1); + text = text.Substring(0, text.Length - 1); console.Write("\b \b"); } continue; } - result += key.KeyChar.ToString(); - if (!char.IsControl(key.KeyChar)) { + text += key.KeyChar.ToString(); console.Write(secret ? "*" : key.KeyChar.ToString(), style); } } } + + private static string AutoComplete(List autocomplete, string text) + { + var found = autocomplete.Find(i => i == text); + var replace = string.Empty; + + if (found == null) + { + // Get the closest match + var next = autocomplete.Find(i => i.StartsWith(text, true, CultureInfo.InvariantCulture)); + if (next != null) + { + replace = next; + } + else if (string.IsNullOrEmpty(text)) + { + // Use the first item + replace = autocomplete[0]; + } + } + else + { + // Get the next match + var index = autocomplete.IndexOf(found) + 1; + if (index >= autocomplete.Count) + { + index = 0; + } + + replace = autocomplete[index]; + } + + return replace; + } } } diff --git a/src/Spectre.Console/Internal/TypeConverterHelper.cs b/src/Spectre.Console/Internal/TypeConverterHelper.cs new file mode 100644 index 0000000..07d847e --- /dev/null +++ b/src/Spectre.Console/Internal/TypeConverterHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Spectre.Console.Internal +{ + internal static class TypeConverterHelper + { + public static string ConvertToString(T input) + { + return GetTypeConverter().ConvertToInvariantString(input); + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types")] + public static bool TryConvertFromString(string input, [MaybeNull] out T result) + { + try + { + result = (T)GetTypeConverter().ConvertFromInvariantString(input); + return true; + } + catch + { + result = default; + return false; + } + } + + public static TypeConverter GetTypeConverter() + { + var converter = TypeDescriptor.GetConverter(typeof(T)); + if (converter != null) + { + return converter; + } + + var attribute = typeof(T).GetCustomAttribute(); + if (attribute != null) + { + var type = Type.GetType(attribute.ConverterTypeName, false, false); + if (type != null) + { + converter = Activator.CreateInstance(type) as TypeConverter; + if (converter != null) + { + return converter; + } + } + } + + throw new InvalidOperationException("Could not find type converter"); + } + } +} diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index 7a3d999..4db6bb2 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -2,16 +2,17 @@ net5.0;netstandard2.0 + 9.0 enable - + - + diff --git a/src/Spectre.Console/Figlet/FigletCharacter.cs b/src/Spectre.Console/Widgets/Figlet/FigletCharacter.cs similarity index 100% rename from src/Spectre.Console/Figlet/FigletCharacter.cs rename to src/Spectre.Console/Widgets/Figlet/FigletCharacter.cs diff --git a/src/Spectre.Console/Figlet/FigletFont.cs b/src/Spectre.Console/Widgets/Figlet/FigletFont.cs similarity index 94% rename from src/Spectre.Console/Figlet/FigletFont.cs rename to src/Spectre.Console/Widgets/Figlet/FigletFont.cs index bb86cd9..c388cef 100644 --- a/src/Spectre.Console/Figlet/FigletFont.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletFont.cs @@ -10,6 +10,8 @@ namespace Spectre.Console /// public sealed class FigletFont { + private const string StandardFont = "Spectre.Console/Widgets/Figlet/Fonts/Standard.flf"; + private readonly Dictionary _characters; private static readonly Lazy _standard; @@ -40,7 +42,8 @@ namespace Spectre.Console static FigletFont() { - _standard = new Lazy(() => Parse(ResourceReader.ReadManifestData("Spectre.Console/Figlet/Fonts/Standard.flf"))); + _standard = new Lazy(() => Parse( + ResourceReader.ReadManifestData(StandardFont))); } internal FigletFont(IEnumerable characters, FigletHeader header) diff --git a/src/Spectre.Console/Figlet/FigletFontParser.cs b/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs similarity index 100% rename from src/Spectre.Console/Figlet/FigletFontParser.cs rename to src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs diff --git a/src/Spectre.Console/Figlet/FigletHeader.cs b/src/Spectre.Console/Widgets/Figlet/FigletHeader.cs similarity index 100% rename from src/Spectre.Console/Figlet/FigletHeader.cs rename to src/Spectre.Console/Widgets/Figlet/FigletHeader.cs diff --git a/src/Spectre.Console/Figlet/FigletText.cs b/src/Spectre.Console/Widgets/Figlet/FigletText.cs similarity index 100% rename from src/Spectre.Console/Figlet/FigletText.cs rename to src/Spectre.Console/Widgets/Figlet/FigletText.cs diff --git a/src/Spectre.Console/Figlet/Fonts/Standard.flf b/src/Spectre.Console/Widgets/Figlet/Fonts/Standard.flf similarity index 100% rename from src/Spectre.Console/Figlet/Fonts/Standard.flf rename to src/Spectre.Console/Widgets/Figlet/Fonts/Standard.flf diff --git a/src/Spectre.Console/Progress/Columns/PercentageColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/PercentageColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/Columns/PercentageColumn.cs rename to src/Spectre.Console/Widgets/Progress/Columns/PercentageColumn.cs diff --git a/src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/Columns/ProgressBarColumn.cs rename to src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs diff --git a/src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/RemainingTimeColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/Columns/RemainingTimeColumn.cs rename to src/Spectre.Console/Widgets/Progress/Columns/RemainingTimeColumn.cs diff --git a/src/Spectre.Console/Progress/Columns/SpinnerColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/SpinnerColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/Columns/SpinnerColumn.cs rename to src/Spectre.Console/Widgets/Progress/Columns/SpinnerColumn.cs diff --git a/src/Spectre.Console/Progress/Columns/TaskDescriptionColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/TaskDescriptionColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/Columns/TaskDescriptionColumn.cs rename to src/Spectre.Console/Widgets/Progress/Columns/TaskDescriptionColumn.cs diff --git a/src/Spectre.Console/Progress/Progress.cs b/src/Spectre.Console/Widgets/Progress/Progress.cs similarity index 100% rename from src/Spectre.Console/Progress/Progress.cs rename to src/Spectre.Console/Widgets/Progress/Progress.cs diff --git a/src/Spectre.Console/Progress/ProgressColumn.cs b/src/Spectre.Console/Widgets/Progress/ProgressColumn.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressColumn.cs rename to src/Spectre.Console/Widgets/Progress/ProgressColumn.cs diff --git a/src/Spectre.Console/Progress/ProgressContext.cs b/src/Spectre.Console/Widgets/Progress/ProgressContext.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressContext.cs rename to src/Spectre.Console/Widgets/Progress/ProgressContext.cs diff --git a/src/Spectre.Console/Progress/ProgressRefreshThread.cs b/src/Spectre.Console/Widgets/Progress/ProgressRefreshThread.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressRefreshThread.cs rename to src/Spectre.Console/Widgets/Progress/ProgressRefreshThread.cs diff --git a/src/Spectre.Console/Progress/ProgressRenderer.cs b/src/Spectre.Console/Widgets/Progress/ProgressRenderer.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressRenderer.cs rename to src/Spectre.Console/Widgets/Progress/ProgressRenderer.cs diff --git a/src/Spectre.Console/Progress/ProgressSample.cs b/src/Spectre.Console/Widgets/Progress/ProgressSample.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressSample.cs rename to src/Spectre.Console/Widgets/Progress/ProgressSample.cs diff --git a/src/Spectre.Console/Progress/ProgressTask.cs b/src/Spectre.Console/Widgets/Progress/ProgressTask.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressTask.cs rename to src/Spectre.Console/Widgets/Progress/ProgressTask.cs diff --git a/src/Spectre.Console/Progress/ProgressTaskSettings.cs b/src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressTaskSettings.cs rename to src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs diff --git a/src/Spectre.Console/Progress/ProgressTaskState.cs b/src/Spectre.Console/Widgets/Progress/ProgressTaskState.cs similarity index 100% rename from src/Spectre.Console/Progress/ProgressTaskState.cs rename to src/Spectre.Console/Widgets/Progress/ProgressTaskState.cs diff --git a/src/Spectre.Console/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs similarity index 100% rename from src/Spectre.Console/Progress/Renderers/DefaultProgressRenderer.cs rename to src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs diff --git a/src/Spectre.Console/Progress/Renderers/FallbackProgressRenderer.cs b/src/Spectre.Console/Widgets/Progress/Renderers/FallbackProgressRenderer.cs similarity index 100% rename from src/Spectre.Console/Progress/Renderers/FallbackProgressRenderer.cs rename to src/Spectre.Console/Widgets/Progress/Renderers/FallbackProgressRenderer.cs diff --git a/src/Spectre.Console/Progress/Renderers/StatusFallbackRenderer.cs b/src/Spectre.Console/Widgets/Progress/Renderers/StatusFallbackRenderer.cs similarity index 100% rename from src/Spectre.Console/Progress/Renderers/StatusFallbackRenderer.cs rename to src/Spectre.Console/Widgets/Progress/Renderers/StatusFallbackRenderer.cs diff --git a/src/Spectre.Console/Progress/Spinner.Generated.cs b/src/Spectre.Console/Widgets/Progress/Spinner.Generated.cs similarity index 100% rename from src/Spectre.Console/Progress/Spinner.Generated.cs rename to src/Spectre.Console/Widgets/Progress/Spinner.Generated.cs diff --git a/src/Spectre.Console/Progress/Spinner.cs b/src/Spectre.Console/Widgets/Progress/Spinner.cs similarity index 100% rename from src/Spectre.Console/Progress/Spinner.cs rename to src/Spectre.Console/Widgets/Progress/Spinner.cs diff --git a/src/Spectre.Console/Progress/Status.cs b/src/Spectre.Console/Widgets/Progress/Status.cs similarity index 100% rename from src/Spectre.Console/Progress/Status.cs rename to src/Spectre.Console/Widgets/Progress/Status.cs diff --git a/src/Spectre.Console/Progress/StatusContext.cs b/src/Spectre.Console/Widgets/Progress/StatusContext.cs similarity index 100% rename from src/Spectre.Console/Progress/StatusContext.cs rename to src/Spectre.Console/Widgets/Progress/StatusContext.cs diff --git a/src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs b/src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs new file mode 100644 index 0000000..c78b210 --- /dev/null +++ b/src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs @@ -0,0 +1,12 @@ +namespace Spectre.Console +{ + internal sealed class DefaultPromptValue + { + public T Value { get; } + + public DefaultPromptValue(T value) + { + Value = value; + } + } +} diff --git a/src/Spectre.Console/IPrompt.cs b/src/Spectre.Console/Widgets/Prompt/IPrompt.cs similarity index 100% rename from src/Spectre.Console/IPrompt.cs rename to src/Spectre.Console/Widgets/Prompt/IPrompt.cs diff --git a/src/Spectre.Console/TextPrompt.cs b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs similarity index 67% rename from src/Spectre.Console/TextPrompt.cs rename to src/Spectre.Console/Widgets/Prompt/TextPrompt.cs index e50484c..e6cb2ef 100644 --- a/src/Spectre.Console/TextPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Reflection; using System.Text; +using Spectre.Console.Internal; namespace Spectre.Console { @@ -68,27 +67,7 @@ namespace Spectre.Console /// /// Gets or sets the default value. /// - internal DefaultValueContainer? DefaultValue { get; set; } - - /// - /// A nullable container for a default value. - /// - internal sealed class DefaultValueContainer - { - /// - /// Gets the default value. - /// - public T Value { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The default value. - public DefaultValueContainer(T value) - { - Value = value; - } - } + internal DefaultPromptValue? DefaultValue { get; set; } /// /// Initializes a new instance of the class. @@ -116,19 +95,20 @@ namespace Spectre.Console } var promptStyle = PromptStyle ?? Style.Plain; + var choices = Choices.Select(choice => TypeConverterHelper.ConvertToString(choice)); WritePrompt(console); while (true) { - var input = console.ReadLine(promptStyle, IsSecret); + var input = console.ReadLine(promptStyle, IsSecret, choices); // Nothing entered? if (string.IsNullOrWhiteSpace(input)) { if (DefaultValue != null) { - console.Write(TextPrompt.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value), promptStyle); + console.Write(TypeConverterHelper.ConvertToString(DefaultValue.Value), promptStyle); console.WriteLine(); return DefaultValue.Value; } @@ -142,7 +122,7 @@ namespace Spectre.Console console.WriteLine(); // Try convert the value to the expected type. - if (!TextPrompt.TryConvert(input, out var result) || result == null) + if (!TypeConverterHelper.TryConvertFromString(input, out var result) || result == null) { console.MarkupLine(ValidationErrorMessage); WritePrompt(console); @@ -191,7 +171,7 @@ namespace Spectre.Console if (ShowChoices && Choices.Count > 0) { - var choices = string.Join("/", Choices.Select(choice => TextPrompt.GetTypeConverter().ConvertToInvariantString(choice))); + var choices = string.Join("/", Choices.Select(choice => TypeConverterHelper.ConvertToString(choice))); builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices); } @@ -200,7 +180,7 @@ namespace Spectre.Console builder.AppendFormat( CultureInfo.InvariantCulture, " [green]({0})[/]", - TextPrompt.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value)); + TypeConverterHelper.ConvertToString(DefaultValue.Value)); } var markup = builder.ToString().Trim(); @@ -213,58 +193,6 @@ namespace Spectre.Console console.Markup(markup + " "); } - /// - /// Tries to convert the input string to . - /// - /// The input to convert. - /// The result. - /// true if the conversion succeeded, otherwise false. - [SuppressMessage("Design", "CA1031:Do not catch general exception types")] - private static bool TryConvert(string input, [MaybeNull] out T result) - { - try - { - result = (T)TextPrompt.GetTypeConverter().ConvertFromInvariantString(input); - return true; - } - catch - { -#pragma warning disable CS8601 // Possible null reference assignment. - result = default; -#pragma warning restore CS8601 // Possible null reference assignment. - return false; - } - } - - /// - /// Gets the type converter that's used to convert values. - /// - /// The type converter that's used to convert values. - private static TypeConverter GetTypeConverter() - { - var converter = TypeDescriptor.GetConverter(typeof(T)); - if (converter != null) - { - return converter; - } - - var attribute = typeof(T).GetCustomAttribute(); - if (attribute != null) - { - var type = Type.GetType(attribute.ConverterTypeName, false, false); - if (type != null) - { - converter = Activator.CreateInstance(type) as TypeConverter; - if (converter != null) - { - return converter; - } - } - } - - throw new InvalidOperationException("Could not find type converter"); - } - private bool ValidateResult(T value, [NotNullWhen(false)] out string? message) { if (Validator != null) diff --git a/src/Spectre.Console/Extensions/TextPromptExtensions.cs b/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs similarity index 99% rename from src/Spectre.Console/Extensions/TextPromptExtensions.cs rename to src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs index 1cfa1c0..9713ea1 100644 --- a/src/Spectre.Console/Extensions/TextPromptExtensions.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs @@ -177,7 +177,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(obj)); } - obj.DefaultValue = new TextPrompt.DefaultValueContainer(value); + obj.DefaultValue = new DefaultPromptValue(value); return obj; } diff --git a/src/Spectre.Console/ValidationResult.cs b/src/Spectre.Console/Widgets/Prompt/ValidationResult.cs similarity index 94% rename from src/Spectre.Console/ValidationResult.cs rename to src/Spectre.Console/Widgets/Prompt/ValidationResult.cs index d96f413..90d7167 100644 --- a/src/Spectre.Console/ValidationResult.cs +++ b/src/Spectre.Console/Widgets/Prompt/ValidationResult.cs @@ -1,7 +1,7 @@ -namespace Spectre.Console +namespace Spectre.Console { /// - /// Represents a validation result. + /// Represents a prompt validation result. /// public sealed class ValidationResult {