mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2024-11-25 14:07:35 +00:00
Add zoxide query --all flag (#206)
This commit is contained in:
parent
d33bfd111f
commit
ba8a5f3167
@ -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
|
||||
|
||||
|
7
build.rs
7
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();
|
||||
}
|
||||
|
@ -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]' \
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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 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 {
|
||||
|
@ -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")?;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }),
|
||||
|
Loading…
Reference in New Issue
Block a user