Skip to content

Commit a2037a8

Browse files
Suggest directive - register with DotnetSuggest
and place app in path so it can be found by the shell script. https://learn.microsoft.com/en-us/dotnet/standard/commandline/tab-completion
1 parent c6bad82 commit a2037a8

File tree

17 files changed

+445
-44
lines changed

17 files changed

+445
-44
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.CommandLine.Suggest;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using CommandDotNet.Builders;
7+
using CommandDotNet.DotnetSuggest;
8+
9+
namespace CommandDotNet.Example.Commands;
10+
11+
[Command(Description = "(Un)registers this sample app with `dotnet suggest` to provide auto complete")]
12+
public class DotnetSuggest
13+
{
14+
public void Register(IConsole console, IEnvironment environment)
15+
{
16+
var registered = DotnetTools.EnsureRegisteredWithDotnetSuggest(environment, out var results, console);
17+
var appInfo = AppInfo.Instance;
18+
console.WriteLine(registered
19+
? results is null
20+
? appInfo.IsGlobalTool
21+
? "Already registered. Global tools are registered by default."
22+
: "Already registered"
23+
: $"Succeeded:{Environment.NewLine}{results.ToString(new Indent(depth: 1), skipOutputs: true)}"
24+
: $"Failed:{Environment.NewLine}{results!.ToString(new Indent(depth: 1), skipOutputs: true)}");
25+
}
26+
27+
public async Task Unregister(IConsole console)
28+
{
29+
if (AppInfo.Instance.IsGlobalTool)
30+
{
31+
console.WriteLine("This is a global tool. Global tools are registered by default and cannot be unregistered.");
32+
}
33+
34+
var path = new FileSuggestionRegistration().RegistrationConfigurationFilePath;
35+
var lines = await File.ReadAllLinesAsync(path);
36+
var newLines = lines.Where(l => !l.StartsWith(AppInfo.Instance.FilePath)).ToArray();
37+
38+
if (lines.Length == newLines.Length)
39+
{
40+
console.WriteLine("Not registered with dotnet-suggest");
41+
}
42+
else
43+
{
44+
await File.WriteAllLinesAsync(path, newLines);
45+
console.WriteLine("Unregistered with dotnet-suggest");
46+
}
47+
}
48+
}

CommandDotNet.Example/Examples.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,8 @@ public void StartSession(
5555

5656
[Subcommand]
5757
public Commands.Prompts Prompts { get; set; } = null!;
58+
59+
[Subcommand]
60+
public Commands.DotnetSuggest DotnetSuggest { get; set; } = null!;
5861
}
5962
}

CommandDotNet.Example/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static AppRunner GetAppRunner(NameValueCollection? appConfigSettings = nu
2323
appConfigSettings ??= new NameValueCollection();
2424
return new AppRunner<Examples>(appNameForTests is null ? null : new AppSettings{Help = {UsageAppName = appNameForTests}})
2525
.UseDefaultMiddleware()
26+
.UseSuggestDirective_Experimental()
2627
.UseCommandLogger()
2728
.UseNameCasing(Case.KebabCase)
2829
.UsePrompter()

CommandDotNet.TestTools/TestEnvironment.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using CommandDotNet.Extensions;
56
using CommandDotNet.Tokens;
67

78
namespace CommandDotNet.TestTools
@@ -11,7 +12,7 @@ namespace CommandDotNet.TestTools
1112
public class TestEnvironment : IEnvironment
1213
{
1314
public string[]? CommandLineArgs;
14-
public Dictionary<string, string?> EnvVar = new();
15+
public Dictionary<EnvironmentVariableTarget, Dictionary<string, string?>> EnvVarByTarget = new();
1516
public Action<int>? OnExit;
1617
public Action<(string? message, Exception? exception)>? OnFailFast;
1718
public Func<string, string>? OnExpandEnvironmentVariables;
@@ -56,14 +57,24 @@ OnExpandEnvironmentVariables is null
5657
? Environment.ExpandEnvironmentVariables(name)
5758
: OnExpandEnvironmentVariables(name);
5859

59-
public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget? target = null) =>
60-
EnvVar.GetValueOrDefault(variable);
60+
public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget? target = null) =>
61+
target is not null
62+
? EnvVarByTarget.GetValueOrDefault(target.Value)?.GetValueOrDefault(variable)
63+
: (EnvVarByTarget.GetValueOrDefault(EnvironmentVariableTarget.Process)
64+
?? EnvVarByTarget.GetValueOrDefault(EnvironmentVariableTarget.User)
65+
?? EnvVarByTarget.GetValueOrDefault(EnvironmentVariableTarget.Machine))
66+
?.GetValueOrDefault(variable);
6167

