Skip to content

Commit 23fc096

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 23fc096

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-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: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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(var: &str, name: &str, completer: &str, buf : &mut dyn Write) -> Result<(), Error> {
30+
let mode = ModeVar(var);
31+
writeln!(
32+
buf,
33+
r#"
34+
# Refresh completer integration for {name} (must be in env.nu)
35+
do {{
36+
# Search for existing script to avoid duplicates in case autoload dirs change
37+
let completer_script_name = '{name}-completer.nu'
38+
let autoload_dir = $nu.user-autoload-dirs
39+
| where {{ path join $completer_script_name | path exists }}
40+
| get 0 --optional
41+
| default ($nu.user-autoload-dirs | get 0 --optional)
42+
mkdir $autoload_dir
43+
44+
let completer_path = ($autoload_dir | path join $completer_script_name)
45+
{var}=nushell {mode}=integration ^r#'{completer}'# | save --raw --force $completer_path
46+
}}
47+
"#)
48+
}
49+
50+
fn write_completion_script(var: &str, name: &str, bin: &str, completer: &str, buf : &mut dyn Write) -> Result<(), Error>{
51+
writeln!(
52+
buf,
53+
r#"
54+
# Determines whether the completer for {name} is supposed to handle the command line
55+
def handles [
56+
spans: list # The spans that were passed to the external completer closure
57+
]: nothing -> bool {{
58+
($spans | get --optional 0) == r#'{bin}'#
59+
}}
60+
61+
# Performs the completion for {name}
62+
def complete [
63+
spans: list # The spans that were passed to the external completer closure
64+
]: nothing -> list {{
65+
{var}=nushell ^r#'{completer}'# -- ...$spans | from json
66+
}}
67+
68+
# Installs this module as an external completer for {name} globally.
69+
#
70+
# For commands other {name}, it will fall back to whatever external completer
71+
# was defined previously (if any).
72+
$env.config = $env.config
73+
| upsert completions.external.enable true
74+
| upsert completions.external.completer {{ |original_config|
75+
let previous_completer = $original_config
76+
| get --optional completions.external.completer
77+
| default {{ |spans| null }}
78+
{{ |spans|
79+
if (handles $spans) {{
80+
complete $spans
81+
}} else {{
82+
do $previous_completer $spans
83+
}}
84+
}}
85+
}}
86+
"#
87+
)
88+
}
89+
90+
impl EnvCompleter for super::Nushell {
91+
fn name(&self) -> &'static str {
92+
"nushell"
93+
}
94+
95+
fn is(&self, name: &str) -> bool {
96+
name.eq_ignore_ascii_case("nushell") || name.eq_ignore_ascii_case("nu")
97+
}
98+
99+
fn write_registration(
100+
&self,
101+
var: &str,
102+
name: &str,
103+
bin: &str,
104+
completer: &str,
105+
buf: &mut dyn Write,
106+
) -> Result<(), Error> {
107+
let mode_var = format!("{}", ModeVar(var));
108+
if std::env::var_os(&mode_var).as_ref().map(|x|x.as_os_str()) == Some(OsStr::new("integration")) {
109+
write_completion_script(var, name, bin, completer, buf)
110+
} else {
111+
write_refresh_completion_integration(var, name, completer, buf)
112+
}
113+
}
114+
115+
fn write_complete(
116+
&self,
117+
cmd: &mut Command,
118+
args: Vec<OsString>,
119+
current_dir: Option<&Path>,
120+
buf: &mut dyn Write,
121+
) -> Result<(), Error> {
122+
let idx = (args.len() - 1).max(0);
123+
let candidates = clap_complete::engine::complete(cmd, args, idx, current_dir)?;
124+
let mut strbuf = String::new();
125+
{
126+
let mut records = write_json::array(&mut strbuf);
127+
for candidate in candidates {
128+
let mut record = records.object();
129+
record.string("value", candidate.get_value().to_string_lossy().as_ref());
130+
if let Some(help) = candidate.get_help() {
131+
record.string("description", &help.to_string()[..]);
132+
}
133+
}
134+
}
135+
write!(buf, "{strbuf}")
136+
}
137+
}

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)