From 5365c43459529a1ecae77eccff57d71267673873 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Date: Tue, 11 Aug 2026 22:43:07 +0200 Subject: [PATCH] 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 --- src/db/stream.rs | 23 +++++++++++++++++++++-- src/util.rs | 13 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index 24c84e0..843faed 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -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, { - 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)); + } } diff --git a/src/util.rs b/src/util.rs index 4c0a27c..3a62e2a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -378,3 +378,16 @@ pub fn to_lowercase(s: impl AsRef) -> 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 +}