From 7bfe997100ca0e96241c80c47d63e6812dc03f28 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 2 Apr 2026 16:20:48 +0200 Subject: [PATCH] Improve completion for 'help'. --- src/completion_.toit | 29 +++++++++++++++++- tests/completion_test.toit | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/completion_.toit b/src/completion_.toit index 024010d..ba188b4 100644 --- a/src/completion_.toit +++ b/src/completion_.toit @@ -72,6 +72,11 @@ complete_ root/Command arguments/List -> CompletionResult_: add-options-for-command_ current-command all-named-options all-short-options + // Whether we are completing after "help" at root level. In help mode + // only subcommand names are completed (no options or rest args), since + // "help" only navigates the command tree. + in-help-mode := false + past-dashdash := false // The option that is expecting a value (the previous arg was a non-flag option). pending-option/Option? := null @@ -154,7 +159,16 @@ complete_ root/Command arguments/List -> CompletionResult_: current-command = subcommand is-root = false positional-index = 0 - add-options-for-command_ current-command all-named-options all-short-options + // In help mode, don't accumulate subcommand options since + // help only navigates the tree and doesn't use them. + if not in-help-mode: + add-options-for-command_ current-command all-named-options all-short-options + else if is-root and arg == "help": + // "help" is a synthetic command at root level. Enter help + // mode which only completes subcommand names. + in-help-mode = true + all-named-options.clear + all-short-options.clear else: // It's a positional/rest argument. positional-index++ @@ -162,6 +176,19 @@ complete_ root/Command arguments/List -> CompletionResult_: // Now determine what to complete for the last argument (the word being typed). current-word := arguments.is-empty ? "" : arguments.last + // In help mode, only complete subcommand names. No options or rest args. + if in-help-mode: + candidates := [] + if not current-command.run-callback_: + current-command.subcommands_.do: | sub/Command | + if sub.is-hidden_: continue.do + if sub.name.starts-with current-word: + candidates.add (CompletionCandidate_ sub.name --description=sub.short-help) + sub.aliases_.do: | alias/string | + if alias.starts-with current-word: + candidates.add (CompletionCandidate_ alias --description=sub.short-help) + return CompletionResult_ candidates --directive=DIRECTIVE-NO-FILE-COMPLETION_ + // If we were expecting a value for an option, complete that option's values. if pending-option: context := CompletionContext.private_ diff --git a/tests/completion_test.toit b/tests/completion_test.toit index dba52c5..146921e 100644 --- a/tests/completion_test.toit +++ b/tests/completion_test.toit @@ -35,6 +35,7 @@ main: test-short-option-pending-value test-packed-short-options test-custom-completer-no-file-fallback + test-help-completion test-help-gated-on-availability test-empty-input: @@ -558,6 +559,65 @@ test-custom-completer-no-file-fallback: expect-equals 0 result.candidates.size expect-equals DIRECTIVE-FILE-COMPLETION_ result.directive +test-help-completion: + root := cli.Command "app" + --options=[ + cli.Flag "verbose" --help="Be verbose.", + ] + --subcommands=[ + cli.Command "serve" --help="Start a server." + --options=[ + cli.Option "port" --help="Port number.", + ] + --run=:: null, + cli.Command "build" --help="Build the project." --run=:: null, + cli.Command "device" + --help="Device commands." + --options=[ + cli.Option "name" --help="Device name.", + ] + --subcommands=[ + cli.Command "list" --help="List devices." --run=:: null, + cli.Command "show" --help="Show device." --run=:: null, + ], + ] + // "app help " should complete subcommands but not "help" itself. + result := complete_ root ["help", ""] + values := result.candidates.map: it.value + expect (values.contains "serve") + expect (values.contains "build") + expect (values.contains "device") + expect (not (values.contains "help")) + + // "app help s" should filter by prefix. + result = complete_ root ["help", "s"] + values = result.candidates.map: it.value + expect (values.contains "serve") + expect (not (values.contains "build")) + + // "app help device " should complete device's subcommands. + result = complete_ root ["help", "device", ""] + values = result.candidates.map: it.value + expect (values.contains "list") + expect (values.contains "show") + expect (not (values.contains "help")) + + // "app help -" should NOT complete options (help doesn't use them). + result = complete_ root ["help", "-"] + values = result.candidates.map: it.value + expect (values.is-empty) + + // "app help device -" should NOT complete device's options. + result = complete_ root ["help", "device", "-"] + values = result.candidates.map: it.value + expect (values.is-empty) + + // "app help serve " should complete nothing (leaf command in help mode). + result = complete_ root ["help", "serve", ""] + values = result.candidates.map: it.value + expect (values.is-empty) + expect-equals DIRECTIVE-NO-FILE-COMPLETION_ result.directive + test-help-gated-on-availability: // When a command defines its own "help" option, --help should not be suggested. root := cli.Command "app"