Skip to content

Commit f58c6ee

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. Nushell currently doesn't have a good story for adding what nushell calls "external completers" in a modular fashion. Users can define a single, global external completer. If they wish to combine multiple external completers, they have to manually write a sort of meta-completer that dispatches to the individual completers based on the first argument (the binary). This PR generates a nushell module that offers three commands: - `complete`, which performs the actual completion - `handles`, which asks the module whether it is the correct completer for a particular command line - `install`, which globally registers a completer that falls back to whatever completer was previously installed if `handles` rejects completing a command line. The idea is that user's who already have a custom external completer can integrate the generated module's `handles` and `complete` commands into their completer. Everyone else just puts ```nushell use my-app-completer.nu my-app-completer install ``` into their nushell config.
1 parent 8e3d036 commit f58c6ee

File tree

4 files changed

+164
-0
lines changed

4 files changed

+164
-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: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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+
# Determines whether the completer for {name} is supposed to handle the command line
67+
def handles [
68+
spans: list # The spans that were passed to the external completer closure
69+
]: nothing -> bool {{
70+
($spans | get --optional 0) == r#'{bin}'#
71+
}}
72+
73+
# Performs the completion for {name}
74+
def complete [
75+
spans: list # The spans that were passed to the external completer closure
76+
]: nothing -> list {{
77+
{var}=nushell ^r#'{completer}'# -- ...$spans | from json
78+
}}
79+
80+
# Installs this module as an external completer for {name} globally.
81+
#
82+
# For commands other {name}, it will fall back to whatever external completer
83+
# was defined previously (if any).
84+
$env.config = $env.config
85+
| upsert completions.external.enable true
86+
| upsert completions.external.completer {{ |original_config|
87+
let previous_completer = $original_config
88+
| get --optional completions.external.completer
89+
| default {{ |spans| null }}
90+
{{ |spans|
91+
if (handles $spans) {{
92+
complete $spans
93+
}} else {{
94+
do $previous_completer $spans
95+
}}
96+
}}
97+
}}
98+
"#
99+
)
100+
}
101+
102+
impl EnvCompleter for super::Nushell {
103+
fn name(&self) -> &'static str {
104+
"nushell"
105+
}
106+
107+
fn is(&self, name: &str) -> bool {
108+
name.eq_ignore_ascii_case("nushell") || name.eq_ignore_ascii_case("nu")
109+
}
110+
111+
fn write_registration(
112+
&self,
113+
var: &str,
114+
name: &str,
115+
bin: &str,
116+
completer: &str,
117+
buf: &mut dyn Write,
118+
) -> Result<(), Error> {
119+
let mode_var = format!("{}", ModeVar(var));
120+
if std::env::var_os(&mode_var).as_ref().map(|x| x.as_os_str())
121+
== Some(OsStr::new("integration"))
122+
{
123+
write_completion_script(var, name, bin, completer, buf)
124+
} else {
125+
write_refresh_completion_integration(var, name, completer, buf)
126+
}
127+
}
128+
129+
fn write_complete(
130+
&self,
131+
cmd: &mut Command,
132+
args: Vec<OsString>,
133+
current_dir: Option<&Path>,
134+
buf: &mut dyn Write,
135+
) -> Result<(), Error> {
136+
let idx = (args.len() - 1).max(0);
137+
let candidates = clap_complete::engine::complete(cmd, args, idx, current_dir)?;
138+
let mut strbuf = String::new();
139+
{
140+
let mut records = write_json::array(&mut strbuf);
141+
for candidate in candidates {
142+
let mut record = records.object();
143+
record.string("value", candidate.get_value().to_string_lossy().as_ref());
144+
if let Some(help) = candidate.get_help() {
145+
record.string("description", &help.to_string()[..]);
146+
}
147+
}
148+
}
149+
write!(buf, "{strbuf}")
150+
}
151+
}

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)