diff --git a/README.md b/README.md index 533dca7..51337c1 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,27 @@ soleil bench --suite smoke soleil bench --suite core --runs 3 --json ``` +### PowerShell tab completion + +Windows PowerShell users can load deterministic completions for the `soleil` +command: + +```powershell +. "$PWD\completions\soleil.ps1" +``` + +To keep it available, add the same line to your PowerShell profile: + +```powershell +notepad $PROFILE +``` + +Remove the line from `$PROFILE` to uninstall the completion hook. The script +completes the documented commands (`doctor`, `setup`, `language`, `run`, +`bench`), common options (`--cwd`, `--mode`, `--language`, `--yes`, `--json`, +`--suite`, `--runs`), and known values for `--mode`, `--language`, and +`--suite`. + Benchmarks use the configured free/local routes and therefore consume their quota. Each case runs in a new temporary project that is removed after verification. See the [benchmark methodology and latest verified snapshot](docs/BENCHMARKS.md). diff --git a/completions/soleil.ps1 b/completions/soleil.ps1 new file mode 100644 index 0000000..960ad65 --- /dev/null +++ b/completions/soleil.ps1 @@ -0,0 +1,73 @@ +# SoleilCode PowerShell tab completion. +# Load with: . "$PWD\completions\soleil.ps1" + +$script:SoleilCommands = @( + 'doctor', + 'setup', + 'language', + 'languages', + 'run', + 'bench', + 'benchmark' +) + +$script:SoleilOptions = @( + '--cwd', + '--mode', + '--language', + '--lang', + '--yes', + '-y', + '--json', + '--prompt-file', + '--suite', + '--runs', + '--version', + '-v', + '--help', + '-h' +) + +$script:SoleilModes = @('auto', 'free', 'local', 'private') +$script:SoleilLanguages = @('en', 'tr', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko') +$script:SoleilSuites = @('smoke', 'core') + +function script:New-SoleilCompletionResult { + param( + [string] $Value, + [string] $ToolTip = $Value + ) + + [System.Management.Automation.CompletionResult]::new( + $Value, + $Value, + [System.Management.Automation.CompletionResultType]::ParameterValue, + $ToolTip + ) +} + +Register-ArgumentCompleter -Native -CommandName soleil -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $elements = @($commandAst.CommandElements | ForEach-Object { $_.ToString() }) + $previous = if ($elements.Count -ge 2) { $elements[$elements.Count - 2] } else { '' } + + $candidates = switch ($previous) { + '--mode' { $script:SoleilModes; break } + '--language' { $script:SoleilLanguages; break } + '--lang' { $script:SoleilLanguages; break } + '--suite' { $script:SoleilSuites; break } + default { + if ($wordToComplete -like '-*') { + $script:SoleilOptions + } else { + @($script:SoleilCommands + $script:SoleilOptions) + } + } + } + + $candidates | + Where-Object { $_ -like "$wordToComplete*" } | + Sort-Object -Unique | + ForEach-Object { script:New-SoleilCompletionResult $_ } +} diff --git a/package.json b/package.json index 8168abb..d49046c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "files": [ "bin", + "completions", "dist", "README.md", "CHANGELOG.md", diff --git a/tests/powershell-completion.test.ts b/tests/powershell-completion.test.ts new file mode 100644 index 0000000..a73bd96 --- /dev/null +++ b/tests/powershell-completion.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; + +const completionPath = path.resolve("completions/soleil.ps1"); + +test("PowerShell completion covers documented commands and options", async () => { + const script = await readFile(completionPath, "utf8"); + + for (const command of ["doctor", "setup", "language", "run", "bench"]) { + assert.match(script, new RegExp(`'${command}'`)); + } + + for (const option of ["--cwd", "--mode", "--language", "--yes", "--json", "--suite", "--runs"]) { + assert.match(script, new RegExp(`'${option}'`)); + } + + for (const value of ["auto", "free", "local", "private", "smoke", "core"]) { + assert.match(script, new RegExp(`'${value}'`)); + } + + assert.match(script, /Register-ArgumentCompleter -Native -CommandName soleil/); +}); + +test("PowerShell completion candidate lists are deterministic and unique", async () => { + const script = await readFile(completionPath, "utf8"); + const listMatches = script.matchAll(/\$script:Soleil(?:Commands|Options|Modes|Languages|Suites) = @\((?[\s\S]*?)\)/g); + + for (const match of listMatches) { + const body = match.groups?.body ?? ""; + const values = [...body.matchAll(/'([^']+)'/g)].map((entry) => entry[1]); + assert.deepEqual(values, [...new Set(values)], `duplicate completion values in ${match[0].split(" = ")[0]}`); + assert.ok(values.length > 0); + } +});