Skip to content

Commit 774fa49

Browse files
authored
Expose earlier rest arguments to completion callbacks. (#96)
Record positional values in CompletionContext.seen-options under the owning rest option's name so a completion callback for a later rest argument can condition on earlier rest values.
1 parent 037dab3 commit 774fa49

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

src/cli.toit

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,11 @@ class CompletionContext:
596596
/**
597597
A map from option name to a list of values that have been provided
598598
for that option so far.
599+
600+
Includes both named options and rest (positional) options. Rest options
601+
are keyed by their name, and their value list preserves the order of
602+
the positionals consumed. This lets a completion callback for a later
603+
rest argument condition its output on earlier rest values.
599604
*/
600605
seen-options/Map
601606

src/completion_.toit

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ complete_ root/Command arguments/List -> CompletionResult_:
9191
arg/string := args-to-process[index]
9292

9393
if past-dashdash:
94-
// After --, everything is a rest argument. Track positional index.
94+
// After --, everything is a rest argument. Track positional index
95+
// and record the value under its owning rest option's name.
96+
rest-option := rest-option-for-index_ current-command positional-index
97+
if rest-option:
98+
(seen-options.get rest-option.name --init=:[]).add arg
9599
positional-index++
96100
continue.repeat
97101

@@ -171,7 +175,12 @@ complete_ root/Command arguments/List -> CompletionResult_:
171175
all-named-options.clear
172176
all-short-options.clear
173177
else:
174-
// It's a positional/rest argument.
178+
// It's a positional/rest argument. Record its value under its
179+
// owning rest option's name so that completion callbacks can
180+
// see earlier rest arguments via context.seen-options.
181+
rest-option := rest-option-for-index_ current-command positional-index
182+
if rest-option:
183+
(seen-options.get rest-option.name --init=:[]).add arg
175184
positional-index++
176185

177186
// Now determine what to complete for the last argument (the word being typed).
@@ -366,6 +375,22 @@ complete-rest_ command/Command seen-options/Map current-word/string --positional
366375

367376
return CompletionResult_ [] --directive=DIRECTIVE-FILE-COMPLETION_
368377

378+
/**
379+
Returns the rest $Option that owns the positional at the given $positional-index
380+
in $command, or null if there is no such rest option.
381+
382+
Mirrors the parser's consumption order: each non-multi rest option consumes
383+
one positional slot, and a multi rest option absorbs all remaining
384+
positionals.
385+
*/
386+
rest-option-for-index_ command/Command positional-index/int -> Option?:
387+
skip := positional-index
388+
command.rest_.do: | option/Option |
389+
if option.is-multi: return option
390+
if skip == 0: return option
391+
skip--
392+
return null
393+
369394
/**
370395
Whether the given $option has meaningful completion support.
371396

tests/completion_test.toit

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ main:
3131
test-rest-positional-index
3232
test-rest-positional-index-after-dashdash
3333
test-rest-multi-not-skipped
34+
test-rest-dependent-completion
35+
test-rest-multi-records-all
36+
test-rest-after-dashdash-recorded
3437
test-short-option-marks-seen
3538
test-short-option-pending-value
3639
test-packed-short-options
@@ -476,6 +479,68 @@ test-rest-multi-not-skipped:
476479
expect (values.contains "a.txt")
477480
expect (values.contains "b.txt")
478481

482+
test-rest-dependent-completion:
483+
// A completion callback for a later rest argument can condition on an
484+
// earlier rest argument by reading context.seen-options.
485+
seen/Map? := null
486+
root := cli.Command "app"
487+
--rest=[
488+
cli.OptionEnum "kind" ["user", "group"] --help="Resource kind.",
489+
cli.Option "name" --help="Resource name."
490+
--completion=:: | context/cli.CompletionContext |
491+
seen = context.seen-options
492+
kind := (context.seen-options.get "kind" --if-absent=: [""]).first
493+
candidates := kind == "user"
494+
? ["alice", "bob"]
495+
: ["admins", "devs"]
496+
candidates.map: cli.CompletionCandidate it,
497+
]
498+
--run=:: null
499+
500+
result := complete_ root ["user", ""]
501+
values := result.candidates.map: it.value
502+
expect-equals ["user"] seen["kind"]
503+
expect (values.contains "alice")
504+
expect (values.contains "bob")
505+
expect (not (values.contains "admins"))
506+
507+
result = complete_ root ["group", ""]
508+
values = result.candidates.map: it.value
509+
expect-equals ["group"] seen["kind"]
510+
expect (values.contains "admins")
511+
expect (values.contains "devs")
512+
expect (not (values.contains "alice"))
513+
514+
test-rest-multi-records-all:
515+
// For a multi rest option, seen-options records every value in order.
516+
seen/Map? := null
517+
root := cli.Command "app"
518+
--rest=[
519+
cli.Option "files" --multi --help="Input files."
520+
--completion=:: | context/cli.CompletionContext |
521+
seen = context.seen-options
522+
[],
523+
]
524+
--run=:: null
525+
complete_ root ["a.txt", "b.txt", "c.txt", ""]
526+
expect-equals ["a.txt", "b.txt", "c.txt"] seen["files"]
527+
528+
test-rest-after-dashdash-recorded:
529+
// Positionals after -- are also recorded in seen-options under the
530+
// owning rest option's name.
531+
seen/Map? := null
532+
root := cli.Command "app"
533+
--rest=[
534+
cli.OptionEnum "kind" ["user", "group"] --help="Kind.",
535+
cli.Option "name" --help="Name."
536+
--completion=:: | context/cli.CompletionContext |
537+
seen = context.seen-options
538+
[],
539+
]
540+
--run=:: null
541+
complete_ root ["--", "user", ""]
542+
expect-equals ["user"] seen["kind"]
543+
479544
test-short-option-marks-seen:
480545
root := cli.Command "app"
481546
--options=[

0 commit comments

Comments
 (0)