62-
public IDictionary GetEnvironmentVariables() => EnvVar;
68+
public IDictionary GetEnvironmentVariables() => EnvVarByTarget
69+
.SelectMany(d => d.Value)
70+
.ToDictionary(d => d.Key, d => d.Value);
6371

6472
public void SetEnvironmentVariables(string variable, string? value, EnvironmentVariableTarget? target)
6573
{
66-
EnvVar[variable] = value;
74+
target ??= EnvironmentVariableTarget.Process;
75+
var vars = EnvVarByTarget.GetOrAdd(target.Value,
76+
key => new Dictionary<string, string?>());
77+
vars[variable] = value;
6778
}
6879
}
6980
}

CommandDotNet.Tests/FeatureTests/SuggestDirective/DotNetSuggestSync.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.IO;
33
using System.Net.Http;
44
using System.Threading.Tasks;
5+
using FluentAssertions;
56
using Xunit;
67

78
namespace CommandDotNet.Tests.FeatureTests.SuggestDirective;
@@ -16,7 +17,16 @@ public async Task Sync()
1617
var client = new HttpClient();
1718
await SyncFile(client, "DotnetProfileDirectory.cs");
1819
await SyncFile(client, "FileSuggestionRegistration.cs",
19-
f => f.Replace(" : ISuggestionRegistration", ""));
20+
text =>
21+
{
22+
var fileField = "private readonly string _registrationConfigurationFilePath;";
23+
text.Should().Contain(fileField);
24+
text = text.Replace(fileField,
25+
$"{fileField}{Environment.NewLine} " +
26+
"public string RegistrationConfigurationFilePath => _registrationConfigurationFilePath;");
27+
text = text.Replace(" : ISuggestionRegistration", "");
28+
return text;
29+
});
2030
await SyncFile(client, "RegistrationPair.cs");
2131
}
2232

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.CommandLine.Suggest;
4+
using System.IO;
5+
using System.Linq;
6+
using CommandDotNet.Builders;
7+
using CommandDotNet.DotnetSuggest;
8+
using CommandDotNet.TestTools;
9+
using CommandDotNet.TestTools.Scenarios;
10+
using FluentAssertions;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace CommandDotNet.Tests.FeatureTests.SuggestDirective;
15+
16+
public class SuggestDirectiveRegistrationTests
17+
{
18+
private readonly ITestOutputHelper _output;
19+
private readonly string _filePath = Path.Join(nameof(SuggestDirectiveRegistrationTests), "suggest-test.exe");
20+
21+
public SuggestDirectiveRegistrationTests(ITestOutputHelper output)
22+
{
23+
_output = output;
24+
Ambient.Output = output;
25+
}
26+
27+
[Theory]
28+
[InlineData("[suggest]", RegistrationStrategy.None, null, false, false)]
29+
[InlineData("", RegistrationStrategy.None, null, false, false)]
30+
[InlineData("[suggest]", RegistrationStrategy.EnsureOnEveryRun, null, false, false)]
31+
[InlineData("", RegistrationStrategy.EnsureOnEveryRun, null, false, true)]
32+
[InlineData("[suggest]", RegistrationStrategy.UseRegistrationDirective, "sug", false, false)]
33+
[InlineData("", RegistrationStrategy.UseRegistrationDirective, "sug", false, false)]
34+
[InlineData("[sug]", RegistrationStrategy.UseRegistrationDirective, "sug", false, true)]
35+
[InlineData("[sug]", RegistrationStrategy.None, "sug", false, false)]
36+
[InlineData("", RegistrationStrategy.EnsureOnEveryRun, null, true, false)]
37+
[InlineData("[sug]", RegistrationStrategy.UseRegistrationDirective, "sug", true, false)]
38+
public void Suggest_can_register_with_Dotnet_Suggest(string args, RegistrationStrategy strategy, string? directive, bool isGlobal, bool shouldRegister)
39+
{
40+
#region ensure test cases are not misconfigured and summarize a few of the rules
41+
if (strategy == RegistrationStrategy.None)
42+
{
43+
shouldRegister.Should().Be(false, "should never register unless ensureRegisteredWithDotnetSuggest=true");
44+
}
45+
if(args.StartsWith("[suggest]"))
46+
{
47+
shouldRegister.Should().Be(false, "should never register when providing suggestions");
48+
}
49+
if(isGlobal)
50+
{
51+
shouldRegister.Should().Be(false, "should never register when app is global tool");
52+
}
53+
#endregion
54+
55+
var result = new AppRunner<App>()
56+
.UseDefaultMiddleware()
57+
.UseCommandLogger()
58+
.UseSuggestDirective_Experimental(strategy, directive!)
59+
.UseTestEnv(new ())
60+
.Verify(new Scenario
61+
{
62+
When = {Args = args},
63+
Then = { Output = args.StartsWith("[sug") ? null : "lala" }
64+
},
65+
config: TestConfig.Default.Where(a => a.AppInfoOverride = BuildAppInfo(isGlobal)));
66+
67+
result.ExitCode.Should().Be(0);
68+
69+
ConfirmPathEnvVar(shouldRegister, result);
70+
71+
ConfirmRegistration(shouldRegister);
72+
}
73+
74+
private void ConfirmPathEnvVar(bool shouldRegister, AppRunnerResult result)
75+
{
76+
var testEnvironment = (TestEnvironment) result.CommandContext.Environment;
77+
var userEnvVars = testEnvironment.EnvVarByTarget.GetValueOrDefault(EnvironmentVariableTarget.User);
78+
if (shouldRegister)
79+
{
80+
userEnvVars.Should().NotBeNull();
81+
var pathEnvVar = userEnvVars!.GetValueOrDefault("PATH");
82+
pathEnvVar.Should().NotBeNull();
83+
pathEnvVar.Should().Contain(_filePath);
84+
}
85+
else
86+
{
87+
userEnvVars?.GetValueOrDefault("PATH")?.Should().NotContain(_filePath);
88+
}
89+
}
90+
91+
private static void ConfirmRegistration(bool shouldRegister)
92+
{
93+
var path = new FileSuggestionRegistration().RegistrationConfigurationFilePath;
94+
if (File.Exists(path))
95+
{
96+
var lines = File.ReadAllLines(path);
97+
98+
// _output.WriteLine($"contents of {path}");
99+
// lines.ForEach(l => _output.WriteLine(l));
100+
//
101+
// contents of /Users/{user}/.dotnet-suggest-registration.txt
102+
// SuggestDirectiveRegistrationTests/suggest-test.exe
103+
104+
var cleanedLines = lines.Where(l => !l.StartsWith(nameof(SuggestDirectiveRegistrationTests))).ToArray();
105+
File.WriteAllLines(path, cleanedLines);
106+
107+
if (shouldRegister)
108+
{
109+
lines.Length.Should().NotBe(cleanedLines.Length);
110+
}
111+
else
112+
{
113+
lines.Length.Should().Be(cleanedLines.Length);
114+
}
115+
}
116+
}
117+
118+
private AppInfo BuildAppInfo(bool isGlobalTool)
119+
{
120+
return new(
121+
false, false, false, isGlobalTool, GetType().Assembly,
122+
_filePath, Path.GetFileName(_filePath));
123+
}
124+
125+
public class App
126+
{
127+
[DefaultCommand]
128+
public void Do(IConsole console) => console.WriteLine("lala");
129+
}
130+
}

