From 45d6e6aeb3777da40f691eefb2fd2db27f464146 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 21 Aug 2024 13:40:07 -0500 Subject: [PATCH] feat(complete): Add PathCompleter --- clap_complete/src/engine/custom.rs | 59 +++++++++++++++++++++++++ clap_complete/src/engine/mod.rs | 1 + clap_complete/tests/testsuite/engine.rs | 54 +++++++++++++++++++--- 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/clap_complete/src/engine/custom.rs b/clap_complete/src/engine/custom.rs index f17b30a84811..2cf453f8df5b 100644 --- a/clap_complete/src/engine/custom.rs +++ b/clap_complete/src/engine/custom.rs @@ -150,6 +150,65 @@ where } } +/// Complete a value as a [`std::path::Path`] +pub struct PathCompleter { + current_dir: Option, + filter: Option bool + Send + Sync>>, +} + +impl PathCompleter { + /// Any path is allowed + pub fn any() -> Self { + Self { + filter: None, + current_dir: None, + } + } + + /// Complete only files + pub fn file() -> Self { + Self::any().filter(|p| p.is_file()) + } + + /// Complete only directories + pub fn dir() -> Self { + Self::any().filter(|p| p.is_dir()) + } + + /// Select which paths should be completed + pub fn filter( + mut self, + filter: impl Fn(&std::path::Path) -> bool + Send + Sync + 'static, + ) -> Self { + self.filter = Some(Box::new(filter)); + self + } + + /// Override [`std::env::current_dir`] + pub fn current_dir(mut self, path: impl Into) -> Self { + self.current_dir = Some(path.into()); + self + } +} + +impl Default for PathCompleter { + fn default() -> Self { + Self::any() + } +} + +impl ValueCompleter for PathCompleter { + fn complete(&self, current: &OsStr) -> Vec { + let filter = self.filter.as_deref().unwrap_or(&|_| true); + let mut current_dir_actual = None; + let current_dir = self.current_dir.as_deref().or_else(|| { + current_dir_actual = std::env::current_dir().ok(); + current_dir_actual.as_deref() + }); + complete_path(current, current_dir, filter) + } +} + pub(crate) fn complete_path( value_os: &OsStr, current_dir: Option<&std::path::Path>, diff --git a/clap_complete/src/engine/mod.rs b/clap_complete/src/engine/mod.rs index 2e7e25d22c62..43a1f9f0c108 100644 --- a/clap_complete/src/engine/mod.rs +++ b/clap_complete/src/engine/mod.rs @@ -10,5 +10,6 @@ pub use candidate::CompletionCandidate; pub use complete::complete; pub use custom::ArgValueCandidates; pub use custom::ArgValueCompleter; +pub use custom::PathCompleter; pub use custom::ValueCandidates; pub use custom::ValueCompleter; diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index 40a96ba7b6a5..13e7f99f06f8 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -4,7 +4,9 @@ use std::fs; use std::path::Path; use clap::{builder::PossibleValue, Command}; -use clap_complete::engine::{ArgValueCandidates, ArgValueCompleter, CompletionCandidate}; +use clap_complete::engine::{ + ArgValueCandidates, ArgValueCompleter, CompletionCandidate, PathCompleter, +}; use snapbox::assert_data_eq; macro_rules! complete { @@ -575,6 +577,42 @@ d_dir/ ); } +#[test] +fn suggest_value_path_file() { + 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(); + + 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()), + )), + ) + .args_conflicts_with_subcommands(true); + + assert_data_eq!( + complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)), + snapbox::str![[r#" +b_file +c_dir/ +d_dir/ +a_file +"#]], + ); + + assert_data_eq!( + complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)), + snapbox::str!["a_file"], + ); +} + #[test] fn suggest_custom_arg_value() { fn custom_completer() -> Vec { @@ -635,15 +673,21 @@ fn suggest_custom_arg_completer() { .add(ArgValueCompleter::new(custom_completer)), ); - assert_data_eq!(complete!(cmd, "--custom [TAB]"), snapbox::str![[r#" + assert_data_eq!( + complete!(cmd, "--custom [TAB]"), + snapbox::str![[r#" foo bar baz -"#]]); - assert_data_eq!(complete!(cmd, "--custom b[TAB]"), snapbox::str![[r#" +"#]] + ); + assert_data_eq!( + complete!(cmd, "--custom b[TAB]"), + snapbox::str![[r#" bar baz -"#]]); +"#]] + ); } #[test]