Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
749808c
Add completion
floitsch Mar 18, 2026
34a646d
Add support for powershell.
floitsch Mar 18, 2026
8c1deba
Introduce `OptionPath`.
floitsch Mar 18, 2026
4bc9ddf
Feedback.
floitsch Mar 18, 2026
7e878ca
Feedback.
floitsch Mar 18, 2026
b6d700b
Feedback.
floitsch Mar 18, 2026
4f2285a
Feedback.
floitsch Mar 18, 2026
1f3e28b
Feedback.
floitsch Mar 18, 2026
a3cf143
More tests.
floitsch Mar 18, 2026
074a00c
Fix.
floitsch Mar 19, 2026
8502aab
Speed up test.
floitsch Mar 19, 2026
c615c34
Speed up more and simplify.
floitsch Mar 19, 2026
a5bb971
Test fixes.
floitsch Mar 19, 2026
b4d4ca6
Use a socket.
floitsch Mar 19, 2026
ebe5943
Fixes.
floitsch Mar 19, 2026
1b4c08d
Skip if too old.
floitsch Mar 19, 2026
528b011
Don't hide errors.
floitsch Mar 19, 2026
94b2eea
Merge branch 'floitsch/completion' into floitsch/completion.pwsh
floitsch Mar 19, 2026
4369886
Merge floitsch/completion.pwsh into floitsch/completion.pwsh.OptionPath
floitsch Mar 19, 2026
6ba435a
Add test.
floitsch Mar 19, 2026
9fead8f
Fix bad merge.
floitsch Mar 19, 2026
5d68726
Add powershell test and fix it.
floitsch Mar 19, 2026
df62c75
Merge branch 'main' into floitsch/completion
floitsch Mar 19, 2026
25bbba2
Merge branch 'floitsch/completion' into floitsch/completion.pwsh
floitsch Mar 19, 2026
c51a238
Merge branch 'floitsch/completion.pwsh' into floitsch/completion.pwsh…
floitsch Mar 19, 2026
36cb03a
Merge branch 'floitsch/completion.pwsh.OptionPath' into floitsch/comp…
floitsch Mar 19, 2026
7753ebc
Merge branch 'main' into floitsch/completion.pwsh
floitsch Mar 19, 2026
eced3f3
Merge branch 'floitsch/completion.pwsh' into floitsch/completion.pwsh…
floitsch Mar 19, 2026
fe327d1
Merge branch 'floitsch/completion.pwsh.OptionPath' into floitsch/comp…
floitsch Mar 19, 2026
eb0e557
Increase timeout.
floitsch Mar 19, 2026
e50bb11
Merge branch 'main' into floitsch/completion.pwsh.OptionPath
floitsch Mar 19, 2026
b02570b
Merge branch 'floitsch/completion.pwsh.OptionPath' into floitsch/comp…
floitsch Mar 19, 2026
18ed91c
Fixes for Windows.
floitsch Mar 19, 2026
74a11fe
Fixes for Windows.
floitsch Mar 19, 2026
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
29 changes: 21 additions & 8 deletions src/completion-scripts_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
// Use of this source code is governed by an MIT-style license that can be
// found in the package's LICENSE file.

import fs

/**
Extracts the basename from the given $path, stripping any directory components.
Extracts the basename from the given $path, stripping any directory components
and the .exe extension on Windows.
*/
basename_ path/string -> string:
slash := path.index-of --last "/"
if slash >= 0: return path[slash + 1..]
return path
name := fs.basename path
// Strip .exe suffix so that completions work on Windows where
// system.program-path includes the extension but users type without it.
if name.ends-with ".exe": name = name[..name.size - 4]
return name

