Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cli.toit
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ class CompletionContext:
/**
A map from option name to a list of values that have been provided
for that option so far.

Includes both named options and rest (positional) options. Rest options
are keyed by their name, and their value list preserves the order of
the positionals consumed. This lets a completion callback for a later
rest argument condition its output on earlier rest values.
*/
seen-options/Map

Expand Down
29 changes: 27 additions & 2 deletions src/completion_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ complete_ root/Command arguments/List -> CompletionResult_:
arg/string := args-to-process[index]

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

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

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

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

/**
Returns the rest $Option that owns the positional at the given $positional-index
in $command, or null if there is no such rest option.

Mirrors the parser's consumption order: each non-multi rest option consumes
one positional slot, and a multi rest option absorbs all remaining
positionals.
*/
rest-option-for-index_ command/Command positional-index/int -> Option?:
skip := positional-index
command.rest_.do: | option/Option |
if option.is-multi: return option
if skip == 0: return option
skip--
return null

/**
Whether the given $option has meaningful completion support.

Expand Down
65 changes: 65 additions & 0 deletions tests/completion_test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ main:
test-rest-positional-index
test-rest-positional-index-after-dashdash
test-rest-multi-not-skipped
test-rest-dependent-completion
test-rest-multi-records-all
test-rest-after-dashdash-recorded
test-short-option-marks-seen
test-short-option-pending-value
test-packed-short-options
Expand Down Expand Up @@ -476,6 +479,68 @@ test-rest-multi-not-skipped:
expect (values.contains "a.txt")
expect (values.contains "b.txt")

test-rest-dependent-completion:
// A completion callback for a later rest argument can condition on an
// earlier rest argument by reading context.seen-options.
seen/Map? := null
root := cli.Command "app"
--rest=[
cli.OptionEnum "kind" ["user", "group"] --help="Resource kind.",
cli.Option "name" --help="Resource name."
--completion=:: | context/cli.CompletionContext |
seen = context.seen-options
kind := (context.seen-options.get "kind" --if-absent=: [""]).first
candidates := kind == "user"
? ["alice", "bob"]
: ["admins", "devs"]
candidates.map: cli.CompletionCandidate it,
]
--run=:: null

result := complete_ root ["user", ""]
values := result.candidates.map: it.value
expect-equals ["user"] seen["kind"]
expect (values.contains "alice")
expect (values.contains "bob")
expect (not (values.contains "admins"))

result = complete_ root ["group", ""]
values = result.candidates.map: it.value
expect-equals ["group"] seen["kind"]
expect (values.contains "admins")
expect (values.contains "devs")
expect (not (values.contains "alice"))

test-rest-multi-records-all:
// For a multi rest option, seen-options records every value in order.
seen/Map? := null
root := cli.Command "app"
--rest=[
cli.Option "files" --multi --help="Input files."
--completion=:: | context/cli.CompletionContext |
seen = context.seen-options
[],
]
--run=:: null
complete_ root ["a.txt", "b.txt", "c.txt", ""]
expect-equals ["a.txt", "b.txt", "c.txt"] seen["files"]

test-rest-after-dashdash-recorded:
// Positionals after -- are also recorded in seen-options under the
// owning rest option's name.
seen/Map? := null
root := cli.Command "app"
--rest=[
cli.OptionEnum "kind" ["user", "group"] --help="Kind.",
cli.Option "name" --help="Name."
--completion=:: | context/cli.CompletionContext |
seen = context.seen-options
[],
]
--run=:: null
complete_ root ["--", "user", ""]
expect-equals ["user"] seen["kind"]

test-short-option-marks-seen:
root := cli.Command "app"
--options=[
Expand Down