Skip to content

Add System.CommandLine.StaticCompletions project#2799

Draft
slang25 wants to merge 5 commits intodotnet:mainfrom
slang25:slang25/static-completions
Draft

Add System.CommandLine.StaticCompletions project#2799
slang25 wants to merge 5 commits intodotnet:mainfrom
slang25:slang25/static-completions

Conversation

@slang25
Copy link
Copy Markdown

@slang25 slang25 commented Apr 16, 2026

Summary

Imports the System.CommandLine.StaticCompletions library and its test project from dotnet/sdk into this repo. The library generates static shell completion scripts (bash, zsh, pwsh, fish, nushell) from a System.CommandLine Command tree. In-repo, the System.CommandLine dependency becomes a ProjectReference; tests run against a real build via Verify.Xunit snapshots (replacing the SDK's Helix-specific test harness).

This is step 1 of a 3-step migration: land here first, wait for Maestro to flow a package, then remove the sources from dotnet/sdk.

Test plan

  • CI builds the full solution without warnings
  • dotnet test src/System.CommandLine.StaticCompletions.Tests passes all 15 snapshot tests

slang25 added 2 commits April 17, 2026 00:46
Imports the StaticCompletions library and test project from dotnet/sdk,
converting the System.CommandLine dependency from a NuGet PackageReference
to a ProjectReference. Tests run against a real System.CommandLine build
via Verify.Xunit snapshots.
@slang25
Copy link
Copy Markdown
Author

slang25 commented Apr 17, 2026

@baronfel what do you think to this, does this need a bit more coordination?

Copy link
Copy Markdown
Member

@baronfel baronfel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass on some things I suspected would need changing before making part of this repo.

Comment on lines +151 to +155
/// <summary>
/// Generates a call to <code>dotnet complete &lt;string&gt; --position &lt;int&gt;</code> for dynamic completions where necessary, but in a more generic way
/// </summary>
/// <returns></returns>
/// <remarks>TODO: this is currently bound to the .NET CLI's 'dotnet complete' command - this should be definable/injectable per-host instead.</remarks>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <summary>
/// Generates a call to <code>dotnet complete &lt;string&gt; --position &lt;int&gt;</code> for dynamic completions where necessary, but in a more generic way
/// </summary>
/// <returns></returns>
/// <remarks>TODO: this is currently bound to the .NET CLI's 'dotnet complete' command - this should be definable/injectable per-host instead.</remarks>
/// <summary>
/// Generates a call to <code>&lt;appname&gt; complete &lt;string&gt; --position &lt;int&gt;</code> for dynamic completions where necessary - for example when completing NuGet package identifiers.
/// </summary>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel it should use a directive rather than a subcommand.


private static readonly string _dynamicCompletionScript =
"""
# fish parameter completion for the dotnet CLI
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have an open PR for fish completions in the SDK right now that would need syncing here.

Comment on lines +225 to +235
/// <summary>
/// Generate a call into `dotnet complete` for dynamic argument completions, then binds the returned values as CompletionResults.
/// </summary>
/// <remarks>TODO: this is currently bound to the .NET CLI's 'dotnet complete' command - this should be definable/injectable per-host instead.</remarks>
private static void GenerateDynamicCompletionsCall(IndentedTextWriter writer)
{
writer.WriteLine("$text = $commandAst.ToString()");
writer.WriteLine("$dotnetCompleteResults = @(dotnet complete --position $cursorPosition \"$text\") | Where-Object { $_ -NotMatch \"^-|^/\" }");
writer.WriteLine("$dynamicCompletions = $dotnetCompleteResults | Foreach-Object { [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_) }");
writer.WriteLine("$completions += $dynamicCompletions");
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be changed to not rely on dotnet.

writer.Indent++;
writer.WriteLine("local completions=()");
// TODO: we're directly calling dotnet complete here - we need something pluggable.
writer.WriteLine("local result=$(dotnet complete -- \"${original_args[@]}\")");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to change this invocation to not rely on dotnet.

Replaces the hardcoded `dotnet complete` invocation in the generated
bash, zsh, pwsh, fish, and nushell scripts with a call to the
application's own `[suggest]` directive (built into System.CommandLine).
Makes the generated scripts work for any System.CommandLine host without
requiring a separate `complete` subcommand or the dotnet CLI on PATH.
@baronfel
Copy link
Copy Markdown
Member

@slang25 I wouldn't use the suggest directive for this, and I'm not sure about using directives at all for the feature - the suggest directive requires that the tool support the directive, and have dotnet-suggest installed, which will almost certainly not be the case for AOT tools meant for non .NET audiences. The most resilient thing is for the app itself to also implement completion.

@jonsequitur
Copy link
Copy Markdown
Contributor

jonsequitur commented Apr 17, 2026

Agreed that following the design of dotnet-suggest or being compatible with it isn't a goal. Why not use a directive though?

slang25 added 2 commits April 17, 2026 20:04
Adds CompletionInvocation, a strategy object on each shell provider that
controls how generated scripts call back into the application for dynamic
completions. Default remains the built-in [suggest] directive; tool
authors can opt into CompletionInvocation.Subcommand("complete") if they
prefer a subcommand — useful for tools targeting non-.NET audiences or
where a directive isn't wanted.
Updates XML docs on IsDynamic and internal comments in the bash and zsh
providers that still described the SDK-era behaviour (calling into the
dotnet process) or used the dotnet CLI as a load-bearing example. The
emitted scripts themselves already call the application binary directly
and never reference dotnet.
/// Describes how generated shell completion scripts invoke the target application
/// to resolve dynamic completions at runtime.
/// </summary>
public abstract record CompletionInvocation
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking on the strategy approach here - it has me thinking about the 'protocol' of each of these modes.

  • dotnet suggest has a known calling 'API' expressed by its CLI grammar
  • 'Subcommand' would be defining a new calling protocol that happens to match what the dotnet CLI has today. Is that API sufficient for a more general library? I suspect not because it only provides the name/label of the completion item today - for example NuGet package identifier completions don't provide any deeper integration into the various shell providers.
  • would these protocols need to be extensible by consumers of this library? If so, how would the shell providers in this library know about the new providers? It feels coupled today in a way that is hard to eject out of from a consumer's point of view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants