feat: add value completion

This commit is contained in:
nibon7 2022-10-31 20:11:01 +08:00
parent 07dbefef3f
commit 547bd288bf
No known key found for this signature in database
GPG key ID: 281CE465D8EEC03B
8 changed files with 297 additions and 33 deletions

View file

@ -66,13 +66,17 @@ fn main() {
```nu
module completions {
def "myapp choice" [] {
[ "first" "second" ]
}
# Tests completions
export extern myapp [
file?: string # some input file
--config(-c) # some config file
--conf # some config file
-C # some config file
choice?: string
choice?: string@"myapp choice"
--version(-V) # Print version information
]
@ -87,9 +91,13 @@ module completions {
--version(-V) # Print version information
]
def "myapp some_cmd sub_cmd config" [] {
[ "Lest quotes aren't escaped." ]
}
# sub-subcommand
export extern "myapp some_cmd sub_cmd" [
--config: string # the other case to test
--config: string@"myapp some_cmd sub_cmd config" # the other case to test
--version(-V) # Print version information
]

View file

@ -1,6 +1,6 @@
//! Generates [Nushell](https://github.com/nushell/nushell) completions for [`clap`](https://github.com/clap-rs/clap) based CLIs
use clap::{Arg, Command};
use clap::{builder::PossibleValue, Arg, Command};
use clap_complete::Generator;
/// Generate Nushell complete file
@ -10,46 +10,42 @@ enum Argument {
Short(Vec<char>),
Long(Vec<String>),
ShortAndLong(Vec<char>, Vec<String>),
Positional(String, bool),
Positional(bool),
}
struct ArgumentLine {
id: String,
name: String,
arg: Argument,
takes_values: bool,
possible_values: Vec<PossibleValue>,
help: Option<String>,
}
impl ArgumentLine {
fn append_type_and_help(&self, s: &mut String) {
if self.takes_values {
s.push_str(": string");
}
fn new(arg: &Arg, bin_name: &str) -> Self {
let id = arg.get_id().to_string();
let name = bin_name.to_string();
if let Some(help) = &self.help {
s.push_str(format!("\t# {}", help).as_str());
}
s.push('\n');
}
}
impl From<&Arg> for ArgumentLine {
fn from(arg: &Arg) -> Self {
let takes_values = arg
.get_num_args()
.map(|v| v.takes_values())
.unwrap_or(false);
let possible_values = arg.get_possible_values();
let help = arg.get_help().map(|s| s.to_string());
if arg.is_positional() {
let id = arg.get_id().to_string();
let required = arg.is_required_set();
let arg = Argument::Positional(id, required);
let arg = Argument::Positional(required);
return Self {
id,
name,
arg,
takes_values,
possible_values,
help,
};
}
@ -60,29 +56,71 @@ impl From<&Arg> for ArgumentLine {
match shorts {
Some(shorts) => match longs {
Some(longs) => Self {
id,
name,
arg: Argument::ShortAndLong(
shorts,
longs.iter().map(|s| s.to_string()).collect(),
),
takes_values,
possible_values,
help,
},
None => Self {
id,
name,
arg: Argument::Short(shorts),
takes_values,
possible_values,
help,
},
},
None => match longs {
Some(long) => Self {
arg: Argument::Long(long.iter().map(|s| s.to_string()).collect()),
Some(longs) => Self {
id,
name,
arg: Argument::Long(longs.iter().map(|s| s.to_string()).collect()),
takes_values,
possible_values,
help,
},
None => unreachable!("No short or long option found"),
},
}
}
fn generate_value_hints(&self) -> Option<String> {
if self.possible_values.is_empty() {
return None;
}
let mut s = format!(r#" def "{} {}" [] {{"#, self.name, self.id);
s.push_str("\n [");
for value in &self.possible_values {
s.push_str(format!(r#" "{}""#, value.get_name()).as_str());
}
s.push_str(" ]\n }\n\n");
Some(s)
}
fn append_type_and_help(&self, s: &mut String) {
if self.takes_values {
s.push_str(": string");
if !self.possible_values.is_empty() {
s.push_str(format!(r#"@"{} {}""#, self.name, self.id).as_str())
}
}
if let Some(help) = &self.help {
s.push_str(format!("\t# {}", help).as_str());
}
s.push('\n');
}
}
impl ToString for ArgumentLine {
@ -125,8 +163,8 @@ impl ToString for ArgumentLine {
self.append_type_and_help(&mut s);
}
}
Argument::Positional(positional, required) => {
s.push_str(format!(" {}", positional).as_str());
Argument::Positional(required) => {
s.push_str(format!(" {}", self.id).as_str());
if !*required {
s.push('?');
@ -165,23 +203,30 @@ impl Generator for Nushell {
}
fn generate_completion(completions: &mut String, cmd: &Command, is_subcommand: bool) {
if let Some(about) = cmd.get_about() {
completions.push_str(format!(" # {}\n", about).as_str());
}
let bin_name = cmd.get_bin_name().expect("Failed to get bin name");
for hint in cmd
.get_arguments()
.filter_map(|arg| ArgumentLine::new(arg, bin_name).generate_value_hints())
{
completions.push_str(&hint);
}
let name = if is_subcommand {
format!(r#""{}""#, bin_name)
} else {
bin_name.into()
};
if let Some(about) = cmd.get_about() {
completions.push_str(format!(" # {}\n", about).as_str());
}
completions.push_str(format!(" export extern {} [\n", name).as_str());
let s: String = cmd
.get_arguments()
.map(|arg| ArgumentLine::from(arg).to_string())
.map(|arg| ArgumentLine::new(arg, bin_name).to_string())
.collect();
completions.push_str(&s);

View file

@ -51,6 +51,25 @@ pub fn feature_sample_command(name: &'static str) -> Command {
)
}
pub fn special_commands_command(name: &'static str) -> clap::Command {
feature_sample_command(name)
.subcommand(
clap::Command::new("some_cmd")
.about("tests other things")
.arg(
clap::Arg::new("config")
.long("config")
.hide(true)
.action(clap::ArgAction::Set)
.require_equals(true)
.help("the other case to test"),
)
.arg(clap::Arg::new("path").num_args(1..)),
)
.subcommand(clap::Command::new("some-cmd-with-hyphens").alias("hyphen"))
.subcommand(clap::Command::new("some-hidden-cmd").hide(true))
}
pub fn aliases_command(name: &'static str) -> Command {
Command::new(name)
.version("3.0")
@ -80,18 +99,107 @@ pub fn sub_subcommands_command(name: &'static str) -> Command {
feature_sample_command(name).subcommand(
Command::new("some_cmd")
.about("top level subcommand")
.visible_alias("some_cmd_alias")
.subcommand(
Command::new("sub_cmd").about("sub-subcommand").arg(
Arg::new("config")
.long("config")
.action(ArgAction::Set)
.value_parser([PossibleValue::new("Lest quotes aren't escaped.")])
.value_parser([
PossibleValue::new("Lest quotes, aren't escaped.")
.help("help,with,comma"),
PossibleValue::new("Second to trigger display of options"),
])
.help("the other case to test"),
),
),
)
}
pub fn value_hint_command(name: &'static str) -> clap::Command {
clap::Command::new(name)
.arg(
clap::Arg::new("choice")
.long("choice")
.action(clap::ArgAction::Set)
.value_parser(["bash", "fish", "zsh"]),
)
.arg(
clap::Arg::new("unknown")
.long("unknown")
.value_hint(clap::ValueHint::Unknown),
)
.arg(
clap::Arg::new("other")
.long("other")
.value_hint(clap::ValueHint::Other),
)
.arg(
clap::Arg::new("path")
.long("path")
.short('p')
.value_hint(clap::ValueHint::AnyPath),
)
.arg(
clap::Arg::new("file")
.long("file")
.short('f')
.value_hint(clap::ValueHint::FilePath),
)
.arg(
clap::Arg::new("dir")
.long("dir")
.short('d')
.value_hint(clap::ValueHint::DirPath),
)
.arg(
clap::Arg::new("exe")
.long("exe")
.short('e')
.value_hint(clap::ValueHint::ExecutablePath),
)
.arg(
clap::Arg::new("cmd_name")
.long("cmd-name")
.value_hint(clap::ValueHint::CommandName),
)
.arg(
clap::Arg::new("cmd")
.long("cmd")
.short('c')
.value_hint(clap::ValueHint::CommandString),
)
.arg(
clap::Arg::new("command_with_args")
.action(clap::ArgAction::Set)
.num_args(1..)
.trailing_var_arg(true)
.value_hint(clap::ValueHint::CommandWithArguments),
)
.arg(
clap::Arg::new("user")
.short('u')
.long("user")
.value_hint(clap::ValueHint::Username),
)
.arg(
clap::Arg::new("host")
.short('H')
.long("host")
.value_hint(clap::ValueHint::Hostname),
)
.arg(
clap::Arg::new("url")
.long("url")
.value_hint(clap::ValueHint::Url),
)
.arg(
clap::Arg::new("email")
.long("email")
.value_hint(clap::ValueHint::EmailAddress),
)
}
pub fn assert_matches_path(
expected_path: impl AsRef<std::path::Path>,
gen: impl clap_complete::Generator,
@ -103,5 +211,6 @@ pub fn assert_matches_path(
snapbox::Assert::new()
.action_env("SNAPSHOTS")
.normalize_paths(false)
.matches_path(expected_path, buf);
}

View file

@ -24,6 +24,18 @@ fn feature_sample() {
);
}
#[test]
fn special_commands() {
let name = "my-app";
let cmd = common::special_commands_command(name);
common::assert_matches_path(
"tests/snapshots/special_commands.nu",
clap_complete_nushell::Nushell,
cmd,
name,
);
}
#[test]
fn aliases() {
let name = "my-app";
@ -47,3 +59,15 @@ fn sub_subcommands() {
name,
);
}
#[test]
fn value_hint() {
let name = "my-app";
let cmd = common::value_hint_command(name);
common::assert_matches_path(
"tests/snapshots/value_hint.nu",
clap_complete_nushell::Nushell,
cmd,
name,
);
}

View file

@ -1,12 +1,16 @@
module completions {
def "my-app choice" [] {
[ "first" "second" ]
}
# Tests completions
export extern my-app [
file?: string # some input file
--config(-c) # some config file
--conf # some config file
-C # some config file
choice?: string
choice?: string@"my-app choice"
--version(-V) # Print version information
]

View file

@ -0,0 +1,40 @@
module completions {
def "my-app choice" [] {
[ "first" "second" ]
}
# Tests completions
export extern my-app [
file?: string # some input file
--config(-c) # some config file
--conf # some config file
-C # some config file
choice?: string@"my-app choice"
--version(-V) # Print version information
]
# tests things
export extern "my-app test" [
--case: string # the case to test
--version(-V) # Print version information
]
# tests other things
export extern "my-app some_cmd" [
--config: string # the other case to test
path?: string
--version(-V) # Print version information
]
export extern "my-app some-cmd-with-hyphens" [
--version(-V) # Print version information
]
export extern "my-app some-hidden-cmd" [
--version(-V) # Print version information
]
}
use completions *

View file

@ -1,12 +1,16 @@
module completions {
def "my-app choice" [] {
[ "first" "second" ]
}
# Tests completions
export extern my-app [
file?: string # some input file
--config(-c) # some config file
--conf # some config file
-C # some config file
choice?: string
choice?: string@"my-app choice"
--version(-V) # Print version information
]
@ -21,9 +25,13 @@ module completions {
--version(-V) # Print version information
]
def "my-app some_cmd sub_cmd config" [] {
[ "Lest quotes, aren't escaped." "Second to trigger display of options" ]
}
# sub-subcommand
export extern "my-app some_cmd sub_cmd" [
--config: string # the other case to test
--config: string@"my-app some_cmd sub_cmd config" # the other case to test
--version(-V) # Print version information
]

View file

@ -0,0 +1,26 @@
module completions {
def "my-app choice" [] {
[ "bash" "fish" "zsh" ]
}
export extern my-app [
--choice: string@"my-app choice"
--unknown: string
--other: string
--path(-p): string
--file(-f): string
--dir(-d): string
--exe(-e): string
--cmd-name: string
--cmd(-c): string
command_with_args?: string
--user(-u): string
--host(-H): string
--url: string
--email: string
]
}
use completions *