Add zoxide query --all flag (#206)

This commit is contained in:
Ajeet D'Souza 2021-05-08 08:35:34 +05:30 committed by GitHub
parent d33bfd111f
commit ba8a5f3167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 101 additions and 70 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Auto-generated shell completions. - Auto-generated shell completions.
- `zoxide query --all` for listing deleted directories.
### Fixed ### Fixed

View File

@ -41,5 +41,12 @@ fn generate_completions() {
fn main() { fn main() {
let version = git_version().unwrap_or_else(crate_version); let version = git_version().unwrap_or_else(crate_version);
println!("cargo:rustc-env=ZOXIDE_VERSION={}", 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(); generate_completions();
} }

View File

@ -57,6 +57,7 @@ _arguments "${_arguments_options[@]}" \
(query) (query)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--exclude=[Exclude a path from results]' \ '--exclude=[Exclude a path from results]' \
'--all[Show deleted directories]' \
'(-l --list)-i[Use interactive selection]' \ '(-l --list)-i[Use interactive selection]' \
'(-l --list)--interactive[Use interactive selection]' \ '(-l --list)--interactive[Use interactive selection]' \
'(-i --interactive)-l[List all matching directories]' \ '(-i --interactive)-l[List all matching directories]' \

View File

@ -53,6 +53,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
} }
'zoxide;query' { 'zoxide;query' {
[CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude a path from results') [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('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories')

View File

@ -108,7 +108,7 @@ _zoxide() {
return 0 return 0
;; ;;
zoxide__query) zoxide__query)
opts=" -i -l -s -h --interactive --list --score --exclude --help <keywords>... " opts=" -i -l -s -h --all --interactive --list --score --exclude --help <keywords>... "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@ -44,6 +44,7 @@ edit:completion:arg-completer[zoxide] = [@words]{
} }
&'zoxide;query'= { &'zoxide;query'= {
cand --exclude 'Exclude a path from results' cand --exclude 'Exclude a path from results'
cand --all 'Show deleted directories'
cand -i 'Use interactive selection' cand -i 'Use interactive selection'
cand --interactive 'Use interactive selection' cand --interactive 'Use interactive selection'
cand -l 'List all matching directories' cand -l 'List all matching directories'

View File

@ -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 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" -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 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 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 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' complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results'

View File

@ -15,7 +15,7 @@ If you'd like to prevent a directory from being added to the database, see the
.SH OPTIONS .SH OPTIONS
.TP .TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp

View File

@ -18,7 +18,7 @@ l l.
algorithm is too different to import the scores. algorithm is too different to import the scores.
.TP .TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.TP .TP
.B --merge .B --merge
By default, the import fails if the current database is not already empty. This By default, the import fails if the current database is not already empty. This

View File

@ -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. e.g. --cmd j would change the aliases to j and ji respectively.
.TP .TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.TP .TP
.B --hook \fIHOOK\fR .B --hook \fIHOOK\fR
Changes how often zoxide increments a directory's score: Changes how often zoxide increments a directory's score:

View File

@ -8,8 +8,11 @@ Queries the database for paths matching the keywords. The exact \fBMATCHING\fR
algorithm is described in \fBzoxide\fR(1). algorithm is described in \fBzoxide\fR(1).
.SH OPTIONS .SH OPTIONS
.TP .TP
.B --all
Show deleted directories.
.TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.TP .TP
.B -i, --interactive .B -i, --interactive
Use interactive selection. This option requires fzf. Use interactive selection. This option requires fzf.

View File

@ -9,7 +9,7 @@ If you'd like to permanently exclude a directory from the database, see the
.SH OPTIONS .SH OPTIONS
.TP .TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.TP .TP
.B -i, --interactive \fI[KEYWORDS]\fR .B -i, --interactive \fI[KEYWORDS]\fR
Use interactive selection. This option requires fzf. Use interactive selection. This option requires fzf.

View File

@ -36,7 +36,7 @@ Remove a directory from the database.
.SH OPTIONS .SH OPTIONS
.TP .TP
.B -h, --help .B -h, --help
Prints help information Prints help information.
.TP .TP
.B -V, --version .B -V, --version
Prints version information Prints version information

View File

@ -99,6 +99,10 @@ pub enum InitShell {
pub struct Query { pub struct Query {
pub keywords: Vec<String>, pub keywords: Vec<String>,
/// Show deleted directories
#[clap(long)]
pub all: bool,
/// Use interactive selection /// Use interactive selection
#[clap(long, short, conflicts_with = "list")] #[clap(long, short, conflicts_with = "list")]
pub interactive: bool, pub interactive: bool,

View File

@ -1,5 +1,4 @@
use super::Run; use crate::app::{Add, Run};
use crate::app::Add;
use crate::config; use crate::config;
use crate::db::DatabaseFile; use crate::db::DatabaseFile;
use crate::util; use crate::util;

View File

@ -1,5 +1,4 @@
use super::Run; use crate::app::{Import, ImportFrom, Run};
use crate::app::{Import, ImportFrom};
use crate::config; use crate::config;
use crate::db::{Database, DatabaseFile, Dir, DirList}; use crate::db::{Database, DatabaseFile, Dir, DirList};

View File

@ -1,7 +1,6 @@
use super::Run; use crate::app::{Init, InitShell, Run};
use crate::app::{Init, InitShell};
use crate::config; use crate::config;
use crate::error::WriteErrorHandler; use crate::error::BrokenPipeHandler;
use crate::shell::{self, Opts}; use crate::shell::{self, Opts};
use anyhow::{Context, Result}; use anyhow::{Context, Result};

View File

@ -1,8 +1,7 @@
use super::Run; use crate::app::{Query, Run};
use crate::app::Query;
use crate::config; use crate::config;
use crate::db::{self, DatabaseFile}; use crate::db::{DatabaseFile, Matcher};
use crate::error::WriteErrorHandler; use crate::error::BrokenPipeHandler;
use crate::fzf::Fzf; use crate::fzf::Fzf;
use crate::util; use crate::util;
@ -15,13 +14,16 @@ impl Run for Query {
let data_dir = config::zo_data_dir()?; let data_dir = config::zo_data_dir()?;
let mut db = DatabaseFile::new(data_dir); let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?; let mut db = db.open()?;
let query = db::Query::new(&self.keywords);
let now = util::current_time()?; 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 let mut matches = db
.iter_matches(&query, now, resolve_symlinks) .iter(&matcher, now)
.filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref()); .filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref());
if self.interactive { if self.interactive {

View File

@ -1,8 +1,7 @@
use super::Run; use crate::app::{Remove, Run};
use crate::app::Remove;
use crate::config; use crate::config;
use crate::db::{DatabaseFile, Query}; use crate::db::{DatabaseFile, Matcher};
use crate::error::WriteErrorHandler; use crate::error::BrokenPipeHandler;
use crate::fzf::Fzf; use crate::fzf::Fzf;
use crate::util; use crate::util;
@ -19,12 +18,11 @@ impl Run for Remove {
let selection; let selection;
match &self.interactive { match &self.interactive {
Some(keywords) => { Some(keywords) => {
let query = Query::new(keywords); let matcher = Matcher::new().with_keywords(keywords);
let now = util::current_time()?; let now = util::current_time()?;
let resolve_symlinks = config::zo_resolve_symlinks();
let mut fzf = Fzf::new(true)?; 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")?; writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
} }

View File

@ -1,12 +1,9 @@
use super::Query;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use bincode::Options as _; use bincode::Options as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::fs;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -95,16 +92,6 @@ pub struct Dir<'a> {
} }
impl Dir<'_> { 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 { pub fn score(&self, now: Epoch) -> Rank {
const HOUR: Epoch = 60 * 60; const HOUR: Epoch = 60 * 60;
const DAY: Epoch = 24 * HOUR; const DAY: Epoch = 24 * HOUR;

View File

@ -2,7 +2,7 @@ mod dir;
mod query; mod query;
pub use dir::{Dir, DirList, Epoch, Rank}; pub use dir::{Dir, DirList, Epoch, Rank};
pub use query::Query; pub use query::Matcher;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
@ -72,17 +72,12 @@ impl<'a> Database<'a> {
self.modified = true; self.modified = true;
} }
pub fn iter_matches<'b>( pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
&'b mut self,
query: &'b Query,
now: Epoch,
resolve_symlinks: bool,
) -> impl DoubleEndedIterator<Item = &'b Dir> {
self.dirs self.dirs
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now)))); .sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
self.dirs self.dirs
.iter() .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. /// Removes the directory with `path` from the store.

View File

@ -1,40 +1,72 @@
use crate::util; use crate::util;
use std::fs;
use std::path; use std::path;
pub struct Query(Vec<String>); #[derive(Debug, Default)]
pub struct Matcher {
keywords: Vec<String>,
check_exists: bool,
resolve_symlinks: bool,
}
impl Query { impl Matcher {
pub fn new<I, S>(keywords: I) -> Query pub fn new() -> Matcher {
where Matcher::default()
I: IntoIterator<Item = S>, }
S: AsRef<str>,
{ pub fn with_exists(mut self, resolve_symlinks: bool) -> Matcher {
Query(keywords.into_iter().map(util::to_lowercase).collect()) self.check_exists = true;
self.resolve_symlinks = resolve_symlinks;
self
}
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Matcher {
self.keywords = keywords.iter().map(util::to_lowercase).collect();
self
} }
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool { pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
let keywords = &self.0; self.matches_keywords(&path) && self.matches_exists(path)
let (keywords_last, keywords) = match keywords.split_last() { }
fn matches_exists<S: AsRef<str>>(&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<S: AsRef<str>>(&self, path: S) -> bool {
let (keywords_last, keywords) = match self.keywords.split_last() {
Some(split) => split, Some(split) => split,
None => return true, None => return true,
}; };
let path = util::to_lowercase(path); let path = util::to_lowercase(path);
let mut subpath = path.as_str(); let mut path = path.as_str();
match subpath.rfind(keywords_last) { match path.rfind(keywords_last) {
Some(idx) => { Some(idx) => {
if subpath[idx + keywords_last.len()..].contains(path::is_separator) { if path[idx + keywords_last.len()..].contains(path::is_separator) {
return false; return false;
} }
subpath = &subpath[..idx]; path = &path[..idx];
} }
None => return false, None => return false,
} }
for keyword in keywords.iter().rev() { for keyword in keywords.iter().rev() {
match subpath.rfind(keyword) { match path.rfind(keyword) {
Some(idx) => subpath = &subpath[..idx], Some(idx) => path = &path[..idx],
None => return false, None => return false,
} }
} }
@ -45,7 +77,7 @@ impl Query {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Query; use super::Matcher;
#[test] #[test]
fn query() { fn query() {
@ -72,7 +104,8 @@ mod tests {
]; ];
for &(keywords, path, is_match) in CASES { 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))
} }
} }
} }

View File

@ -15,11 +15,11 @@ impl Display for SilentExit {
} }
} }
pub trait WriteErrorHandler { pub trait BrokenPipeHandler {
fn pipe_exit(self, device: &str) -> Result<()>; fn pipe_exit(self, device: &str) -> Result<()>;
} }
impl WriteErrorHandler for io::Result<()> { impl BrokenPipeHandler for io::Result<()> {
fn pipe_exit(self, device: &str) -> Result<()> { fn pipe_exit(self, device: &str) -> Result<()> {
match self { match self {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }),