Multi-select for zoxide remove -i (#192)

This commit is contained in:
Ajeet D'Souza 2021-04-29 01:31:00 +05:30 committed by GitHub
parent 0eb4418fd6
commit 027ae1df47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 90 additions and 93 deletions

View File

@ -9,16 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Manpages for each subcommand. - Manpages for all subcommands.
- Default prompt for Nushell. - Default prompt for Nushell.
### Changed ### Changed
- `zoxide remove -i` now accepts multiple selections.
- `zoxide add` no longer accepts zero parameters.
- `$_ZO_EXCLUDE_DIRS` now defaults to `"$HOME"`. - `$_ZO_EXCLUDE_DIRS` now defaults to `"$HOME"`.
### Fixed ### Fixed
- `cd -` on fish shells. - `cd -` on fish shells.
- `__zoxide_hook` no longer changes value of `$?` within `$PROMPT_COMMAND` on bash.
## [0.6.0] - 2021-04-09 ## [0.6.0] - 2021-04-09
@ -122,7 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- `fish` no longer `cd`s to the user's home when no match is found. - fish no longer `cd`s to the user's home when no match is found.
## [0.3.1] - 2020-04-03 ## [0.3.1] - 2020-04-03
@ -162,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Incorrect exit codes in `z` command on `fish`. - Incorrect exit codes in `z` command on fish.
### Removed ### Removed
@ -175,7 +178,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `$_ZO_ECHO` to echo match before `cd`ing. - `$_ZO_ECHO` to echo match before `cd`ing.
- Minimal `ranger` plugin. - Minimal `ranger` plugin.
- PWD hook to only update the database when the current directory is changed. - PWD hook to only update the database when the current directory is changed.
- Support for the `bash` shell. - Support for bash.
- `migrate` subcommand to allow users to migrate from `z`. - `migrate` subcommand to allow users to migrate from `z`.
### Fixed ### Fixed
@ -189,11 +192,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `init` subcommand to remove dependency on shell plugin managers. - `init` subcommand to remove dependency on shell plugin managers.
- Support for `z -` command to go to previous directory. - Support for `z -` command to go to previous directory.
- `Cargo.lock` for more reproducible builds. - `Cargo.lock` for more reproducible builds.
- Support for the `fish` shell. - Support for the fish shell.
### Fixed ### Fixed
- `_zoxide_precmd` overriding other precmd hooks on `zsh`. - `_zoxide_precmd` overriding other precmd hooks on zsh.
## [0.1.1] - 2020-03-08 ## [0.1.1] - 2020-03-08
@ -215,7 +218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- GitHub Actions pipeline to build and upload releases. - GitHub Actions pipeline to build and upload releases.
- Support for the `zsh` shell. - Support for zsh.
[0.6.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.5.0...v0.6.0 [0.6.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.4.3...v0.5.0 [0.5.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.4.3...v0.5.0

25
Cargo.lock generated
View File

@ -86,11 +86,10 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.2" version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d175dfa69e619905c4c3cdb7c3c203fa3bdd5d51184e3afdb2742c0280493772" checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [ dependencies = [
"byteorder",
"serde", "serde",
] ]
@ -125,9 +124,9 @@ dependencies = [
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.3.4" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -282,9 +281,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lexical-core" name = "lexical-core"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags", "bitflags",
@ -511,9 +510,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -554,9 +553,9 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]] [[package]]
name = "seq-macro" name = "seq-macro"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b3bd665f328d73d7079123bfbd14a6edd74187667b5c6f340adfc65ea9d25a" checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
[[package]] [[package]]
name = "serde" name = "serde"
@ -592,9 +591,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.69" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -17,7 +17,6 @@ clap = "3.0.0-beta.2"
dirs-next = "2.0.0" dirs-next = "2.0.0"
dunce = "1.0.1" dunce = "1.0.1"
glob = "0.3.0" glob = "0.3.0"
once_cell = "1.4.1"
ordered-float = "2.0.0" ordered-float = "2.0.0"
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
tempfile = "3.1.0" tempfile = "3.1.0"
@ -27,6 +26,7 @@ rand = "0.7.3"
[dev-dependencies] [dev-dependencies]
assert_cmd = "1.0.1" assert_cmd = "1.0.1"
once_cell = "1.4.1"
seq-macro = "0.2.1" seq-macro = "0.2.1"
[features] [features]

View File

@ -3,7 +3,7 @@ use crate::config;
use crate::db::DatabaseFile; use crate::db::DatabaseFile;
use crate::util; use crate::util;
use anyhow::Result; use anyhow::{bail, Result};
use clap::Clap; use clap::Clap;
use std::path::PathBuf; use std::path::PathBuf;
@ -11,20 +11,15 @@ use std::path::PathBuf;
/// Add a new directory or increment its rank /// Add a new directory or increment its rank
#[derive(Clap, Debug)] #[derive(Clap, Debug)]
pub struct Add { pub struct Add {
path: Option<PathBuf>, path: PathBuf,
} }
impl Cmd for Add { impl Cmd for Add {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let path = match &self.path { let path = if config::zo_resolve_symlinks() {
Some(path) => { util::canonicalize(&self.path)
if config::zo_resolve_symlinks() { } else {
util::canonicalize(&path) util::resolve_path(&self.path)
} else {
util::resolve_path(&path)
}
}
None => util::current_dir(),
}?; }?;
if config::zo_exclude_dirs()? if config::zo_exclude_dirs()?
@ -34,6 +29,9 @@ impl Cmd for Add {
return Ok(()); return Ok(());
} }
if !path.is_dir() {
bail!("not a directory: {}", path.display());
}
let path = util::path_to_str(&path)?; let path = util::path_to_str(&path)?;
let now = util::current_time()?; let now = util::current_time()?;

View File

@ -30,7 +30,7 @@ impl Cmd for Import {
let mut db = DatabaseFile::new(data_dir); let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?; let mut db = db.open()?;
if !self.merge && !db.dirs.is_empty() { if !self.merge && !db.dirs.is_empty() {
bail!("current database is not empty, specify --merge to continue anyway") bail!("current database is not empty, specify --merge to continue anyway");
} }
let resolve_symlinks = config::zo_resolve_symlinks(); let resolve_symlinks = config::zo_resolve_symlinks();

View File

@ -6,13 +6,11 @@ use crate::shell::{self, Hook, Opts};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use askama::Template; use askama::Template;
use clap::{ArgEnum, Clap}; use clap::{ArgEnum, Clap};
use once_cell::sync::OnceCell;
use std::io::{self, Write}; use std::io::{self, Write};
/// Generate shell configuration /// Generate shell configuration
#[derive(Clap, Debug)] #[derive(Clap, Debug)]
#[clap(after_help(env_help()))]
pub struct Init { pub struct Init {
#[clap(arg_enum)] #[clap(arg_enum)]
shell: Shell, shell: Shell,
@ -59,7 +57,7 @@ impl Cmd for Init {
Shell::Zsh => shell::Zsh(opts).render(), Shell::Zsh => shell::Zsh(opts).render(),
} }
.context("could not render template")?; .context("could not render template")?;
writeln!(io::stdout(), "{}", source).handle_err("stdout") writeln!(io::stdout(), "{}", source).wrap_write("stdout")
} }
} }
@ -74,26 +72,3 @@ enum Shell {
Xonsh, Xonsh,
Zsh, Zsh,
} }
fn env_help() -> &'static str {
static ENV_HELP: OnceCell<String> = OnceCell::new();
ENV_HELP.get_or_init(|| {
#[cfg(unix)]
const PATH_SPLIT_SEPARATOR: u8 = b':';
#[cfg(windows)]
const PATH_SPLIT_SEPARATOR: u8 = b';';
format!(
"\
ENVIRONMENT VARIABLES:
_ZO_DATA_DIR Path for zoxide data files
[current: {data_dir}]
_ZO_ECHO Prints the matched directory before navigating to it when set to 1
_ZO_EXCLUDE_DIRS List of directory globs to be excluded, separated by '{path_split_separator}'
_ZO_FZF_OPTS Custom flags to pass to fzf
_ZO_MAXAGE Maximum total age after which entries start getting deleted
_ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths",
data_dir=config::zo_data_dir().unwrap_or_else(|_| "none".into()).display(),
path_split_separator=PATH_SPLIT_SEPARATOR as char)
})
}

