fix: match forward slashes in queries on Windows

Windows accepts both '/' and '\' as path separators, but zoxide only
matched database paths using the separator as typed. As a result,
`z bar/meow` failed to find `C:\Users\Alice\foo\bar\meow` even though
`z bar\meow` succeeded.

Canonicalize separators on Windows for both the query keywords and the
database path before matching. Other platforms are unaffected, since
'\' is a valid filename character there.

Closes #1217
This commit is contained in:
Pablo Garcia 2026-08-11 22:43:07 +02:00
parent ee594e6906
commit 5365c43459
2 changed files with 34 additions and 2 deletions

View File

@ -83,7 +83,7 @@ impl<'a> Stream<'a> {
None => return true,
};
let path = util::to_lowercase(path);
let path = util::normalize_separators(util::to_lowercase(path));
let mut path = path.as_str();
match path.rfind(keywords_last) {
Some(idx) => {
@ -149,7 +149,10 @@ impl StreamOptions {
I: IntoIterator,
I::Item: AsRef<str>,
{
self.keywords = keywords.into_iter().map(util::to_lowercase).collect();
self.keywords = keywords
.into_iter()
.map(|keyword| util::normalize_separators(util::to_lowercase(keyword)))
.collect();
self
}
@ -208,4 +211,20 @@ mod tests {
let stream = Stream::new(db, options);
assert_eq!(is_match, stream.filter_by_keywords(path));
}
#[cfg(windows)]
#[rstest]
// Forward slashes in the query match backslashes in the database
#[case(&["bar/meow"], r"c:\users\alice\foo\bar\meow", true)]
#[case(&["foo/bar"], r"c:\users\alice\foo\bar\meow", false)]
// ...and vice versa
#[case(&[r"bar\meow"], "c:/users/alice/foo/bar/meow", true)]
// Mixed separators
#[case(&["foo/bar", r"\meow"], r"c:\users\alice\foo\bar\meow", true)]
fn query_separators(#[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));
}
}

View File

@ -378,3 +378,16 @@ pub fn to_lowercase(s: impl AsRef<str>) -> String {
let s = s.as_ref();
if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() }
}
/// Rewrite path separators to a single canonical form, so that queries match
/// regardless of which separator was typed.
///
/// On Windows both `/` and `\` are accepted by the OS, so they are treated as
/// equivalent. Elsewhere `\` is a valid filename character and is left alone.
pub fn normalize_separators(s: String) -> String {
#[cfg(windows)]
if s.contains('/') {
return s.replace('/', "\\");
}
s
}