Skip to content

Commit dc8b26c

Browse files
feat(cli): provide shell completion scripts (#477)
* feat(cli): provide shell completion scripts Resolves #262 * Rework completion support to be more like ruff * Add `just install-binary` * Derive `clap::ValueEnum` for `LogLevel` So we get completions and the standardized `[possible values: <enum>]` docs * Add `--help` test for `generate-shell-completion` * CHANGELOG bullet * Add documentation * Normalize executable name --------- Co-authored-by: Davis Vaughan <davis@posit.co>
1 parent 48bb546 commit dc8b26c

26 files changed

Lines changed: 853 additions & 17 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
# Development version
44

5+
- New `air generate-shell-completion <SHELL>` hidden command that emits a script to stdout that generates shell completions. Supports bash, zsh, fish, powershell, and elvish (#477, @salim-b).
6+
7+
For zsh, run the following to add to your `.zshrc`:
8+
9+
```bash
10+
echo 'eval "$(air generate-shell-completion zsh)"' >> ~/.zshrc
11+
```
12+
13+
For powershell, run the following to add to your profile:
14+
15+
```powershell
16+
if (!(Test-Path -Path $PROFILE)) {
17+
New-Item -ItemType File -Path $PROFILE -Force
18+
}
19+
Add-Content -Path $PROFILE -Value '(& air generate-shell-completion powershell) | Out-String | Invoke-Expression'
20+
```
21+
22+
Then restart your shell and type `air <tab>` to see completions.
523

624
# 0.8.2
725

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ bytes = "1.8.0"
3333
cargo_metadata = "0.20.0"
3434
case = "1.0.0"
3535
clap = { version = "4.5.20", features = ["derive"] }
36+
clap_complete = "4.5.66"
3637
colored = "3.0.0"
3738
comments = { path = "./crates/comments" }
3839
crates = { path = "./crates/crates" }

crates/air/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ publish = true
1414
[dependencies]
1515
anyhow = { workspace = true }
1616
clap = { workspace = true, features = ["wrap_help"] }
17+
clap_complete = { workspace = true }
1718
colored = { workspace = true }
1819
crates = { workspace = true }
1920
fs = { workspace = true }

crates/air/src/args.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub(crate) enum Command {
3737

3838
/// Start a language server
3939
LanguageServer(LanguageServerCommand),
40+
41+
/// Generate shell completion scripts
42+
#[clap(hide = true)]
43+
GenerateShellCompletion(GenerateShellCompletionCommand),
4044
}
4145

4246
#[derive(Clone, Debug, Parser)]
@@ -55,12 +59,17 @@ pub(crate) struct FormatCommand {
5559
#[derive(Clone, Debug, Parser)]
5660
pub(crate) struct LanguageServerCommand {}
5761

62+
#[derive(Clone, Debug, Parser)]
63+
pub(crate) struct GenerateShellCompletionCommand {
64+
/// The shell for which to generate the completion script
65+
pub shell: clap_complete::Shell,
66+
}
67+
5868
/// All configuration options that can be passed "globally"
5969
#[derive(Debug, Default, clap::Args)]
6070
#[command(next_help_heading = "Global options")]
6171
pub(crate) struct GlobalOptions {
62-
/// The log level. One of: `error`, `warn`, `info`, `debug`, or `trace`. Defaults
63-
/// to `warn`.
72+
/// The log level [default: warn]
6473
#[arg(long, global = true)]
6574
pub(crate) log_level: Option<logging::LogLevel>,
6675

crates/air/src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub(crate) mod format;
2+
pub(crate) mod generate_shell_completion;
23
pub(crate) mod language_server;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::args::{Args, GenerateShellCompletionCommand};
2+
use crate::status::ExitStatus;
3+
use clap::CommandFactory;
4+
5+
pub(crate) fn generate_shell_completion(
6+
command: GenerateShellCompletionCommand,
7+
) -> anyhow::Result<ExitStatus> {
8+
clap_complete::generate(
9+
command.shell,
10+
&mut Args::command(),
11+
"air",
12+
&mut std::io::stdout(),
13+
);
14+
Ok(ExitStatus::Success)
15+
}

crates/air/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub fn run(args: Args) -> anyhow::Result<ExitStatus> {
1717
}
1818

1919
match args.command {
20+
Command::GenerateShellCompletion(command) => {
21+
commands::generate_shell_completion::generate_shell_completion(command)
22+
}
2023
Command::Format(command) => commands::format::format(command),
2124
Command::LanguageServer(command) => commands::language_server::language_server(command),
2225
}

crates/air/src/logging.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ fn turn_off_colors(
5858
layer.with_ansi(false)
5959
}
6060

61-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
61+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
6262
pub(crate) enum LogLevel {
6363
Error,
6464
#[default]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::process::Command;
2+
3+
use crate::helpers::CommandExt;
4+
use crate::helpers::binary_path;
5+
6+
#[test]
7+
fn test_completions_help() {
8+
insta::assert_snapshot!(
9+
Command::new(binary_path())
10+
.arg("generate-shell-completion")
11+
.arg("--help")
12+
.run()
13+
.normalize_os_executable_name()
14+
);
15+
}
16+
17+
#[test]
18+
fn test_completions_bash() {
19+
insta::assert_snapshot!(
20+
Command::new(binary_path())
21+
.arg("generate-shell-completion")
22+
.arg("bash")
23+
.run()
24+
.normalize_os_executable_name()
25+
);
26+
}
27+
28+
#[test]
29+
fn test_completions_elvish() {
30+
insta::assert_snapshot!(
31+
Command::new(binary_path())
32+
.arg("generate-shell-completion")
33+
.arg("elvish")
34+
.run()
35+
.normalize_os_executable_name()
36+
);
37+
}
38+
39+
#[test]
40+
fn test_completions_fish() {
41+
insta::assert_snapshot!(
42+
Command::new(binary_path())
43+
.arg("generate-shell-completion")
44+
.arg("fish")
45+
.run()
46+
.normalize_os_executable_name()
47+
);
48+
}
49+
50+
#[test]
51+
fn test_completions_powershell() {
52+
insta::assert_snapshot!(
53+
Command::new(binary_path())
54+
.arg("generate-shell-completion")
55+
.arg("powershell")
56+
.run()
57+
.normalize_os_executable_name()
58+
);
59+
}
60+
61+
#[test]
62+
fn test_completions_zsh() {
63+
insta::assert_snapshot!(
64+
Command::new(binary_path())
65+
.arg("generate-shell-completion")
66+
.arg("zsh")
67+
.run()
68+
.normalize_os_executable_name()
69+
);
70+
}

0 commit comments

Comments
 (0)