Improvements to query algorithm

This commit is contained in:
Ajeet D'Souza 2021-05-06 04:31:57 +05:30
parent 1075ba5a50
commit efe11ec924
9 changed files with 104 additions and 118 deletions

View File

@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- GitHub install script. - GitHub install script.
- Release binaries built with `glibc`, use `musl` instead.
## [0.6.0] - 2021-04-09 ## [0.6.0] - 2021-04-09

8
Cargo.lock generated
View File

@ -519,9 +519,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2" checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -600,9 +600,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373" checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -3,8 +3,8 @@ name = "zoxide"
version = "0.7.0" version = "0.7.0"
authors = ["Ajeet D'Souza <98ajeet@gmail.com>"] authors = ["Ajeet D'Souza <98ajeet@gmail.com>"]
edition = "2018" edition = "2018"
description = "A smarter cd command" description = "A smarter cd command for your terminal"
repository = "https://github.com/ajeetdsouza/zoxide/" repository = "https://github.com/ajeetdsouza/zoxide"
license = "MIT" license = "MIT"
keywords = ["cli"] keywords = ["cli"]
categories = ["command-line-utilities", "filesystem"] categories = ["command-line-utilities", "filesystem"]

View File

@ -1,6 +1,6 @@
# `zoxide` # `zoxide`
> A smarter cd command > A smarter cd command for your terminal
[![crates.io][crates.io-badge]][crates.io] [![crates.io][crates.io-badge]][crates.io]

View File

@ -14,14 +14,14 @@ impl Run for Import {
let data_dir = config::zo_data_dir()?; let data_dir = config::zo_data_dir()?;
let mut db = DatabaseFile::new(data_dir); let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?; let db = &mut db.open()?;
if !self.merge && !db.dirs.is_empty() { if !self.merge && !db.dirs.is_empty() {
bail!("current database is not empty, specify --merge to continue anyway"); bail!("current database is not empty, specify --merge to continue anyway");
} }
match self.from { match self.from {
ImportFrom::Autojump => from_autojump(&mut db, &self.path), ImportFrom::Autojump => from_autojump(db, &self.path),
ImportFrom::Z => from_z(&mut db, &self.path), ImportFrom::Z => from_z(db, &self.path),
} }
.context("import error") .context("import error")
} }

View File

