diff --git a/Cargo.lock b/Cargo.lock index bd3ef88d449..512f06d1444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,6 +505,7 @@ dependencies = [ "completest", "completest-nu", "snapbox", + "write-json", ] [[package]] @@ -4043,6 +4044,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 5cdba436160..5f05559d8f8 100644 --- a/clap_complete_nushell/Cargo.toml +++ b/clap_complete_nushell/Cargo.toml @@ -35,6 +35,7 @@ clap = { path = "../", version = "4.0.0", default-features = false, features = [ clap_complete = { path = "../clap_complete", version = "4.0.0" } 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"] } @@ -42,6 +43,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..87584a1ef50 --- /dev/null +++ b/clap_complete_nushell/src/dynamic.rs @@ -0,0 +1,127 @@ +use clap::Command; +use clap_complete::env::EnvCompleter; +use std::ffi::OsString; +use std::io::{Error, Write}; +use std::path::Path; + +impl EnvCompleter for super::Nushell { + fn name(&self) -> &'static str { + "nushell" + } + + fn is(&self, name: &str) -> bool { + name.eq_ignore_ascii_case("nushell") || name.eq_ignore_ascii_case("nu") + } + + fn write_registration( + &self, + var: &str, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn Write, + ) -> Result<(), Error> { + writeln!( + buf, + r#" +# External completer for {name} +# +# This module can either be `source`d for a simplified installation or loaded as a module (`use`) +# to be integrated into your existing external completer setup. +# +# Example 1 (simplified installation): +# ``` +# use {name}-completer.nu # (replace with path to this file) +# {name}-completer install +# ``` +# +# Example 2 (integrate with custom external completer): +# ``` +# use {name}-completer.nu # (replace with path to this file) +# $env.config.completions.external.enable = true +# $env.config.completions.external.completer = {{ |spans| +# if ({name}-completer handles $spans) {{ +# {name}-completer complete $spans +# }} else {{ +# # any other external completers +# }} +# }} +# ``` + +# Workaround for https://github.com/nushell/nushell/issues/8483 +def expand-alias []: list -> list {{ + let spans = $in + if ($spans | length) == 0 {{ + return $spans + }} + + let expanded_alias = (scope aliases | where name == $spans.0 | get --ignore-errors 0.expansion) + if $expanded_alias != null {{ + # put the first word of the expanded alias first in the span + $spans | skip 1 | prepend ($expanded_alias | split row " " | take 1) + }} else {{ + $spans + }} +}} + +# Determines whether the completer for {name} is supposed to handle the command line +export def handles [ + spans: list # The spans that were passed to the external completer closure +]: nothing -> bool {{ + ($spans | expand-alias | get --ignore-errors 0) == r#'{bin}'# +}} + +# Performs the completion for {name} +export def complete [ + spans: list # The spans that were passed to the external completer closure +]: nothing -> list {{ + {var}=nushell ^r#'{completer}'# -- ...$spans | from json +}} + +# Installs this module as an external completer for {name} globally. +# +# For commands other {name}, it will fall back to whatever external completer +# was defined previously (if any). +export def --env install []: nothing -> nothing {{ + $env.config = $env.config + | upsert completions.external.enable true + | upsert completions.external.completer {{ |original_config| + let previous_completer = $original_config + | get --ignore-errors completions.external.completer + | default {{ |spans| null }} + {{ |spans| + if (handles $spans) {{ + complete $spans + }} else {{ + do $previous_completer $spans + }} + }} + }} +}} +"# + ) + } + + fn write_complete( + &self, + cmd: &mut Command, + args: Vec, + current_dir: Option<&Path>, + buf: &mut dyn Write, + ) -> Result<(), Error> { + let idx = (args.len() - 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}") + } +} diff --git a/clap_complete_nushell/src/lib.rs b/clap_complete_nushell/src/lib.rs index e7f064e2d4a..25a492bf283 100644 --- a/clap_complete_nushell/src/lib.rs +++ b/clap_complete_nushell/src/lib.rs @@ -27,8 +27,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")] +mod dynamic; + impl Generator for Nushell { fn file_name(&self, name: &str) -> String { format!("{name}.nu")