View File

@ -13,6 +13,14 @@ pub use init::Init;
pub use query::Query; pub use query::Query;
pub use remove::Remove; pub use remove::Remove;
const ENV_HELP: &str = "ENVIRONMENT VARIABLES:
_ZO_DATA_DIR Path for zoxide data files
_ZO_ECHO Prints the matched directory before navigating to it when set to 1
_ZO_EXCLUDE_DIRS List of directory globs to be excluded
_ZO_FZF_OPTS Custom flags to pass to fzf
_ZO_MAXAGE Maximum total age after which entries start getting deleted
_ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths";
pub trait Cmd { pub trait Cmd {
fn run(&self) -> Result<()>; fn run(&self) -> Result<()>;
} }
@ -21,6 +29,8 @@ pub trait Cmd {
#[clap( #[clap(
about, about,
author, author,
after_help = ENV_HELP,
global_setting(AppSettings::ColoredHelp),
global_setting(AppSettings::DisableHelpSubcommand), global_setting(AppSettings::DisableHelpSubcommand),
global_setting(AppSettings::GlobalVersion), global_setting(AppSettings::GlobalVersion),
global_setting(AppSettings::VersionlessSubcommands), global_setting(AppSettings::VersionlessSubcommands),

View File

@ -47,10 +47,9 @@ impl Cmd for Query {
.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 {
let mut fzf = Fzf::new()?; let mut fzf = Fzf::new(false)?;
let handle = fzf.stdin();
for dir in matches { for dir in matches {
writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?; writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?;
} }
let selection = fzf.wait_select()?; let selection = fzf.wait_select()?;
@ -76,9 +75,9 @@ impl Cmd for Query {
} else { } else {
writeln!(handle, "{}", dir.display()) writeln!(handle, "{}", dir.display())
} }
.handle_err("stdout")?; .wrap_write("stdout")?;
} }
handle.flush().handle_err("stdout")?; handle.flush().wrap_write("stdout")?;
} else { } else {
let dir = matches.next().context("no match found")?; let dir = matches.next().context("no match found")?;
if self.score { if self.score {
@ -86,7 +85,7 @@ impl Cmd for Query {
} else { } else {
writeln!(io::stdout(), "{}", dir.display()) writeln!(io::stdout(), "{}", dir.display())
} }
.handle_err("stdout")?; .wrap_write("stdout")?;
} }
Ok(()) Ok(())

