Skip to content

Commit

Permalink
feat(complete): Add PathCompleter
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Aug 21, 2024
1 parent 9e4c6a6 commit 44f78bf
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 1 deletion.
59 changes: 59 additions & 0 deletions clap_complete/src/engine/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,65 @@ where
}
}

/// Complete a value as a [`std::path::Path`]
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>,
Expand Down
1 change: 1 addition & 0 deletions clap_complete/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
40 changes: 39 additions & 1 deletion clap_complete/tests/testsuite/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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> {
Expand Down

0 comments on commit 44f78bf

Please sign in to comment.