/**
Sanitizes the given $name for use as a shell function name.
Expand Down Expand Up @@ -175,12 +180,19 @@ powershell-completion-script_ --program-path/string -> string:
param(\$wordToComplete, \$commandAst, \$cursorPosition)

\$tokens = \$commandAst.ToString() -split '\\s+'
\$args = \$tokens[1..(\$tokens.Length - 1)]
if (\$tokens.Length -gt 1) {
\$completionArgs = \$tokens[1..(\$tokens.Length - 1)]
} else {
\$completionArgs = @()
}
if (\$completionArgs.Length -eq 0 -or \$completionArgs[-1] -ne \$wordToComplete) {
\$completionArgs += \$wordToComplete
}

\$output = & $program-path __complete -- @args 2>\$null
if (\$LASTEXITCODE -ne 0) { return }
\$output = & '$program-path' __complete -- @completionArgs 2>\$null
if (\$LASTEXITCODE -ne 0 -or -not \$output) { return }

\$lines = \$output -split '\\n'
\$lines = \$output -split '\\r?\\n'
\$directive = (\$lines[-1] -replace '^:', '')
\$lines = \$lines[0..(\$lines.Length - 2)]

Expand All @@ -193,6 +205,7 @@ powershell-completion-script_ --program-path/string -> string:
\$value = \$line
\$desc = \$line
}
if (\$value -notlike "\$wordToComplete*") { continue }
[System.Management.Automation.CompletionResult]::new(
\$value,
\$value,
Expand Down
114 changes: 114 additions & 0 deletions tests/completion_shell.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (C) 2026 Toit contributors.
// Use of this source code is governed by a Zero-Clause BSD license that can
// be found in the tests/LICENSE file.

import fs
import host.directory
import host.file
import host.pipe
import system

/**
A tmux session wrapper for testing interactive shell completions.
*/
class Tmux:
/** The socket name, unique per session to avoid server conflicts. */
socket-name/string

constructor .socket-name --shell-cmd/List --width/int=200 --height/int=50:
args := [
"tmux",
"-L", socket-name, // Use a dedicated server socket.
"new-session",
"-d", // Detached.
"-s", socket-name,
"-x", "$width",
"-y", "$height",
] + shell-cmd
exit-code := pipe.run-program args
if exit-code != 0:
throw "tmux new-session failed with exit code $exit-code for shell '$shell-cmd'"
// Wait for the shell to initialize.
send-line "echo tmux-ready"
wait-for "tmux-ready"

/**
Sends keystrokes to the tmux session.
Each argument is a tmux key name (e.g. "Enter", "Tab", "C-c").
*/
send-keys keys/List -> none:
pipe.run-program ["tmux", "-L", socket-name, "send-keys", "-t", socket-name] + keys

/** Sends text followed by Enter. */
send-line text/string -> none:
send-keys [text, "Enter"]

/** Sends Ctrl-C to cancel the current line, then waits for the shell to be ready. */
cancel -> none:
send-keys ["C-c"]
// Echo a marker and wait for it so we know the shell is ready.
marker := "ready-$Time.monotonic-us"
send-line "echo $marker"
wait-for marker

/** Captures the current pane content as a string. */
capture -> string:
return pipe.backticks ["tmux", "-L", socket-name, "capture-pane", "-t", socket-name, "-p"]

/**
Waits until the pane contains the given $expected string, or throws on timeout.
*/
wait-for expected/string --timeout-ms/int=10000 -> none:
deadline := Time.monotonic-us + timeout-ms * 1000
delay-ms := 10
while Time.monotonic-us < deadline:
content := capture
if content.contains expected: return
sleep --ms=delay-ms
delay-ms = min 500 (delay-ms * 2)
content := capture
throw "Timed out waiting for '$expected' in tmux pane. Content:\n$content"

/** Kills the tmux server (and its session). */
close -> none:
catch: pipe.run-program ["tmux", "-L", socket-name, "kill-server"]

// Unique session prefix for this test run.
session-id_ := 0

next-session-name_ -> string:
session-id_++
return "completion-test-$Time.monotonic-us-$session-id_"

with-tmp-dir [block]:
tmpdir := directory.mkdtemp "/tmp/completion-shell-test-"
try:
block.call tmpdir
finally:
directory.rmdir --recursive --force tmpdir

has-command_ name/string -> bool:
exit-code := pipe.run-program ["which", name]
return exit-code == 0

/**
Compiles the test app binary and creates OptionPath test artifacts.
Returns the path to the compiled binary.
The $tmpdir must already exist.
*/
setup-test-binary_ tmpdir/string -> string:
test-dir := fs.dirname system.program-path
app-source := "$test-dir/completion_shell_test_app.toit"
binary := "$tmpdir/fleet"
if system.platform == system.PLATFORM-WINDOWS:
binary = "$tmpdir/fleet.exe"
print "Compiling test app..."
pipe.run-program ["toit", "compile", "-o", binary, app-source]
print "Binary compiled: $binary"

// Create artifacts for OptionPath completion testing.
file.write-contents --path="$tmpdir/xfirmware.bin" ""
directory.mkdir "$tmpdir/xreleases"

return binary

78 changes: 78 additions & 0 deletions tests/completion_shell_bash_test.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (C) 2026 Toit contributors.
// Use of this source code is governed by a Zero-Clause BSD license that can
// be found in the tests/LICENSE file.

