diff --git a/clap_complete/src/engine/custom.rs b/clap_complete/src/engine/custom.rs index a81f0b02848..b4d32ae7e57 100644 --- a/clap_complete/src/engine/custom.rs +++ b/clap_complete/src/engine/custom.rs @@ -216,6 +216,8 @@ pub struct PathCompleter { #[allow(clippy::type_complexity)] filter: Option bool + Send + Sync>>, stdio: bool, + #[allow(clippy::type_complexity)] + mapper: Option CompletionCandidate + Send + Sync>>, } impl PathCompleter { @@ -225,6 +227,7 @@ impl PathCompleter { filter: None, current_dir: None, stdio: false, + mapper: None, } } @@ -253,6 +256,15 @@ impl PathCompleter { self } + /// Transform completion candidates after filtering + pub fn map( + mut self, + mapper: impl Fn(CompletionCandidate) -> CompletionCandidate + Send + Sync + 'static, + ) -> Self { + self.mapper = Some(Box::new(mapper)); + self + } + /// Override [`std::env::current_dir`] pub fn current_dir(mut self, path: impl Into) -> Self { self.current_dir = Some(path.into()); @@ -275,6 +287,12 @@ impl ValueCompleter for PathCompleter { current_dir_actual.as_deref() }); let mut candidates = complete_path(current, current_dir, filter); + if let Some(mapper) = &self.mapper { + candidates = candidates + .into_iter() + .map(|candidate| mapper(candidate)) + .collect(); + } if self.stdio && current.is_empty() { candidates.push(CompletionCandidate::new("-").help(Some("stdio".into()))); } diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index a3a18912a4e..00f5dd45197 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -1191,3 +1191,97 @@ fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path> .collect::>() .join("\n") } + +#[test] +fn suggest_value_path_with_filter_and_mapper() { + let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap(); + let testdir_path = testdir.path().unwrap(); + fs::write(testdir_path.join("a_file"), "").unwrap(); + fs::write(testdir_path.join("b_file"), "").unwrap(); + fs::create_dir_all(testdir_path.join("c_dir")).unwrap(); + fs::create_dir_all(testdir_path.join("d_dir")).unwrap(); + + fs::write(testdir_path.join("c_dir").join("Cargo.toml"), "").unwrap(); + + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("input") + .long("input") + .short('i') + .add(ArgValueCompleter::new( + PathCompleter::dir() + .current_dir(testdir_path.to_owned()) + .filter(|path| { + path.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |name| name.starts_with('c')) + }) + .map(|candidate| { + if candidate.get_value().to_string_lossy().contains("c_dir") { + candidate + .help(Some("Addable cargo package".into())) + .display_order(Some(0)) + } else { + candidate.display_order(Some(1)) + } + }), + )), + ) + .args_conflicts_with_subcommands(true); + + assert_data_eq!( + complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)), + snapbox::str![[r#" +c_dir/ Addable cargo package +d_dir/ +"#]], + ); +} + +#[test] +fn suggest_value_file_path_with_mapper() { + let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap(); + let testdir_path = testdir.path().unwrap(); + fs::write(testdir_path.join("a_file.txt"), "").unwrap(); + fs::write(testdir_path.join("b_file.md"), "").unwrap(); + fs::write(testdir_path.join("c_file.rs"), "").unwrap(); + fs::create_dir_all(testdir_path.join("d_dir")).unwrap(); + + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("input") + .long("input") + .short('i') + .add(ArgValueCompleter::new( + PathCompleter::file() + .current_dir(testdir_path.to_owned()) + .map(|candidate| { + let path = Path::new(candidate.get_value()); + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + match ext { + "rs" => candidate + .help(Some("Rust source file".into())) + .display_order(Some(0)), + "txt" => candidate + .help(Some("Text file".into())) + .display_order(Some(1)), + _ => candidate.display_order(Some(2)) + } + } else { + candidate.display_order(Some(3)) + } + }), + )), + ) + .args_conflicts_with_subcommands(true); + + assert_data_eq!( + complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)), + snapbox::str![[r#" +c_file.rs Rust source file +a_file.txt Text file +b_file.md +d_dir/ +"#]], + ); +} \ No newline at end of file