From a608ee66d3842cdc2df44ab8e31b387715cea5bd Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 21 Jan 2026 16:16:03 +0530 Subject: [PATCH] remote_server: Fix remote project search include/exclude filters for multiple worktrees (#47280) Closes #45772 Release Notes: - Fixed project search include/exclude filters not working correctly in remote project with multiple folders. --- crates/project/src/search.rs | 4 +- .../remote_server/src/remote_editing_tests.rs | 176 ++++++++++++++---- 2 files changed, 141 insertions(+), 39 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 159328ac05d4d78c12453968e98ca77d9395cb06..5710de36dce6b4cbbd8d38b3e6c3b102fe5df6c0 100644 --- a/crates/project/src/search.rs +++ b/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 ) } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index abefc873b720668c237bcd436759e235107d24c5..084d92c43188ecb66852b497d29afeb87507a04f 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/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, + query: &str, + files_to_include: PathMatcher, + match_full_paths: bool, + expected_paths: &[&str], + mut cx: TestAppContext, +) -> Vec> { + 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, mut cx: TestAppContext) -> Entity { - 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]