summaryrefslogtreecommitdiff
path: root/src/dynamic/completer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dynamic/completer.rs')
-rw-r--r--src/dynamic/completer.rs341
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()
+}