diff --git a/src/cli.toit b/src/cli.toit index 266548d..5b08654 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -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 diff --git a/src/completion_.toit b/src/completion_.toit index 3e651b8..15d7493 100644 --- a/src/completion_.toit +++ b/src/completion_.toit @@ -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 @@ -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). @@ -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. diff --git a/tests/completion_test.toit b/tests/completion_test.toit index 653f429..0d103bb 100644 --- a/tests/completion_test.toit +++ b/tests/completion_test.toit @@ -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 @@ -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=[