From 634e750a70f3e906bdb51620416f40113ba15f18 Mon Sep 17 00:00:00 2001 From: Christian Klauser Date: Wed, 11 Dec 2024 00:12:15 +0100 Subject: [PATCH] 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. --- Cargo.lock | 7 ++ clap_complete_nushell/Cargo.toml | 2 + clap_complete_nushell/src/dynamic.rs | 138 +++++++++++++++++++++++++++ clap_complete_nushell/src/lib.rs | 4 + 4 files changed, 151 insertions(+) create mode 100644 clap_complete_nushell/src/dynamic.rs diff --git a/Cargo.lock b/Cargo.lock index eb35217b707..933030db42c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,7 @@ dependencies = [ "completest", "completest-nu", "snapbox", + "write-json", ] [[package]] @@ -4191,6 +4192,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write-json" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f6174b2566cc4a74f95e1367ec343e7fa80c93cc8087f5c4a3d6a1088b2118" + [[package]] name = "xattr" version = "1.3.1" diff --git a/clap_complete_nushell/Cargo.toml b/clap_complete_nushell/Cargo.toml index 8ff48864b53..ded932e753b 100644 --- a/clap_complete_nushell/Cargo.toml +++ b/clap_complete_nushell/Cargo.toml @@ -36,6 +36,7 @@ clap = { path = "../", version = "4.0.0", default-features = false, features = [ clap_complete = { path = "../clap_complete", version = "4.5.51" } completest = { version = "0.4.0", optional = true } completest-nu = { version = "0.4.0", optional = true } +write-json = { version = "0.1.4", optional = true } [dev-dependencies] snapbox = { version = "0.6.0", features = ["diff", "examples", "dir"] } @@ -43,6 +44,7 @@ clap = { path = "../", version = "4.0.0", default-features = false, features = [ [features] default = [] +unstable-dynamic = ["clap_complete/unstable-dynamic", "dep:write-json"] unstable-shell-tests = ["dep:completest", "dep:completest-nu"] [lints] diff --git a/clap_complete_nushell/src/dynamic.rs b/clap_complete_nushell/src/dynamic.rs new file mode 100644 index 00000000000..5119c04aa6d --- /dev/null +++ b/clap_complete_nushell/src/dynamic.rs @@ -0,0 +1,138 @@ +//! Implements dynamic completion for Nushell. +//! +//! There is no direct equivalent of other shells' `source $(COMPLETE=... your-clap-bin)` in nushell, +//! because code being sourced must exist at parse-time. +//! +//! One way to get close to that is to split the completion integration into two parts: +//! 1. a minimal part that goes into `env.nu`, which updates the actual completion integration +//! 2. the completion integration, which is placed into the user's autoload directory +//! +//! To install the completion integration, the user runs +//! ```nu +//! COMPLETE=nushell your-clap-bin | save --raw --force --append $nu.env-path +//! ``` + +// Std +use std::ffi::{OsStr, OsString}; +use std::fmt::Display; +use std::io::{Error, Write}; +use std::path::Path; + +// External +use clap::Command; +use clap_complete::env::EnvCompleter; + +/// Generate integration for dynamic completion in Nushell +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Nushell; + +impl EnvCompleter for Nushell { + fn name(&self) -> &'static str { + "nushell" + } + + fn is(&self, name: &str) -> bool { + name == "nushell" + } + + fn write_registration( + &self, + var: &str, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn Write, + ) -> Result<(), Error> { + let mode_var = ModeVar(var).to_string(); + if std::env::var_os(&mode_var).as_ref().map(|x| x.as_os_str()) + == Some(OsStr::new("integration")) + { + write_completion_script(var, name, bin, completer, buf) + } else { + write_refresh_completion_integration(var, name, completer, buf) + } + } + + fn write_complete( + &self, + cmd: &mut Command, + args: Vec, + current_dir: Option<&Path>, + buf: &mut dyn Write, + ) -> Result<(), Error> { + let idx = args.len().saturating_sub(1).max(0); + let candidates = clap_complete::engine::complete(cmd, args, idx, current_dir)?; + let mut strbuf = String::new(); + { + let mut records = write_json::array(&mut strbuf); + for candidate in candidates { + let mut record = records.object(); + record.string("value", candidate.get_value().to_string_lossy().as_ref()); + if let Some(help) = candidate.get_help() { + record.string("description", &help.to_string()[..]); + } + } + } + write!(buf, "{strbuf}") + } +} + +struct ModeVar<'a>(&'a str); + +impl<'a> Display for ModeVar<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "_{0}__mode", self.0) + } +} + +fn write_refresh_completion_integration( + var: &str, + name: &str, + completer: &str, + buf: &mut dyn Write, +) -> Result<(), Error> { + let mode = ModeVar(var); + writeln!( + buf, + r#" +# Refresh completer integration for {name} (must be in env.nu) +do {{ + # Search for existing script to avoid duplicates in case autoload dirs change + let completer_script_name = '{name}-completer.nu' + let autoload_dir = $nu.user-autoload-dirs + | where {{ path join $completer_script_name | path exists }} + | get 0 --optional + | default ($nu.user-autoload-dirs | get 0 --optional) + mkdir $autoload_dir + + let completer_path = ($autoload_dir | path join $completer_script_name) + {var}=nushell {mode}=integration ^r#'{completer}'# | save --raw --force $completer_path +}} + "# + ) +} + +fn write_completion_script( + var: &str, + name: &str, + _bin: &str, + completer: &str, + buf: &mut dyn Write, +) -> Result<(), Error> { + writeln!( + buf, + r#" +# Performs the completion for {name} +def {name}-completer [ + spans: list # The spans that were passed to the external completer closure +]: nothing -> list {{ + {var}=nushell ^r#'{completer}'# -- ...$spans | from json +}} + +@complete {name}-completer +def --wrapped {name} [...args] {{ + ^r#'{completer}'# ...$args +}} +"# + ) +} diff --git a/clap_complete_nushell/src/lib.rs b/clap_complete_nushell/src/lib.rs index cc293df5361..c61dcbaffc2 100644 --- a/clap_complete_nushell/src/lib.rs +++ b/clap_complete_nushell/src/lib.rs @@ -28,8 +28,12 @@ use clap::{builder::PossibleValue, Arg, ArgAction, Command}; use clap_complete::Generator; /// Generate Nushell complete file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Nushell; +#[cfg(feature = "unstable-dynamic")] +pub mod dynamic; + impl Generator for Nushell { fn file_name(&self, name: &str) -> String { format!("{name}.nu")