Fix handling of uppercase characters in fuzzy finding

Max Brunsfeld created

Change summary

zed/src/file_finder.rs       |  6 +++---
zed/src/worktree.rs          | 37 ++++++++++++++++++++++---------------
zed/src/worktree/char_bag.rs | 10 ++++++++++
zed/src/worktree/fuzzy.rs    | 23 +++++++++++++++++------
4 files changed, 52 insertions(+), 24 deletions(-)

Detailed changes

zed/src/file_finder.rs 🔗

@@ -141,9 +141,9 @@ impl FileFinder {
 
         self.worktree(tree_id, app).map(|tree| {
             let prefix = if self.include_root_name {
-                tree.root_name_chars()
+                tree.root_name()
             } else {
-                &[]
+                ""
             };
             let path = path_match.path.clone();
             let path_string = path_match.path.to_string_lossy();
@@ -169,7 +169,7 @@ impl FileFinder {
             let highlight_color = ColorU::from_u32(0x304ee2ff);
             let bold = *Properties::new().weight(Weight::BOLD);
 
-            let mut full_path = prefix.iter().collect::<String>();
+            let mut full_path = prefix.to_string();
             full_path.push_str(&path_string);
 
             let mut container = Container::new(

zed/src/worktree.rs 🔗

@@ -68,16 +68,16 @@ struct FileHandleState {
 impl Worktree {
     pub fn new(path: impl Into<Arc<Path>>, ctx: &mut ModelContext<Self>) -> Self {
         let abs_path = path.into();
-        let root_name_chars = abs_path.file_name().map_or(Vec::new(), |n| {
-            n.to_string_lossy().chars().chain(Some('/')).collect()
-        });
+        let root_name = abs_path
+            .file_name()
+            .map_or(String::new(), |n| n.to_string_lossy().to_string() + "/");
         let (scan_state_tx, scan_state_rx) = smol::channel::unbounded();
         let id = ctx.model_id();
         let snapshot = Snapshot {
             id,
             scan_id: 0,
             abs_path,
-            root_name_chars,
+            root_name,
             ignores: Default::default(),
             entries: Default::default(),
         };
@@ -224,7 +224,7 @@ pub struct Snapshot {
     id: usize,
     scan_id: usize,
     abs_path: Arc<Path>,
-    root_name_chars: Vec<char>,
+    root_name: String,
     ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
     entries: SumTree<Entry>,
 }
@@ -261,12 +261,10 @@ impl Snapshot {
         self.entry_for_path("").unwrap()
     }
 
-    pub fn root_name(&self) -> Option<&OsStr> {
-        self.abs_path.file_name()
-    }
-
-    pub fn root_name_chars(&self) -> &[char] {
-        &self.root_name_chars
+    /// Returns the filename of the snapshot's root directory,
+    /// with a trailing slash.
+    pub fn root_name(&self) -> &str {
+        &self.root_name
     }
 
     fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
@@ -613,7 +611,12 @@ impl BackgroundScanner {
         notify: Sender<ScanState>,
         worktree_id: usize,
     ) -> Self {
-        let root_char_bag = CharBag::from(snapshot.lock().root_name_chars.as_slice());
+        let root_char_bag = snapshot
+            .lock()
+            .root_name
+            .chars()
+            .map(|c| c.to_ascii_lowercase())
+            .collect();
         let mut scanner = Self {
             root_char_bag,
             snapshot,
@@ -1069,7 +1072,11 @@ impl BackgroundScanner {
 
     fn char_bag(&self, path: &Path) -> CharBag {
         let mut result = self.root_char_bag;
-        result.extend(path.to_string_lossy().chars());
+        result.extend(
+            path.to_string_lossy()
+                .chars()
+                .map(|c| c.to_ascii_lowercase()),
+        );
         result
     }
 }
@@ -1465,7 +1472,7 @@ mod tests {
                     abs_path: root_dir.path().into(),
                     entries: Default::default(),
                     ignores: Default::default(),
-                    root_name_chars: Default::default(),
+                    root_name: Default::default(),
                 })),
                 Arc::new(Mutex::new(Default::default())),
                 notify_tx,
@@ -1500,7 +1507,7 @@ mod tests {
                     abs_path: root_dir.path().into(),
                     entries: Default::default(),
                     ignores: Default::default(),
-                    root_name_chars: Default::default(),
+                    root_name: Default::default(),
                 })),
                 Arc::new(Mutex::new(Default::default())),
                 notify_tx,

zed/src/worktree/char_bag.rs 🔗

@@ -1,3 +1,5 @@
+use std::iter::FromIterator;
+
 #[derive(Copy, Clone, Debug, Default)]
 pub struct CharBag(u64);
 
@@ -31,6 +33,14 @@ impl Extend<char> for CharBag {
     }
 }
 
+impl FromIterator<char> for CharBag {
+    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
+        let mut result = Self::default();
+        result.extend(iter);
+        result
+    }
+}
+
 impl From<&str> for CharBag {
     fn from(s: &str) -> Self {
         let mut bag = Self(0);

zed/src/worktree/fuzzy.rs 🔗

@@ -171,10 +171,16 @@ fn match_single_tree_paths<'a>(
     let mut lowercase_path_chars = Vec::new();
 
     let prefix = if include_root_name {
-        snapshot.root_name_chars.as_slice()
+        snapshot.root_name()
     } else {
-        &[]
-    };
+        ""
+    }
+    .chars()
+    .collect::<Vec<_>>();
+    let lowercase_prefix = prefix
+        .iter()
+        .map(|c| c.to_ascii_lowercase())
+        .collect::<Vec<_>>();
 
     for path_entry in path_entries {
         if !path_entry.char_bag.is_superset(query_chars) {
@@ -190,7 +196,7 @@ fn match_single_tree_paths<'a>(
 
         if !find_last_positions(
             last_positions,
-            prefix,
+            &lowercase_prefix,
             &lowercase_path_chars,
             &lowercase_query[..],
         ) {
@@ -209,6 +215,7 @@ fn match_single_tree_paths<'a>(
             &path_chars,
             &lowercase_path_chars,
             &prefix,
+            &lowercase_prefix,
             smart_case,
             &last_positions,
             score_matrix,
@@ -257,6 +264,7 @@ fn score_match(
     path: &[char],
     path_cased: &[char],
     prefix: &[char],
+    lowercase_prefix: &[char],
     smart_case: bool,
     last_positions: &[usize],
     score_matrix: &mut [Option<f64>],
@@ -270,6 +278,7 @@ fn score_match(
         path,
         path_cased,
         prefix,
+        lowercase_prefix,
         smart_case,
         last_positions,
         score_matrix,
@@ -300,6 +309,7 @@ fn recursive_score_match(
     path: &[char],
     path_cased: &[char],
     prefix: &[char],
+    lowercase_prefix: &[char],
     smart_case: bool,
     last_positions: &[usize],
     score_matrix: &mut [Option<f64>],
@@ -328,7 +338,7 @@ fn recursive_score_match(
     let mut last_slash = 0;
     for j in path_idx..=limit {
         let path_char = if j < prefix.len() {
-            prefix[j]
+            lowercase_prefix[j]
         } else {
             path_cased[j - prefix.len()]
         };
@@ -404,6 +414,7 @@ fn recursive_score_match(
                 path,
                 path_cased,
                 prefix,
+                lowercase_prefix,
                 smart_case,
                 last_positions,
                 score_matrix,
@@ -547,7 +558,7 @@ mod tests {
                 abs_path: PathBuf::new().into(),
                 ignores: Default::default(),
                 entries: Default::default(),
-                root_name_chars: Vec::new(),
+                root_name: Default::default(),
             },
             false,
             path_entries.into_iter(),