From 261cf8846a0ee03ce072b7632010341a03a66e23 Mon Sep 17 00:00:00 2001 From: Azalea Colburn <62953415+azaleacolburn@users.noreply.github.com> Date: Sun, 10 Aug 2025 00:56:52 -0700 Subject: [PATCH] --basedir query option (#1027) --- contrib/completions/_zoxide | 1 + contrib/completions/_zoxide.ps1 | 1 + contrib/completions/zoxide.bash | 9 ++++- contrib/completions/zoxide.elv | 1 + contrib/completions/zoxide.fish | 1 + contrib/completions/zoxide.nu | 1 + contrib/completions/zoxide.ts | 10 ++++++ src/cmd/cmd.rs | 4 +++ src/cmd/query.rs | 3 +- src/db/mod.rs | 4 +-- src/db/stream.rs | 59 +++++++++++++++++++++++---------- src/shell.rs | 2 +- src/util.rs | 2 +- 13 files changed, 74 insertions(+), 24 deletions(-) diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 33aaace..97e654f 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -120,6 +120,7 @@ _arguments "${_arguments_options[@]}" : \ (query) _arguments "${_arguments_options[@]}" : \ '--exclude=[Exclude the current directory]:path:_files -/' \ +'--base-dir=[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 a26adda..bb47d3a 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -102,6 +102,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { } 'zoxide;query' { [CompletionResult]::new('--exclude', '--exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory') + [CompletionResult]::new('--base-dir', '--base-dir', [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 1c2ed84..82b174e 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -199,7 +199,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 --base-dir --help --version [KEYWORDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -212,6 +212,13 @@ _zoxide() { fi return 0 ;; + --base-dir) + 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 e4cb36b..93c57af 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -90,6 +90,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| } &'zoxide;query'= { cand --exclude 'Exclude the current directory' + cand --base-dir '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 85c3c49..3a0bfe7 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -62,6 +62,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 base-dir -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.nu b/contrib/completions/zoxide.nu index 34b3ac0..642908e 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -82,6 +82,7 @@ module completions { --list(-l) # List all matching directories --score(-s) # Print score with results --exclude: path # Exclude the current directory + --base-dir: path # Only search within this directory --help(-h) # Print help --version(-V) # Print version ] diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 9e593d0..1e0d404 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -214,6 +214,16 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "--base-dir", + description: "Only search within this directory", + isRepeatable: true, + args: { + name: "base_dir", + isOptional: true, + template: "folders", + }, + }, { name: ["-a", "--all"], description: "Show unavailable directories", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index d25cda3..7359786 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -186,6 +186,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")] + pub base_dir: Option, } /// Remove a directory from the database diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 362d80a..6539c2e 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_base_dir(self.base_dir.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/mod.rs b/src/db/mod.rs index a19efe9..d459f39 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -183,7 +183,7 @@ impl Database { *self.borrow_dirty() } - pub fn dirs(&self) -> &[Dir] { + pub fn dirs(&self) -> &[Dir<'_>] { self.borrow_dirs() } @@ -203,7 +203,7 @@ impl Database { .context("could not serialize database") } - fn deserialize(bytes: &[u8]) -> Result> { + fn deserialize(bytes: &[u8]) -> Result>> { // Assume a maximum size for the database. This prevents bincode from throwing // strange errors when it encounters invalid data. const MAX_SIZE: u64 = 32 << 20; // 32 MiB diff --git a/src/db/stream.rs b/src/db/stream.rs index 4af7d7a..4b06193 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; use std::{fs, path}; use glob::Pattern; @@ -20,7 +21,7 @@ impl<'a> Stream<'a> { Stream { db, idxs, options } } - pub fn next(&mut self) -> Option<&Dir> { + pub fn next(&mut self) -> Option<&Dir<'_>> { while let Some(idx) = self.idxs.next() { let dir = &self.db.dirs()[idx]; @@ -28,11 +29,16 @@ impl<'a> Stream<'a> { continue; } + if !self.filter_by_base_dir(&dir.path) { + continue; + } + if !self.filter_by_exclude(&dir.path) { self.db.swap_remove(idx); continue; } + // Exists queries are slow, this should always be checked last. if !self.filter_by_exists(&dir.path) { if dir.last_accessed < self.options.ttl { self.db.swap_remove(idx); @@ -47,6 +53,30 @@ impl<'a> Stream<'a> { None } + fn filter_by_base_dir(&self, path: &str) -> bool { + match &self.options.base_dir { + Some(base_dir) => Path::new(path).starts_with(base_dir), + None => true, + } + } + + fn filter_by_exclude(&self, path: &str) -> bool { + !self.options.exclude.iter().any(|pattern| pattern.matches(path)) + } + + fn filter_by_exists(&self, path: &str) -> bool { + if !self.options.exists { + return true; + } + + // The logic here is reversed - if we resolve symlinks when adding entries to + // the database, we should not return symlinks when querying back from + // the database. + let resolver = + if self.options.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; + resolver(path).map(|metadata| metadata.is_dir()).unwrap_or_default() + } + fn filter_by_keywords(&self, path: &str) -> bool { let (keywords_last, keywords) = match self.options.keywords.split_last() { Some(split) => split, @@ -74,23 +104,6 @@ impl<'a> Stream<'a> { true } - - fn filter_by_exclude(&self, path: &str) -> bool { - !self.options.exclude.iter().any(|pattern| pattern.matches(path)) - } - - fn filter_by_exists(&self, path: &str) -> bool { - if !self.options.exists { - return true; - } - - // The logic here is reversed - if we resolve symlinks when adding entries to - // the database, we should not return symlinks when querying back from - // the database. - let resolver = - if self.options.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; - resolver(path).map(|metadata| metadata.is_dir()).unwrap_or_default() - } } pub struct StreamOptions { @@ -112,6 +125,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 + base_dir: Option, } impl StreamOptions { @@ -123,6 +140,7 @@ impl StreamOptions { exists: false, resolve_symlinks: false, ttl: now.saturating_sub(3 * MONTH), + base_dir: None, } } @@ -149,6 +167,11 @@ impl StreamOptions { self.resolve_symlinks = resolve_symlinks; self } + + pub fn with_base_dir(mut self, base_dir: Option) -> Self { + self.base_dir = base_dir; + self + } } #[cfg(test)] diff --git a/src/shell.rs b/src/shell.rs index e77ddd1..37fe1a2 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -97,7 +97,7 @@ mod tests { #[apply(opts)] fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; - let mut source = String::default(); + let mut source = String::new(); // Filter out lines using edit:*, since those functions are only available in // the interactive editor. diff --git a/src/util.rs b/src/util.rs index f74acaf..996f61d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -135,7 +135,7 @@ impl FzfChild { mem::drop(self.0.stdin.take()); let mut stdout = self.0.stdout.take().unwrap(); - let mut output = String::default(); + let mut output = String::new(); stdout.read_to_string(&mut output).context("failed to read from fzf")?; let status = self.0.wait().context("wait failed on fzf")?;