Allow printing nu completion script with just --completions nushell (#2140)

This commit is contained in:
Casey Rodarmor 2024-06-08 23:56:21 +02:00 committed by GitHub
parent 1ca53e8b22
commit 0de971942a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 162 additions and 116 deletions

View File

@ -77,7 +77,8 @@ jobs:
run: | run: |
set -euxo pipefail set -euxo pipefail
cargo build 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 ./target/debug/just --completions $shell > completions/just.$shell
done done
mkdir -p man mkdir -p man

13
Cargo.lock generated
View File

@ -226,6 +226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
] ]
[[package]] [[package]]
@ -250,6 +251,18 @@ dependencies = [
"clap 4.5.4", "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]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.0" version = "0.7.0"

View File

@ -22,7 +22,7 @@ ansi_term = "0.12.0"
blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
camino = "1.0.4" camino = "1.0.4"
chrono = "0.4.38" 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_complete = "4.0.0"
clap_mangen = "0.2.20" clap_mangen = "0.2.20"
ctrlc = { version = "3.1.1", features = ["termination"] } ctrlc = { version = "3.1.1", features = ["termination"] }

View File

@ -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)
]

View File

@ -165,9 +165,6 @@ watch-readme:
just render-readme just render-readme
fswatch -ro README.adoc | xargs -n1 -I{} just render-readme fswatch -ro README.adoc | xargs -n1 -I{} just render-readme
update-completions:
./bin/update-completions
test-completions: test-completions:
./tests/completions/just.bash ./tests/completions/just.bash

View File

@ -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 '{ just --list 2> /dev/null | tail -n +2 | awk '{
command = $1; command = $1;
args = $0; args = $0;
@ -37,7 +132,7 @@ complete -c just -a '(__fish_just_complete_recipes)'
# autogenerated completions # autogenerated completions
"#; "#;
pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
( (
r#" _arguments "${_arguments_options[@]}" \"#, r#" _arguments "${_arguments_options[@]}" \"#,
r" local common=(", 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*" } | r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText"#, Sort-Object -Property ListItemText"#,
r#"function Get-JustFileRecipes([string[]]$CommandElements) { r#"function Get-JustFileRecipes([string[]]$CommandElements) {
@ -178,7 +273,7 @@ pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
Sort-Object -Property ListItemText"#, 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 r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )

View File

@ -381,10 +381,9 @@ impl Config {
.arg( .arg(
Arg::new(cmd::COMPLETIONS) Arg::new(cmd::COMPLETIONS)
.long("completions") .long("completions")
.action(ArgAction::Append) .action(ArgAction::Set)
.num_args(1..)
.value_name("SHELL") .value_name("SHELL")
.value_parser(value_parser!(clap_complete::Shell)) .value_parser(value_parser!(completions::Shell))
.ignore_case(true) .ignore_case(true)
.help("Print shell completion script for <SHELL>"), .help("Print shell completion script for <SHELL>"),
) )
@ -686,7 +685,7 @@ impl Config {
arguments, arguments,
overrides, overrides,
} }
} else if let Some(&shell) = matches.get_one::<clap_complete::Shell>(cmd::COMPLETIONS) { } else if let Some(&shell) = matches.get_one::<completions::Shell>(cmd::COMPLETIONS) {
Subcommand::Completions { shell } Subcommand::Completions { shell }
} else if matches.get_flag(cmd::EDIT) { } else if matches.get_flag(cmd::EDIT) {
Subcommand::Edit Subcommand::Edit
@ -1255,13 +1254,13 @@ mod tests {
test! { test! {
name: subcommand_completions, name: subcommand_completions,
args: ["--completions", "bash"], args: ["--completions", "bash"],
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
} }
test! { test! {
name: subcommand_completions_uppercase, name: subcommand_completions_uppercase,
args: ["--completions", "BASH"], args: ["--completions", "BASH"],
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
} }
error! { error! {
@ -1544,15 +1543,30 @@ mod tests {
} }
error_matches! { error_matches! {
name: completions_arguments, name: completions_argument,
args: ["--completions", "zsh", "foo"], args: ["--completions", "foo"],
error: error, error: error,
check: { check: {
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
assert_eq!(error.context().collect::<Vec<_>>(), vec![ assert_eq!(error.context().collect::<Vec<_>>(), vec![
(ContextKind::InvalidArg, &ContextValue::String("--completions <SHELL>...".into())), (
(ContextKind::InvalidValue, &ContextValue::String("foo".into())), ContextKind::InvalidArg,
(ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())), &ContextValue::String("--completions <SHELL>".into())),
(
ContextKind::InvalidValue,
&ContextValue::String("foo".into()),
),
(
ContextKind::ValidValue,
&ContextValue::Strings([
"bash".into(),
"elvish".into(),
"fish".into(),
"nushell".into(),
"powershell".into(),
"zsh".into()].into()
),
),
]); ]);
}, },
} }

View File

@ -39,6 +39,18 @@ pub(crate) use {
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning, 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::{ std::{
borrow::Cow, borrow::Cow,
cmp, cmp,
@ -47,7 +59,7 @@ pub(crate) use {
ffi::OsString, ffi::OsString,
fmt::{self, Debug, Display, Formatter}, fmt::{self, Debug, Display, Formatter},
fs, fs,
io::{self, Write}, io::{self, Read, Seek, Write},
iter::{self, FromIterator}, iter::{self, FromIterator},
mem, mem,
ops::Deref, ops::Deref,
@ -59,23 +71,10 @@ pub(crate) use {
sync::{Mutex, MutexGuard, OnceLock}, sync::{Mutex, MutexGuard, OnceLock},
vec, vec,
}, },
{ strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
camino::Utf8Path, tempfile::tempfile,
derivative::Derivative, typed_arena::Arena,
edit_distance::edit_distance, unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
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},
},
}; };
#[cfg(test)] #[cfg(test)]

View File

@ -1,9 +1,4 @@
use { use {super::*, clap_mangen::Man};
super::*,
clap_mangen::Man,
std::io::{Read, Seek},
tempfile::tempfile,
};
const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n";
@ -20,7 +15,7 @@ pub(crate) enum Subcommand {
overrides: BTreeMap<String, String>, overrides: BTreeMap<String, String>,
}, },
Completions { Completions {
shell: clap_complete::Shell, shell: completions::Shell,
}, },
Dump, Dump,
Edit, Edit,
@ -296,68 +291,8 @@ impl Subcommand {
justfile.run(config, search, overrides, &recipes) justfile.run(config, search, overrides, &recipes)
} }
fn completions(shell: clap_complete::Shell) -> RunResult<'static, ()> { fn completions(shell: completions::Shell) -> RunResult<'static, ()> {
use clap_complete::Shell; println!("{}", shell.script()?);
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());
Ok(()) Ok(())
} }

View File

@ -28,7 +28,7 @@ fn bash() {
#[test] #[test]
fn replacements() { 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")) let output = Command::new(executable_path("just"))
.args(["--completions", shell]) .args(["--completions", shell])
.output() .output()