CommandDotNet.Tests/FeatureTests/SuggestDirective/SuggestDirectiveTests.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Threading.Tasks;
4-
using CommandDotNet.DotNetSuggest;
5-
using CommandDotNet.Extensions;
6-
using CommandDotNet.Parsing;
74
using CommandDotNet.Tests.FeatureTests.Suggestions;
85
using CommandDotNet.TestTools.Scenarios;
96
using Xunit;
@@ -14,11 +11,8 @@ namespace CommandDotNet.Tests.FeatureTests.SuggestDirective;
1411

1512
public class SuggestDirectiveTests
1613
{
17-
public SuggestDirectiveTests(ITestOutputHelper output)
18-
{
19-
Ambient.Output = output;
20-
}
21-
14+
public SuggestDirectiveTests(ITestOutputHelper output) => Ambient.Output = output;
15+
2216
/* Test list:
2317
* - operands
2418
* - extra operand
@@ -87,12 +81,12 @@ public void Suggest_works_with_default_middleware()
8781
{
8882
var expected = "--togo/n--version/nClosed/nOpened/nOrder/nReserve";
8983
new AppRunner<DinerApp>()
84+
.UseSuggestDirective_Experimental()
9085
.UseDefaultMiddleware()
9186
.UseCommandLogger()
92-
.UseSuggestDirective_Experimental()
9387
.Verify(new Scenario
9488
{
95-
When = { Args = "[suggest]"},
89+
When = {Args = "[suggest]"},
9690
Then =
9791
{
9892
Output = NewLine == "/n" ? expected : expected.Replace("/n", NewLine)

CommandDotNet/AppRunnerConfigExtensions.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
using CommandDotNet.Builders.ArgumentDefaults;
77
using CommandDotNet.ClassModeling.Definitions;
88
using CommandDotNet.Diagnostics;
9-
using CommandDotNet.DotNetSuggest;
9+
using CommandDotNet.DotnetSuggest;
1010
using CommandDotNet.Execution;
1111
using CommandDotNet.Localization;
12-
using CommandDotNet.Parsing;
1312
using CommandDotNet.Parsing.Typos;
1413
using CommandDotNet.Prompts;
1514
using CommandDotNet.Tokens;
@@ -73,9 +72,19 @@ static void Register(bool exclude, string paramName, Action register)
7372
()=> appRunner.UseTypoSuggestions());
7473
return appRunner;
7574
}
76-
77-
public static AppRunner UseAutoSuggest_Experimental(this AppRunner appRunner) =>
78-
appRunner.UseSuggestDirective_Experimental();
75+
76+
/// <summary>
77+
/// Registers the [suggest] directive, to use in conjunction with System.CommandLine's dotnet-suggest.
78+
/// see https://learn.microsoft.com/en-us/dotnet/standard/commandline/tab-completion
79+
/// </summary>
80+
/// <param name="appRunner"></param>
81+
/// <param name="registrationStrategy">the registration strategy to use</param>
82+
/// <param name="directiveName">must be specified when using <see cref="RegistrationStrategy.UseRegistrationDirective"/></param>
83+
/// <returns></returns>
84+
public static AppRunner UseSuggestDirective_Experimental(this AppRunner appRunner,
85+
RegistrationStrategy registrationStrategy = RegistrationStrategy.None,
86+
string directiveName = "enable-tab-completion") =>
87+
SuggestDirectiveMiddleware.UseSuggestDirective_Experimental(appRunner, registrationStrategy, directiveName);
7988

8089
/// <summary>When an invalid arguments is entered, suggests context based alternatives</summary>
8190
public static AppRunner UseTypoSuggestions(this AppRunner appRunner, int maxSuggestionCount = 5)

CommandDotNet/Builders/AppInfo.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.IO;
44
using System.Reflection;
5+
using CommandDotNet.DotnetSuggest;
56
using CommandDotNet.Extensions;
67
using CommandDotNet.Logging;
78

@@ -135,7 +136,7 @@ private static AppInfo BuildAppInfo()
135136
isSelfContainedExe = isExe = mainModuleFileName.EndsWith($"{entryAssemblyFileNameWithoutExt}.exe");
136137
}
137138

138-
var globalToolsDirectory = DotNetSuggest.DotNetTools.GlobalToolDirectory;
139+
var globalToolsDirectory = DotnetTools.GlobalToolDirectory;
139140
isGlobalTool = globalToolsDirectory is not null
140141
&& mainModuleFilePath!.StartsWith(globalToolsDirectory);
141142
}

CommandDotNet/CommandDotNet.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
<None Remove="CommandDotNet.csproj.DotSettings" />
2929
</ItemGroup>
3030
<ItemGroup>
31-
<Folder Include="DotNetSuggest\System.CommandLine.Suggest" />
31+
<Folder Include="DotnetSuggest\System.CommandLine.Suggest" />
3232
</ItemGroup>
3333
</Project>

0 commit comments

Comments
 (0)