feat(complete): Add PathCompleter

This commit is contained in:
Ed Page 2024-08-21 13:40:07 -05:00
parent 82a360aa54
commit 49b8108f8c
4 changed files with 114 additions and 1 deletions

View file

@ -150,6 +150,78 @@ where
}
}
/// Complete a value as a [`std::path::Path`]
///
/// # Example
///
/// ```rust
/// use clap::Parser;
/// use clap_complete::engine::{ArgValueCompleter, PathCompleter};
///
/// #[derive(Debug, Parser)]
/// struct Cli {
/// #[arg(long, add = ArgValueCompleter::new(PathCompleter::file()))]
/// custom: Option<String>,
/// }
/// ```
pub struct PathCompleter {
current_dir: Option<std::path::PathBuf>,
filter: Option<Box<dyn Fn(&std::path::Path) -> 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<std::path::PathBuf>) -> 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<CompletionCandidate> {
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>,

View file

@ -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;

View file

@ -87,6 +87,8 @@ pub use engine::ArgValueCompleter;
#[cfg(feature = "unstable-dynamic")]
pub use engine::CompletionCandidate;
#[cfg(feature = "unstable-dynamic")]
pub use engine::PathCompleter;
#[cfg(feature = "unstable-dynamic")]
pub use env::CompleteEnv;
/// Deprecated, see [`aot`]

View file

@ -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#"
a_file
b_file
c_dir/
d_dir/
"#]],
);
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<CompletionCandidate> {