Move fuzzy::match_paths to Project::match_paths

Nathan Sobo and Max Brunsfeld created

This prepares the way to extract the rest of fuzzy to its own crate.

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

zed/src/file_finder.rs |  73 ++++++------
zed/src/fuzzy.rs       | 116 -------------------
zed/src/project.rs     | 249 ++++++++++++++++++++++++++++++++++++++++++-
zed/src/workspace.rs   |   4 
zed/src/worktree.rs    | 131 ----------------------
5 files changed, 293 insertions(+), 280 deletions(-)

Detailed changes

zed/src/file_finder.rs 🔗

@@ -1,9 +1,10 @@
 use crate::{
     editor::{self, Editor},
+    project::Project,
     settings::Settings,
     util,
     workspace::Workspace,
-    worktree::{match_paths, PathMatch},
+    worktree::PathMatch,
 };
 use gpui::{
     action,
@@ -13,8 +14,8 @@ use gpui::{
         menu::{SelectNext, SelectPrev},
         Binding,
     },
-    AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use postage::watch;
 use std::{
@@ -29,7 +30,7 @@ use std::{
 pub struct FileFinder {
     handle: WeakViewHandle<Self>,
     settings: watch::Receiver<Settings>,
-    workspace: WeakViewHandle<Workspace>,
+    project: ModelHandle<Project>,
     query_editor: ViewHandle<Editor>,
     search_count: usize,
     latest_search_id: usize,
@@ -241,8 +242,8 @@ impl FileFinder {
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
         workspace.toggle_modal(cx, |cx, workspace| {
-            let handle = cx.handle();
-            let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx));
+            let project = workspace.project().clone();
+            let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), project, cx));
             cx.subscribe(&finder, Self::on_event).detach();
             finder
         });
