Add zoxide query --all flag

This commit is contained in:
Ajeet D'Souza 2021-05-08 04:24:38 +05:30
parent d33bfd111f
commit 03a30939f3
16 changed files with 85 additions and 64 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

@ -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

@ -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 mut matcher = Matcher::new().with_keywords(&self.keywords);
if !self.all {
let resolve_symlinks = config::zo_resolve_symlinks(); 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 }),