Skip to content

Interactive Tab completion never invokes WithCompletion value providers while typing a parameter value #45

Description

@hiadamhere

Description

A route parameter with a registered completion provider (WithCompletion("name", ...)) never has that provider invoked by the interactive autocomplete engine while the user is typing the value. Pressing Tab on a partial value shows only the parameter's live hint, never the provider's candidates.

The provider is reachable through the complete ambient command and the shell-completion bridge, which is why the gap is easy to miss — but the in-REPL Tab experience the provider exists for never fires.

Repro steps

var app = ReplApp.Create().UseDefaultInteractive();
app.Map("contact inspect {clientId}", (string clientId) => clientId)
   .WithCompletion("clientId", (_, input, _) =>
       ValueTask.FromResult<IReadOnlyList<string>>([$"{input}001", $"{input}002"]));
return app.Run(args);

In an interactive session:

> contact inspect ab + Tab
Param clientId                     # live hint only — provider is NOT called; no ab001 / ab002

> contact inspect  + Tab           # empty value position
Param clientId                     # provider still not called

> contact inspect ab <space> + Tab # value already complete, cursor on the NEXT token
ab001, ab002                       # provider fires here — one token too late, on an argument
                                   # position that can no longer bind to clientId

The complete command against the same app returns candidates correctly, confirming the provider itself is fine:

> complete contact inspect --target clientId --input ab
ab001
ab002

Expected vs actual

  • Expected: typing contact inspect ab + Tab suggests ab001 / ab002 (the provider's candidates for the value being typed), the same way command tokens are completed.
  • Actual: the provider is only invoked once the value token is complete and has been absorbed into the command prefix — i.e. when the cursor has moved to the next token — so its candidates apply to a position that cannot bind to the parameter. While actually typing the value, nothing is suggested.

Root cause

AutocompleteEngine.CollectDynamicAutocompleteCandidatesAsync (src/Repl.Core/Autocomplete/AutocompleteEngine.cs; identical on release/0.10.0 and main at time of writing) gates on the route being a completed terminal match:

var exactRoute = matchingRoutes.FirstOrDefault(route =>
    route.Template.Segments.Count == commandPrefix.Length
    && MatchesTemplatePrefix(route.Template, commandPrefix, prefixComparison, parsingOptions));
if (exactRoute is null || exactRoute.Command.Completions.Count != 1)
{
    return [];
}
var completion = exactRoute.Command.Completions.Values.Single();

While the value is being typed, that token is not part of commandPrefix (see AnalyzeAutocompleteInput — the current token is held separately as currentTokenPrefix). So for contact inspect {clientId} (3 segments), commandPrefix.Length is 2 while typing the value, and Segments.Count == commandPrefix.Length is 3 == 2 → false → the provider is skipped. It only passes the check once the value is tokenized into the prefix (length 3), which is one token too late.

Two secondary issues in the same block:

  • Completions.Count != 1 / .Values.Single() means the provider only fires when a command has exactly one completion registered, and it ignores which parameter the completion targets — so multi-parameter commands complete the wrong argument (or nothing).

Suggested fix

Target the dynamic segment at the current token index (commandPrefix.Length) and look the provider up by that segment's name:

var segmentIndex = commandPrefix.Length;
CompletionDelegate? completion = null;
foreach (var route in matchingRoutes)
{
    if (segmentIndex >= route.Template.Segments.Count
        || route.Template.Segments[segmentIndex] is not DynamicRouteSegment dynamic
        || !MatchesTemplatePrefix(route.Template, commandPrefix, prefixComparison, parsingOptions)
        || !route.Command.Completions.TryGetValue(dynamic.Name, out completion))
    {
        completion = null;
        continue;
    }

    break;
}

if (completion is null)
{
    return [];
}

This fires the provider while the value is being typed, stops firing on the following (non-bindable) token, and resolves the correct provider by parameter name so multi-parameter commands work.

Verification

Implemented against release/0.10.0 and validated:

  • Added a keystroke-driven regression test using the repo's own TerminalHarness / FakeKeyReader (drives the real ConsoleLineReader menu). It types a parameter value and asserts the Tab menu renders the provider's candidates. Fails on unpatched release/0.10.0, passes with the fix.
  • Full Repl.Tests suite: 289/289 pass with the fix applied (all existing Autocomplete / Completion / Menu / LiveHint guards included). No regressions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions