Skip to content

Commit e517e7e

Browse files
[dotnet-watch] Use Spectre.Console for target framework selection
Replace the basic numeric-key console prompt with Spectre.Console's SelectionPrompt for target framework selection in dotnet-watch. This matches the dotnet-run behavior and provides arrow key navigation, search, and pagination. - Add Spectre.Console dependency to Watch library - Add Spectre.Console.Testing for unit tests - Accept IAnsiConsole in TargetFrameworkSelectionPrompt for testability - Use ShowAsync with CancellationToken for cancellation support - Localize prompt strings in Resources.resx - Rewrite tests to drive the prompt via TestConsole key presses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0e67373 commit e517e7e

21 files changed

+302
-39
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
<PackageVersion Include="runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
118118
<PackageVersion Include="runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
119119
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
120+
<PackageVersion Include="Spectre.Console.Testing" Version="0.54.0" />
120121
<PackageVersion Include="StyleCop.Analyzers" Version="$(StyleCopAnalyzersPackageVersion)" />
121122
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
122123
<PackageVersion Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />

src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal sealed class HotReloadDotNetWatcher
2525

2626
internal Task? Test_FileChangesCompletedTask { get; set; }
2727

28-
public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRuntimeProcessLauncherFactory? runtimeProcessLauncherFactory)
28+
public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRuntimeProcessLauncherFactory? runtimeProcessLauncherFactory, TargetFrameworkSelectionPrompt? targetFrameworkSelectionPrompt = null)
2929
{
3030
_context = context;
3131
_console = console;
@@ -41,7 +41,7 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun
4141
}
4242

4343
_rudeEditRestartPrompt = new RestartPrompt(context.Logger, consoleInput, noPrompt ? true : null);
44-
_targetFrameworkSelectionPrompt = new TargetFrameworkSelectionPrompt(consoleInput);
44+
_targetFrameworkSelectionPrompt = targetFrameworkSelectionPrompt;
4545
}
4646

4747
_designTimeBuildGraphFactory = new ProjectGraphFactory(

src/Dotnet.Watch/Watch/Microsoft.DotNet.HotReload.Watch.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" />
3333
<PackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.HotReload" />
3434
<PackageReference Include="Microsoft.DotNet.ProjectTools" Condition="'$(ProjectToolsProjectDir)' == ''" />
35+
<PackageReference Include="Spectre.Console" />
3536
</ItemGroup>
3637

3738
<ItemGroup>
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Spectre.Console;
5+
46
namespace Microsoft.DotNet.Watch;
57

6-
internal sealed class TargetFrameworkSelectionPrompt(ConsoleInputReader inputReader)
8+
internal sealed class TargetFrameworkSelectionPrompt(string title, string moreChoicesText, string searchPlaceholderText, IAnsiConsole? console = null)
79
{
810
public IReadOnlyList<string>? PreviousTargetFrameworks { get; set; }
911
public string? PreviousSelection { get; set; }
1012

1113
public async ValueTask<string> SelectAsync(IReadOnlyList<string> targetFrameworks, CancellationToken cancellationToken)
1214
{
13-
var orderedTargetFrameworks = targetFrameworks.Order().ToArray();
15+
var orderedTargetFrameworks = targetFrameworks.Order(StringComparer.OrdinalIgnoreCase).ToArray();
1416

1517
if (PreviousSelection != null && PreviousTargetFrameworks?.SequenceEqual(orderedTargetFrameworks, StringComparer.OrdinalIgnoreCase) == true)
1618
{
@@ -19,21 +21,15 @@ public async ValueTask<string> SelectAsync(IReadOnlyList<string> targetFramework
1921

2022
PreviousTargetFrameworks = orderedTargetFrameworks;
2123

22-
var keyInfo = await inputReader.GetKeyAsync(
23-
$"Select target framework:{Environment.NewLine}{string.Join(Environment.NewLine, targetFrameworks.Select((tfm, i) => $"{i + 1}) {tfm}"))}",
24-
AcceptKey,
25-
cancellationToken);
26-
27-
_ = TryGetIndex(keyInfo, out var index);
28-
return PreviousSelection = targetFrameworks[index];
29-
30-
bool TryGetIndex(ConsoleKeyInfo info, out int index)
31-
{
32-
index = info.KeyChar - '1';
33-
return index >= 0 && index < targetFrameworks.Count;
34-
}
24+
var prompt = new SelectionPrompt<string>()
25+
.Title($"[cyan]{Markup.Escape(title)}[/]")
26+
.PageSize(10)
27+
.MoreChoicesText($"[gray]({Markup.Escape(moreChoicesText)})[/]")
28+
.AddChoices(targetFrameworks)
29+
.EnableSearch()
30+
.SearchPlaceholderText(searchPlaceholderText);
3531

36-
bool AcceptKey(ConsoleKeyInfo info)
37-
=> info is { Modifiers: ConsoleModifiers.None } && TryGetIndex(info, out _);
32+
PreviousSelection = await prompt.ShowAsync(console ?? AnsiConsole.Console, cancellationToken);
33+
return PreviousSelection;
3834
}
3935
}

src/Dotnet.Watch/dotnet-watch/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ internal async Task<int> RunAsync()
265265

266266
if (IsHotReloadEnabled())
267267
{
268-
var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null);
268+
var tfmPrompt = context.Options.NonInteractive ? null
269+
: new TargetFrameworkSelectionPrompt(Resources.SelectTargetFrameworkPrompt, Resources.MoreFrameworksText, Resources.SearchPlaceholderText);
270+
var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null, tfmPrompt);
269271
await watcher.WatchAsync(shutdownHandler.CancellationToken);
270272
}
271273
else if (mainProjectOptions.Representation.EntryPointFilePath != null)

src/Dotnet.Watch/dotnet-watch/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@
138138
<value>Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project.</value>
139139
<comment>{Locked="-p"}{Locked="--project"}</comment>
140140
</data>
141+
<data name="SelectTargetFrameworkPrompt" xml:space="preserve">
142+
<value>Select the target framework to run:</value>
143+
</data>
144+
<data name="MoreFrameworksText" xml:space="preserve">
145+
<value>Move up and down to reveal more frameworks</value>
146+
</data>
147+
<data name="SearchPlaceholderText" xml:space="preserve">
148+
<value>Type to search</value>
149+
</data>
141150
<data name="Help_NonInteractive" xml:space="preserve">
142151
<value>
143152
Runs dotnet-watch in non-interactive mode. This option is only supported when running with Hot Reload enabled.

src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)