Skip to content

Commit 3ef74cb

Browse files
committed
feat(complete): Add dynamic completion for nushell
Adds an implementation of dynamic completion to `clap_complete_nushell` under the `unstable-dynamic` feature flag. The user runs ```nushell COMPLETE=nushell the-clap-tool | save --append --raw $nu.env-path ``` and the dynamic completion will emit an "auto-update" script into the user's `env.nu`. The auto-update script snippet will maintain a script in the user's autoload directory that installs a command-scoped completer.
1 parent 8e3d036 commit 3ef74cb

File tree

4 files changed

+143
-0
lines changed

4 files changed

+143
-0
lines changed

Cargo.lock

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

clap_complete_nushell/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ clap = { path = "../", version = "4.0.0", default-features = false, features = [
3636
clap_complete = { path = "../clap_complete", version = "4.5.51" }
3737
completest = { version = "0.4.0", optional = true }
3838
completest-nu = { version = "0.4.0", optional = true }
39+
write-json = { version = "0.1.4", optional = true }
3940

4041
[dev-dependencies]
4142
snapbox = { version = "0.6.0", features = ["diff", "examples", "dir"] }
4243
clap = { path = "../", version = "4.0.0", default-features = false, features = ["std", "help"] }
4344

4445
[features]
4546
default = []
47+
unstable-dynamic = ["clap_complete/unstable-dynamic", "dep:write-json"]
4648
unstable-shell-tests = ["dep:completest", "dep:completest-nu"]
4749

4850
[lints]
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Implements dynamic completion for Nushell.
2+
//!
3+
//! There is no direct equivalent of other shells' `source $(COMPLETE=... your-clap-bin)` in nushell,
4+
//! because code being sourced must exist at parse-time.
5+
//!
6+
//! One way to get close to that is to split the completion integration into two parts:
7+
//! 1. a minimal part that goes into `env.nu`, which updates the actual completion integration
8+
//! 2. the completion integration, which is placed into the user's autoload directory
9+
//!
10+
//! To install the completion integration, the user runs
11+
//! ```nu
12+
//! COMPLETE=nushell your-clap-bin | save --raw --force --append $nu.env-path
13+
//! ```
14+
15+
use clap::Command;
16+
use clap_complete::env::EnvCompleter;
17+
use std::ffi::{OsStr, OsString};
18+
use std::fmt::Display;
19+
use std::io::{Error, Write};
20+
use std::path::Path;
21+
22+
struct ModeVar<'a>(&'a str);
23+
impl<'a> Display for ModeVar<'a> {
24+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25+
write!(f, "_{0}__mode", self.0)
26+
}
27+
}
28+
29+
fn write_refresh_completion_integration(
30+
var: &str,
31+
name: &str,
32+
completer: &str,
33+
buf: &mut dyn Write,
34+
) -> Result<(), Error> {
35+
let mode = ModeVar(var);
36+
writeln!(
37+
buf,
38+
r#"
39+
# Refresh completer integration for {name} (must be in env.nu)
40+
do {{
41+
# Search for existing script to avoid duplicates in case autoload dirs change
42+
let completer_script_name = '{name}-completer.nu'
43+
let autoload_dir = $nu.user-autoload-dirs
44+
| where {{ path join $completer_script_name | path exists }}
45+
| get 0 --optional
46+
| default ($nu.user-autoload-dirs | get 0 --optional)
47+
mkdir $autoload_dir
48+
49+
let completer_path = ($autoload_dir | path join $completer_script_name)
50+
{var}=nushell {mode}=integration ^r#'{completer}'# | save --raw --force $completer_path
51+
}}
52+
"#
53+
)
54+
}
55+
56+
fn write_completion_script(
57+
var: &str,
58+
name: &str,
59+
_bin: &str,
60+
completer: &str,
61+
buf: &mut dyn Write,
62+
) -> Result<(), Error> {
63+
writeln!(
64+
buf,
65+
r#"
66+
# Performs the completion for {name}
67+
def {name}-completer [
68+
spans: list<string> # The spans that were passed to the external completer closure
69+
]: nothing -> list {{
70+
{var}=nushell ^r#'{completer}'# -- ...$spans | from json
71+
}}
72+
73+
@complete {name}-completer
74+
def --wrapped {name} [...args] {{
75+
^r#'{completer}'# ...$args
76+
}}
77+
"#
78+
)
79+
}
80+
81+
impl EnvCompleter for super::Nushell {
82+
fn name(&self) -> &'static str {
83+
"nushell"
84+
}
85+
86+
fn is(&self, name: &str) -> bool {
87+
name.eq_ignore_ascii_case("nushell") || name.eq_ignore_ascii_case("nu")
88+
}
89+
90+
fn write_registration(
91+
&self,
92+
var: &str,
93+
name: &str,
94+
bin: &str,
95+
completer: &str,
96+
buf: &mut dyn Write,
97+
) -> Result<(), Error> {
98+
let mode_var = format!("{}", ModeVar(var));
99+
if std::env::var_os(&mode_var).as_ref().map(|x| x.as_os_str())
100+
== Some(OsStr::new("integration"))
101+
{
102+
write_completion_script(var, name, bin, completer, buf)
103+
} else {
104+
write_refresh_completion_integration(var, name, completer, buf)
105+
}
106+
}
107+
108+
fn write_complete(
109+
&self,
110+
cmd: &mut Command,
111+
args: Vec<OsString>,
112+
current_dir: Option<&Path>,
113+
buf: &mut dyn Write,
114+
) -> Result<(), Error> {
115+
let idx = (args.len() - 1).max(0);
116+
let candidates = clap_complete::engine::complete(cmd, args, idx, current_dir)?;
117+
let mut strbuf = String::new();
118+
{
119+
let mut records = write_json::array(&mut strbuf);
120+
for candidate in candidates {
121+
let mut record = records.object();
122+
record.string("value", candidate.get_value().to_string_lossy().as_ref());
123+
if let Some(help) = candidate.get_help() {
124+
record.string("description", &help.to_string()[..]);
125+
}
126+
}
127+
}
128+
write!(buf, "{strbuf}")
129+
}
130+
}

clap_complete_nushell/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ use clap::{builder::PossibleValue, Arg, ArgAction, Command};
2828
use clap_complete::Generator;
2929

3030
/// Generate Nushell complete file
31+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
3132
pub struct Nushell;
3233

34+
#[cfg(feature = "unstable-dynamic")]
35+
mod dynamic;
36+
3337
impl Generator for Nushell {
3438
fn file_name(&self, name: &str) -> String {
3539
format!("{name}.nu")

0 commit comments

Comments
 (0)