Skip to content

Commit

Permalink
feat(complete): Filter options by argument's dashes
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Jul 30, 2023
1 parent 1828437 commit d8ca556
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 81 deletions.
33 changes: 30 additions & 3 deletions clap_complete/src/dynamic/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ use std::ffi::OsString;
use clap::builder::StyledStr;
use clap_lex::OsStrExt as _;

/// Specifies the number of dashes an argument must already
/// contain to complete short and long options.
#[derive(PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub enum ShowOptions {
/// One `-` for short, two `--` for long options.
ExactDash,
/// At least one `-` to show options.
MinOneDash,
/// Always complete options.
Always,
}

/// Shell-specific completions
pub trait Completer {
/// The recommended file name for the registration code
Expand Down Expand Up @@ -32,6 +44,7 @@ pub fn complete(
args: Vec<std::ffi::OsString>,
arg_index: usize,
current_dir: Option<&std::path::Path>,
show_options: ShowOptions,
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
cmd.build();

Expand All @@ -55,7 +68,14 @@ pub fn complete(
let mut is_escaped = false;
while let Some(arg) = raw_args.next(&mut cursor) {
if cursor == target_cursor {
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
return complete_arg(
&arg,
current_cmd,
current_dir,
pos_index,
is_escaped,
show_options,
);
}

debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),);
Expand Down Expand Up @@ -91,6 +111,7 @@ fn complete_arg(
current_dir: Option<&std::path::Path>,
pos_index: usize,
is_escaped: bool,
show_options: ShowOptions,
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
debug!(
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
Expand Down Expand Up @@ -126,7 +147,10 @@ fn complete_arg(
);
}
}
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
} else if arg.is_escape()
|| (show_options >= ShowOptions::MinOneDash && arg.is_stdio())
|| (show_options == ShowOptions::Always && arg.is_empty())
{
// HACK: Assuming knowledge of is_escape / is_stdio
completions.extend(
crate::generator::utils::longs_and_visible_aliases(cmd)
Expand All @@ -135,7 +159,10 @@ fn complete_arg(
);
}

if arg.is_empty() || arg.is_stdio() || arg.is_short() {
if arg.is_stdio()
|| arg.is_short()
|| (arg.is_empty() && show_options == ShowOptions::Always)
{
let dash_or_arg = if arg.is_empty() {
"-".into()
} else {
Expand Down
5 changes: 4 additions & 1 deletion clap_complete/src/dynamic/shells/bash.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use unicode_xid::UnicodeXID as _;

use crate::dynamic::ShowOptions;

/// Bash completions
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Bash;
Expand Down Expand Up @@ -71,7 +73,8 @@ complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
.ok()
.and_then(|i| i.parse().ok());
let ifs: Option<String> = std::env::var("IFS").ok().and_then(|i| i.parse().ok());
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;
let completions =
crate::dynamic::complete(cmd, args, index, current_dir, ShowOptions::Always)?;

for (i, (completion, _)) in completions.iter().enumerate() {
if i != 0 {
Expand Down
5 changes: 4 additions & 1 deletion clap_complete/src/dynamic/shells/fish.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::dynamic::ShowOptions;

/// Fish completions
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Fish;
Expand Down Expand Up @@ -28,7 +30,8 @@ impl crate::dynamic::Completer for Fish {
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error> {
let index = args.len() - 1;
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;
let completions =
crate::dynamic::complete(cmd, args, index, current_dir, ShowOptions::ExactDash)?;

for (completion, help) in completions {
write!(buf, "{}", completion.to_string_lossy())?;
Expand Down
154 changes: 83 additions & 71 deletions clap_complete/tests/testsuite/dynamic.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
#![cfg(feature = "unstable-dynamic")]

#[test]
fn suggest_subcommand_subset() {
let name = "exhaustive";
let mut cmd = clap::Command::new(name)
.subcommand(clap::Command::new("hello-world"))
.subcommand(clap::Command::new("hello-moon"))
.subcommand(clap::Command::new("goodbye-world"));

let args = [name, "he"];
let arg_index = 1;
let args = IntoIterator::into_iter(args)
.map(std::ffi::OsString::from)
.collect::<Vec<_>>();
use std::{ffi::OsString, iter};

use clap_complete::dynamic::ShowOptions;

fn assert_complete<'a>(
cmd: &mut clap::Command,
args: impl IntoIterator<Item = &'a str>,
expected: impl AsRef<[&'a str]>,
options: ShowOptions,
) {
let args: Vec<_> = iter::once(OsString::from(cmd.get_name()))
.chain(args.into_iter().map(OsString::from))
.collect();
let arg_index = args.len() - 1;
let current_dir = None;

let completions =
clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap();
clap_complete::dynamic::complete(cmd, args, arg_index, current_dir, options).unwrap();

let completions = completions
.into_iter()
.map(|s| s.0.to_string_lossy().into_owned())
.collect::<Vec<_>>();

assert_eq!(completions, ["hello-moon", "hello-world", "help"]);
assert_eq!(completions, expected.as_ref());
}

#[test]
fn suggest_subcommand_subset() {
let mut cmd = clap::Command::new("exhaustive")
.subcommand(clap::Command::new("hello-world"))
.subcommand(clap::Command::new("hello-moon"))
.subcommand(clap::Command::new("goodbye-world"));

assert_complete(
&mut cmd,
["he"],
["hello-moon", "hello-world", "help"],
ShowOptions::Always,
);
}

#[test]
fn suggest_long_flag_subset() {
let name = "exhaustive";
let mut cmd = clap::Command::new(name)
let mut cmd = clap::Command::new("exhaustive")
.arg(
clap::Arg::new("hello-world")
.long("hello-world")
Expand All @@ -45,53 +61,34 @@ fn suggest_long_flag_subset() {
.action(clap::ArgAction::Count),
);

let args = [name, "--he"];
let arg_index = 1;
let args = IntoIterator::into_iter(args)
.map(std::ffi::OsString::from)
.collect::<Vec<_>>();
let current_dir = None;

let completions =
clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap();
let completions = completions
.into_iter()
.map(|s| s.0.to_string_lossy().into_owned())
.collect::<Vec<_>>();

assert_eq!(completions, ["--hello-world", "--hello-moon", "--help"]);
assert_complete(
&mut cmd,
["--he"],
["--hello-world", "--hello-moon", "--help"],
ShowOptions::Always,
);
}

#[test]
fn suggest_possible_value_subset() {
let name = "exhaustive";
let mut cmd = clap::Command::new(name).arg(clap::Arg::new("hello-world").value_parser([
"hello-world",
"hello-moon",
"goodbye-world",
]));

let args = [name, "hello"];
let arg_index = 1;
let args = IntoIterator::into_iter(args)
.map(std::ffi::OsString::from)
.collect::<Vec<_>>();
let current_dir = None;

let completions =
clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap();
let completions = completions
.into_iter()
.map(|s| s.0.to_string_lossy().into_owned())
.collect::<Vec<_>>();

assert_eq!(completions, ["hello-world", "hello-moon"]);
let mut cmd =
clap::Command::new("exhaustive").arg(clap::Arg::new("hello-world").value_parser([
"hello-world",
"hello-moon",
"goodbye-world",
]));

assert_complete(
&mut cmd,
["hello"],
["hello-world", "hello-moon"],
ShowOptions::Always,
);
}

#[test]
fn suggest_additional_short_flags() {
let name = "exhaustive";
let mut cmd = clap::Command::new(name)
let mut cmd = clap::Command::new("exhaustive")
.arg(
clap::Arg::new("a")
.short('a')
Expand All @@ -107,20 +104,35 @@ fn suggest_additional_short_flags() {
.short('c')
.action(clap::ArgAction::Count),
);
assert_complete(
&mut cmd,
["-a"],
["-aa", "-ab", "-ac", "-ah"],
ShowOptions::Always,
);
}

let args = [name, "-a"];
let arg_index = 1;
let args = IntoIterator::into_iter(args)
.map(std::ffi::OsString::from)
.collect::<Vec<_>>();
let current_dir = None;

let completions =
clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap();
let completions = completions
.into_iter()
.map(|s| s.0.to_string_lossy().into_owned())
.collect::<Vec<_>>();

assert_eq!(completions, ["-aa", "-ab", "-ac", "-ah"]);
#[test]
fn suggest_flags_only_on_dash() {
let mut cmd = clap::Command::new("exhaustive")
.arg(clap::Arg::new("a").short('a'))
.arg(clap::Arg::new("b").long("b"));

assert_complete(&mut cmd, [""], [], ShowOptions::ExactDash);
assert_complete(&mut cmd, ["-"], ["-a", "-h"], ShowOptions::ExactDash);
assert_complete(&mut cmd, ["--"], ["--b", "--help"], ShowOptions::ExactDash);

assert_complete(
&mut cmd,
["-"],
["--b", "--help", "-a", "-h"],
ShowOptions::MinOneDash,
);

assert_complete(
&mut cmd,
[""],
["--b", "--help", "-a", "-h"],
ShowOptions::Always,
);
}
7 changes: 2 additions & 5 deletions clap_complete/tests/testsuite/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,8 @@ fn complete_dynamic() {

let input = "exhaustive \t";
let expected = r#"% exhaustive
action last -V (Print version)
alias pacman --generate (generate)
complete (Register shell completions for this program) quote --global (everywhere)
help (Print this message or the help of the given subcommand(s)) value --help (Print help)
hint -h (Print help) --version (Print version)"#;
action complete (Register shell completions for this program) hint pacman value
alias help (Print this message or the help of the given subcommand(s)) last quote"#;
let actual = runtime.complete(input, &term).unwrap();
snapbox::assert_eq(expected, actual);
}

0 comments on commit d8ca556

Please sign in to comment.