mirror of
https://github.com/clap-rs/clap
synced 2024-11-10 06:44:16 +00:00
feat(complete): Look up partial matches
This commit is contained in:
parent
63e0fd5c50
commit
a574f226eb
3 changed files with 93 additions and 29 deletions
|
@ -39,6 +39,7 @@ clap = { path = "../", version = "3.1.10", default-features = false, features =
|
|||
clap_lex = { path = "../clap_lex", version = "0.1.0", optional = true }
|
||||
is_executable = { version = "1.0.1", optional = true }
|
||||
os_str_bytes = { version = "6.0", default-features = false, features = ["raw_os_str"], optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
shlex = { version = "1.1.0", optional = true }
|
||||
unicode-xid = { version = "0.2.2", optional = true }
|
||||
|
||||
|
@ -55,7 +56,7 @@ required-features = ["unstable-dynamic"]
|
|||
|
||||
[features]
|
||||
default = []
|
||||
unstable-dynamic = ["clap_lex", "shlex", "unicode-xid", "os_str_bytes", "clap/derive", "is_executable"]
|
||||
unstable-dynamic = ["clap_lex", "shlex", "unicode-xid", "os_str_bytes", "clap/derive", "is_executable", "pathdiff"]
|
||||
debug = ["clap/debug"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -324,7 +324,7 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
let mut is_escaped = false;
|
||||
while let Some(arg) = raw_args.next(&mut cursor) {
|
||||
if cursor == target_cursor {
|
||||
return complete_new(current_cmd, current_dir, pos_index, is_escaped);
|
||||
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
|
||||
}
|
||||
if let Ok(value) = arg.to_value() {
|
||||
if let Some(next_cmd) = current_cmd.find_subcommand(value) {
|
||||
|
@ -351,7 +351,8 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
))
|
||||
}
|
||||
|
||||
fn complete_new(
|
||||
fn complete_arg(
|
||||
arg: &clap_lex::ParsedArg<'_>,
|
||||
cmd: &clap::Command,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
pos_index: usize,
|
||||
|
@ -360,57 +361,100 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
let mut completions = Vec::new();
|
||||
|
||||
if !is_escaped {
|
||||
completions.extend(
|
||||
crate::generator::utils::longs_and_visible_aliases(cmd)
|
||||
.into_iter()
|
||||
.map(|f| format!("--{}", f).into()),
|
||||
);
|
||||
completions.extend(
|
||||
crate::generator::utils::shorts_and_visible_aliases(cmd)
|
||||
.into_iter()
|
||||
.map(|f| format!("-{}", f).into()),
|
||||
);
|
||||
if let Some((flag, value)) = arg.to_long() {
|
||||
if let Ok(flag) = flag {
|
||||
if let Some(value) = value {
|
||||
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag))
|
||||
{
|
||||
completions.extend(
|
||||
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
|
||||
.into_iter()
|
||||
.map(|os| {
|
||||
// HACK: Need better `OsStr` manipulation
|
||||
format!("--{}={}", flag, os.to_string_lossy()).into()
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
completions.extend(
|
||||
crate::generator::utils::longs_and_visible_aliases(cmd)
|
||||
.into_iter()
|
||||
.filter_map(|f| {
|
||||
f.starts_with(flag).then(|| format!("--{}", f).into())
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
|
||||
// HACK: Assuming knowledge of is_escape / is_stdio
|
||||
completions.extend(
|
||||
crate::generator::utils::longs_and_visible_aliases(cmd)
|
||||
.into_iter()
|
||||
.map(|f| format!("--{}", f).into()),
|
||||
);
|
||||
}
|
||||
|
||||
if arg.is_empty() || arg.is_stdio() || arg.is_short() {
|
||||
// HACK: Assuming knowledge of is_stdio
|
||||
completions.extend(
|
||||
crate::generator::utils::shorts_and_visible_aliases(cmd)
|
||||
.into_iter()
|
||||
.map(|f| format!("-{}", f).into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(positional) = cmd
|
||||
.get_positionals()
|
||||
.find(|p| p.get_index() == Some(pos_index))
|
||||
{
|
||||
completions.extend(complete_arg_value(positional, current_dir));
|
||||
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
|
||||
}
|
||||
|
||||
if !is_escaped {
|
||||
completions.extend(all_subcommands(cmd).into_iter().map(|s| s));
|
||||
if let Ok(value) = arg.to_value() {
|
||||
completions.extend(complete_subcommand(value, cmd));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(completions)
|
||||
}
|
||||
|
||||
fn complete_arg_value(
|
||||
value: Result<&str, &clap_lex::RawOsStr>,
|
||||
arg: &clap::Arg<'_>,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
) -> Vec<OsString> {
|
||||
let mut values = Vec::new();
|
||||
|
||||
if let Some(possible_values) = arg.get_possible_values() {
|
||||
values.extend(possible_values.into_iter().map(|p| p.get_name().into()));
|
||||
if let Ok(value) = value {
|
||||
values.extend(possible_values.into_iter().filter_map(|p| {
|
||||
let name = p.get_name();
|
||||
name.starts_with(value).then(|| name.into())
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
let value_os = match value {
|
||||
Ok(value) => clap_lex::RawOsStr::from_str(value),
|
||||
Err(value_os) => value_os,
|
||||
};
|
||||
match arg.get_value_hint() {
|
||||
clap::ValueHint::Other => {
|
||||
// Should not complete
|
||||
}
|
||||
clap::ValueHint::Unknown | clap::ValueHint::AnyPath => {
|
||||
values.extend(complete_path(current_dir, |_| true));
|
||||
values.extend(complete_path(value_os, current_dir, |_| true));
|
||||
}
|
||||
clap::ValueHint::FilePath => {
|
||||
values.extend(complete_path(current_dir, |p| p.is_file()));
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_file()));
|
||||
}
|
||||
clap::ValueHint::DirPath => {
|
||||
values.extend(complete_path(current_dir, |p| p.is_dir()));
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_dir()));
|
||||
}
|
||||
clap::ValueHint::ExecutablePath => {
|
||||
use is_executable::IsExecutable;
|
||||
values.extend(complete_path(current_dir, |p| p.is_executable()));
|
||||
values.extend(complete_path(value_os, current_dir, |p| p.is_executable()));
|
||||
}
|
||||
clap::ValueHint::CommandName
|
||||
| clap::ValueHint::CommandString
|
||||
|
@ -423,7 +467,7 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
}
|
||||
_ => {
|
||||
// Safe-ish fallback
|
||||
values.extend(complete_path(current_dir, |_| true));
|
||||
values.extend(complete_path(value_os, current_dir, |_| true));
|
||||
}
|
||||
}
|
||||
values.sort();
|
||||
|
@ -433,6 +477,7 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
}
|
||||
|
||||
fn complete_path(
|
||||
value_os: &clap_lex::RawOsStr,
|
||||
current_dir: Option<&std::path::Path>,
|
||||
is_wanted: impl Fn(&std::path::Path) -> bool,
|
||||
) -> Vec<OsString> {
|
||||
|
@ -441,23 +486,35 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
let current_dir = match current_dir {
|
||||
Some(current_dir) => current_dir,
|
||||
None => {
|
||||
// Can't complete without a `current_dir`
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
for entry in std::fs::read_dir(current_dir)
|
||||
let (existing, prefix) = value_os
|
||||
.split_once('\\')
|
||||
.unwrap_or((clap_lex::RawOsStr::from_str(""), value_os));
|
||||
let root = current_dir.join(existing.to_os_str());
|
||||
|
||||
for entry in std::fs::read_dir(&root)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let mut path = entry.path();
|
||||
let raw_file_name = clap_lex::RawOsString::new(entry.file_name());
|
||||
if !raw_file_name.starts_with_os(prefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
|
||||
path.push(""); // Ensure trailing `/`
|
||||
let suggestion = path.strip_prefix(current_dir).unwrap();
|
||||
let path = entry.path();
|
||||
let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
|
||||
suggestion.push(""); // Ensure trailing `/`
|
||||
completions.push(suggestion.as_os_str().to_owned());
|
||||
} else {
|
||||
let path = entry.path();
|
||||
if is_wanted(&path) {
|
||||
let suggestion = path.strip_prefix(current_dir).unwrap();
|
||||
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
|
||||
completions.push(suggestion.as_os_str().to_owned());
|
||||
}
|
||||
}
|
||||
|
@ -466,11 +523,12 @@ complete OPTIONS -F _clap_complete_NAME EXECUTABLES
|
|||
completions
|
||||
}
|
||||
|
||||
fn all_subcommands(cmd: &clap::Command) -> Vec<OsString> {
|
||||
debug!("all_subcommands");
|
||||
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
|
||||
debug!("complete_subcommand");
|
||||
|
||||
let mut scs = crate::generator::utils::all_subcommands(cmd)
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|x| x.0.starts_with(value))
|
||||
.map(|x| OsString::from(&x.0))
|
||||
.collect::<Vec<_>>();
|
||||
scs.sort();
|
||||
|
|
|
@ -275,6 +275,11 @@ impl<'s> ParsedArg<'s> {
|
|||
Self { inner, utf8 }
|
||||
}
|
||||
|
||||
/// Argument is length of 0
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.as_ref().is_empty()
|
||||
}
|
||||
|
||||
/// Does the argument look like a stdio argument (`-`)
|
||||
pub fn is_stdio(&self) -> bool {
|
||||
self.inner.as_ref() == "-"
|
||||
|
|
Loading…
Reference in a new issue