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.
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
completeambient 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
In an interactive session:
The
completecommand against the same app returns candidates correctly, confirming the provider itself is fine:Expected vs actual
contact inspect ab+ Tab suggestsab001/ab002(the provider's candidates for the value being typed), the same way command tokens are completed.Root cause
AutocompleteEngine.CollectDynamicAutocompleteCandidatesAsync(src/Repl.Core/Autocomplete/AutocompleteEngine.cs; identical onrelease/0.10.0andmainat time of writing) gates on the route being a completed terminal match:While the value is being typed, that token is not part of
commandPrefix(seeAnalyzeAutocompleteInput— the current token is held separately ascurrentTokenPrefix). So forcontact inspect {clientId}(3 segments),commandPrefix.Lengthis 2 while typing the value, andSegments.Count == commandPrefix.Lengthis3 == 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: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.0and validated:TerminalHarness/FakeKeyReader(drives the realConsoleLineReadermenu). It types a parameter value and asserts the Tab menu renders the provider's candidates. Fails on unpatchedrelease/0.10.0, passes with the fix.Repl.Testssuite: 289/289 pass with the fix applied (all existing Autocomplete / Completion / Menu / LiveHint guards included). No regressions.