|
2 | 2 |
|
3 | 3 | # tab |
4 | 4 |
|
5 | | -- [x] zsh test in git |
| 5 | +Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool. |
6 | 6 |
|
7 | | -```zsh |
8 | | -source <(pnpm tsx demo.ts complete zsh) |
| 7 | +Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces. |
| 8 | + |
| 9 | +```ts |
| 10 | +import { Completion, script } from "@bombsh/tab"; |
| 11 | + |
| 12 | +const name = "vite" |
| 13 | +const completion = new Completion() |
| 14 | + |
| 15 | +completion.addCommand("start", "Start the application", async (previousArgs, toComplete, endsWithSpace) => { |
| 16 | + // suggestions |
| 17 | + return [ |
| 18 | + { value: "dev", description: "Start in development mode" }, |
| 19 | + { value: "prod", description: "Start in production mode" } |
| 20 | + ] |
| 21 | +}) |
9 | 22 |
|
10 | | -vite # rest of the completions |
| 23 | +completion.addOption("start", "--port", "Specify the port number", async (previousArgs, toComplete, endsWithSpace) => { |
| 24 | + return [ |
| 25 | + { value: "3000", description: "Development port" }, |
| 26 | + { value: "8080", description: "Production port" } |
| 27 | + ] |
| 28 | +}) |
11 | 29 |
|
12 | | -pnpm tsx demo.ts complete -- --po |
| 30 | + |
| 31 | +// a way of getting the executable path to pass to the shell autocompletion script |
| 32 | +function quoteIfNeeded(path: string) { |
| 33 | + return path.includes(" ") ? `'${path}'` : path; |
| 34 | +} |
| 35 | +const execPath = process.execPath; |
| 36 | +const processArgs = process.argv.slice(1); |
| 37 | +const quotedExecPath = quoteIfNeeded(execPath); |
| 38 | +const quotedProcessArgs = processArgs.map(quoteIfNeeded); |
| 39 | +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); |
| 40 | +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; |
| 41 | + |
| 42 | +if (process.argv[2] === "--") { |
| 43 | + // autocompletion logic |
| 44 | + await completion.parse(process.argv.slice(2), "start") // TODO: remove "start" |
| 45 | +} else { |
| 46 | + // process.argv[2] can be "zsh", "bash", "fish", "powershell" |
| 47 | + script(process.argv[2], name, x) |
| 48 | +} |
13 | 49 | ``` |
14 | 50 |
|
15 | | -- [x] tests vitest (this should mostly test the completions array, e.g. logs) |
16 | | -- [x] powershell completions generation |
17 | | -- [x] citty support `@bomsh/tab/citty` |
18 | | -- [] `@bombsh/tab` |
| 51 | +Now your user can run `source <(vite complete zsh)` and they will get completions for the `vite` command using the [autocompletion server](#autocompletion-server). |
19 | 52 |
|
20 | | -- [] fish |
21 | | -- [] bash |
| 53 | +## Adapters |
| 54 | + |
| 55 | +Since we are heavy users of tools like `cac` and `citty`, we have created adapters for both of them. Ideally, tab would be integrated internally into these tools, but for now, this is a good compromise. |
| 56 | + |
| 57 | +### `@bombsh/tab/cac` |
22 | 58 |
|
23 | 59 | ```ts |
24 | | -const completion = new Completion() |
25 | | -completion.addCommand() |
26 | | -completion.addOption() |
| 60 | +import cac from "cac"; |
| 61 | +import tab from "@bombsh/tab/cac"; |
| 62 | + |
| 63 | +const cli = cac("vite"); |
| 64 | + |
| 65 | +cli |
| 66 | + .command("dev", "Start dev server") |
| 67 | + .option("--port <port>", "Specify port"); |
| 68 | + |
| 69 | +const completion = tab(cli); |
| 70 | + |
| 71 | +// Get the dev command completion handler |
| 72 | +const devCommandCompletion = completion.commands.get("dev"); |
| 73 | + |
| 74 | +// Get and configure the port option completion handler |
| 75 | +const portOptionCompletion = devCommandCompletion.options.get("--port"); |
| 76 | +portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { |
| 77 | + return [ |
| 78 | + { value: "3000", description: "Development port" }, |
| 79 | + { value: "8080", description: "Production port" } |
| 80 | + ] |
| 81 | +} |
| 82 | + |
| 83 | +cli.parse(); |
| 84 | +``` |
| 85 | +Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `vite dev --po`, they will get suggestions for the `--port` option. Or if they write `vite d` they will get suggestions for the `dev` command. |
| 86 | + |
| 87 | +Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation. |
| 88 | + |
| 89 | +### `@bombsh/tab/citty` |
| 90 | + |
| 91 | +```ts |
| 92 | +import citty, { defineCommand, createMain } from "citty"; |
| 93 | +import tab from "@bombsh/tab/citty"; |
| 94 | + |
| 95 | +const main = defineCommand({ |
| 96 | + meta: { |
| 97 | + name: "vite", |
| 98 | + description: "Vite CLI tool", |
| 99 | + }, |
| 100 | +}); |
| 101 | + |
| 102 | +const devCommand = defineCommand({ |
| 103 | + meta: { |
| 104 | + name: "dev", |
| 105 | + description: "Start dev server", |
| 106 | + }, |
| 107 | + args: { |
| 108 | + port: { type: "string", description: "Specify port" }, |
| 109 | + } |
| 110 | +}); |
| 111 | + |
| 112 | +main.subCommands = { |
| 113 | + dev: devCommand |
| 114 | +}; |
| 115 | + |
| 116 | +const completion = await tab(main); |
| 117 | + |
| 118 | +// TODO: addHandler function to export |
| 119 | +const devCommandCompletion = completion.commands.get("dev"); |
27 | 120 |
|
28 | | -// better name |
29 | | -completion.parse() |
| 121 | +const portOptionCompletion = devCommandCompletion.options.get("--port"); |
| 122 | + |
| 123 | +portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { |
| 124 | + return [ |
| 125 | + { value: "3000", description: "Development port" }, |
| 126 | + { value: "8080", description: "Production port" } |
| 127 | + ] |
| 128 | +} |
| 129 | + |
| 130 | +const cli = createMain(main); |
| 131 | +cli(); |
30 | 132 | ``` |
| 133 | + |
| 134 | +## Autocompletion Server |
| 135 | + |
| 136 | +By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server. |
| 137 | + |
| 138 | +```zsh |
| 139 | +vite complete -- --po |
| 140 | +--port Specify the port number |
| 141 | +:0 |
| 142 | +``` |
| 143 | + |
| 144 | +The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`). |
| 145 | + |
| 146 | +In situations like `vite dev --po` you'd have autocompletions! But in the case of `pnpm vite dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. |
| 147 | + |
| 148 | +Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available. |
| 149 | + |
| 150 | +This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box. |
| 151 | + |
| 152 | +Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support. |
| 153 | + |
| 154 | +## Inspiration |
| 155 | +- git |
| 156 | +- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go) |
| 157 | + |
| 158 | + |
| 159 | +## TODO |
| 160 | +- [] fish |
| 161 | +- [] bash |
0 commit comments