blob: 8c8cb93cc6f95973b33b44b1b37f57759febb5de [file] [log] [blame]
use std::ffi::OsStr;
use std::ffi::OsString;
use clap::builder::StyledStr;
use clap_lex::OsStrExt as _;
/// Shell-specific completions
pub trait Completer {
/// The recommended file name for the registration code
fn file_name(&self, name: &str) -> String;
/// Register for completions
fn write_registration(
&self,
name: &str,
bin: &str,
completer: &str,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error>;
/// Complete the command
fn write_complete(
&self,
cmd: &mut clap::Command,
args: Vec<std::ffi::OsString>,
current_dir: Option<&std::path::Path>,
buf: &mut dyn std::io::Write,
) -> Result<(), std::io::Error>;
}
/// Complete the command specified
pub fn complete(
cmd: &mut clap::Command,
args: Vec<std::ffi::OsString>,
arg_index: usize,
current_dir: Option<&std::path::Path>,
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
cmd.build();
let raw_args = clap_lex::RawArgs::new(args);
let mut cursor = raw_args.cursor();
let mut target_cursor = raw_args.cursor();
raw_args.seek(
&mut target_cursor,
clap_lex::SeekFrom::Start(arg_index as u64),
);
// As we loop, `cursor` will always be pointing to the next item
raw_args.next_os(&mut target_cursor);
// TODO: Multicall support
if !cmd.is_no_binary_name_set() {
raw_args.next_os(&mut cursor);
}
let mut current_cmd = &*cmd;
let mut pos_index = 1;
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);
}
debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),);
if let Ok(value) = arg.to_value() {
if let Some(next_cmd) = current_cmd.find_subcommand(value) {
current_cmd = next_cmd;
pos_index = 1;
continue;
}
}
if is_escaped {
pos_index += 1;
} else if arg.is_escape() {
is_escaped = true;
} else if let Some(_long) = arg.to_long() {
} else if let Some(_short) = arg.to_short() {
} else {
pos_index += 1;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"no completion generated",
))
}
fn complete_arg(
arg: &clap_lex::ParsedArg<'_>,
cmd: &clap::Command,
current_dir: Option<&std::path::Path>,
pos_index: usize,
is_escaped: bool,
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
debug!(
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
arg,
cmd.get_name(),
current_dir,
pos_index,
is_escaped
);
let mut completions = Vec::new();
if !is_escaped {
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, help)| {
// HACK: Need better `OsStr` manipulation
(format!("--{}={}", flag, os.to_string_lossy()).into(), help)
}),
)
}
} else {
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map(
|(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)),
));
}
}
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
// HACK: Assuming knowledge of is_escape / is_stdio
completions.extend(
longs_and_visible_aliases(cmd)
.into_iter()
.map(|(f, help)| (format!("--{f}").into(), help)),
);
}
if arg.is_empty() || arg.is_stdio() || arg.is_short() {
let dash_or_arg = if arg.is_empty() {
"-".into()
} else {
arg.to_value_os().to_string_lossy()
};
// HACK: Assuming knowledge of is_stdio
completions.extend(
shorts_and_visible_aliases(cmd)
.into_iter()
// HACK: Need better `OsStr` manipulation
.map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)),
);
}
}
if let Some(positional) = cmd
.get_positionals()
.find(|p| p.get_index() == Some(pos_index))
{
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
}
if let Ok(value) = arg.to_value() {
completions.extend(complete_subcommand(value, cmd));
}
Ok(completions)
}
fn complete_arg_value(
value: Result<&str, &OsStr>,
arg: &clap::Arg,
current_dir: Option<&std::path::Path>,
) -> Vec<(OsString, Option<StyledStr>)> {
let mut values = Vec::new();
debug!("complete_arg_value: arg={arg:?}, value={value:?}");
if let Some(possible_values) = possible_values(arg) {
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(), p.get_help().cloned()))
}));
}
} else {
let value_os = match value {
Ok(value) => OsStr::new(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(value_os, current_dir, |_| true));
}
clap::ValueHint::FilePath => {
values.extend(complete_path(value_os, current_dir, |p| p.is_file()));
}
clap::ValueHint::DirPath => {
values.extend(complete_path(value_os, current_dir, |p| p.is_dir()));
}
clap::ValueHint::ExecutablePath => {
use is_executable::IsExecutable;
values.extend(complete_path(value_os, current_dir, |p| p.is_executable()));
}
clap::ValueHint::CommandName
| clap::ValueHint::CommandString
| clap::ValueHint::CommandWithArguments
| clap::ValueHint::Username
| clap::ValueHint::Hostname
| clap::ValueHint::Url
| clap::ValueHint::EmailAddress => {
// No completion implementation
}
_ => {
// Safe-ish fallback
values.extend(complete_path(value_os, current_dir, |_| true));
}
}
values.sort();
}
values
}
fn complete_path(
value_os: &OsStr,
current_dir: Option<&std::path::Path>,
is_wanted: impl Fn(&std::path::Path) -> bool,
) -> Vec<(OsString, Option<StyledStr>)> {
let mut completions = Vec::new();
let current_dir = match current_dir {
Some(current_dir) => current_dir,
None => {
// Can't complete without a `current_dir`
return Vec::new();
}
};
let (existing, prefix) = value_os
.split_once("\\")
.unwrap_or((OsStr::new(""), value_os));
let root = current_dir.join(existing);
debug!("complete_path: root={root:?}, prefix={prefix:?}");
let prefix = prefix.to_string_lossy();
for entry in std::fs::read_dir(&root)
.ok()
.into_iter()
.flatten()
.filter_map(Result::ok)
{
let raw_file_name = entry.file_name();
if !raw_file_name.starts_with(&prefix) {
continue;
}
if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
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(), None));
} else {
let path = entry.path();
if is_wanted(&path) {
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
completions.push((suggestion.as_os_str().to_owned(), None));
}
}
}
completions
}
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option<StyledStr>)> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
cmd.get_name(),
value
);
let mut scs = subcommands(cmd)
.into_iter()
.filter(|x| x.0.starts_with(value))
.map(|x| (OsString::from(&x.0), x.1))
.collect::<Vec<_>>();
scs.sort();
scs.dedup();
scs
}
/// Gets all the long options, their visible aliases and flags of a [`clap::Command`].
/// Includes `help` and `version` depending on the [`clap::Command`] settings.
fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
debug!("longs: name={}", p.get_name());
p.get_arguments()
.filter_map(|a| {
a.get_long_and_visible_aliases().map(|longs| {
longs
.into_iter()
.map(|s| (s.to_string(), a.get_help().cloned()))
})
})
.flatten()
.collect()
}
/// Gets all the short options, their visible aliases and flags of a [`clap::Command`].
/// Includes `h` and `V` depending on the [`clap::Command`] settings.
fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<(char, Option<StyledStr>)> {
debug!("shorts: name={}", p.get_name());
p.get_arguments()
.filter_map(|a| {
a.get_short_and_visible_aliases()
.map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned())))
})
.flatten()
.collect()
}
/// Get the possible values for completion
fn possible_values(a: &clap::Arg) -> Option<Vec<clap::builder::PossibleValue>> {
if !a.get_num_args().expect("built").takes_values() {
None
} else {
a.get_value_parser()
.possible_values()
.map(|pvs| pvs.collect())
}
}
/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`.
///
/// Subcommand `rustup toolchain install` would be converted to
/// `("install", "rustup toolchain install")`.
fn subcommands(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
debug!("subcommands: name={}", p.get_name());
debug!("subcommands: Has subcommands...{:?}", p.has_subcommands());
p.get_subcommands()
.map(|sc| (sc.get_name().to_string(), sc.get_about().cloned()))
.collect()
}