diff --git a/CHANGELOG.md b/CHANGELOG.md index c617e8e..4b1e7c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Auto-generated shell completions. +- `zoxide query --all` for listing deleted directories. ### Fixed diff --git a/build.rs b/build.rs index ccb77d8..0a3ac9c 100644 --- a/build.rs +++ b/build.rs @@ -41,5 +41,12 @@ fn generate_completions() { fn main() { let version = git_version().unwrap_or_else(crate_version); println!("cargo:rustc-env=ZOXIDE_VERSION={}", version); + + // Since we are generating completions in the package directory, we need to + // set this so that Cargo doesn't rebuild every time. + println!("cargo:rerun-if-changed=src"); + println!("cargo:rerun-if-changed=templates"); + println!("cargo:rerun-if-changed=tests"); + generate_completions(); } diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 1d56a50..81a4efd 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -57,6 +57,7 @@ _arguments "${_arguments_options[@]}" \ (query) _arguments "${_arguments_options[@]}" \ '--exclude=[Exclude a path from results]' \ +'--all[Show deleted directories]' \ '(-l --list)-i[Use interactive selection]' \ '(-l --list)--interactive[Use interactive selection]' \ '(-i --interactive)-l[List all matching directories]' \ diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index 1392f3b..31a93a3 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -53,6 +53,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { } 'zoxide;query' { [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude a path from results') + [CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show deleted directories') [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 5bfc32a..2ab89f9 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -108,7 +108,7 @@ _zoxide() { return 0 ;; zoxide__query) - opts=" -i -l -s -h --interactive --list --score --exclude --help ... " + opts=" -i -l -s -h --all --interactive --list --score --exclude --help ... " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 16ff14f..b24cc19 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -44,6 +44,7 @@ edit:completion:arg-completer[zoxide] = [@words]{ } &'zoxide;query'= { cand --exclude 'Exclude a path from results' + cand --all 'Show deleted directories' cand -i 'Use interactive selection' cand --interactive 'Use interactive selection' cand -l 'List all matching directories' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 450b73f..84373f8 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -18,6 +18,7 @@ complete -c zoxide -n "__fish_seen_subcommand_from init" -l no-aliases -d 'Preve complete -c zoxide -n "__fish_seen_subcommand_from init" -s h -l help -d 'Prints help information' complete -c zoxide -n "__fish_seen_subcommand_from query" -r complete -c zoxide -n "__fish_seen_subcommand_from query" -l exclude -d 'Exclude a path from results' -r +complete -c zoxide -n "__fish_seen_subcommand_from query" -l all -d 'Show deleted directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List all matching directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results' diff --git a/man/zoxide-add.1 b/man/zoxide-add.1 index 6760a4b..20d0de1 100644 --- a/man/zoxide-add.1 +++ b/man/zoxide-add.1 @@ -15,7 +15,7 @@ If you'd like to prevent a directory from being added to the database, see the .SH OPTIONS .TP .B -h, --help -Prints help information +Prints help information. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp diff --git a/man/zoxide-import.1 b/man/zoxide-import.1 index cdb77de..de58470 100644 --- a/man/zoxide-import.1 +++ b/man/zoxide-import.1 @@ -18,7 +18,7 @@ l l. algorithm is too different to import the scores. .TP .B -h, --help -Prints help information +Prints help information. .TP .B --merge By default, the import fails if the current database is not already empty. This diff --git a/man/zoxide-init.1 b/man/zoxide-init.1 index 91ed1a6..c38e011 100644 --- a/man/zoxide-init.1 +++ b/man/zoxide-init.1 @@ -82,7 +82,7 @@ Changes the prefix of predefined aliases (\fBz\fR, \fBzi\fR). e.g. --cmd j would change the aliases to j and ji respectively. .TP .B -h, --help -Prints help information +Prints help information. .TP .B --hook \fIHOOK\fR Changes how often zoxide increments a directory's score: diff --git a/man/zoxide-query.1 b/man/zoxide-query.1 index 01dcd7c..ff535be 100644 --- a/man/zoxide-query.1 +++ b/man/zoxide-query.1 @@ -8,8 +8,11 @@ Queries the database for paths matching the keywords. The exact \fBMATCHING\fR algorithm is described in \fBzoxide\fR(1). .SH OPTIONS .TP +.B --all +Show deleted directories. +.TP .B -h, --help -Prints help information +Prints help information. .TP .B -i, --interactive Use interactive selection. This option requires fzf. diff --git a/man/zoxide-remove.1 b/man/zoxide-remove.1 index 6d8192a..bc89b4f 100644 --- a/man/zoxide-remove.1 +++ b/man/zoxide-remove.1 @@ -9,7 +9,7 @@ If you'd like to permanently exclude a directory from the database, see the .SH OPTIONS .TP .B -h, --help -Prints help information +Prints help information. .TP .B -i, --interactive \fI[KEYWORDS]\fR Use interactive selection. This option requires fzf. diff --git a/man/zoxide.1 b/man/zoxide.1 index 5364ffe..f3788fb 100644 --- a/man/zoxide.1 +++ b/man/zoxide.1 @@ -36,7 +36,7 @@ Remove a directory from the database. .SH OPTIONS .TP .B -h, --help -Prints help information +Prints help information. .TP .B -V, --version Prints version information diff --git a/src/app/_app.rs b/src/app/_app.rs index 92a7c34..4989883 100644 --- a/src/app/_app.rs +++ b/src/app/_app.rs @@ -99,6 +99,10 @@ pub enum InitShell { pub struct Query { pub keywords: Vec, + /// Show deleted directories + #[clap(long)] + pub all: bool, + /// Use interactive selection #[clap(long, short, conflicts_with = "list")] pub interactive: bool, diff --git a/src/app/add.rs b/src/app/add.rs index 2edd318..490e8fe 100644 --- a/src/app/add.rs +++ b/src/app/add.rs @@ -1,5 +1,4 @@ -use super::Run; -use crate::app::Add; +use crate::app::{Add, Run}; use crate::config; use crate::db::DatabaseFile; use crate::util; diff --git a/src/app/import.rs b/src/app/import.rs index 169c51c..2cde1c3 100644 --- a/src/app/import.rs +++ b/src/app/import.rs @@ -1,5 +1,4 @@ -use super::Run; -use crate::app::{Import, ImportFrom}; +use crate::app::{Import, ImportFrom, Run}; use crate::config; use crate::db::{Database, DatabaseFile, Dir, DirList}; diff --git a/src/app/init.rs b/src/app/init.rs index d9c2965..19f9562 100644 --- a/src/app/init.rs +++ b/src/app/init.rs @@ -1,7 +1,6 @@ -use super::Run; -use crate::app::{Init, InitShell}; +use crate::app::{Init, InitShell, Run}; use crate::config; -use crate::error::WriteErrorHandler; +use crate::error::BrokenPipeHandler; use crate::shell::{self, Opts}; use anyhow::{Context, Result}; diff --git a/src/app/query.rs b/src/app/query.rs index 1381315..0e4e8e7 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -1,8 +1,7 @@ -use super::Run; -use crate::app::Query; +use crate::app::{Query, Run}; use crate::config; -use crate::db::{self, DatabaseFile}; -use crate::error::WriteErrorHandler; +use crate::db::{DatabaseFile, Matcher}; +use crate::error::BrokenPipeHandler; use crate::fzf::Fzf; use crate::util; @@ -15,13 +14,16 @@ impl Run for Query { let data_dir = config::zo_data_dir()?; let mut db = DatabaseFile::new(data_dir); let mut db = db.open()?; - - let query = db::Query::new(&self.keywords); let now = util::current_time()?; - let resolve_symlinks = config::zo_resolve_symlinks(); + let mut matcher = Matcher::new().with_keywords(&self.keywords); + if !self.all { + let resolve_symlinks = config::zo_resolve_symlinks(); + matcher = matcher.with_exists(resolve_symlinks); + } + let mut matches = db - .iter_matches(&query, now, resolve_symlinks) + .iter(&matcher, now) .filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref()); if self.interactive { diff --git a/src/app/remove.rs b/src/app/remove.rs index 9f69c1d..d9295fe 100644 --- a/src/app/remove.rs +++ b/src/app/remove.rs @@ -1,8 +1,7 @@ -use super::Run; -use crate::app::Remove; +use crate::app::{Remove, Run}; use crate::config; -use crate::db::{DatabaseFile, Query}; -use crate::error::WriteErrorHandler; +use crate::db::{DatabaseFile, Matcher}; +use crate::error::BrokenPipeHandler; use crate::fzf::Fzf; use crate::util; @@ -19,12 +18,11 @@ impl Run for Remove { let selection; match &self.interactive { Some(keywords) => { - let query = Query::new(keywords); + let matcher = Matcher::new().with_keywords(keywords); let now = util::current_time()?; - let resolve_symlinks = config::zo_resolve_symlinks(); let mut fzf = Fzf::new(true)?; - for dir in db.iter_matches(&query, now, resolve_symlinks) { + for dir in db.iter(&matcher, now) { writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?; } diff --git a/src/db/dir.rs b/src/db/dir.rs index 62f3701..6880c36 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -1,12 +1,9 @@ -use super::Query; - use anyhow::{bail, Context, Result}; use bincode::Options as _; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; -use std::fs; use std::ops::{Deref, DerefMut}; #[derive(Debug, Deserialize, Serialize)] @@ -95,16 +92,6 @@ pub struct Dir<'a> { } impl Dir<'_> { - pub fn is_match(&self, query: &Query, resolve_symlinks: bool) -> bool { - let resolver = if resolve_symlinks { - fs::symlink_metadata - } else { - fs::metadata - }; - let path = self.path.as_ref(); - query.matches(path) && resolver(path).map(|m| m.is_dir()).unwrap_or(false) - } - pub fn score(&self, now: Epoch) -> Rank { const HOUR: Epoch = 60 * 60; const DAY: Epoch = 24 * HOUR; diff --git a/src/db/mod.rs b/src/db/mod.rs index c16243f..5a93d66 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,7 +2,7 @@ mod dir; mod query; pub use dir::{Dir, DirList, Epoch, Rank}; -pub use query::Query; +pub use query::Matcher; use anyhow::{Context, Result}; use ordered_float::OrderedFloat; @@ -72,17 +72,12 @@ impl<'a> Database<'a> { self.modified = true; } - pub fn iter_matches<'b>( - &'b mut self, - query: &'b Query, - now: Epoch, - resolve_symlinks: bool, - ) -> impl DoubleEndedIterator { + pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator { self.dirs .sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now)))); self.dirs .iter() - .filter(move |dir| dir.is_match(&query, resolve_symlinks)) + .filter(move |dir| m.matches(dir.path.as_ref())) } /// Removes the directory with `path` from the store. diff --git a/src/db/query.rs b/src/db/query.rs index 145ba41..add5483 100644 --- a/src/db/query.rs +++ b/src/db/query.rs @@ -1,40 +1,72 @@ use crate::util; +use std::fs; use std::path; -pub struct Query(Vec); +#[derive(Debug, Default)] +pub struct Matcher { + keywords: Vec, + check_exists: bool, + resolve_symlinks: bool, +} -impl Query { - pub fn new(keywords: I) -> Query - where - I: IntoIterator, - S: AsRef, - { - Query(keywords.into_iter().map(util::to_lowercase).collect()) +impl Matcher { + pub fn new() -> Matcher { + Matcher::default() + } + + pub fn with_exists(mut self, resolve_symlinks: bool) -> Matcher { + self.check_exists = true; + self.resolve_symlinks = resolve_symlinks; + self + } + + pub fn with_keywords>(mut self, keywords: &[S]) -> Matcher { + self.keywords = keywords.iter().map(util::to_lowercase).collect(); + self } pub fn matches>(&self, path: S) -> bool { - let keywords = &self.0; - let (keywords_last, keywords) = match keywords.split_last() { + self.matches_keywords(&path) && self.matches_exists(path) + } + + fn matches_exists>(&self, path: S) -> bool { + if !self.check_exists { + return true; + } + + let resolver = if self.resolve_symlinks { + fs::symlink_metadata + } else { + fs::metadata + }; + + resolver(path.as_ref()) + .map(|m| m.is_dir()) + .unwrap_or_default() + } + + fn matches_keywords>(&self, path: S) -> bool { + let (keywords_last, keywords) = match self.keywords.split_last() { Some(split) => split, None => return true, }; let path = util::to_lowercase(path); - let mut subpath = path.as_str(); - match subpath.rfind(keywords_last) { + let mut path = path.as_str(); + match path.rfind(keywords_last) { Some(idx) => { - if subpath[idx + keywords_last.len()..].contains(path::is_separator) { + if path[idx + keywords_last.len()..].contains(path::is_separator) { return false; } - subpath = &subpath[..idx]; + path = &path[..idx]; } None => return false, } for keyword in keywords.iter().rev() { - match subpath.rfind(keyword) { - Some(idx) => subpath = &subpath[..idx], + match path.rfind(keyword) { + Some(idx) => path = &path[..idx], None => return false, } } @@ -45,7 +77,7 @@ impl Query { #[cfg(test)] mod tests { - use super::Query; + use super::Matcher; #[test] fn query() { @@ -72,7 +104,8 @@ mod tests { ]; for &(keywords, path, is_match) in CASES { - assert_eq!(is_match, Query::new(keywords).matches(path)) + let matcher = Matcher::new().with_keywords(keywords); + assert_eq!(is_match, matcher.matches(path)) } } } diff --git a/src/error.rs b/src/error.rs index a1942cd..34954d5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,11 +15,11 @@ impl Display for SilentExit { } } -pub trait WriteErrorHandler { +pub trait BrokenPipeHandler { fn pipe_exit(self, device: &str) -> Result<()>; } -impl WriteErrorHandler for io::Result<()> { +impl BrokenPipeHandler for io::Result<()> { fn pipe_exit(self, device: &str) -> Result<()> { match self { Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }),