Skip to content
Open
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
8 changes: 7 additions & 1 deletion src/cli.toit
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,17 @@ class Command:

// Handle __complete requests before any other processing.
if add-completion and not arguments.is-empty and arguments[0] == "__complete":
// Silence the default logger: the completion candidates are printed to
// stdout, so any library logging from completion callbacks would
// corrupt the completion output.
log.set-default (log.default.with-level log.FATAL-LEVEL)
if add-ui-help: add-ui-options_
completion-args := arguments[1..]
if not completion-args.is-empty and completion-args[0] == "--":
completion-args = completion-args[1..]
result := complete_ this completion-args
result := complete-with-timeout_ this completion-args
// The completion scripts treat a non-zero exit as "no completions".
if not result: exit 1
result.candidates.do: | candidate/CompletionCandidate_ |
print candidate.to-string
if result.extensions and not result.extensions.is-empty:
Expand Down
26 changes: 26 additions & 0 deletions src/completion_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ class CompletionResult_:

constructor .candidates --.directive=DIRECTIVE-DEFAULT_ --.extensions=null:

/**
The maximum time a completion request may take.

Completion requests run while the user is waiting for the shell to
react to a tab-press, and shells don't interrupt slow completion
commands. If a completion callback misbehaves (for example by hanging
on a network operation), this limit bounds how long the user's prompt
is blocked.
*/
COMPLETION-TIMEOUT-MS_ ::= 5_000

/**
Computes completion candidates like $complete_, but guards against
misbehaving completion callbacks.

Returns null if the computation throws or takes longer than
$timeout-ms. The caller should then exit with a non-zero exit code,
which the generated completion scripts treat as "no completions".
*/
complete-with-timeout_ root/Command arguments/List --timeout-ms/int=COMPLETION-TIMEOUT-MS_ -> CompletionResult_?:
result/CompletionResult_? := null
catch:
with-timeout --ms=timeout-ms:
result = complete_ root arguments
return result

/**
Computes completion candidates for the given $arguments.

Expand Down
29 changes: 29 additions & 0 deletions tests/completion_test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ main:
test-command-group-completion
test-command-group-after-default-entered
test-command-group-with-extensions
test-slow-callback-times-out
test-throwing-callback-yields-null

test-empty-input:
root := cli.Command "app"
Expand Down Expand Up @@ -867,3 +869,30 @@ test-command-group-with-extensions:
expect-equals 1 result.extensions.size
expect (result.extensions.contains ".toit")


test-slow-callback-times-out:
root := cli.Command "app"
--options=[
cli.Option "host" --help="Target host."
--completion=:: | context/cli.CompletionContext |
sleep --ms=1_000
[cli.CompletionCandidate "too-late"],
]
--run=:: null
result := complete-with-timeout_ root ["--host", ""] --timeout-ms=50
expect-null result

// A fast callback is unaffected.
result = complete-with-timeout_ root ["--", ""] --timeout-ms=1_000
expect-not-null result

test-throwing-callback-yields-null:
root := cli.Command "app"
--options=[
cli.Option "host" --help="Target host."
--completion=:: | context/cli.CompletionContext |
throw "callback is broken",
]
--run=:: null
result := complete-with-timeout_ root ["--host", ""]
expect-null result
Loading