View File

@ -5,7 +5,7 @@ use crate::error::WriteErrorHandler;
use crate::fzf::Fzf; use crate::fzf::Fzf;
use crate::util; use crate::util;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Result};
use clap::Clap; use clap::Clap;
use std::io::Write; use std::io::Write;
@ -30,31 +30,45 @@ impl Cmd for Remove {
let mut db = db.open()?; let mut db = db.open()?;
let selection; let selection;
let path = match &self.interactive { match &self.interactive {
Some(keywords) => { Some(keywords) => {
let query = Query::new(keywords); let query = Query::new(keywords);
let now = util::current_time()?; let now = util::current_time()?;
let mut fzf = Fzf::new()?;
let handle = fzf.stdin();
let resolve_symlinks = config::zo_resolve_symlinks(); 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_matches(&query, now, resolve_symlinks) {
writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?; writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?;
} }
selection = fzf.wait_select()?; selection = fzf.wait_select()?;
selection let paths = selection.lines().filter_map(|line| line.get(5..));
.get(5..selection.len().saturating_sub(1)) let mut not_found = Vec::new();
.context("fzf returned invalid output")? for path in paths {
} if !db.remove(&path) {
None => self.path.as_ref().unwrap(), not_found.push(path);
}; }
}
if !db.remove(path) { if !not_found.is_empty() {
let path = util::resolve_path(&path)?; let mut err = "path not found in database:".to_string();
let path = util::path_to_str(&path)?; for path in not_found {
if !db.remove(path) { err.push_str("\n ");
bail!("path not found in database: {}", &path) err.push_str(path.as_ref());
}
bail!(err);
}
}
None => {
// unwrap is safe here because path is required_unless_present = "interactive"
let path = self.path.as_ref().unwrap();
if !db.remove(path) {
let path_abs = util::resolve_path(&path)?;
let path_abs = util::path_to_str(&path_abs)?;
if path_abs != path && !db.remove(path) {
bail!("path not found in database:\n {}", &path)
}
}
} }
} }

View File

@ -16,11 +16,11 @@ impl Display for SilentExit {
} }
pub trait WriteErrorHandler { pub trait WriteErrorHandler {
fn handle_err(self, device: &str) -> Result<()>; fn wrap_write(self, device: &str) -> Result<()>;
} }
impl WriteErrorHandler for io::Result<()> { impl WriteErrorHandler for io::Result<()> {
fn handle_err(self, device: &str) -> Result<()> { fn wrap_write(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 }),
result => result.with_context(|| format!("could not write to {}", device)), result => result.with_context(|| format!("could not write to {}", device)),

View File

@ -10,13 +10,15 @@ pub struct Fzf {
} }
impl Fzf { impl Fzf {
pub fn new() -> Result<Self> { pub fn new(multiple: bool) -> Result<Self> {
let mut command = Command::new("fzf"); let mut command = Command::new("fzf");
if multiple {
command.arg("-m");
}
command command
.arg("-n2..") .arg("-n2..")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()); .stdout(Stdio::piped());
if let Some(fzf_opts) = config::zo_fzf_opts() { if let Some(fzf_opts) = config::zo_fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts); command.env("FZF_DEFAULT_OPTS", fzf_opts);
} }
@ -27,6 +29,7 @@ impl Fzf {
} }
pub fn stdin(&mut self) -> &mut ChildStdin { pub fn stdin(&mut self) -> &mut ChildStdin {
// unwrap is safe here because command.stdin() has been piped
self.child.stdin.as_mut().unwrap() self.child.stdin.as_mut().unwrap()
} }

View File

@ -147,9 +147,5 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
} }
} }
let result = stack.iter().collect::<PathBuf>(); Ok(stack.iter().collect())
if !result.is_dir() {
bail!("could not resolve path: {}", result.display());
}
Ok(result)
} }

View File

@ -32,9 +32,9 @@ if (not (and (builtin:has-env __zoxide_hooked) (builtin:eq (builtin:get-env __zo
{%- when Hook::None %} {%- when Hook::None %}
{{ not_configured }} {{ not_configured }}
{%- when Hook::Prompt %} {%- when Hook::Prompt %}
edit:before-readline = [$@edit:before-readline []{ zoxide add $pwd }] edit:before-readline = [$@edit:before-readline []{ zoxide add -- $pwd }]
{%- when Hook::Pwd %} {%- when Hook::Pwd %}
after-chdir = [$@after-chdir [_]{ zoxide add $pwd }] after-chdir = [$@after-chdir [_]{ zoxide add -- $pwd }]
{%- endmatch %} {%- endmatch %}
} }