From f433f9ad842e30483a12b83d57589cf77b4b4d77 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Mon, 13 Jul 2026 04:41:52 +0300 Subject: [PATCH 01/13] Add core alias support additions and structural changes --- contrib/completions/_zoxide | 2 + contrib/completions/_zoxide.ps1 | 2 + contrib/completions/zoxide.bash | 10 ++++- contrib/completions/zoxide.elv | 2 + contrib/completions/zoxide.fish | 1 + contrib/completions/zoxide.nu | 1 + contrib/completions/zoxide.ts | 8 ++++ src/cmd/add.rs | 5 ++- src/cmd/cmd.rs | 3 ++ src/cmd/edit.rs | 8 +++- src/db/dir.rs | 2 + src/db/mod.rs | 68 ++++++++++++++++++++++++++++----- src/import.rs | 2 +- src/import/atuin.rs | 1 + src/import/autojump.rs | 2 +- src/import/z.rs | 2 +- 16 files changed, 102 insertions(+), 17 deletions(-) diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 2f4c9f0..6ed3bb9 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -32,6 +32,8 @@ _zoxide() { _arguments "${_arguments_options[@]}" : \ '-s+[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ '--score=[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ +'-a+[]:ALIAS:_default' \ +'--alias=[]:ALIAS:_default' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index a76c12c..8552066 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -36,6 +36,8 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { 'zoxide;add' { [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') + [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'a') + [CompletionResult]::new('--alias', '--alias', [CompletionResultType]::ParameterName, 'alias') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 1e6fdcf..76a67ee 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -85,7 +85,7 @@ _zoxide() { return 0 ;; zoxide__subcmd__add) - opts="-s -h -V --score --help --version ..." + opts="-s -a -h -V --score --alias --help --version ..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -99,6 +99,14 @@ _zoxide() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --alias) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -a) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 5ba33fa..1f5ba2f 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -32,6 +32,8 @@ set edit:completion:arg-completer[zoxide] = {|@words| &'zoxide;add'= { cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' cand --score 'The rank to increment the entry if it exists or initialize it with if it doesn''t' + cand -a 'a' + cand --alias 'alias' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 6d7e5b2..aa24023 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -33,6 +33,7 @@ complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate sh complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove" -d 'Remove a directory from the database' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r +complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s a -l alias -r complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s h -l help -d 'Print help' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 4d07049..db0422c 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -9,6 +9,7 @@ module completions { # Add a new directory or increment its rank export extern "zoxide add" [ --score(-s): string # The rank to increment the entry if it exists or initialize it with if it doesn't + --alias(-a): string --help(-h) # Print help --version(-V) # Print version ...paths: path diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 207da2f..080dae3 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -15,6 +15,14 @@ const completion: Fig.Spec = { isOptional: true, }, }, + { + name: ["-a", "--alias"], + isRepeatable: true, + args: { + name: "alias", + isOptional: true, + }, + }, { name: ["-h", "--help"], description: "Print help", diff --git a/src/cmd/add.rs b/src/cmd/add.rs index 302ae0a..403590b 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -18,6 +18,7 @@ impl Run for Add { let mut db = Database::open()?; + let mut first = true; for path in &self.paths { let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( @@ -35,7 +36,9 @@ impl Run for Add { } let by = self.score.unwrap_or(1.0); - db.add_update(path, by, now); + let alias = if first { self.alias.clone() } else { None }; + db.add_update(path, by, now, alias); + first = false; } if db.dirty() { diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 0de2ee5..108df68 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -63,6 +63,9 @@ pub struct Add { /// doesn't #[clap(short, long)] pub score: Option, + + #[clap(short, long)] + pub alias: Option, } /// Edit the database diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs index 0f37165..958b0fa 100644 --- a/src/cmd/edit.rs +++ b/src/cmd/edit.rs @@ -15,11 +15,15 @@ impl Run for Edit { match &self.cmd { Some(cmd) => { match cmd { - EditCommand::Decrement { path } => db.add(path, -1.0, now), + EditCommand::Decrement { path } => { + db.add(path, -1.0, now, Option::::None) + } EditCommand::Delete { path } => { db.remove(path); } - EditCommand::Increment { path } => db.add(path, 1.0, now), + EditCommand::Increment { path } => { + db.add(path, 1.0, now, Option::::None) + } EditCommand::Reload => {} } db.save()?; diff --git a/src/db/dir.rs b/src/db/dir.rs index 5d6d62c..cc51128 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -11,6 +11,8 @@ pub struct Dir<'a> { pub path: Cow<'a, str>, pub rank: Rank, pub last_accessed: Epoch, + #[serde(default)] + pub aliases: Vec>, } impl Dir<'_> { diff --git a/src/db/mod.rs b/src/db/mod.rs index 1856fda..d8a3841 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -65,11 +65,31 @@ impl Database { } /// Increments the rank of a directory, or creates it if it does not exist. - pub fn add(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { + pub fn add( + &mut self, + path: impl AsRef + Into, + by: Rank, + now: Epoch, + alias: Option + Into>, + ) { self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { - Some(dir) => dir.rank = (dir.rank + by).max(0.0), + Some(dir) => { + dir.rank = (dir.rank + by).max(0.0); + if let Some(al) = alias { + dir.aliases.push(al.into().into()); + } + } None => { - dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) + let mut aliases = Vec::new(); + if let Some(al) = alias { + aliases.push(al.into().into()); + } + dirs.push(Dir { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases, + }) } }); self.with_dirty_mut(|dirty| *dirty = true); @@ -79,23 +99,51 @@ impl Database { /// directory is already in the database, it is expected that the user /// either does a check before calling this, or calls `dedup()` /// afterward. - pub fn add_unchecked(&mut self, path: impl AsRef + Into, rank: Rank, now: Epoch) { + pub fn add_unchecked( + &mut self, + path: impl AsRef + Into, + rank: Rank, + now: Epoch, + alias: Option + Into>, + ) { self.with_dirs_mut(|dirs| { - dirs.push(Dir { path: path.into().into(), rank, last_accessed: now }) + let mut aliases = Vec::new(); + if let Some(al) = alias { + aliases.push(al.into().into()); + } + dirs.push(Dir { path: path.into().into(), rank, last_accessed: now, aliases }) }); self.with_dirty_mut(|dirty| *dirty = true); } /// Increments the rank and updates the last_accessed of a directory, or /// creates it if it does not exist. - pub fn add_update(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { + pub fn add_update( + &mut self, + path: impl AsRef + Into, + by: Rank, + now: Epoch, + alias: Option + Into>, + ) { self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { Some(dir) => { dir.rank = (dir.rank + by).max(0.0); dir.last_accessed = now; + if let Some(al) = alias { + dir.aliases.push(al.into().into()); + } } None => { - dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) + let mut aliases = Vec::new(); + if let Some(al) = alias { + aliases.push(al.into().into()); + } + dirs.push(Dir { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases, + }) } }); self.with_dirty_mut(|dirty| *dirty = true); @@ -244,8 +292,8 @@ mod tests { { let mut db = Database::open_dir(data_dir.path()).unwrap(); - db.add(path, 1.0, now); - db.add(path, 1.0, now); + db.add(path, 1.0, now, Option::::None); + db.add(path, 1.0, now, Some(String::from("foo"))); db.save().unwrap(); } @@ -268,7 +316,7 @@ mod tests { { let mut db = Database::open_dir(data_dir.path()).unwrap(); - db.add(path, 1.0, now); + db.add(path, 1.0, now, Option::::None); db.save().unwrap(); } diff --git a/src/import.rs b/src/import.rs index b68f564..2e468c8 100644 --- a/src/import.rs +++ b/src/import.rs @@ -60,7 +60,7 @@ pub(crate) fn run(importer: &impl Importer, db: &mut Database) -> Result<()> { if exclude_dirs.iter().any(|glob| glob.matches(&dir.path)) { continue; } - db.add_unchecked(dir.path, dir.rank, dir.last_accessed); + db.add_unchecked(dir.path, dir.rank, dir.last_accessed, Option::::None); } Err(e) => { let location = match &e.path { diff --git a/src/import/atuin.rs b/src/import/atuin.rs index 513efe5..8535ec9 100644 --- a/src/import/atuin.rs +++ b/src/import/atuin.rs @@ -64,6 +64,7 @@ impl Iter { path: Cow::Owned(path.to_string()), rank: 1.0, last_accessed: timestamp as Epoch, + aliases: Vec::new(), }; Ok(dir) } diff --git a/src/import/autojump.rs b/src/import/autojump.rs index f269e1f..2ff95f0 100644 --- a/src/import/autojump.rs +++ b/src/import/autojump.rs @@ -52,7 +52,7 @@ impl Iter { // take a while to normalize. let rank = sigmoid(rank); - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0 }) + Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0, aliases: Vec::new() }) } } diff --git a/src/import/z.rs b/src/import/z.rs index 511980c..de24238 100644 --- a/src/import/z.rs +++ b/src/import/z.rs @@ -54,7 +54,7 @@ impl Iter { let path = split.next().ok_or_else(err)?; - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed }) + Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) } } From 336d621969646a1260c73d1c580f79900acc9d03 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Wed, 15 Jul 2026 20:34:49 +0300 Subject: [PATCH 02/13] Add alias matching to src/db/stream.rs Fix performance issues in previous commit --- src/db/dir.rs | 2 +- src/db/mod.rs | 18 ++++++----------- src/db/stream.rs | 51 +++++++++++++++++++++++++++++++----------------- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/db/dir.rs b/src/db/dir.rs index cc51128..ec4e2b0 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -11,7 +11,7 @@ pub struct Dir<'a> { pub path: Cow<'a, str>, pub rank: Rank, pub last_accessed: Epoch, - #[serde(default)] + #[serde(borrow)] pub aliases: Vec>, } diff --git a/src/db/mod.rs b/src/db/mod.rs index d8a3841..c650bee 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -80,10 +80,8 @@ impl Database { } } None => { - let mut aliases = Vec::new(); - if let Some(al) = alias { - aliases.push(al.into().into()); - } + let aliases = + if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), @@ -107,10 +105,8 @@ impl Database { alias: Option + Into>, ) { self.with_dirs_mut(|dirs| { - let mut aliases = Vec::new(); - if let Some(al) = alias { - aliases.push(al.into().into()); - } + let aliases = + if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; dirs.push(Dir { path: path.into().into(), rank, last_accessed: now, aliases }) }); self.with_dirty_mut(|dirty| *dirty = true); @@ -134,10 +130,8 @@ impl Database { } } None => { - let mut aliases = Vec::new(); - if let Some(al) = alias { - aliases.push(al.into().into()); - } + let aliases = + if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), diff --git a/src/db/stream.rs b/src/db/stream.rs index 24c84e0..b2a2447 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::iter::Rev; use std::ops::Range; use std::path::Path; @@ -25,25 +26,28 @@ impl<'a> Stream<'a> { while let Some(idx) = self.idxs.next() { let dir = &self.db.dirs()[idx]; - if !self.filter_by_keywords(&dir.path) { - continue; - } - - if !self.filter_by_base_dir(&dir.path) { - continue; - } - - if !self.filter_by_exclude(&dir.path) { - self.db.swap_remove(idx); - continue; - } - - // Exists queries are slow, this should always be checked last. - if !self.filter_by_exists(&dir.path) { - if dir.last_accessed < self.options.ttl { - self.db.swap_remove(idx); + // Return dir if any keyword is an alias + if !self.match_aliases(&dir.aliases) { + if !self.filter_by_keywords(&dir.path) { + continue; + } + + if !self.filter_by_base_dir(&dir.path) { + continue; + } + + if !self.filter_by_exclude(&dir.path) { + self.db.swap_remove(idx); + continue; + } + + // Exists queries are slow, this should always be checked last. + if !self.filter_by_exists(&dir.path) { + if dir.last_accessed < self.options.ttl { + self.db.swap_remove(idx); + } + continue; } - continue; } let dir = &self.db.dirs()[idx]; @@ -104,6 +108,17 @@ impl<'a> Stream<'a> { true } + + fn match_aliases(&self, aliases: &[Cow<'a, str>]) -> bool { + for keyword in self.options.keywords.iter().rev() { + // Alias matching is intended to be case-sensitive + if aliases.iter().any(|a| a == keyword.as_str()) { + return true; + } + } + + false + } } pub struct StreamOptions { From 18d238a463a002e58ba4e71b50d423b8b90d7a83 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Wed, 15 Jul 2026 21:45:29 +0300 Subject: [PATCH 03/13] Add alias backwards compatibility for v3 databases --- src/db/dir.rs | 61 ++++++++++++++++++++++++++++++++++-------- src/db/mod.rs | 43 +++++++++++++++++++++-------- src/db/stream.rs | 4 +-- src/import.rs | 4 +-- src/import/atuin.rs | 10 +++---- src/import/autojump.rs | 15 +++++++---- src/import/fasd.rs | 4 +-- src/import/z.rs | 10 +++---- src/import/z_lua.rs | 4 +-- src/import/zsh_z.rs | 4 +-- src/util.rs | 4 +-- 11 files changed, 114 insertions(+), 49 deletions(-) diff --git a/src/db/dir.rs b/src/db/dir.rs index ec4e2b0..d0d37a4 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::util::{DAY, HOUR, WEEK}; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Dir<'a> { +pub struct DirV4<'a> { #[serde(borrow)] pub path: Cow<'a, str>, pub rank: Rank, @@ -15,12 +15,25 @@ pub struct Dir<'a> { pub aliases: Vec>, } -impl Dir<'_> { - pub fn display(&self) -> DirDisplay<'_> { - DirDisplay::new(self) +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DirV3<'a> { + #[serde(borrow)] + pub path: Cow<'a, str>, + pub rank: Rank, + pub last_accessed: Epoch, +} + +pub trait Dir<'a> { + fn path(&self) -> &str; + fn score(&self, now: Epoch) -> Rank; +} + +impl Dir<'_> for DirV4<'_> { + fn path(&self) -> &str { + &self.path } - pub fn score(&self, now: Epoch) -> Rank { + fn score(&self, now: Epoch) -> Rank { // The older the entry, the lesser its importance. let duration = now.saturating_sub(self.last_accessed); if duration < HOUR { @@ -35,14 +48,40 @@ impl Dir<'_> { } } -pub struct DirDisplay<'a> { - dir: &'a Dir<'a>, +impl DirV4<'_> { + pub fn display(&self) -> DirDisplay<'_, Self> { + DirDisplay::new(self) + } +} + +impl Dir<'_> for DirV3<'_> { + fn path(&self) -> &str { + &self.path + } + + fn score(&self, now: Epoch) -> Rank { + // The older the entry, the lesser its importance. + let duration = now.saturating_sub(self.last_accessed); + if duration < HOUR { + self.rank * 4.0 + } else if duration < DAY { + self.rank * 2.0 + } else if duration < WEEK { + self.rank * 0.5 + } else { + self.rank * 0.25 + } + } +} + +pub struct DirDisplay<'a, T: Dir<'a>> { + dir: &'a T, now: Option, separator: char, } -impl<'a> DirDisplay<'a> { - fn new(dir: &'a Dir) -> Self { +impl<'a, T: Dir<'a>> DirDisplay<'a, T> { + fn new(dir: &'a T) -> Self { Self { dir, separator: ' ', now: None } } @@ -57,13 +96,13 @@ impl<'a> DirDisplay<'a> { } } -impl Display for DirDisplay<'_> { +impl<'a, T: Dir<'a>> Display for DirDisplay<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(now) = self.now { let score = self.dir.score(now).clamp(0.0, 9999.0); write!(f, "{score:>6.1}{}", self.separator)?; } - write!(f, "{}", self.dir.path) + write!(f, "{}", self.dir.path()) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index c650bee..dab0cf0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -8,7 +8,8 @@ use anyhow::{Context, Result, bail}; use bincode::Options; use ouroboros::self_referencing; -pub use crate::db::dir::{Dir, Epoch, Rank}; +use crate::db::dir::{Dir, DirV3}; +pub use crate::db::dir::{DirV4, Epoch, Rank}; pub use crate::db::stream::{Stream, StreamOptions}; use crate::{config, util}; @@ -18,12 +19,13 @@ pub struct Database { bytes: Vec, #[borrows(bytes)] #[covariant] - pub dirs: Vec>, + pub dirs: Vec>, dirty: bool, } impl Database { - const VERSION: u32 = 3; + const PREV_VERSION: u32 = 3; + const VERSION: u32 = 4; pub fn open() -> Result { let data_dir = config::data_dir()?; @@ -82,7 +84,7 @@ impl Database { None => { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { + dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), last_accessed: now, @@ -107,7 +109,7 @@ impl Database { self.with_dirs_mut(|dirs| { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { path: path.into().into(), rank, last_accessed: now, aliases }) + dirs.push(DirV4 { path: path.into().into(), rank, last_accessed: now, aliases }) }); self.with_dirty_mut(|dirty| *dirty = true); } @@ -132,7 +134,7 @@ impl Database { None => { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { + dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), last_accessed: now, @@ -215,7 +217,7 @@ impl Database { pub fn sort_by_score(&mut self, now: Epoch) { self.with_dirs_mut(|dirs| { - dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| { + dirs.sort_unstable_by(|dir1: &DirV4, dir2: &DirV4| { dir1.score(now).total_cmp(&dir2.score(now)) }) }); @@ -226,11 +228,11 @@ impl Database { *self.borrow_dirty() } - pub fn dirs(&self) -> &[Dir<'_>] { + pub fn dirs(&self) -> &[DirV4<'_>] { self.borrow_dirs() } - fn serialize(dirs: &[Dir<'_>]) -> Result> { + fn serialize(dirs: &[DirV4<'_>]) -> Result> { (|| -> bincode::Result<_> { // Preallocate buffer with combined size of sections. let buffer_size = @@ -246,7 +248,7 @@ impl Database { .context("could not serialize database") } - fn deserialize(bytes: &[u8]) -> Result>> { + fn deserialize(bytes: &[u8]) -> Result>> { // Assume a maximum size for the database. This prevents bincode from throwing // strange errors when it encounters invalid data. const MAX_SIZE: u64 = 32 << 20; // 32 MiB @@ -265,8 +267,27 @@ impl Database { Self::VERSION => { deserializer.deserialize(bytes_dirs).context("could not deserialize database")? } + Self::PREV_VERSION => { + let old_dirs = deserializer + .deserialize::>(bytes_dirs) + .context("could not deserialize v3 database")?; + + old_dirs + .into_iter() + .map(|dir: DirV3| DirV4 { + path: dir.path, + rank: dir.rank, + last_accessed: dir.last_accessed, + aliases: Vec::new(), + }) + .collect() + } version => { - bail!("unsupported version (got {version}, supports {})", Self::VERSION) + bail!( + "unsupported version (got {version}, supports {}, {})", + Self::VERSION, + Self::PREV_VERSION + ) } }; diff --git a/src/db/stream.rs b/src/db/stream.rs index b2a2447..62d2efc 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -6,7 +6,7 @@ use std::{fs, path}; use glob::Pattern; -use crate::db::{Database, Dir, Epoch}; +use crate::db::{Database, DirV4, Epoch}; use crate::util::{self, MONTH}; pub struct Stream<'a> { @@ -22,7 +22,7 @@ impl<'a> Stream<'a> { Stream { db, idxs, options } } - pub fn next(&mut self) -> Option<&Dir<'_>> { + pub fn next(&mut self) -> Option<&DirV4<'_>> { while let Some(idx) = self.idxs.next() { let dir = &self.db.dirs()[idx]; diff --git a/src/import.rs b/src/import.rs index 2e468c8..cd976d3 100644 --- a/src/import.rs +++ b/src/import.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; use anyhow::Result; use crate::config; -use crate::db::{Database, Dir}; +use crate::db::{Database, DirV4}; pub(crate) trait Importer { /// Yields directory entries to be imported. @@ -26,7 +26,7 @@ pub(crate) trait Importer { /// The outer `Result` reports failure to fetch the input (e.g. missing /// file, subprocess errored). The per-item `Result` reports a malformed /// row, which doesn't necessarily abort the whole import. - fn dirs(&self) -> Result, ImportError>>>; + fn dirs(&self) -> Result, ImportError>>>; } /// A single record that failed to import. diff --git a/src/import/atuin.rs b/src/import/atuin.rs index 8535ec9..7596013 100644 --- a/src/import/atuin.rs +++ b/src/import/atuin.rs @@ -5,14 +5,14 @@ use std::str; use anyhow::{Context, Result, anyhow}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Atuin {} impl Importer for Atuin { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { // atuin renders `{time}` as `YYYY-MM-DD HH:MM:SS` in UTC. let mut child = Command::new("atuin") .args(["history", "list", "--format={time}\t{directory}", "--print0"]) @@ -46,7 +46,7 @@ impl Iter { ImportError { path: None, line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -60,7 +60,7 @@ impl Iter { .assume_utc() .unix_timestamp(); - let dir = Dir { + let dir = DirV4 { path: Cow::Owned(path.to_string()), rank: 1.0, last_accessed: timestamp as Epoch, @@ -71,7 +71,7 @@ impl Iter { } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/autojump.rs b/src/import/autojump.rs index 2ff95f0..06ba392 100644 --- a/src/import/autojump.rs +++ b/src/import/autojump.rs @@ -6,14 +6,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Autojump {} impl Importer for Autojump { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +37,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -52,12 +52,17 @@ impl Iter { // take a while to normalize. let rank = sigmoid(rank); - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0, aliases: Vec::new() }) + Ok(DirV4 { + path: Cow::Owned(path.to_string()), + rank, + last_accessed: 0, + aliases: Vec::new(), + }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/fasd.rs b/src/import/fasd.rs index ab0788d..1dba739 100644 --- a/src/import/fasd.rs +++ b/src/import/fasd.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Fasd {} impl Importer for Fasd { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/import/z.rs b/src/import/z.rs index de24238..d43bcaf 100644 --- a/src/import/z.rs +++ b/src/import/z.rs @@ -6,14 +6,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Z {} impl Importer for Z { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +37,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; let err = || self.err(anyhow!("invalid entry: {line}")); @@ -54,12 +54,12 @@ impl Iter { let path = split.next().ok_or_else(err)?; - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) + Ok(DirV4 { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/z_lua.rs b/src/import/z_lua.rs index 0ea4584..364c9db 100644 --- a/src/import/z_lua.rs +++ b/src/import/z_lua.rs @@ -6,14 +6,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZLua {} impl Importer for ZLua { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let err = match File::open(&path) { Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)), diff --git a/src/import/zsh_z.rs b/src/import/zsh_z.rs index 652faf8..7d3474c 100644 --- a/src/import/zsh_z.rs +++ b/src/import/zsh_z.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZshZ {} impl Importer for ZshZ { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/util.rs b/src/util.rs index 4c0a27c..d26d4a8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,7 +10,7 @@ use std::{env, mem}; use anyhow::anyhow; use anyhow::{Context, Result, bail}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::error::SilentExit; pub const SECOND: Epoch = 1; @@ -121,7 +121,7 @@ impl Fzf { pub struct FzfChild(Child); impl FzfChild { - pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { + pub fn write(&mut self, dir: &DirV4, now: Epoch) -> Result> { let handle = self.0.stdin.as_mut().unwrap(); match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { Ok(()) => Ok(None), From f8899a4a9137e5087938ebff3b0813128ba823fd Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Fri, 17 Jul 2026 17:53:53 +0300 Subject: [PATCH 04/13] Add aliases to dir displays --- contrib/completions/_zoxide | 1 + contrib/completions/_zoxide.ps1 | 1 + contrib/completions/zoxide.bash | 2 +- contrib/completions/zoxide.elv | 1 + contrib/completions/zoxide.fish | 1 + contrib/completions/zoxide.nu | 1 + contrib/completions/zoxide.ts | 4 ++++ src/cmd/cmd.rs | 4 ++++ src/cmd/edit.rs | 16 ++++++++------ src/cmd/query.rs | 15 +++++++++++-- src/db/dir.rs | 37 ++++++++++++++++++++++++++------- src/util.rs | 14 ++++++++----- 12 files changed, 76 insertions(+), 21 deletions(-) diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 6ed3bb9..2d57f9c 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -195,6 +195,7 @@ _arguments "${_arguments_options[@]}" : \ '(-i --interactive)--list[List all matching directories]' \ '-s[Print score with results]' \ '--score[Print score with results]' \ +'--aliases[Print aliases with results]' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index 8552066..52a8f46 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -166,6 +166,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Print score with results') [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'Print score with results') + [CompletionResult]::new('--aliases', '--aliases', [CompletionResultType]::ParameterName, 'Print aliases with results') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 76a67ee..24ffa9f 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -305,7 +305,7 @@ _zoxide() { return 0 ;; zoxide__subcmd__query) - opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --base-dir --help --version [KEYWORDS]..." + opts="-a -i -l -s -h -V --all --interactive --list --score --aliases --exclude --base-dir --help --version [KEYWORDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 1f5ba2f..b72e26b 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -148,6 +148,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --list 'List all matching directories' cand -s 'Print score with results' cand --score 'Print score with results' + cand --aliases 'Print aliases with results' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index aa24023..df1ad3a 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -90,6 +90,7 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s a -l all -d 'Sho complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s l -l list -d 'List all matching directories' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s s -l score -d 'Print score with results' +complete -c zoxide -n "__fish_zoxide_using_subcommand query" -l aliases -d 'Print aliases with results' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s h -l help -d 'Print help' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index db0422c..c1f49ec 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -117,6 +117,7 @@ module completions { --interactive(-i) # Use interactive selection --list(-l) # List all matching directories --score(-s) # Print score with results + --aliases # Print aliases with results --exclude: path # Exclude the current directory --base-dir: path # Only search within this directory --help(-h) # Print help diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 080dae3..9070556 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -350,6 +350,10 @@ const completion: Fig.Spec = { name: ["-s", "--score"], description: "Print score with results", }, + { + name: "--aliases", + description: "Print aliases with results", + }, { name: ["-h", "--help"], description: "Print help", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 108df68..73add2d 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -193,6 +193,10 @@ pub struct Query { #[clap(long, short)] pub score: bool, + /// Print aliases with results + #[clap(long)] + pub aliases: bool, + /// Exclude the current directory #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] pub exclude: Option, diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs index 958b0fa..6fb7cc6 100644 --- a/src/cmd/edit.rs +++ b/src/cmd/edit.rs @@ -30,8 +30,12 @@ impl Run for Edit { let stdout = &mut io::stdout().lock(); for dir in db.dirs().iter().rev() { - write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t')) - .pipe_exit("fzf")?; + write!( + stdout, + "{}\0", + dir.display().with_score(now).with_aliases(true).with_separator('\t') + ) + .pipe_exit("fzf")?; } Ok(()) } @@ -57,9 +61,9 @@ impl Edit { "--bind=\ btab:up,\ ctrl-r:reload(zoxide edit reload),\ -ctrl-d:reload(zoxide edit delete {2..}),\ -ctrl-w:reload(zoxide edit increment {2..}),\ -ctrl-s:reload(zoxide edit decrement {2..}),\ +ctrl-d:reload(zoxide edit delete {3..}),\ +ctrl-w:reload(zoxide edit increment {3..}),\ +ctrl-s:reload(zoxide edit decrement {3..}),\ ctrl-z:ignore,\ double-click:ignore,\ enter:abort,\ @@ -74,7 +78,7 @@ tab:down", ctrl-r:reload \tctrl-d:delete ctrl-w:increment\tctrl-s:decrement - SCORE\tPATH", + SCORE\tALIASES\tPATH", "--info=inline", "--layout=reverse", "--padding=1,0,0,0", diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 6539c2e..282ce4c 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -35,7 +35,8 @@ impl Query { match stream.next() { Some(dir) if Some(dir.path.as_ref()) == self.exclude.as_deref() => continue, Some(dir) => { - if let Some(selection) = fzf.write(dir, now)? { + // Always enable aliases for interactive queries + if let Some(selection) = fzf.write(dir, now, true)? { break selection; } } @@ -45,9 +46,17 @@ impl Query { if self.score { print!("{selection}"); - } else { + } else if self.aliases { let path = selection.get(7..).context("could not read selection from fzf")?; print!("{path}"); + } else { + let path = selection + .get(7..) + .map(|path| { + path.get(path.find('\t').map(|idx| idx + 1).unwrap_or(0)..).unwrap_or(path) + }) + .context("could not read selection from fzf")?; + print!("{path}"); } Ok(()) } @@ -59,6 +68,7 @@ impl Query { continue; } let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + let dir = if self.aliases { dir.with_aliases(self.aliases) } else { dir }; writeln!(handle, "{dir}").pipe_exit("stdout")?; } Ok(()) @@ -73,6 +83,7 @@ impl Query { } let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + let dir = if self.aliases { dir.with_aliases(self.aliases) } else { dir }; writeln!(handle, "{dir}").pipe_exit("stdout") } diff --git a/src/db/dir.rs b/src/db/dir.rs index d0d37a4..43d6448 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -23,12 +23,13 @@ pub struct DirV3<'a> { pub last_accessed: Epoch, } -pub trait Dir<'a> { +pub trait Dir { fn path(&self) -> &str; fn score(&self, now: Epoch) -> Rank; + fn aliases(&self) -> &[Cow<'_, str>]; } -impl Dir<'_> for DirV4<'_> { +impl Dir for DirV4<'_> { fn path(&self) -> &str { &self.path } @@ -46,6 +47,10 @@ impl Dir<'_> for DirV4<'_> { self.rank * 0.25 } } + + fn aliases(&self) -> &[Cow<'_, str>] { + &self.aliases + } } impl DirV4<'_> { @@ -54,7 +59,7 @@ impl DirV4<'_> { } } -impl Dir<'_> for DirV3<'_> { +impl Dir for DirV3<'_> { fn path(&self) -> &str { &self.path } @@ -72,17 +77,22 @@ impl Dir<'_> for DirV3<'_> { self.rank * 0.25 } } + + fn aliases(&self) -> &[Cow<'_, str>] { + return &[]; + } } -pub struct DirDisplay<'a, T: Dir<'a>> { +pub struct DirDisplay<'a, T: Dir> { dir: &'a T, now: Option, separator: char, + aliases: bool, } -impl<'a, T: Dir<'a>> DirDisplay<'a, T> { +impl<'a, T: Dir> DirDisplay<'a, T> { fn new(dir: &'a T) -> Self { - Self { dir, separator: ' ', now: None } + Self { dir, separator: ' ', now: None, aliases: false } } pub fn with_score(mut self, now: Epoch) -> Self { @@ -94,14 +104,27 @@ impl<'a, T: Dir<'a>> DirDisplay<'a, T> { self.separator = separator; self } + + pub fn with_aliases(mut self, enable: bool) -> Self { + self.aliases = enable; + self + } } -impl<'a, T: Dir<'a>> Display for DirDisplay<'a, T> { +impl<'a, T: Dir> Display for DirDisplay<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(now) = self.now { let score = self.dir.score(now).clamp(0.0, 9999.0); write!(f, "{score:>6.1}{}", self.separator)?; } + + if self.aliases { + for alias in self.dir.aliases() { + write!(f, "{} ", alias)?; + } + write!(f, "{}", self.separator)?; + } + write!(f, "{}", self.dir.path()) } } diff --git a/src/util.rs b/src/util.rs index d26d4a8..9b59f27 100644 --- a/src/util.rs +++ b/src/util.rs @@ -41,7 +41,7 @@ impl Fzf { cmd.args([ // Search mode "--delimiter=\t", - "--nth=2", + "--nth=3", // Scripting "--read0", ]) @@ -60,9 +60,9 @@ impl Fzf { self.args([ // Non-POSIX args are only available on certain operating systems. if cfg!(target_os = "linux") { - r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}" + r"--preview=\command -p ls -Cp --color=always --group-directories-first {3..}" } else { - r"--preview=\command -p ls -Cp {2..}" + r"--preview=\command -p ls -Cp {3..}" }, // Rounded edges don't display correctly on some terminals. "--preview-window=down,30%,sharp", @@ -121,9 +121,13 @@ impl Fzf { pub struct FzfChild(Child); impl FzfChild { - pub fn write(&mut self, dir: &DirV4, now: Epoch) -> Result> { + pub fn write(&mut self, dir: &DirV4, now: Epoch, aliases: bool) -> Result> { let handle = self.0.stdin.as_mut().unwrap(); - match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { + match write!( + handle, + "{}\0", + dir.display().with_score(now).with_aliases(aliases).with_separator('\t') + ) { Ok(()) => Ok(None), Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some), Err(e) => Err(e).context("could not write to fzf"), From b3ae1e6a28ddacb06d825289523a3c67a1013df7 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Tue, 21 Jul 2026 01:09:46 +0300 Subject: [PATCH 05/13] Use std::collections::HashSet for aliases for better perfromance and guaranteed uniqueness --- src/db/dir.rs | 19 +++++++++++-------- src/db/mod.rs | 28 +++++++++++++++++++--------- src/db/stream.rs | 5 +++-- src/import/atuin.rs | 3 ++- src/import/autojump.rs | 3 ++- src/import/z.rs | 8 +++++++- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/db/dir.rs b/src/db/dir.rs index 43d6448..4dae2cf 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fmt::{self, Display, Formatter}; use serde::{Deserialize, Serialize}; @@ -12,7 +13,7 @@ pub struct DirV4<'a> { pub rank: Rank, pub last_accessed: Epoch, #[serde(borrow)] - pub aliases: Vec>, + pub aliases: HashSet>, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -26,7 +27,7 @@ pub struct DirV3<'a> { pub trait Dir { fn path(&self) -> &str; fn score(&self, now: Epoch) -> Rank; - fn aliases(&self) -> &[Cow<'_, str>]; + fn aliases(&self) -> Option<&HashSet>>; } impl Dir for DirV4<'_> { @@ -48,8 +49,8 @@ impl Dir for DirV4<'_> { } } - fn aliases(&self) -> &[Cow<'_, str>] { - &self.aliases + fn aliases(&self) -> Option<&HashSet>> { + Some(&self.aliases) } } @@ -78,8 +79,8 @@ impl Dir for DirV3<'_> { } } - fn aliases(&self) -> &[Cow<'_, str>] { - return &[]; + fn aliases(&self) -> Option<&HashSet>> { + None } } @@ -118,8 +119,10 @@ impl<'a, T: Dir> Display for DirDisplay<'a, T> { write!(f, "{score:>6.1}{}", self.separator)?; } - if self.aliases { - for alias in self.dir.aliases() { + if self.aliases + && let Some(aliases) = self.dir.aliases() + { + for alias in aliases { write!(f, "{} ", alias)?; } write!(f, "{}", self.separator)?; diff --git a/src/db/mod.rs b/src/db/mod.rs index dab0cf0..046297c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,7 @@ mod dir; mod stream; +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -78,12 +79,15 @@ impl Database { Some(dir) => { dir.rank = (dir.rank + by).max(0.0); if let Some(al) = alias { - dir.aliases.push(al.into().into()); + dir.aliases.insert(al.into().into()); } } None => { - let aliases = - if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; + let aliases = if let Some(alias) = alias { + HashSet::from([alias.into().into()]) + } else { + HashSet::new() + }; dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), @@ -107,8 +111,11 @@ impl Database { alias: Option + Into>, ) { self.with_dirs_mut(|dirs| { - let aliases = - if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; + let aliases = if let Some(alias) = alias { + HashSet::from([alias.into().into()]) + } else { + HashSet::new() + }; dirs.push(DirV4 { path: path.into().into(), rank, last_accessed: now, aliases }) }); self.with_dirty_mut(|dirty| *dirty = true); @@ -128,12 +135,15 @@ impl Database { dir.rank = (dir.rank + by).max(0.0); dir.last_accessed = now; if let Some(al) = alias { - dir.aliases.push(al.into().into()); + dir.aliases.insert(al.into().into()); } } None => { - let aliases = - if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; + let aliases = if let Some(alias) = alias { + HashSet::from([alias.into().into()]) + } else { + HashSet::new() + }; dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), @@ -278,7 +288,7 @@ impl Database { path: dir.path, rank: dir.rank, last_accessed: dir.last_accessed, - aliases: Vec::new(), + aliases: HashSet::new(), }) .collect() } diff --git a/src/db/stream.rs b/src/db/stream.rs index 62d2efc..aa9251c 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::iter::Rev; use std::ops::Range; use std::path::Path; @@ -109,10 +110,10 @@ impl<'a> Stream<'a> { true } - fn match_aliases(&self, aliases: &[Cow<'a, str>]) -> bool { + fn match_aliases(&self, aliases: &HashSet>) -> bool { for keyword in self.options.keywords.iter().rev() { // Alias matching is intended to be case-sensitive - if aliases.iter().any(|a| a == keyword.as_str()) { + if aliases.contains(keyword.as_str()) { return true; } } diff --git a/src/import/atuin.rs b/src/import/atuin.rs index 7596013..310e850 100644 --- a/src/import/atuin.rs +++ b/src/import/atuin.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::io::{BufRead, BufReader}; use std::process::{Child, ChildStdout, Command, Stdio}; use std::str; @@ -64,7 +65,7 @@ impl Iter { path: Cow::Owned(path.to_string()), rank: 1.0, last_accessed: timestamp as Epoch, - aliases: Vec::new(), + aliases: HashSet::new(), }; Ok(dir) } diff --git a/src/import/autojump.rs b/src/import/autojump.rs index 06ba392..df59fb9 100644 --- a/src/import/autojump.rs +++ b/src/import/autojump.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -56,7 +57,7 @@ impl Iter { path: Cow::Owned(path.to_string()), rank, last_accessed: 0, - aliases: Vec::new(), + aliases: HashSet::new(), }) } } diff --git a/src/import/z.rs b/src/import/z.rs index d43bcaf..43e51c5 100644 --- a/src/import/z.rs +++ b/src/import/z.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -54,7 +55,12 @@ impl Iter { let path = split.next().ok_or_else(err)?; - Ok(DirV4 { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) + Ok(DirV4 { + path: Cow::Owned(path.to_string()), + rank, + last_accessed, + aliases: HashSet::new(), + }) } } From 5528680cc6558a90554ec1ec8f55b9a55b488ed3 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Tue, 21 Jul 2026 03:32:17 +0300 Subject: [PATCH 06/13] Remove --alias flag from add command and use a separate add-alias command instead --- contrib/completions/_zoxide | 19 +++++- contrib/completions/_zoxide.ps1 | 12 +++- contrib/completions/zoxide.bash | 35 +++++++++-- contrib/completions/zoxide.elv | 11 +++- contrib/completions/zoxide.fish | 5 +- contrib/completions/zoxide.nu | 9 ++- contrib/completions/zoxide.ts | 34 ++++++++--- src/cmd/add.rs | 5 +- src/cmd/add_alias.rs | 43 +++++++++++++ src/cmd/cmd.rs | 16 ++++- src/cmd/edit.rs | 8 +-- src/cmd/mod.rs | 2 + src/db/mod.rs | 103 ++++++++++++++------------------ src/import.rs | 2 +- 14 files changed, 212 insertions(+), 92 deletions(-) create mode 100644 src/cmd/add_alias.rs diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 2d57f9c..d033796 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -32,8 +32,6 @@ _zoxide() { _arguments "${_arguments_options[@]}" : \ '-s+[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ '--score=[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ -'-a+[]:ALIAS:_default' \ -'--alias=[]:ALIAS:_default' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ @@ -41,6 +39,17 @@ _arguments "${_arguments_options[@]}" : \ '*::paths:_files -/' \ && ret=0 ;; +(add-alias) +_arguments "${_arguments_options[@]}" : \ +'-p+[]:PATH:_files -/' \ +'--path=[]:PATH:_files -/' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +'*::aliases:_default' \ +&& ret=0 +;; (edit) _arguments "${_arguments_options[@]}" : \ '-h[Print help]' \ @@ -221,6 +230,7 @@ esac _zoxide_commands() { local commands; commands=( 'add:Add a new directory or increment its rank' \ +'add-alias:Add aliases for a directory' \ 'edit:Edit the database' \ 'import:Import entries from another application' \ 'init:Generate shell configuration' \ @@ -234,6 +244,11 @@ _zoxide__subcmd__add_commands() { local commands; commands=() _describe -t commands 'zoxide add commands' commands "$@" } +(( $+functions[_zoxide__subcmd__add-alias_commands] )) || +_zoxide__subcmd__add-alias_commands() { + local commands; commands=() + _describe -t commands 'zoxide add-alias commands' commands "$@" +} (( $+functions[_zoxide__subcmd__edit_commands] )) || _zoxide__subcmd__edit_commands() { local commands; commands=( diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index 52a8f46..c282274 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -26,6 +26,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') + [CompletionResult]::new('add-alias', 'add-alias', [CompletionResultType]::ParameterValue, 'Add aliases for a directory') [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') @@ -36,8 +37,15 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { 'zoxide;add' { [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') - [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'a') - [CompletionResult]::new('--alias', '--alias', [CompletionResultType]::ParameterName, 'alias') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;add-alias' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'p') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'path') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 24ffa9f..5e99429 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -19,6 +19,9 @@ _zoxide() { zoxide,add) cmd="zoxide__subcmd__add" ;; + zoxide,add-alias) + cmd="zoxide__subcmd__add__subcmd__alias" + ;; zoxide,edit) cmd="zoxide__subcmd__edit" ;; @@ -71,7 +74,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add edit import init query remove" + opts="-h -V --help --version add add-alias edit import init query remove" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -85,7 +88,7 @@ _zoxide() { return 0 ;; zoxide__subcmd__add) - opts="-s -a -h -V --score --alias --help --version ..." + opts="-s -h -V --score --help --version ..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -99,12 +102,32 @@ _zoxide() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --alias) - COMPREPLY=($(compgen -f "${cur}")) + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__subcmd__add__subcmd__alias) + opts="-p -h -V --path --help --version ..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --path) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi return 0 ;; - -a) - COMPREPLY=($(compgen -f "${cur}")) + -p) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi return 0 ;; *) diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index b72e26b..7294b26 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -23,6 +23,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' cand add 'Add a new directory or increment its rank' + cand add-alias 'Add aliases for a directory' cand edit 'Edit the database' cand import 'Import entries from another application' cand init 'Generate shell configuration' @@ -32,8 +33,14 @@ set edit:completion:arg-completer[zoxide] = {|@words| &'zoxide;add'= { cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' cand --score 'The rank to increment the entry if it exists or initialize it with if it doesn''t' - cand -a 'a' - cand --alias 'alias' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;add-alias'= { + cand -p 'p' + cand --path 'path' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index df1ad3a..9eda694 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -27,15 +27,18 @@ end complete -c zoxide -n "__fish_zoxide_needs_command" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_needs_command" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add" -d 'Add a new directory or increment its rank' +complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add-alias" -d 'Add aliases for a directory' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "edit" -d 'Edit the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import entries from another application' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove" -d 'Remove a directory from the database' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r -complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s a -l alias -r complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s p -l path -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "decrement" diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index c1f49ec..e25b011 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -9,12 +9,19 @@ module completions { # Add a new directory or increment its rank export extern "zoxide add" [ --score(-s): string # The rank to increment the entry if it exists or initialize it with if it doesn't - --alias(-a): string --help(-h) # Print help --version(-V) # Print version ...paths: path ] + # Add aliases for a directory + export extern "zoxide add-alias" [ + --path(-p): path + --help(-h) # Print help + --version(-V) # Print version + ...aliases: string + ] + # Edit the database export extern "zoxide edit" [ --help(-h) # Print help diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 9070556..f34b4a1 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -15,14 +15,6 @@ const completion: Fig.Spec = { isOptional: true, }, }, - { - name: ["-a", "--alias"], - isRepeatable: true, - args: { - name: "alias", - isOptional: true, - }, - }, { name: ["-h", "--help"], description: "Print help", @@ -38,6 +30,32 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "add-alias", + description: "Add aliases for a directory", + options: [ + { + name: ["-p", "--path"], + isRepeatable: true, + args: { + name: "path", + template: "folders", + }, + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "aliases", + isVariadic: true, + }, + }, { name: "edit", description: "Edit the database", diff --git a/src/cmd/add.rs b/src/cmd/add.rs index 403590b..302ae0a 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -18,7 +18,6 @@ impl Run for Add { let mut db = Database::open()?; - let mut first = true; for path in &self.paths { let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( @@ -36,9 +35,7 @@ impl Run for Add { } let by = self.score.unwrap_or(1.0); - let alias = if first { self.alias.clone() } else { None }; - db.add_update(path, by, now, alias); - first = false; + db.add_update(path, by, now); } if db.dirty() { diff --git a/src/cmd/add_alias.rs b/src/cmd/add_alias.rs new file mode 100644 index 0000000..fb955d0 --- /dev/null +++ b/src/cmd/add_alias.rs @@ -0,0 +1,43 @@ +use std::path::Path; + +use anyhow::{Result, bail}; + +use crate::cmd::{AddAlias, Run}; +use crate::db::Database; +use crate::{config, util}; + +impl Run for AddAlias { + fn run(&self) -> Result<()> { + // These characters can't be printed cleanly to a single line, so they can cause + // confusion when writing to stdout. + const EXCLUDE_CHARS: &[char] = &['\n', '\r']; + + let exclude_dirs = config::exclude_dirs()?; + let max_age = config::maxage()?; + let now = util::current_time()?; + + let mut db = Database::open()?; + + let path = + if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( + &self.path, + )?; + let path = util::path_to_str(&path)?; + + // Ignore path if it contains unsupported characters, or if it's in the exclude + // list. + if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { + return Ok(()); + } + if !Path::new(path).is_dir() { + bail!("not a directory: {path}"); + } + + db.add_alias_update(path, self.aliases.iter(), now); + + if db.dirty() { + db.age(max_age); + } + db.save() + } +} diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 73add2d..1d4052c 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -42,6 +42,7 @@ https://github.com/ajeetdsouza/zoxide )] pub enum Cmd { Add(Add), + AddAlias(AddAlias), Edit(Edit), Import(Import), Init(Init), @@ -63,9 +64,20 @@ pub struct Add { /// doesn't #[clap(short, long)] pub score: Option, +} - #[clap(short, long)] - pub alias: Option, +/// Add aliases for a directory +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct AddAlias { + #[clap(num_args = 1.., required = true)] + pub aliases: Vec, + + #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] + pub path: PathBuf, } /// Edit the database diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs index 6fb7cc6..1dc3055 100644 --- a/src/cmd/edit.rs +++ b/src/cmd/edit.rs @@ -15,15 +15,11 @@ impl Run for Edit { match &self.cmd { Some(cmd) => { match cmd { - EditCommand::Decrement { path } => { - db.add(path, -1.0, now, Option::::None) - } + EditCommand::Decrement { path } => db.add(path, -1.0, now), EditCommand::Delete { path } => { db.remove(path); } - EditCommand::Increment { path } => { - db.add(path, 1.0, now, Option::::None) - } + EditCommand::Increment { path } => db.add(path, 1.0, now), EditCommand::Reload => {} } db.save()?; diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 5c17474..3b05c87 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,4 +1,5 @@ mod add; +mod add_alias; mod cmd; mod edit; mod import; @@ -18,6 +19,7 @@ impl Run for Cmd { fn run(&self) -> Result<()> { match self { Cmd::Add(cmd) => cmd.run(), + Cmd::AddAlias(cmd) => cmd.run(), Cmd::Edit(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(), diff --git a/src/db/mod.rs b/src/db/mod.rs index 046297c..e81dd2f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -68,33 +68,17 @@ impl Database { } /// Increments the rank of a directory, or creates it if it does not exist. - pub fn add( - &mut self, - path: impl AsRef + Into, - by: Rank, - now: Epoch, - alias: Option + Into>, - ) { + pub fn add(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { Some(dir) => { dir.rank = (dir.rank + by).max(0.0); - if let Some(al) = alias { - dir.aliases.insert(al.into().into()); - } - } - None => { - let aliases = if let Some(alias) = alias { - HashSet::from([alias.into().into()]) - } else { - HashSet::new() - }; - dirs.push(DirV4 { - path: path.into().into(), - rank: by.max(0.0), - last_accessed: now, - aliases, - }) } + None => dirs.push(DirV4 { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases: HashSet::new(), + }), }); self.with_dirty_mut(|dirty| *dirty = true); } @@ -103,58 +87,63 @@ impl Database { /// directory is already in the database, it is expected that the user /// either does a check before calling this, or calls `dedup()` /// afterward. - pub fn add_unchecked( - &mut self, - path: impl AsRef + Into, - rank: Rank, - now: Epoch, - alias: Option + Into>, - ) { + pub fn add_unchecked(&mut self, path: impl AsRef + Into, rank: Rank, now: Epoch) { self.with_dirs_mut(|dirs| { - let aliases = if let Some(alias) = alias { - HashSet::from([alias.into().into()]) - } else { - HashSet::new() - }; - dirs.push(DirV4 { path: path.into().into(), rank, last_accessed: now, aliases }) + dirs.push(DirV4 { + path: path.into().into(), + rank, + last_accessed: now, + aliases: HashSet::new(), + }) }); self.with_dirty_mut(|dirty| *dirty = true); } /// Increments the rank and updates the last_accessed of a directory, or /// creates it if it does not exist. - pub fn add_update( - &mut self, - path: impl AsRef + Into, - by: Rank, - now: Epoch, - alias: Option + Into>, - ) { + pub fn add_update(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { Some(dir) => { dir.rank = (dir.rank + by).max(0.0); dir.last_accessed = now; - if let Some(al) = alias { - dir.aliases.insert(al.into().into()); - } + } + None => dirs.push(DirV4 { + path: path.into().into(), + rank: by.max(0.0), + last_accessed: now, + aliases: HashSet::new(), + }), + }); + self.with_dirty_mut(|dirty| *dirty = true); + } + + /// Adds aliases to a directory and updates its last_accessed, or + /// creates it and adds aliases to it if it does not exist. + pub fn add_alias_update( + &mut self, + path: impl AsRef + Into, + aliases: impl Iterator + Into>, + now: Epoch, + ) { + self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => { + dir.aliases.extend(aliases.map(|alias| alias.into().into())); + dir.last_accessed = now } None => { - let aliases = if let Some(alias) = alias { - HashSet::from([alias.into().into()]) - } else { - HashSet::new() - }; + let mut set = HashSet::new(); + set.extend(aliases.map(|alias| alias.into().into())); + dirs.push(DirV4 { path: path.into().into(), - rank: by.max(0.0), + rank: 0.0, last_accessed: now, - aliases, + aliases: set, }) } }); self.with_dirty_mut(|dirty| *dirty = true); } - /// Removes the directory with `path` from the store. This does not preserve /// ordering, but is O(1). pub fn remove(&mut self, path: impl AsRef) -> bool { @@ -317,8 +306,8 @@ mod tests { { let mut db = Database::open_dir(data_dir.path()).unwrap(); - db.add(path, 1.0, now, Option::::None); - db.add(path, 1.0, now, Some(String::from("foo"))); + db.add(path, 1.0, now); + db.add(path, 1.0, now); db.save().unwrap(); } @@ -341,7 +330,7 @@ mod tests { { let mut db = Database::open_dir(data_dir.path()).unwrap(); - db.add(path, 1.0, now, Option::::None); + db.add(path, 1.0, now); db.save().unwrap(); } diff --git a/src/import.rs b/src/import.rs index cd976d3..2eda0bc 100644 --- a/src/import.rs +++ b/src/import.rs @@ -60,7 +60,7 @@ pub(crate) fn run(importer: &impl Importer, db: &mut Database) -> Result<()> { if exclude_dirs.iter().any(|glob| glob.matches(&dir.path)) { continue; } - db.add_unchecked(dir.path, dir.rank, dir.last_accessed, Option::::None); + db.add_unchecked(dir.path, dir.rank, dir.last_accessed); } Err(e) => { let location = match &e.path { From 1b1100821f9e70f653536a53cbeecef4493d0c37 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Tue, 21 Jul 2026 05:00:17 +0300 Subject: [PATCH 07/13] Add remove-alias command --- contrib/completions/_zoxide | 17 +++++++++++++++++ contrib/completions/_zoxide.ps1 | 10 ++++++++++ contrib/completions/zoxide.bash | 33 ++++++++++++++++++++++++++++++++- contrib/completions/zoxide.elv | 9 +++++++++ contrib/completions/zoxide.fish | 4 ++++ contrib/completions/zoxide.nu | 8 ++++++++ contrib/completions/zoxide.ts | 26 ++++++++++++++++++++++++++ src/cmd/cmd.rs | 15 +++++++++++++++ src/cmd/mod.rs | 2 ++ src/cmd/remove_alias.rs | 21 +++++++++++++++++++++ src/db/mod.rs | 24 ++++++++++++++++++++++++ 11 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/cmd/remove_alias.rs diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index d033796..f34310d 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -220,6 +220,17 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ '*::paths:_files -/' \ && ret=0 +;; +(remove-alias) +_arguments "${_arguments_options[@]}" : \ +'-p+[]:PATH:_files -/' \ +'--path=[]:PATH:_files -/' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +'*::aliases:_default' \ +&& ret=0 ;; esac ;; @@ -236,6 +247,7 @@ _zoxide_commands() { 'init:Generate shell configuration' \ 'query:Search for a directory in the database' \ 'remove:Remove a directory from the database' \ +'remove-alias:Remove aliases from a directory' \ ) _describe -t commands 'zoxide commands' commands "$@" } @@ -336,6 +348,11 @@ _zoxide__subcmd__remove_commands() { local commands; commands=() _describe -t commands 'zoxide remove commands' commands "$@" } +(( $+functions[_zoxide__subcmd__remove-alias_commands] )) || +_zoxide__subcmd__remove-alias_commands() { + local commands; commands=() + _describe -t commands 'zoxide remove-alias commands' commands "$@" +} if [ "$funcstack[1]" = "_zoxide" ]; then _zoxide "$@" diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index c282274..1d248cf 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -32,6 +32,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Remove a directory from the database') + [CompletionResult]::new('remove-alias', 'remove-alias', [CompletionResultType]::ParameterValue, 'Remove aliases from a directory') break } 'zoxide;add' { @@ -188,6 +189,15 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + 'zoxide;remove-alias' { + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'p') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'path') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } }) $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 5e99429..1af8e12 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -37,6 +37,9 @@ _zoxide() { zoxide,remove) cmd="zoxide__subcmd__remove" ;; + zoxide,remove-alias) + cmd="zoxide__subcmd__remove__subcmd__alias" + ;; zoxide__subcmd__edit,decrement) cmd="zoxide__subcmd__edit__subcmd__decrement" ;; @@ -74,7 +77,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add add-alias edit import init query remove" + opts="-h -V --help --version add add-alias edit import init query remove remove-alias" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -369,6 +372,34 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__subcmd__remove__subcmd__alias) + opts="-p -h -V --path --help --version ..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --path) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + -p) + COMPREPLY=() + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o plusdirs + fi + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; esac } diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 7294b26..7c5d301 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -29,6 +29,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand init 'Generate shell configuration' cand query 'Search for a directory in the database' cand remove 'Remove a directory from the database' + cand remove-alias 'Remove aliases from a directory' } &'zoxide;add'= { cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' @@ -167,6 +168,14 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' } + &'zoxide;remove-alias'= { + cand -p 'p' + cand --path 'path' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } ] $completions[$command] } diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 9eda694..8a3da15 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -33,6 +33,7 @@ complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import en complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove" -d 'Remove a directory from the database' +complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove-alias" -d 'Remove aliases from a directory' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' @@ -98,3 +99,6 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Pr complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s p -l path -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s V -l version -d 'Print version' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index e25b011..d882076 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -139,6 +139,14 @@ module completions { ...paths: path ] + # Remove aliases from a directory + export extern "zoxide remove-alias" [ + --path(-p): path + --help(-h) # Print help + --version(-V) # Print version + ...aliases: string + ] + } export use completions * diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index f34b4a1..e42b3bd 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -407,6 +407,32 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "remove-alias", + description: "Remove aliases from a directory", + options: [ + { + name: ["-p", "--path"], + isRepeatable: true, + args: { + name: "path", + template: "folders", + }, + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "aliases", + isVariadic: true, + }, + }, ], options: [ { diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 1d4052c..f30353c 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -48,6 +48,7 @@ pub enum Cmd { Init(Init), Query(Query), Remove(Remove), + RemoveAlias(RemoveAlias), } /// Add a new directory or increment its rank @@ -228,3 +229,17 @@ pub struct Remove { #[clap(value_hint = ValueHint::DirPath)] pub paths: Vec, } + +/// Remove aliases from a directory +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct RemoveAlias { + #[clap(num_args = 1.., required = true)] + pub aliases: Vec, + + #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] + pub path: String, +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 3b05c87..3b93fe8 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -6,6 +6,7 @@ mod import; mod init; mod query; mod remove; +mod remove_alias; use anyhow::Result; @@ -25,6 +26,7 @@ impl Run for Cmd { Cmd::Init(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(), Cmd::Remove(cmd) => cmd.run(), + Cmd::RemoveAlias(cmd) => cmd.run(), } } } diff --git a/src/cmd/remove_alias.rs b/src/cmd/remove_alias.rs new file mode 100644 index 0000000..7ef3832 --- /dev/null +++ b/src/cmd/remove_alias.rs @@ -0,0 +1,21 @@ +use anyhow::{Result, bail}; + +use crate::cmd::{RemoveAlias, Run}; +use crate::db::Database; +use crate::util; + +impl Run for RemoveAlias { + fn run(&self) -> Result<()> { + let mut db = Database::open()?; + + if !db.remove_alias(&self.path, self.aliases.iter()) { + let path_abs = util::resolve_path(&self.path)?; + let path_abs = util::path_to_str(&path_abs)?; + if path_abs == self.path || !db.remove_alias(path_abs, self.aliases.iter()) { + bail!("path not found in database: {}", &self.path) + } + } + + db.save() + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index e81dd2f..5f13bf0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -144,6 +144,7 @@ impl Database { }); self.with_dirty_mut(|dirty| *dirty = true); } + /// Removes the directory with `path` from the store. This does not preserve /// ordering, but is O(1). pub fn remove(&mut self, path: impl AsRef) -> bool { @@ -161,6 +162,29 @@ impl Database { self.with_dirty_mut(|dirty| *dirty = true); } + /// Removes aliases from a directory + pub fn remove_alias( + &mut self, + path: impl AsRef, + aliases: impl Iterator>, + ) -> bool { + let res = self.with_dirs_mut(|dirs| { + match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => { + let mut res = false; + aliases.for_each(|alias| { + dir.aliases.remove(alias.as_ref()); + res = true; + }); + res + } + None => false, + } + }); + self.with_dirty_mut(|dirty| *dirty |= res); + res + } + pub fn age(&mut self, max_age: Rank) { let mut dirty = false; self.with_dirs_mut(|dirs| { From e4956833aa3b90dd414467e532eb1b41a1406cf9 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Tue, 21 Jul 2026 23:51:29 +0300 Subject: [PATCH 08/13] Fix Database::add_alias() and Database::remove_alias() unnecessary save bugs Add tests for Database::add_alias() and Database::remove_alias() --- src/db/dir.rs | 13 +++++---- src/db/mod.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/db/dir.rs b/src/db/dir.rs index 4dae2cf..9fac1d2 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -27,7 +27,7 @@ pub struct DirV3<'a> { pub trait Dir { fn path(&self) -> &str; fn score(&self, now: Epoch) -> Rank; - fn aliases(&self) -> Option<&HashSet>>; + fn aliases(&self) -> Option>>; } impl Dir for DirV4<'_> { @@ -49,8 +49,8 @@ impl Dir for DirV4<'_> { } } - fn aliases(&self) -> Option<&HashSet>> { - Some(&self.aliases) + fn aliases(&self) -> Option>> { + Some(self.aliases.iter()) } } @@ -79,8 +79,9 @@ impl Dir for DirV3<'_> { } } - fn aliases(&self) -> Option<&HashSet>> { - None + fn aliases(&self) -> Option>> { + let arr: Option<&[&str]> = None; + arr.map(|a| a.iter()) } } @@ -123,7 +124,7 @@ impl<'a, T: Dir> Display for DirDisplay<'a, T> { && let Some(aliases) = self.dir.aliases() { for alias in aliases { - write!(f, "{} ", alias)?; + write!(f, "{} ", alias.as_ref())?; } write!(f, "{}", self.separator)?; } diff --git a/src/db/mod.rs b/src/db/mod.rs index 5f13bf0..11aa3ed 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -125,10 +125,14 @@ impl Database { aliases: impl Iterator + Into>, now: Epoch, ) { + let mut is_dirty = false; self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { Some(dir) => { + let starting_len = dir.aliases.len(); dir.aliases.extend(aliases.map(|alias| alias.into().into())); - dir.last_accessed = now + dir.last_accessed = now; + + is_dirty = dir.aliases.len() > starting_len; } None => { let mut set = HashSet::new(); @@ -139,10 +143,12 @@ impl Database { rank: 0.0, last_accessed: now, aliases: set, - }) + }); + + is_dirty = true; } }); - self.with_dirty_mut(|dirty| *dirty = true); + self.with_dirty_mut(|dirty| *dirty |= is_dirty); } /// Removes the directory with `path` from the store. This does not preserve @@ -173,8 +179,7 @@ impl Database { Some(dir) => { let mut res = false; aliases.for_each(|alias| { - dir.aliases.remove(alias.as_ref()); - res = true; + res |= dir.aliases.remove(alias.as_ref()); }); res } @@ -346,6 +351,35 @@ mod tests { } } + #[test] + fn add_alias() { + let data_dir = tempfile::tempdir().unwrap(); + let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; + let now = 946684800; + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add_alias_update(path, ["bar", "fb"].into_iter(), now); + db.add_alias_update(path, ["foobar"].into_iter(), now); + db.save().unwrap(); + } + + { + let db = Database::open_dir(data_dir.path()).unwrap(); + assert_eq!(db.dirs().len(), 1); + + let mut aliases = HashSet::from(["bar", "fb", "foobar"]); + let dir = &db.dirs()[0]; + assert_eq!(dir.path, path); + assert!( + dir.aliases() + .is_some_and(|mut iter| iter.all(|alias| aliases.remove(alias.as_ref()))) + && aliases.is_empty() + ); + assert_eq!(dir.last_accessed, now); + } + } + #[test] fn remove() { let data_dir = tempfile::tempdir().unwrap(); @@ -371,4 +405,36 @@ mod tests { db.save().unwrap(); } } + + #[test] + fn remove_alias() { + let data_dir = tempfile::tempdir().unwrap(); + let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; + let now = 946684800; + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add_alias_update(path, ["fb", "bar", "foobar"].into_iter(), now); + db.save().unwrap(); + } + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + assert!(db.remove_alias(path, ["bar", "foobar"].into_iter())); + db.save().unwrap(); + } + + { + let mut db = Database::open_dir(data_dir.path()).unwrap(); + let mut aliases = HashSet::from(["fb"]); + assert_eq!(db.dirs().len(), 1); + assert!( + db.dirs()[0] + .aliases() + .is_some_and(|mut iter| iter.all(|alias| aliases.remove(alias.as_ref()))) + && aliases.is_empty() + ); + db.save().unwrap(); + } + } } From 2f9ea9b12e6dac8afea443cc9e9f8891f76d2047 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Wed, 22 Jul 2026 04:38:26 +0300 Subject: [PATCH 09/13] Add alias test cases to query test --- src/db/stream.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index aa9251c..8f8bce2 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -111,8 +111,7 @@ impl<'a> Stream<'a> { } fn match_aliases(&self, aliases: &HashSet>) -> bool { - for keyword in self.options.keywords.iter().rev() { - // Alias matching is intended to be case-sensitive + for keyword in &self.options.keywords { if aliases.contains(keyword.as_str()) { return true; } @@ -218,10 +217,32 @@ mod tests { #[case(&["foo", "o", "bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] + // Aliases + // Case normalization + #[case(&["fOo", "BaR"], "ALIASES=foo,bar", true)] + #[case(&["foo", "BaR"], "ALIASES=foo,bar", true)] + // Exact matches + #[case(&["fo", "ar"], "ALIASES=foo,bar", false)] + #[case(&["foo", "bar"], "ALIASES=foo,bar", true)] + // Mixed aliases and paths + #[case(&["/foo/", "bar", "/baz"], "ALIASES=foo,bar", true)] fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options); - assert_eq!(is_match, stream.filter_by_keywords(path)); + assert_eq!( + is_match, + if path.starts_with("ALIASES=") { + stream.match_aliases( + &path + .trim_start_matches("ALIASES=") + .split(",") + .map(|alias| Cow::Borrowed(alias)) + .collect::>>(), + ) + } else { + stream.filter_by_keywords(path) + } + ); } } From b039289b1132b17c84e77327a8b55754f4768eef Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Wed, 22 Jul 2026 04:56:25 +0300 Subject: [PATCH 10/13] Fix unnecessary `Option` wrappers --- src/db/dir.rs | 18 ++++++++---------- src/db/mod.rs | 8 ++------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/db/dir.rs b/src/db/dir.rs index 9fac1d2..a022205 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -27,7 +27,7 @@ pub struct DirV3<'a> { pub trait Dir { fn path(&self) -> &str; fn score(&self, now: Epoch) -> Rank; - fn aliases(&self) -> Option>>; + fn aliases(&self) -> impl Iterator>; } impl Dir for DirV4<'_> { @@ -49,8 +49,8 @@ impl Dir for DirV4<'_> { } } - fn aliases(&self) -> Option>> { - Some(self.aliases.iter()) + fn aliases(&self) -> impl Iterator> { + self.aliases.iter() } } @@ -79,9 +79,9 @@ impl Dir for DirV3<'_> { } } - fn aliases(&self) -> Option>> { - let arr: Option<&[&str]> = None; - arr.map(|a| a.iter()) + fn aliases(&self) -> impl Iterator> { + let arr: &[&str] = &[]; + arr.iter() } } @@ -120,10 +120,8 @@ impl<'a, T: Dir> Display for DirDisplay<'a, T> { write!(f, "{score:>6.1}{}", self.separator)?; } - if self.aliases - && let Some(aliases) = self.dir.aliases() - { - for alias in aliases { + if self.aliases { + for alias in self.dir.aliases() { write!(f, "{} ", alias.as_ref())?; } write!(f, "{}", self.separator)?; diff --git a/src/db/mod.rs b/src/db/mod.rs index 11aa3ed..fc13a30 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -372,9 +372,7 @@ mod tests { let dir = &db.dirs()[0]; assert_eq!(dir.path, path); assert!( - dir.aliases() - .is_some_and(|mut iter| iter.all(|alias| aliases.remove(alias.as_ref()))) - && aliases.is_empty() + dir.aliases().all(|alias| aliases.remove(alias.as_ref())) && aliases.is_empty() ); assert_eq!(dir.last_accessed, now); } @@ -429,9 +427,7 @@ mod tests { let mut aliases = HashSet::from(["fb"]); assert_eq!(db.dirs().len(), 1); assert!( - db.dirs()[0] - .aliases() - .is_some_and(|mut iter| iter.all(|alias| aliases.remove(alias.as_ref()))) + db.dirs()[0].aliases().all(|alias| aliases.remove(alias.as_ref())) && aliases.is_empty() ); db.save().unwrap(); From 16356aae4eea1d38a60c76f0cc4c0d22e0b9b253 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Wed, 22 Jul 2026 05:23:52 +0300 Subject: [PATCH 11/13] Fix lint warnings --- src/cmd/remove_alias.rs | 2 +- src/db/stream.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/remove_alias.rs b/src/cmd/remove_alias.rs index 7ef3832..780434d 100644 --- a/src/cmd/remove_alias.rs +++ b/src/cmd/remove_alias.rs @@ -12,7 +12,7 @@ impl Run for RemoveAlias { let path_abs = util::resolve_path(&self.path)?; let path_abs = util::path_to_str(&path_abs)?; if path_abs == self.path || !db.remove_alias(path_abs, self.aliases.iter()) { - bail!("path not found in database: {}", &self.path) + bail!("path not found in database: {}", self.path) } } diff --git a/src/db/stream.rs b/src/db/stream.rs index 8f8bce2..e68046c 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -237,7 +237,7 @@ mod tests { &path .trim_start_matches("ALIASES=") .split(",") - .map(|alias| Cow::Borrowed(alias)) + .map(Cow::Borrowed) .collect::>>(), ) } else { From 84d1d830f986dbc57ddd234fe67e4418664a1b0f Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Sun, 26 Jul 2026 20:57:51 +0300 Subject: [PATCH 12/13] Add alias matching to fzf search mode --- src/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index 9b59f27..3579323 100644 --- a/src/util.rs +++ b/src/util.rs @@ -41,7 +41,7 @@ impl Fzf { cmd.args([ // Search mode "--delimiter=\t", - "--nth=3", + "--nth=2,3", // Scripting "--read0", ]) From 88aac1850f491652b2637172bd18f1252ed60213 Mon Sep 17 00:00:00 2001 From: Tahaa-Dev Date: Sun, 2 Aug 2026 22:27:38 +0300 Subject: [PATCH 13/13] Add doc comments to path flag for add-alias and remove-alias commands --- contrib/completions/_zoxide | 8 ++++---- contrib/completions/_zoxide.ps1 | 8 ++++---- contrib/completions/zoxide.elv | 8 ++++---- contrib/completions/zoxide.fish | 4 ++-- contrib/completions/zoxide.nu | 4 ++-- contrib/completions/zoxide.ts | 2 ++ src/cmd/cmd.rs | 2 ++ 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index f34310d..79cd8d7 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -41,8 +41,8 @@ _arguments "${_arguments_options[@]}" : \ ;; (add-alias) _arguments "${_arguments_options[@]}" : \ -'-p+[]:PATH:_files -/' \ -'--path=[]:PATH:_files -/' \ +'-p+[Path to add aliases to]:PATH:_files -/' \ +'--path=[Path to add aliases to]:PATH:_files -/' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ @@ -223,8 +223,8 @@ _arguments "${_arguments_options[@]}" : \ ;; (remove-alias) _arguments "${_arguments_options[@]}" : \ -'-p+[]:PATH:_files -/' \ -'--path=[]:PATH:_files -/' \ +'-p+[Path to remove aliases from]:PATH:_files -/' \ +'--path=[Path to remove aliases from]:PATH:_files -/' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index 1d248cf..c0c0f10 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -45,8 +45,8 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;add-alias' { - [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'p') - [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'path') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Path to add aliases to') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'Path to add aliases to') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') @@ -190,8 +190,8 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;remove-alias' { - [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'p') - [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'path') + [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Path to remove aliases from') + [CompletionResult]::new('--path', '--path', [CompletionResultType]::ParameterName, 'Path to remove aliases from') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 7c5d301..de24d81 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -40,8 +40,8 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version' } &'zoxide;add-alias'= { - cand -p 'p' - cand --path 'path' + cand -p 'Path to add aliases to' + cand --path 'Path to add aliases to' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' @@ -169,8 +169,8 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version' } &'zoxide;remove-alias'= { - cand -p 'p' - cand --path 'path' + cand -p 'Path to remove aliases from' + cand --path 'Path to remove aliases from' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 8a3da15..6c77c0c 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -37,7 +37,7 @@ complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove-alias" -d 'Rem complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' -complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s p -l path -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s p -l path -d 'Path to add aliases to' -r -f -a "(__fish_complete_directories)" complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand add-alias" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s h -l help -d 'Print help' @@ -99,6 +99,6 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Pr complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s V -l version -d 'Print version' -complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s p -l path -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s p -l path -d 'Path to remove aliases from' -r -f -a "(__fish_complete_directories)" complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand remove-alias" -s V -l version -d 'Print version' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index d882076..23aa5e2 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -16,7 +16,7 @@ module completions { # Add aliases for a directory export extern "zoxide add-alias" [ - --path(-p): path + --path(-p): path # Path to add aliases to --help(-h) # Print help --version(-V) # Print version ...aliases: string @@ -141,7 +141,7 @@ module completions { # Remove aliases from a directory export extern "zoxide remove-alias" [ - --path(-p): path + --path(-p): path # Path to remove aliases from --help(-h) # Print help --version(-V) # Print version ...aliases: string diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index e42b3bd..e6d37cf 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -36,6 +36,7 @@ const completion: Fig.Spec = { options: [ { name: ["-p", "--path"], + description: "Path to add aliases to", isRepeatable: true, args: { name: "path", @@ -413,6 +414,7 @@ const completion: Fig.Spec = { options: [ { name: ["-p", "--path"], + description: "Path to remove aliases from", isRepeatable: true, args: { name: "path", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index f30353c..012ddbd 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -77,6 +77,7 @@ pub struct AddAlias { #[clap(num_args = 1.., required = true)] pub aliases: Vec, + /// Path to add aliases to #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] pub path: PathBuf, } @@ -240,6 +241,7 @@ pub struct RemoveAlias { #[clap(num_args = 1.., required = true)] pub aliases: Vec, + /// Path to remove aliases from #[clap(short, long, required = true, value_hint = ValueHint::DirPath)] pub path: String, }