import expect show *
import host.pipe
import .completion_shell

main:
if not has-command_ "tmux":
print "tmux not found, skipping bash completion tests."
return

// Bash 3.x (macOS default) lacks compopt and has limited programmable
// completion support. Skip if bash is too old.
bash-version := (pipe.backticks ["bash", "-c", "echo \$BASH_VERSINFO"]).trim
if bash-version == "" or bash-version[0] < '4':
print ""
print "=== Skipping bash tests (bash $bash-version too old, need 4+) ==="
return

with-tmp-dir: | tmpdir |
binary := setup-test-binary_ tmpdir
test-bash binary tmpdir

print ""
print "All bash completion tests passed!"

test-bash binary/string tmpdir/string:
print ""
print "=== Testing bash completion ==="
tmux := Tmux (next-session-name_) --shell-cmd=["bash", "--norc", "--noprofile"]
try:
tmux.send-line "source <($binary completion bash); echo sourced"
tmux.wait-for "sourced"

// Subcommand completion (double Tab for ambiguous matches).
tmux.send-keys ["$binary ", "Tab", "Tab"]
tmux.wait-for "deploy"
content := tmux.capture
expect (content.contains "status")
expect (content.contains "help")
expect (content.contains "completion")
tmux.cancel

// Unique prefix auto-completes inline.
tmux.send-keys ["$binary dep", "Tab"]
tmux.wait-for "deploy"
tmux.cancel

// Enum value completion.
tmux.send-keys ["$binary deploy --channel ", "Tab", "Tab"]
tmux.wait-for "stable"
content = tmux.capture
expect (content.contains "beta")
expect (content.contains "dev")
tmux.cancel

// Short option -d triggers device completion.
tmux.send-keys ["$binary deploy -d ", "Tab", "Tab"]
tmux.wait-for "d3b07384"
tmux.cancel

// OptionPath: file option falls back to file completion.
tmux.send-line "cd $tmpdir && echo cd-done"
tmux.wait-for "cd-done"
tmux.send-keys ["$binary deploy --firmware xfirm", "Tab"]
tmux.wait-for "xfirmware.bin"
tmux.cancel

// OptionPath --directory: falls back to directory-only completion.
tmux.send-keys ["$binary deploy --output-dir xrel", "Tab"]
tmux.wait-for "xreleases"
tmux.cancel

print " All bash tests passed."
finally:
tmux.close
66 changes: 66 additions & 0 deletions tests/completion_shell_fish_test.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (C) 2026 Toit contributors.
// Use of this source code is governed by a Zero-Clause BSD license that can
// be found in the tests/LICENSE file.

import expect show *
import .completion_shell

main:
if not has-command_ "tmux":
print "tmux not found, skipping fish completion tests."
return

if not has-command_ "fish":
print ""
print "=== Skipping fish tests (fish not installed) ==="
return

with-tmp-dir: | tmpdir |
binary := setup-test-binary_ tmpdir
test-fish binary tmpdir

print ""
print "All fish completion tests passed!"

test-fish binary/string tmpdir/string:
print ""
print "=== Testing fish completion ==="
tmux := Tmux (next-session-name_) --shell-cmd=["fish", "--no-config"]
try:
tmux.send-line "$binary completion fish | source; echo sourced"
tmux.wait-for "sourced"

// Subcommand completion.
tmux.send-keys ["$binary ", "Tab"]
tmux.wait-for "deploy"
content := tmux.capture
expect (content.contains "status")
tmux.cancel

// Enum value completion.
tmux.send-keys ["$binary deploy --channel ", "Tab"]
tmux.wait-for "stable"
content = tmux.capture
expect (content.contains "beta")
tmux.cancel

// Device completion with descriptions.
tmux.send-keys ["$binary deploy --device ", "Tab"]
tmux.wait-for "Living Room Sensor"
tmux.cancel

// OptionPath: file option falls back to file completion.
tmux.send-line "cd $tmpdir && echo cd-done"
tmux.wait-for "cd-done"
tmux.send-keys ["$binary deploy --firmware xfirm", "Tab"]
tmux.wait-for "xfirmware.bin"
tmux.cancel

// OptionPath --directory: falls back to directory-only completion.
tmux.send-keys ["$binary deploy --output-dir xrel", "Tab"]
tmux.wait-for "xreleases"
tmux.cancel

print " All fish tests passed."
finally:
tmux.close
Loading