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
- Auto-generated shell completions.
- `zoxide query --all` for listing deleted directories.
### Fixed

View File

@ -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();
}

View File

@ -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]' \

View File

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

View File

@ -108,7 +108,7 @@ _zoxide() {
return 0
;;
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
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View File

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

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

View File

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

View File

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

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.
.TP
.B -h, --help
Prints help information
Prints help information.
.TP
.B --hook \fIHOOK\fR
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).
.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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")?;
}

View File

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

View File

@ -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<Item = &'b Dir> {
pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
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.

View File

@ -1,40 +1,72 @@
use crate::util;
use std::fs;
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 {
pub fn new<I, S>(keywords: I) -> Query
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
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<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 {
let keywords = &self.0;
let (keywords_last, keywords) = match keywords.split_last() {
self.matches_keywords(&path) && self.matches_exists(path)
}
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,
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))
}
}
}

View File

@ -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 }),