remote_server: Fix remote project search include/exclude filters for multiple worktrees (#47280)

Smit Barmase created

Closes #45772

Release Notes:

- Fixed project search include/exclude filters not working correctly in
remote project with multiple folders.

Change summary

crates/project/src/search.rs                     |   4 
crates/remote_server/src/remote_editing_tests.rs | 176 ++++++++++++++---
2 files changed, 141 insertions(+), 39 deletions(-)

Detailed changes

crates/project/src/search.rs 🔗

@@ -89,7 +89,7 @@ impl SearchQuery {
     /// Create a text query
     ///
     /// If `match_full_paths` is true, include/exclude patterns will always be matched against fully qualified project paths beginning with a project root.
-    /// If `match_full_paths` is false, patterns will be matched against full paths only when the project has multiple roots.
+    /// If `match_full_paths` is false, patterns will be matched against worktree-relative paths.
     pub fn text(
         query: impl ToString,
         whole_word: bool,
@@ -290,7 +290,7 @@ impl SearchQuery {
                 message.include_ignored,
                 PathMatcher::new(files_to_include, path_style)?,
                 PathMatcher::new(files_to_exclude, path_style)?,
-                false,
+                message.match_full_paths,
                 None, // search opened only don't need search remote
             )
         }

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -33,7 +33,7 @@ use std::{
     sync::Arc,
 };
 use unindent::Unindent as _;
-use util::{path, rel_path::rel_path};
+use util::{path, paths::PathMatcher, rel_path::rel_path};
 
 #[gpui::test]
 async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
@@ -168,6 +168,51 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
     });
 }
 
+async fn do_search_and_assert(
+    project: &Entity<Project>,
+    query: &str,
+    files_to_include: PathMatcher,
+    match_full_paths: bool,
+    expected_paths: &[&str],
+    mut cx: TestAppContext,
+) -> Vec<Entity<Buffer>> {
+    let query = query.to_string();
+    let receiver = project.update(&mut cx, |project, cx| {
+        project.search(
+            SearchQuery::text(
+                query,
+                false,
+                true,
+                false,
+                files_to_include,
+                Default::default(),
+                match_full_paths,
+                None,
+            )
+            .unwrap(),
+            cx,
+        )
+    });
+
+    let mut buffers = Vec::new();
+    for expected_path in expected_paths {
+        let response = receiver.rx.recv().await.unwrap();
+        let SearchResult::Buffer { buffer, .. } = response else {
+            panic!("incorrect result");
+        };
+        buffer.update(&mut cx, |buffer, cx| {
+            assert_eq!(
+                buffer.file().unwrap().full_path(cx).to_string_lossy(),
+                *expected_path
+            )
+        });
+        buffers.push(buffer);
+    }
+
+    assert!(receiver.rx.recv().await.is_err());
+    buffers
+}
+
 #[gpui::test]
 async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let fs = FakeFs::new(server_cx.executor());
@@ -196,47 +241,31 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes
 
     cx.run_until_parked();
 
-    async fn do_search(project: &Entity<Project>, mut cx: TestAppContext) -> Entity<Buffer> {
-        let receiver = project.update(&mut cx, |project, cx| {
-            project.search(
-                SearchQuery::text(
-                    "project",
-                    false,
-                    true,
-                    false,
-                    Default::default(),
-                    Default::default(),
-                    false,
-                    None,
-                )
-                .unwrap(),
-                cx,
-            )
-        });
-
-        let first_response = receiver.rx.recv().await.unwrap();
-        let SearchResult::Buffer { buffer, .. } = first_response else {
-            panic!("incorrect result");
-        };
-        buffer.update(&mut cx, |buffer, cx| {
-            assert_eq!(
-                buffer.file().unwrap().full_path(cx).to_string_lossy(),
-                path!("project1/README.md")
-            )
-        });
-
-        assert!(receiver.rx.recv().await.is_err());
-        buffer
-    }
-
-    let buffer = do_search(&project, cx.clone()).await;
+    let buffers = do_search_and_assert(
+        &project,
+        "project",
+        Default::default(),
+        false,
+        &[path!("project1/README.md")],
+        cx.clone(),
+    )
+    .await;
+    let buffer = buffers.into_iter().next().unwrap();
 
     // test that the headless server is tracking which buffers we have open correctly.
     cx.run_until_parked();
     headless.update(server_cx, |headless, cx| {
         assert!(headless.buffer_store.read(cx).has_shared_buffers())
     });
-    do_search(&project, cx.clone()).await;
+    do_search_and_assert(
+        &project,
+        "project",
+        Default::default(),
+        false,
+        &[path!("project1/README.md")],
+        cx.clone(),
+    )
+    .await;
     server_cx.run_until_parked();
     cx.update(|_| {
         drop(buffer);
@@ -247,7 +276,80 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes
         assert!(!headless.buffer_store.read(cx).has_shared_buffers())
     });
 
-    do_search(&project, cx.clone()).await;
+    do_search_and_assert(
+        &project,
+        "project",
+        Default::default(),
+        false,
+        &[path!("project1/README.md")],
+        cx.clone(),
+    )
+    .await;
+}
+
+#[gpui::test]
+async fn test_remote_project_search_inclusion(
+    cx: &mut TestAppContext,
+    server_cx: &mut TestAppContext,
+) {
+    let fs = FakeFs::new(server_cx.executor());
+    fs.insert_tree(
+        path!("/code"),
+        json!({
+            "project1": {
+                "README.md": "# project 1",
+            },
+            "project2": {
+                "README.md": "# project 2",
+            },
+        }),
+    )
+    .await;
+
+    let (project, _) = init_test(&fs, cx, server_cx).await;
+
+    project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree(path!("/code/project1"), true, cx)
+        })
+        .await
+        .unwrap();
+
+    project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree(path!("/code/project2"), true, cx)
+        })
+        .await
+        .unwrap();
+
+    cx.run_until_parked();
+
+    // Case 1: Test search with path matcher limiting to only one worktree
+    let path_matcher = PathMatcher::new(
+        &["project1/*.md".to_owned()],
+        util::paths::PathStyle::local(),
+    )
+    .unwrap();
+    do_search_and_assert(
+        &project,
+        "project",
+        path_matcher,
+        true, // should be true in case of multiple worktrees
+        &[path!("project1/README.md")],
+        cx.clone(),
+    )
+    .await;
+
+    // Case 2: Test search without path matcher, matching both worktrees
+    do_search_and_assert(
+        &project,
+        "project",
+        Default::default(),
+        true, // should be true in case of multiple worktrees
+        &[path!("project1/README.md"), path!("project2/README.md")],
+        cx.clone(),
+    )
+    .await;
 }
 
 #[gpui::test]