From 0de971942a79c5366e29c02cae197285204c3105 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 8 Jun 2024 23:56:21 +0200 Subject: [PATCH] Allow printing nu completion script with `just --completions nushell` (#2140) --- .github/workflows/release.yaml | 3 +- Cargo.lock | 13 +++++ Cargo.toml | 2 +- completions/just.nu | 8 --- justfile | 3 - src/completions.rs | 103 +++++++++++++++++++++++++++++++-- src/config.rs | 36 ++++++++---- src/lib.rs | 35 ++++++----- src/subcommand.rs | 73 ++--------------------- tests/completions.rs | 2 +- 10 files changed, 162 insertions(+), 116 deletions(-) delete mode 100644 completions/just.nu diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4e58e6f..7905090 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -77,7 +77,8 @@ jobs: run: | set -euxo pipefail cargo build - for shell in bash elvish fish powershell zsh; do + mkdir -p completions + for shell in bash elvish fish nu powershell zsh; do ./target/debug/just --completions $shell > completions/just.$shell done mkdir -p man diff --git a/Cargo.lock b/Cargo.lock index dd55d16..86be2a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -250,6 +251,18 @@ dependencies = [ "clap 4.5.4", ] +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "clap_lex" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index b553875..f497cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ ansi_term = "0.12.0" blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } camino = "1.0.4" chrono = "0.4.38" -clap = { version = "4.0.0", features = ["env", "wrap_help"] } +clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] } clap_complete = "4.0.0" clap_mangen = "0.2.20" ctrlc = { version = "3.1.1", features = ["termination"] } diff --git a/completions/just.nu b/completions/just.nu deleted file mode 100644 index 82b7266..0000000 --- a/completions/just.nu +++ /dev/null @@ -1,8 +0,0 @@ -def "nu-complete just" [] { - (^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description -} - -# Just: A Command Runner -export extern "just" [ - ...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s) -] diff --git a/justfile b/justfile index e711cf9..d7f6725 100755 --- a/justfile +++ b/justfile @@ -165,9 +165,6 @@ watch-readme: just render-readme fswatch -ro README.adoc | xargs -n1 -I{} just render-readme -update-completions: - ./bin/update-completions - test-completions: ./tests/completions/just.bash diff --git a/src/completions.rs b/src/completions.rs index 23f2902..193700d 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -1,4 +1,99 @@ -pub(crate) const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes +use {super::*, clap::ValueEnum}; + +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)] +pub(crate) enum Shell { + Bash, + Elvish, + Fish, + #[value(alias = "nu")] + Nushell, + Powershell, + Zsh, +} + +impl Shell { + pub(crate) fn script(self) -> RunResult<'static, String> { + match self { + Self::Bash => completions::clap(clap_complete::Shell::Bash), + Self::Elvish => completions::clap(clap_complete::Shell::Elvish), + Self::Fish => completions::clap(clap_complete::Shell::Fish), + Self::Nushell => Ok(completions::NUSHELL_COMPLETION_SCRIPT.into()), + Self::Powershell => completions::clap(clap_complete::Shell::PowerShell), + Self::Zsh => completions::clap(clap_complete::Shell::Zsh), + } + } +} + +fn clap(shell: clap_complete::Shell) -> RunResult<'static, String> { + fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { + if let Some(index) = haystack.find(needle) { + haystack.replace_range(index..index + needle.len(), replacement); + Ok(()) + } else { + Err(Error::internal(format!( + "Failed to find text:\n{needle}\n…in completion script:\n{haystack}" + ))) + } + } + + let mut script = { + let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?; + + clap_complete::generate( + shell, + &mut crate::config::Config::app(), + env!("CARGO_PKG_NAME"), + &mut tempfile, + ); + + tempfile + .rewind() + .map_err(|io_error| Error::TempfileIo { io_error })?; + + let mut buffer = String::new(); + + tempfile + .read_to_string(&mut buffer) + .map_err(|io_error| Error::TempfileIo { io_error })?; + + buffer + }; + + match shell { + clap_complete::Shell::Bash => { + for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS { + replace(&mut script, needle, replacement)?; + } + } + clap_complete::Shell::Fish => { + script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS); + } + clap_complete::Shell::PowerShell => { + for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS { + replace(&mut script, needle, replacement)?; + } + } + clap_complete::Shell::Zsh => { + for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS { + replace(&mut script, needle, replacement)?; + } + } + _ => {} + } + + Ok(script.trim().into()) +} + +const NUSHELL_COMPLETION_SCRIPT: &str = r#"def "nu-complete just" [] { + (^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description +} + +# Just: A Command Runner +export extern "just" [ + ...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s) +]"#; + +const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes just --list 2> /dev/null | tail -n +2 | awk '{ command = $1; args = $0; @@ -37,7 +132,7 @@ complete -c just -a '(__fish_just_complete_recipes)' # autogenerated completions "#; -pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ +const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ ( r#" _arguments "${_arguments_options[@]}" \"#, r" local common=(", @@ -151,7 +246,7 @@ _just "$@""#, ), ]; -pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( +const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText"#, r#"function Get-JustFileRecipes([string[]]$CommandElements) { @@ -178,7 +273,7 @@ pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( Sort-Object -Property ListItemText"#, )]; -pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ +const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ ( r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) diff --git a/src/config.rs b/src/config.rs index 6c19c70..d2056d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -381,10 +381,9 @@ impl Config { .arg( Arg::new(cmd::COMPLETIONS) .long("completions") - .action(ArgAction::Append) - .num_args(1..) + .action(ArgAction::Set) .value_name("SHELL") - .value_parser(value_parser!(clap_complete::Shell)) + .value_parser(value_parser!(completions::Shell)) .ignore_case(true) .help("Print shell completion script for "), ) @@ -686,7 +685,7 @@ impl Config { arguments, overrides, } - } else if let Some(&shell) = matches.get_one::(cmd::COMPLETIONS) { + } else if let Some(&shell) = matches.get_one::(cmd::COMPLETIONS) { Subcommand::Completions { shell } } else if matches.get_flag(cmd::EDIT) { Subcommand::Edit @@ -1255,13 +1254,13 @@ mod tests { test! { name: subcommand_completions, args: ["--completions", "bash"], - subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, + subcommand: Subcommand::Completions{ shell: completions::Shell::Bash }, } test! { name: subcommand_completions_uppercase, args: ["--completions", "BASH"], - subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, + subcommand: Subcommand::Completions{ shell: completions::Shell::Bash }, } error! { @@ -1544,15 +1543,30 @@ mod tests { } error_matches! { - name: completions_arguments, - args: ["--completions", "zsh", "foo"], + name: completions_argument, + args: ["--completions", "foo"], error: error, check: { assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); assert_eq!(error.context().collect::>(), vec![ - (ContextKind::InvalidArg, &ContextValue::String("--completions ...".into())), - (ContextKind::InvalidValue, &ContextValue::String("foo".into())), - (ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())), + ( + ContextKind::InvalidArg, + &ContextValue::String("--completions ".into())), + ( + ContextKind::InvalidValue, + &ContextValue::String("foo".into()), + ), + ( + ContextKind::ValidValue, + &ContextValue::Strings([ + "bash".into(), + "elvish".into(), + "fish".into(), + "nushell".into(), + "powershell".into(), + "zsh".into()].into() + ), + ), ]); }, } diff --git a/src/lib.rs b/src/lib.rs index b0faaf0..a315a84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,18 @@ pub(crate) use { unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }, + camino::Utf8Path, + derivative::Derivative, + edit_distance::edit_distance, + lexiclean::Lexiclean, + libc::EXIT_FAILURE, + log::{info, warn}, + regex::Regex, + serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, Serializer, + }, + snafu::{ResultExt, Snafu}, std::{ borrow::Cow, cmp, @@ -47,7 +59,7 @@ pub(crate) use { ffi::OsString, fmt::{self, Debug, Display, Formatter}, fs, - io::{self, Write}, + io::{self, Read, Seek, Write}, iter::{self, FromIterator}, mem, ops::Deref, @@ -59,23 +71,10 @@ pub(crate) use { sync::{Mutex, MutexGuard, OnceLock}, vec, }, - { - camino::Utf8Path, - derivative::Derivative, - edit_distance::edit_distance, - lexiclean::Lexiclean, - libc::EXIT_FAILURE, - log::{info, warn}, - regex::Regex, - serde::{ - ser::{SerializeMap, SerializeSeq}, - Serialize, Serializer, - }, - snafu::{ResultExt, Snafu}, - strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr}, - typed_arena::Arena, - unicode_width::{UnicodeWidthChar, UnicodeWidthStr}, - }, + strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr}, + tempfile::tempfile, + typed_arena::Arena, + unicode_width::{UnicodeWidthChar, UnicodeWidthStr}, }; #[cfg(test)] diff --git a/src/subcommand.rs b/src/subcommand.rs index 77658ac..e143315 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,9 +1,4 @@ -use { - super::*, - clap_mangen::Man, - std::io::{Read, Seek}, - tempfile::tempfile, -}; +use {super::*, clap_mangen::Man}; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; @@ -20,7 +15,7 @@ pub(crate) enum Subcommand { overrides: BTreeMap, }, Completions { - shell: clap_complete::Shell, + shell: completions::Shell, }, Dump, Edit, @@ -296,68 +291,8 @@ impl Subcommand { justfile.run(config, search, overrides, &recipes) } - fn completions(shell: clap_complete::Shell) -> RunResult<'static, ()> { - use clap_complete::Shell; - - fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { - if let Some(index) = haystack.find(needle) { - haystack.replace_range(index..index + needle.len(), replacement); - Ok(()) - } else { - Err(Error::internal(format!( - "Failed to find text:\n{needle}\n…in completion script:\n{haystack}" - ))) - } - } - - let mut script = { - let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?; - - clap_complete::generate( - shell, - &mut crate::config::Config::app(), - env!("CARGO_PKG_NAME"), - &mut tempfile, - ); - - tempfile - .rewind() - .map_err(|io_error| Error::TempfileIo { io_error })?; - - let mut buffer = String::new(); - - tempfile - .read_to_string(&mut buffer) - .map_err(|io_error| Error::TempfileIo { io_error })?; - - buffer - }; - - match shell { - Shell::Bash => { - for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; - } - } - Shell::Fish => { - script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS); - } - Shell::PowerShell => { - for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; - } - } - - Shell::Zsh => { - for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; - } - } - _ => {} - } - - println!("{}", script.trim()); - + fn completions(shell: completions::Shell) -> RunResult<'static, ()> { + println!("{}", shell.script()?); Ok(()) } diff --git a/tests/completions.rs b/tests/completions.rs index 96d35ca..9de2787 100644 --- a/tests/completions.rs +++ b/tests/completions.rs @@ -28,7 +28,7 @@ fn bash() { #[test] fn replacements() { - for shell in ["bash", "elvish", "fish", "powershell", "zsh"] { + for shell in ["bash", "elvish", "fish", "nushell", "powershell", "zsh"] { let output = Command::new(executable_path("just")) .args(["--completions", shell]) .output()