@@ -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::<SettingsStore, _>(|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::<SettingsStore, _>(|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);
@@ -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()
@@ -13,8 +13,11 @@ pub struct WorktreeSettings {
pub project_name: Option<String>,
/// 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(),