diff options
Diffstat (limited to 'src/dynamic/completer.rs')
-rw-r--r-- | src/dynamic/completer.rs | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/src/dynamic/completer.rs b/src/dynamic/completer.rs new file mode 100644 index 0000000..8c8cb93 --- /dev/null +++ b/src/dynamic/completer.rs @@ -0,0 +1,341 @@ +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() +} |