|
| 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 | +} |
0 commit comments