@@ -269,10 +270,10 @@ impl FileFinder {
 
     pub fn new(
         settings: watch::Receiver<Settings>,
-        workspace: ViewHandle<Workspace>,
+        project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        cx.observe(&workspace, Self::workspace_updated).detach();
+        cx.observe(&project, Self::project_updated).detach();
 
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
@@ -290,7 +291,7 @@ impl FileFinder {
         Self {
             handle: cx.handle().downgrade(),
             settings,
-            workspace: workspace.downgrade(),
+            project,
             query_editor,
             search_count: 0,
             latest_search_id: 0,
@@ -303,7 +304,7 @@ impl FileFinder {
         }
     }
 
-    fn workspace_updated(&mut self, _: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) {
+    fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
         let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
         if let Some(task) = self.spawn_search(query, cx) {
             task.detach();
@@ -381,30 +382,17 @@ impl FileFinder {
 
     #[must_use]
     fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Option<Task<()>> {
-        let snapshots = self
-            .workspace
-            .upgrade(&cx)?
-            .read(cx)
-            .worktrees(cx)
-            .iter()
-            .map(|tree| tree.read(cx).snapshot())
-            .collect::<Vec<_>>();
         let search_id = util::post_inc(&mut self.search_count);
-        let background = cx.as_ref().background().clone();
         self.cancel_flag.store(true, atomic::Ordering::Relaxed);
         self.cancel_flag = Arc::new(AtomicBool::new(false));
         let cancel_flag = self.cancel_flag.clone();
+        let project = self.project.clone();
         Some(cx.spawn(|this, mut cx| async move {
-            let matches = match_paths(
-                &snapshots,
-                &query,
-                false,
-                false,
-                100,
-                cancel_flag.as_ref(),
-                background,
-            )
-            .await;
+            let matches = project
+                .read_with(&cx, |project, cx| {
+                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
+                })
+                .await;
             let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
             this.update(&mut cx, |this, cx| {
                 this.update_matches((search_id, did_cancel, query, matches), cx)
@@ -536,8 +524,13 @@ mod tests {
             .unwrap();
         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
             .await;
-        let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
+        let (_, finder) = cx.add_window(|cx| {
+            FileFinder::new(
+                app_state.settings.clone(),
+                workspace.read(cx).project().clone(),
+                cx,
+            )
+        });
 
         let query = "hi".to_string();
         finder
@@ -596,8 +589,13 @@ mod tests {
             .unwrap();
         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
             .await;
-        let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
+        let (_, finder) = cx.add_window(|cx| {
+            FileFinder::new(
+                app_state.settings.clone(),
+                workspace.read(cx).project().clone(),
+                cx,
+            )
+        });
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -654,8 +652,13 @@ mod tests {
         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
             .await;
 
-        let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
+        let (_, finder) = cx.add_window(|cx| {
+            FileFinder::new(
+                app_state.settings.clone(),
+                workspace.read(cx).project().clone(),
+                cx,
+            )
+        });
 
         // Run a search that matches two files with the same relative path.
         finder

zed/src/fuzzy.rs 🔗

@@ -19,7 +19,7 @@ const BASE_DISTANCE_PENALTY: f64 = 0.6;
 const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
 const MIN_DISTANCE_PENALTY: f64 = 0.2;
 
-struct Matcher<'a> {
+pub struct Matcher<'a> {
     query: &'a [char],
     lowercase_query: &'a [char],
     query_char_bag: CharBag,
@@ -213,116 +213,8 @@ pub async fn match_strings(
     results
 }
 
-pub async fn match_paths(
-    snapshots: &[Snapshot],
-    query: &str,
-    include_ignored: bool,
-    smart_case: bool,
-    max_results: usize,
-    cancel_flag: &AtomicBool,
-    background: Arc<executor::Background>,
-) -> Vec<PathMatch> {
-    let path_count: usize = if include_ignored {
-        snapshots.iter().map(Snapshot::file_count).sum()
-    } else {
-        snapshots.iter().map(Snapshot::visible_file_count).sum()
-    };
-    if path_count == 0 {
-        return Vec::new();
-    }
-
-    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
-    let query = query.chars().collect::<Vec<_>>();
-
-    let lowercase_query = &lowercase_query;
-    let query = &query;
-    let query_char_bag = CharBag::from(&lowercase_query[..]);
-
-    let num_cpus = background.num_cpus().min(path_count);
-    let segment_size = (path_count + num_cpus - 1) / num_cpus;
-    let mut segment_results = (0..num_cpus)
-        .map(|_| Vec::with_capacity(max_results))
-        .collect::<Vec<_>>();
-
-    background
-        .scoped(|scope| {
-            for (segment_idx, results) in segment_results.iter_mut().enumerate() {
-                scope.spawn(async move {
-                    let segment_start = segment_idx * segment_size;
-                    let segment_end = segment_start + segment_size;
-                    let mut matcher = Matcher::new(
-                        query,
-                        lowercase_query,
-                        query_char_bag,
-                        smart_case,
-                        max_results,
-                    );
-
-                    let mut tree_start = 0;
-                    for snapshot in snapshots {
-                        let tree_end = if include_ignored {
-                            tree_start + snapshot.file_count()
-                        } else {
-                            tree_start + snapshot.visible_file_count()
-                        };
-
-                        if tree_start < segment_end && segment_start < tree_end {
-                            let path_prefix: Arc<str> =
-                                if snapshot.root_entry().map_or(false, |e| e.is_file()) {
-                                    snapshot.root_name().into()
-                                } else if snapshots.len() > 1 {
-                                    format!("{}/", snapshot.root_name()).into()
-                                } else {
-                                    "".into()
-                                };
-
-                            let start = max(tree_start, segment_start) - tree_start;
-                            let end = min(tree_end, segment_end) - tree_start;
-                            let paths = snapshot
-                                .files(include_ignored, start)
-                                .take(end - start)
-                                .map(|entry| {
-                                    if let EntryKind::File(char_bag) = entry.kind {
-                                        PathMatchCandidate {
-                                            path: &entry.path,
-                                            char_bag,
-                                        }
-                                    } else {
-                                        unreachable!()
-                                    }
-                                });
-
-                            matcher.match_paths(
-                                snapshot.id(),
-                                path_prefix,
-                                paths,
-                                results,
-                                &cancel_flag,
-                            );
-                        }
-                        if tree_end >= segment_end {
-                            break;
-                        }
-                        tree_start = tree_end;
-                    }
-                })
-            }
-        })
-        .await;
-
-    let mut results = Vec::new();
-    for segment_result in segment_results {
-        if results.is_empty() {
-            results = segment_result;
-        } else {
-            util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a));
-        }
-    }
-    results
-}
-
 impl<'a> Matcher<'a> {
-    fn new(
+    pub fn new(
         query: &'a [char],
         lowercase_query: &'a [char],
         query_char_bag: CharBag,
@@ -343,7 +235,7 @@ impl<'a> Matcher<'a> {
         }
     }
 
-    fn match_strings(
+    pub fn match_strings(
         &mut self,
         candidates: &[StringMatchCandidate],
         results: &mut Vec<StringMatch>,
@@ -363,7 +255,7 @@ impl<'a> Matcher<'a> {
         )
     }
 
-    fn match_paths(
+    pub fn match_paths(
         &mut self,
         tree_id: usize,
         path_prefix: Arc<str>,

zed/src/project.rs 🔗

@@ -1,14 +1,20 @@
 use crate::{
     fs::Fs,
+    fuzzy::{CharBag, Matcher, PathMatchCandidate},
     language::LanguageRegistry,
     rpc::Client,
-    util::TryFutureExt as _,
-    worktree::{self, Worktree},
+    util::{self, TryFutureExt as _},
+    worktree::{self, EntryKind, PathMatch, Snapshot, Worktree},
     AppState,
 };
 use anyhow::Result;
-use gpui::{Entity, ModelContext, ModelHandle, Task};
-use std::{path::Path, sync::Arc};
+use futures::Future;
+use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
+use std::{
+    cmp,
+    path::Path,
+    sync::{atomic::AtomicBool, Arc},
+};
 
 pub struct Project {
     worktrees: Vec<ModelHandle<Worktree>>,
@@ -47,13 +53,13 @@ impl Project {
 
     pub fn add_local_worktree(
         &mut self,
-        path: &Path,
+        abs_path: &Path,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Worktree>>> {
         let fs = self.fs.clone();
         let rpc = self.rpc.clone();
         let languages = self.languages.clone();
-        let path = Arc::from(path);
+        let path = Arc::from(abs_path);
         cx.spawn(|this, mut cx| async move {
             let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
             this.update(&mut cx, |this, cx| {
@@ -182,8 +188,239 @@ impl Project {
             keep
         });
     }
+
+    pub fn match_paths<'a>(
+        &self,
+        query: &str,
+        include_ignored: bool,
+        smart_case: bool,
+        max_results: usize,
+        cancel_flag: &'a AtomicBool,
+        cx: &AppContext,
+    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
+        let snapshots = self
+            .worktrees
+            .iter()
+            .map(|worktree| worktree.read(cx).snapshot())
+            .collect::<Vec<_>>();
+
+        let path_count: usize = if include_ignored {
+            snapshots.iter().map(Snapshot::file_count).sum()
+        } else {
+            snapshots.iter().map(Snapshot::visible_file_count).sum()
+        };
+
+        let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+        let query = query.chars().collect::<Vec<_>>();
+        let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+        let background = cx.background().clone();
+
+        async move {
+            if path_count == 0 {
+                return Vec::new();
+            }
+
+            let num_cpus = background.num_cpus().min(path_count);
+            let segment_size = (path_count + num_cpus - 1) / num_cpus;
+            let mut segment_results = (0..num_cpus)
+                .map(|_| Vec::with_capacity(max_results))
+                .collect::<Vec<_>>();
+
+            let lowercase_query = &lowercase_query;
+            let query = &query;
+            let snapshots = snapshots.as_slice();
+
+            background
+                .scoped(|scope| {
+                    for (segment_idx, results) in segment_results.iter_mut().enumerate() {
+                        scope.spawn(async move {
+                            let segment_start = segment_idx * segment_size;
+                            let segment_end = segment_start + segment_size;
+                            let mut matcher = Matcher::new(
+                                query,
+                                lowercase_query,
+                                query_char_bag,
+                                smart_case,
+                                max_results,
+                            );
+
+                            let mut tree_start = 0;
+                            for snapshot in snapshots {
+                                let tree_end = if include_ignored {
+                                    tree_start + snapshot.file_count()
+                                } else {
+                                    tree_start + snapshot.visible_file_count()
+                                };
+
+                                if tree_start < segment_end && segment_start < tree_end {
+                                    let path_prefix: Arc<str> =
+                                        if snapshot.root_entry().map_or(false, |e| e.is_file()) {
+                                            snapshot.root_name().into()
+                                        } else if snapshots.len() > 1 {
+                                            format!("{}/", snapshot.root_name()).into()
+                                        } else {
+                                            "".into()
+                                        };
+
+                                    let start = cmp::max(tree_start, segment_start) - tree_start;
+                                    let end = cmp::min(tree_end, segment_end) - tree_start;
+                                    let paths = snapshot
+                                        .files(include_ignored, start)
+                                        .take(end - start)
+                                        .map(|entry| {
+                                            if let EntryKind::File(char_bag) = entry.kind {
+                                                PathMatchCandidate {
+                                                    path: &entry.path,
+                                                    char_bag,
+                                                }
+                                            } else {
+                                                unreachable!()
+                                            }
+                                        });
+
+                                    matcher.match_paths(
+                                        snapshot.id(),
+                                        path_prefix,
+                                        paths,
+                                        results,
+                                        &cancel_flag,
+                                    );
+                                }
+                                if tree_end >= segment_end {
+                                    break;
+                                }
+                                tree_start = tree_end;
+                            }
+                        })
+                    }
+                })
+                .await;
+
+            let mut results = Vec::new();
+            for segment_result in segment_results {
+                if results.is_empty() {
+                    results = segment_result;
+                } else {
+                    util::extend_sorted(&mut results, segment_result, max_results, |a, b| {
+                        b.cmp(&a)
+                    });
+                }
+            }
+            results
+        }
+    }
 }
 
 impl Entity for Project {
     type Event = Event;
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        fs::RealFs,
+        test::{temp_tree, test_app_state},
+    };
+    use serde_json::json;
+    use std::{os::unix, path::PathBuf};
+
+    #[gpui::test]
+    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
+        let mut app_state = cx.update(test_app_state);
+        Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
+        let dir = temp_tree(json!({
+            "root": {
+                "apple": "",
+                "banana": {
+                    "carrot": {
+                        "date": "",
+                        "endive": "",
+                    }
+                },
+                "fennel": {
+                    "grape": "",
+                }
+            }
+        }));
+
+        let root_link_path = dir.path().join("root_link");
+        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
+        unix::fs::symlink(
+            &dir.path().join("root/fennel"),
+            &dir.path().join("root/finnochio"),
+        )
+        .unwrap();
+
+        let project = cx.add_model(|_| Project::new(app_state.as_ref()));
+        let tree = project
+            .update(&mut cx, |project, cx| {
+                project.add_local_worktree(&root_link_path, cx)
+            })
+            .await
+            .unwrap();
+
+        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+            .await;
+        cx.read(|cx| {
+            let tree = tree.read(cx);
+            assert_eq!(tree.file_count(), 5);
+            assert_eq!(
+                tree.inode_for_path("fennel/grape"),
+                tree.inode_for_path("finnochio/grape")
+            );
+        });
+
+        let cancel_flag = Default::default();
+        let results = project
+            .read_with(&cx, |project, cx| {
+                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
+            })
+            .await;
+        assert_eq!(
+            results
+                .into_iter()
+                .map(|result| result.path)
+                .collect::<Vec<Arc<Path>>>(),
+            vec![
+                PathBuf::from("banana/carrot/date").into(),
+                PathBuf::from("banana/carrot/endive").into(),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
+        let mut app_state = cx.update(test_app_state);
+        Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
+        let dir = temp_tree(json!({
+            "root": {
+                "dir1": {},
+                "dir2": {
+                    "dir3": {}
+                }
+            }
+        }));
+
+        let project = cx.add_model(|_| Project::new(app_state.as_ref()));
+        let tree = project
+            .update(&mut cx, |project, cx| {
+                project.add_local_worktree(&dir.path(), cx)
+            })
+            .await
+            .unwrap();
+
+        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+            .await;
+
+        let cancel_flag = Default::default();
+        let results = project
+            .read_with(&cx, |project, cx| {
+                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
+            })
+            .await;
+
+        assert!(results.is_empty());
+    }
+}

zed/src/workspace.rs 🔗

@@ -454,6 +454,10 @@ impl Workspace {
         }
     }
 
+    pub fn project(&self) -> &ModelHandle<Project> {
+        &self.project
+    }
+
     pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
         &self.project.read(cx).worktrees()
     }

zed/src/worktree.rs 🔗

@@ -14,12 +14,10 @@ use crate::{
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Result};
 use futures::{Stream, StreamExt};
-pub use fuzzy::{match_paths, PathMatch};
-use sum_tree::{self, Edit, SeekTarget, SumTree};
+pub use fuzzy::PathMatch;
 use gpui::{
-    executor,
-    AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
-    UpgradeModelHandle, WeakModelHandle,
+    executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
+    Task, UpgradeModelHandle, WeakModelHandle,
 };
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -44,6 +42,7 @@ use std::{
     },
     time::{Duration, SystemTime},
 };
+use sum_tree::{self, Edit, SeekTarget, SumTree};
 use zrpc::{PeerId, TypedEnvelope};
 
 lazy_static! {
@@ -2841,128 +2840,6 @@ mod tests {
         })
     }
 
-    #[gpui::test]
-    async fn test_populate_and_search(cx: gpui::TestAppContext) {
-        let dir = temp_tree(json!({
-            "root": {
-                "apple": "",
-                "banana": {
-                    "carrot": {
-                        "date": "",
-                        "endive": "",
-                    }
-                },
-                "fennel": {
-                    "grape": "",
-                }
-            }
-        }));
-
-        let root_link_path = dir.path().join("root_link");
-        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
-        unix::fs::symlink(
-            &dir.path().join("root/fennel"),
-            &dir.path().join("root/finnochio"),
-        )
-        .unwrap();
-
-        let tree = Worktree::open_local(
-            rpc::Client::new(),
-            root_link_path,
-            Arc::new(RealFs),
-            Default::default(),
-            &mut cx.to_async(),
-        )
-        .await
-        .unwrap();
-
-        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-            .await;
-        let snapshots = [cx.read(|cx| {
-            let tree = tree.read(cx);
-            assert_eq!(tree.file_count(), 5);
-            assert_eq!(
-                tree.inode_for_path("fennel/grape"),
-                tree.inode_for_path("finnochio/grape")
-            );
-            tree.snapshot()
-        })];
-        let cancel_flag = Default::default();
-        let results = cx
-            .read(|cx| {
-                match_paths(
-                    &snapshots,
-                    "bna",
-                    false,
-                    false,
-                    10,
-                    &cancel_flag,
-                    cx.background().clone(),
-                )
-            })
-            .await;
-        assert_eq!(
-            results
-                .into_iter()
-                .map(|result| result.path)
-                .collect::<Vec<Arc<Path>>>(),
-            vec![
-                PathBuf::from("banana/carrot/date").into(),
-                PathBuf::from("banana/carrot/endive").into(),
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_search_worktree_without_files(cx: gpui::TestAppContext) {
-        let dir = temp_tree(json!({
-            "root": {
-                "dir1": {},
-                "dir2": {
-                    "dir3": {}
-                }
-            }
-        }));
-        let tree = Worktree::open_local(
-            rpc::Client::new(),
-            dir.path(),
-            Arc::new(RealFs),
-            Default::default(),
-            &mut cx.to_async(),
-        )
-        .await
-        .unwrap();
-
-        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-            .await;
-        let snapshots = [cx.read(|cx| {
-            let tree = tree.read(cx);
-            assert_eq!(tree.file_count(), 0);
-            tree.snapshot()
-        })];
-        let cancel_flag = Default::default();
-        let results = cx
-            .read(|cx| {
-                match_paths(
-                    &snapshots,
-                    "dir",
-                    false,
-                    false,
-                    10,
-                    &cancel_flag,
-                    cx.background().clone(),
-                )
-            })
-            .await;
-        assert_eq!(
-            results
-                .into_iter()
-                .map(|result| result.path)
-                .collect::<Vec<Arc<Path>>>(),
-            vec![]
-        );
-    }
-
     #[gpui::test]
     async fn test_save_file(mut cx: gpui::TestAppContext) {
         let dir = temp_tree(json!({