diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 69882b41..ed494773 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -118,6 +118,7 @@ _arguments "${_arguments_options[@]}" : \ (query) _arguments "${_arguments_options[@]}" : \ '--exclude=[Exclude the current directory]:path:_files -/' \ +'(--exclude)--basedir=[Only search within this directory]:path:_files -/' \ '-a[Show unavailable directories]' \ '--all[Show unavailable directories]' \ '(-l --list)-i[Use interactive selection]' \ diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index af15c66d..c7c56ee7 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -100,6 +100,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { } 'zoxide;query' { [CompletionResult]::new('--exclude', '--exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory') + [CompletionResult]::new('--basedir', '--basedir', [CompletionResultType]::ParameterName, 'Only search within this directory') [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'Show unavailable directories') [CompletionResult]::new('--all', '--all', [CompletionResultType]::ParameterName, 'Show unavailable directories') [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Use interactive selection') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 73dbd45f..2b3509c0 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -187,7 +187,7 @@ _zoxide() { return 0 ;; zoxide__query) - opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --help --version [KEYWORDS]..." + opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --basedir --help --version [KEYWORDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -200,6 +200,13 @@ _zoxide() { fi return 0 ;; + --basedir) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; *) COMPREPLY=() ;; diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 6183d37b..c18bd786 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -88,6 +88,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| } &'zoxide;query'= { cand --exclude 'Exclude the current directory' + cand --basedir 'Only search within this directory' cand -a 'Show unavailable directories' cand --all 'Show unavailable directories' cand -i 'Use interactive selection' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 96dd86e1..f192328d 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -58,6 +58,7 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l no-cmd -d 'Preven complete -c zoxide -n "__fish_zoxide_using_subcommand init" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand init" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -l exclude -d 'Exclude the current directory' -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand query" -l basedir -d 'Only search within this directory' -r -f -a "(__fish_complete_directories)" complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s a -l all -d 'Show unavailable directories' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s l -l list -d 'List all matching directories' diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 0200591b..fc796294 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -204,6 +204,19 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "--basedir", + description: "Only search within this directory", + exclusiveOn: [ + "--exclude", + ], + isRepeatable: true, + args: { + name: "basedir", + isOptional: true, + template: "folders", + }, + }, { name: ["-a", "--all"], description: "Show unavailable directories", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index cff7e790..ceffb3e4 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -180,6 +180,10 @@ pub struct Query { /// Exclude the current directory #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] pub exclude: Option, + + /// Only search within this directory + #[clap(long, value_hint = ValueHint::DirPath, value_name = "path", conflicts_with = "exclude")] + pub basedir: Option, } /// Remove a directory from the database diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 362d80a3..b050ab48 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -79,7 +79,8 @@ impl Query { fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Result> { let mut options = StreamOptions::new(now) .with_keywords(self.keywords.iter().map(|s| s.as_str())) - .with_exclude(config::exclude_dirs()?); + .with_exclude(config::exclude_dirs()?) + .with_basedir(self.basedir.clone()); if !self.all { let resolve_symlinks = config::resolve_symlinks(); options = options.with_exists(true).with_resolve_symlinks(resolve_symlinks); diff --git a/src/db/stream.rs b/src/db/stream.rs index 4af7d7a9..575ceb8c 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -1,5 +1,6 @@ use std::iter::Rev; use std::ops::Range; +use std::path::{Path, PathBuf}; use std::{fs, path}; use glob::Pattern; @@ -40,6 +41,10 @@ impl<'a> Stream<'a> { continue; } + if !self.filter_by_basedir(&dir.path) { + continue; + } + let dir = &self.db.dirs()[idx]; return Some(dir); } @@ -91,6 +96,15 @@ impl<'a> Stream<'a> { if self.options.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; resolver(path).map(|metadata| metadata.is_dir()).unwrap_or_default() } + + fn filter_by_basedir(&self, path: &str) -> bool { + if let Some(basedir) = &self.options.basedir { + let path = Path::new(path); + return path.starts_with(basedir); + } + + true + } } pub struct StreamOptions { @@ -112,6 +126,10 @@ pub struct StreamOptions { /// Directories that do not exist and haven't been accessed since TTL will /// be lazily removed. ttl: Epoch, + + /// Only return directories within this parent directory + /// Does not check if the path exists + basedir: Option, } impl StreamOptions { @@ -123,6 +141,7 @@ impl StreamOptions { exists: false, resolve_symlinks: false, ttl: now.saturating_sub(3 * MONTH), + basedir: None, } } @@ -149,6 +168,11 @@ impl StreamOptions { self.resolve_symlinks = resolve_symlinks; self } + + pub fn with_basedir(mut self, basedir: Option) -> Self { + self.basedir = basedir; + self + } } #[cfg(test)] @@ -180,7 +204,7 @@ mod tests { #[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { - let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let db = &mut Database::new(PathBuf::default(), Vec::default(), |_| Vec::default(), false); let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options); assert_eq!(is_match, stream.filter_by_keywords(path));