From a827f25d00247d53f8a80bb4a080868b263f317f Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Tue, 4 Nov 2025 13:11:17 +0100 Subject: [PATCH] file_finder: Respect .gitignore and file_scan_inclusions with ** in glob (#40654) Closes #39037 Previously, the code split the `**/.env` glob in `file_scan_inclusions` into two sources for the `PathMatcher`: `["**", "**/.env"]`. This approach works for directories, but including `**` will match all directories and their files. To address this, I now select the appropriate `PathMatcher` using only `**/.env` when specifically targeting a file to determine whether to include it in the file finder. Release Notes: - Fixed: respect `.gitignore` and `file_scan_inclusions` settings with `**` in glob for file finder --------- Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> Co-authored-by: Julia Ryan --- crates/file_finder/src/file_finder_tests.rs | 142 ++++++++++++++++++++ crates/util/src/paths.rs | 15 +++ crates/worktree/src/worktree.rs | 17 ++- crates/worktree/src/worktree_settings.rs | 19 ++- crates/worktree/src/worktree_tests.rs | 1 + 5 files changed, 183 insertions(+), 11 deletions(-) diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 9670de072a5d7c10c2a82c2e384bd7bc4adcd848..16aa4f899607e902f4077753d350c7d950db4c3c 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -7,6 +7,7 @@ use menu::{Confirm, SelectNext, SelectPrevious}; use pretty_assertions::{assert_eq, assert_matches}; use project::{FS_WATCH_LATENCY, RemoveOptions}; use serde_json::json; +use settings::SettingsStore; use util::{path, rel_path::rel_path}; use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths}; @@ -658,6 +659,147 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_inclusions = Some(vec![ + "height_demo/**/hi_bonjour".to_string(), + "**/height_1".to_string(), + ]); + }); + }) + }); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height*", + "happiness": "", + "height": "", + "heights": { + "height_1": "", + "height_2": "", + }, + "height_demo": { + "test_1": { + "hi_bonjour": "hi_bonjour", + "hi": "hello", + }, + "hihi": "bye", + "test_2": { + "hoi": "nl" + } + }, + "height_include": { + "height_1_include": "", + "height_2_include": "", + }, + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + Path::new(path!("/ancestor/tracked-root")), + Path::new(path!("/ancestor/ignored-root")), + ], + cx, + ) + .await; + let (picker, _workspace, cx) = build_find_picker(project, cx); + + picker + .update_in(cx, |picker, window, cx| { + picker + .delegate + .spawn_search(test_path_position("hi"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![ + rel_path("ignored-root/hi").into(), + rel_path("tracked-root/hi").into(), + rel_path("ignored-root/hiccup").into(), + rel_path("tracked-root/hiccup").into(), + rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(), + rel_path("ignored-root/height").into(), + rel_path("tracked-root/heights/height_1").into(), + rel_path("ignored-root/happiness").into(), + rel_path("tracked-root/happiness").into(), + ], + "All ignored files that were indexed are found for default ignored mode" + ); + }); +} + +#[gpui::test] +async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]); + }); + }) + }); + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + ".gitignore": "node_modules", + "node_modules": { + "package.json": "// package.json", + ".env": "BAR=FOO" + }, + ".env": "FOO=BAR" + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await; + let (picker, _workspace, cx) = build_find_picker(project, cx); + + picker + .update_in(cx, |picker, window, cx| { + picker + .delegate + .spawn_search(test_path_position("json"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![], + "All ignored files that were indexed are found for default ignored mode" + ); + }); +} + #[gpui::test] async fn test_ignored_root(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index d8c84c4938a14386863cce3fd920ba61d1bb4644..015711fa2a17a12f7e47e37c5bee3a6941a29691 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1573,6 +1573,21 @@ mod tests { ); } + #[perf] + fn file_in_dirs() { + let path = Path::new("/work/.env"); + let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap(); + assert!( + path_matcher.is_match(path), + "Path matcher should match {path:?}" + ); + let path = Path::new("/work/package.json"); + assert!( + !path_matcher.is_match(path), + "Path matcher should not match {path:?}" + ); + } + #[perf] fn project_search() { let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a4d3f61141c8b05a7ff2ccf2ef0df5896833f199..bd2437ff56366bad041097ff797f6d05046dafc0 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -505,7 +505,7 @@ impl Worktree { project_id, replica_id, snapshot, - file_scan_inclusions: settings.file_scan_inclusions.clone(), + file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(background_updates_tx), update_observer: None, @@ -520,8 +520,10 @@ impl Worktree { while let Some(update) = background_updates_rx.next().await { { let mut lock = background_snapshot.lock(); - lock.0 - .apply_remote_update(update.clone(), &settings.file_scan_inclusions); + lock.0.apply_remote_update( + update.clone(), + &settings.parent_dir_scan_inclusions, + ); lock.1.push(update); } snapshot_updated_tx.send(()).await.ok(); @@ -4290,7 +4292,8 @@ impl BackgroundScanner { if child_entry.is_dir() { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); - child_entry.is_always_included = self.settings.is_path_always_included(&child_path); + child_entry.is_always_included = + self.settings.is_path_always_included(&child_path, true); // Avoid recursing until crash in the case of a recursive symlink if job.ancestor_inodes.contains(&child_entry.inode) { @@ -4315,7 +4318,8 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - child_entry.is_always_included = self.settings.is_path_always_included(&child_path); + child_entry.is_always_included = + self.settings.is_path_always_included(&child_path, false); } { @@ -4450,7 +4454,8 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir); fs_entry.is_external = is_external; fs_entry.is_private = self.is_path_private(path); - fs_entry.is_always_included = self.settings.is_path_always_included(path); + fs_entry.is_always_included = + self.settings.is_path_always_included(path, is_dir); let parent_is_hidden = path .parent() diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index e536256e51401e5cb4d9cbece0e5a52b3ff22b3c..9fb88af9d9cd83c6de7afe1962b2640ae8e81515 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -13,8 +13,11 @@ pub struct WorktreeSettings { pub project_name: Option, /// Whether to prevent this project from being shared in public channels. pub prevent_sharing_in_public_channels: bool, - pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, + pub file_scan_inclusions: PathMatcher, + /// This field contains all ancestors of the `file_scan_inclusions`. It's used to + /// determine whether to terminate worktree scanning for a given dir. + pub parent_dir_scan_inclusions: PathMatcher, pub private_files: PathMatcher, } @@ -29,9 +32,12 @@ impl WorktreeSettings { .any(|ancestor| self.file_scan_exclusions.is_match(ancestor.as_std_path())) } - pub fn is_path_always_included(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.file_scan_inclusions.is_match(ancestor.as_std_path())) + pub fn is_path_always_included(&self, path: &RelPath, is_dir: bool) -> bool { + if is_dir { + self.parent_dir_scan_inclusions.is_match(path.as_std_path()) + } else { + self.file_scan_inclusions.is_match(path.as_std_path()) + } } } @@ -46,6 +52,7 @@ impl Settings for WorktreeSettings { .flat_map(|glob| { Path::new(glob) .ancestors() + .skip(1) .map(|a| a.to_string_lossy().into()) }) .filter(|p: &String| !p.is_empty()) @@ -57,11 +64,13 @@ impl Settings for WorktreeSettings { file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions") .log_err() .unwrap_or_default(), - file_scan_inclusions: path_matchers( + parent_dir_scan_inclusions: path_matchers( parsed_file_scan_inclusions, "file_scan_inclusions", ) .unwrap(), + file_scan_inclusions: path_matchers(file_scan_inclusions, "file_scan_inclusions") + .unwrap(), private_files: path_matchers(private_files, "private_files") .log_err() .unwrap_or_default(), diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d89e1ef4e4df7dbef3cf51789c1f1fc8a5309eb1..a3dab96cc69f553bcf01e4d8357d68cde2f05fce 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -760,6 +760,7 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) { "prettier": { "package.json": "{}", }, + "package.json": "//package.json" }, "src": { ".DS_Store": "",