@ -107,7 +107,6 @@ impl<'a> Database<'a> {
for idx in (0..self.dirs.len()).rev() { for idx in (0..self.dirs.len()).rev() {
let dir = &mut self.dirs[idx]; let dir = &mut self.dirs[idx];
dir.rank *= factor; dir.rank *= factor;
if dir.rank < 1.0 { if dir.rank < 1.0 {
self.dirs.swap_remove(idx); self.dirs.swap_remove(idx);

View File

@ -1,4 +1,6 @@
use std::path::Path; use crate::util;
use std::path;
pub struct Query(Vec<String>); pub struct Query(Vec<String>);
@ -8,28 +10,31 @@ impl Query {
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: AsRef<str>, S: AsRef<str>,
{ {
Query(keywords.into_iter().map(to_lowercase).collect()) Query(keywords.into_iter().map(util::to_lowercase).collect())
} }
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool { pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
let keywords = &self.0; let keywords = &self.0;
let keywords_last = match keywords.last() { let (keywords_last, keywords) = match keywords.split_last() {
Some(keyword) => keyword, Some(split) => split,
None => return true, None => return true,
}; };
let path = to_lowercase(path); let path = util::to_lowercase(path);
let mut subpath = path.as_str();
let query_name = get_filename(keywords_last); match subpath.rfind(keywords_last) {
let dir_name = get_filename(&path); Some(idx) => {
if !dir_name.contains(query_name) { if subpath[idx + keywords_last.len()..].contains(path::is_separator) {
return false; return false;
}
subpath = &subpath[..idx];
}
None => return false,
} }
let mut subpath = path.as_str(); for keyword in keywords.iter().rev() {
for keyword in keywords.iter() { match subpath.rfind(keyword) {
match subpath.find(keyword) { Some(idx) => subpath = &subpath[..idx],
Some(idx) => subpath = &subpath[idx + keyword.len()..],
None => return false, None => return false,
} }
} }
@ -38,79 +43,36 @@ impl Query {
} }
} }
fn get_filename(mut path: &str) -> &str {
if cfg!(windows) {
Path::new(path)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
} else {
if path.ends_with('/') {
path = &path[..path.len() - 1];
}
match path.rfind('/') {
Some(idx) => &path[idx + 1..],
None => path,
}
}
}
fn to_lowercase<S: AsRef<str>>(s: S) -> String {
let s = s.as_ref();
if s.is_ascii() {
s.to_ascii_lowercase()
} else {
s.to_lowercase()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Query; use super::Query;
#[test] #[test]
fn query_normalization() { fn query() {
assert!(Query::new(&["fOo", "bAr"]).matches("/foo/bar")); const CASES: &[(&[&str], &str, bool)] = &[
} // Case normalization
(&["fOo", "bAr"], "/foo/bar", true),
// Last component
(&["ba"], "/foo/bar", true),
(&["fo"], "/foo/bar", false),
// Slash as suffix
(&["foo/"], "/foo", false),
(&["foo/"], "/foo/bar", true),
(&["foo/"], "/foo/bar/baz", false),
(&["foo", "/"], "/foo", false),
(&["foo", "/"], "/foo/bar", true),
(&["foo", "/"], "/foo/bar/baz", true),
// Split components
(&["/", "fo", "/", "ar"], "/foo/bar", true),
(&["oo/ba"], "/foo/bar", true),
// Overlap
(&["foo", "o", "bar"], "/foo/bar", false),
(&["/foo/", "/bar"], "/foo/bar", false),
(&["/foo/", "/bar"], "/foo/baz/bar", true),
];
#[test] for &(keywords, path, is_match) in CASES {
fn query_filename() { assert_eq!(is_match, Query::new(keywords).matches(path))
assert!(Query::new(&["ba"]).matches("/foo/bar")); }
}
#[test]
fn query_not_filename() {
assert!(!Query::new(&["fo"]).matches("/foo/bar"));
}
#[test]
fn query_not_filename_slash() {
assert!(!Query::new(&["foo/"]).matches("/foo/bar"));
}
#[test]
fn query_path_separator() {
assert!(Query::new(&["/", "fo", "/", "ar"]).matches("/foo/bar"));
}
#[test]
fn query_path_separator_between() {
assert!(Query::new(&["oo/ba"]).matches("/foo/bar"));
}
#[test]
fn query_overlap_text() {
assert!(!Query::new(&["foo", "o", "bar"]).matches("/foo/bar"));
}
#[test]
fn query_overlap_slash() {
assert!(!Query::new(&["/foo/", "/bar"]).matches("/foo/bar"));
}
#[test]
fn query_consecutive_slash() {
assert!(Query::new(&["/foo/", "/baz"]).matches("/foo/bar/baz"));
} }
} }

View File

@ -149,3 +149,13 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
Ok(stack.iter().collect()) Ok(stack.iter().collect())
} }
// Convert a string to lowercase, with a fast path for ASCII strings.
pub fn to_lowercase<S: AsRef<str>>(s: S) -> String {
let s = s.as_ref();
if s.is_ascii() {
s.to_ascii_lowercase()
} else {
s.to_lowercase()
}
}

View File

@ -1,5 +1,6 @@
{%- let section = "# =============================================================================\n#" -%} {%- let section = "# =============================================================================\n#" -%}
{%- let not_configured = "# -- not configured --" -%} {%- let not_configured = "# -- not configured --" -%}
{%- let newline = "{{$(char newline)}}" -%}
{{ section }} {{ section }}
# Utility functions for zoxide. # Utility functions for zoxide.
@ -27,9 +28,7 @@ def __zoxide_hook [] {
{%- when InitHook::Pwd %} {%- when InitHook::Pwd %}
def __zoxide_hook [] {} def __zoxide_hook [] {}
echo `zoxide: PWD hooks are not supported on Nushell.{{ newline }} Use 'zoxide init nushell --hook prompt' instead.{{ newline }}`
printf "zoxide: PWD hooks are not supported on Nushell.\n Use 'zoxide init nushell --hook prompt' instead.\n"
{%- endmatch %} {%- endmatch %}
{{ section }} {{ section }}
@ -39,37 +38,52 @@ printf "zoxide: PWD hooks are not supported on Nushell.\n Use 'zoxide ini
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
def __zoxide_z [...rest:string] { def __zoxide_z [...rest:string] {
if $(echo $rest | length) == 1 { let args = $(echo $rest | skip 1);
cd ~ if $(shells | where active == $true | get name) != filesystem {
} { if $(echo $args | length) > 1 {
let args = $(echo $rest | skip 1); echo `zoxide: can only jump directories on filesystem{{ newline }}`
if $(echo $args | length) == 1 {
let arg0 = $(echo $args | first 1);
if $arg0 == '-' {
cd -
} {
if $(echo $arg0 | path exists) {
cd $arg0
} {
cd $(zoxide query --exclude $(pwd) -- $args | str trim)
}
}
} { } {
cd $(zoxide query --exclude $(pwd) -- $args | str trim) cd $(echo $args)
}
}
{%- if echo %} {%- if echo %}
echo $(pwd) pwd
{%- endif %} {%- endif %}
}
} {
if $(echo $args | length) == 0 {
cd ~
} {
if $(echo $args | length) == 1 {
let arg0 = $(echo $args | first 1);
if $arg0 == '-' {
cd -
} {
if $(echo $arg0 | path exists) {
cd $arg0
} {
cd $(zoxide query --exclude $(pwd) -- $args | str trim)
}
}
} {
cd $(zoxide query --exclude $(pwd) -- $args | str trim)
}
}
{%- if echo %}
pwd
{%- endif %}
}
} }
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
def __zoxide_zi [...rest:string] { def __zoxide_zi [...rest:string] {
let args = $(echo $rest | skip 1) if $(shells | where active == $true | get name) != filesystem {
cd $(zoxide query -i -- $args | str trim) echo `zoxide: can only jump directories on filesystem{{ newline }}`
} {
let args = $(echo $rest | skip 1)
cd $(zoxide query -i -- $args | str trim)
{%- if echo %} {%- if echo %}
echo $(pwd) pwd
{%- endif %} {%- endif %}
}
} }
{{ section }} {{ section }}