Include ignored files in fuzzy search when root entry is ignored

Antonio Scandurra created

Change summary

crates/file_finder/src/file_finder.rs | 67 +++++++++++++++++++++++++---
crates/project/src/project.rs         | 62 +++++---------------------
crates/project/src/project_tests.rs   | 44 ------------------
3 files changed, 74 insertions(+), 99 deletions(-)

Detailed changes

crates/file_finder/src/file_finder.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use picker::{Picker, PickerDelegate};
-use project::{Project, ProjectPath, WorktreeId};
+use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
 use settings::Settings;
 use std::{
     path::Path,
@@ -134,17 +134,40 @@ impl FileFinder {
     }
 
     fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let worktrees = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                PathMatchCandidateSet {
+                    snapshot: worktree.snapshot(),
+                    include_ignored: worktree
+                        .root_entry()
+                        .map_or(false, |entry| entry.is_ignored),
+                    include_root_name,
+                }
+            })
+            .collect::<Vec<_>>();
+
         let search_id = util::post_inc(&mut self.search_count);
         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();
         cx.spawn(|this, mut cx| async move {
-            let matches = project
-                .read_with(&cx, |project, cx| {
-                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
-                })
-                .await;
+            let matches = fuzzy::match_paths(
+                candidate_sets.as_slice(),
+                &query,
+                false,
+                100,
+                &cancel_flag,
+                cx.background(),
+            )
+            .await;
             let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
             this.update(&mut cx, |this, cx| {
                 this.set_matches(search_id, did_cancel, query, matches, cx)
@@ -475,4 +498,34 @@ mod tests {
             assert_eq!(f.selected_index(), 0);
         });
     }
+
+    #[gpui::test]
+    async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "dir1": {},
+                    "dir2": {
+                        "dir3": {}
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, finder) =
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+        finder
+            .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
+            .await;
+        cx.read(|cx| {
+            let finder = finder.read(cx);
+            assert_eq!(finder.matches.len(), 0);
+        });
+    }
 }

crates/project/src/project.rs 🔗

@@ -13,7 +13,6 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
-use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
@@ -58,7 +57,7 @@ use std::{
     rc::Rc,
     str,
     sync::{
-        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     time::Instant,
@@ -5678,43 +5677,6 @@ impl Project {
         })
     }
 
-    pub fn match_paths<'a>(
-        &self,
-        query: &'a str,
-        include_ignored: bool,
-        smart_case: bool,
-        max_results: usize,
-        cancel_flag: &'a AtomicBool,
-        cx: &AppContext,
-    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
-        let worktrees = self
-            .worktrees(cx)
-            .filter(|worktree| worktree.read(cx).is_visible())
-            .collect::<Vec<_>>();
-        let include_root_name = worktrees.len() > 1;
-        let candidate_sets = worktrees
-            .into_iter()
-            .map(|worktree| CandidateSet {
-                snapshot: worktree.read(cx).snapshot(),
-                include_ignored,
-                include_root_name,
-            })
-            .collect::<Vec<_>>();
-
-        let background = cx.background().clone();
-        async move {
-            fuzzy::match_paths(
-                candidate_sets.as_slice(),
-                query,
-                smart_case,
-                max_results,
-                cancel_flag,
-                background,
-            )
-            .await
-        }
-    }
-
     fn edits_from_lsp(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -5942,14 +5904,14 @@ impl OpenBuffer {
     }
 }
 
-struct CandidateSet {
-    snapshot: Snapshot,
-    include_ignored: bool,
-    include_root_name: bool,
+pub struct PathMatchCandidateSet {
+    pub snapshot: Snapshot,
+    pub include_ignored: bool,
+    pub include_root_name: bool,
 }
 
-impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
-    type Candidates = CandidateSetIter<'a>;
+impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
+    type Candidates = PathMatchCandidateSetIter<'a>;
 
     fn id(&self) -> usize {
         self.snapshot.id().to_usize()
@@ -5974,23 +5936,23 @@ impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
     }
 
     fn candidates(&'a self, start: usize) -> Self::Candidates {
-        CandidateSetIter {
+        PathMatchCandidateSetIter {
             traversal: self.snapshot.files(self.include_ignored, start),
         }
     }
 }
 
-struct CandidateSetIter<'a> {
+pub struct PathMatchCandidateSetIter<'a> {
     traversal: Traversal<'a>,
 }
 
-impl<'a> Iterator for CandidateSetIter<'a> {
-    type Item = PathMatchCandidate<'a>;
+impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
+    type Item = fuzzy::PathMatchCandidate<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         self.traversal.next().map(|entry| {
             if let EntryKind::File(char_bag) = entry.kind {
-                PathMatchCandidate {
+                fuzzy::PathMatchCandidate {
                     path: &entry.path,
                     char_bag,
                 }

crates/project/src/project_tests.rs 🔗

@@ -8,12 +8,12 @@ use language::{
 };
 use lsp::Url;
 use serde_json::json;
-use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
+use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
 use unindent::Unindent as _;
 use util::{assert_set_eq, test::temp_tree};
 
 #[gpui::test]
-async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
+async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     let dir = temp_tree(json!({
         "root": {
             "apple": "",
@@ -38,7 +38,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
     .unwrap();
 
     let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
-
     project.read_with(cx, |project, cx| {
         let tree = project.worktrees(cx).next().unwrap().read(cx);
         assert_eq!(tree.file_count(), 5);
@@ -47,23 +46,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
             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]
@@ -1645,28 +1627,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
     chunks
 }
 
-#[gpui::test]
-async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
-    let dir = temp_tree(json!({
-        "root": {
-            "dir1": {},
-            "dir2": {
-                "dir3": {}
-            }
-        }
-    }));
-
-    let project = Project::test(Arc::new(RealFs), [dir.path()], cx).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());
-}
-
 #[gpui::test(iterations = 10)]
 async fn test_definition(cx: &mut gpui::TestAppContext) {
     let mut language = Language::new(