Test and filter data draft

Kirill Bulatov created

Change summary

crates/project/src/ignore.rs           |  22 -----
crates/project/src/project_settings.rs |   6 +
crates/project/src/worktree.rs         | 105 +++++++++++++++++++++++++--
crates/project/src/worktree_tests.rs   |  85 ++++++++++++++++++++++
4 files changed, 186 insertions(+), 32 deletions(-)

Detailed changes

crates/project/src/ignore.rs 🔗

@@ -1,5 +1,5 @@
 use ignore::gitignore::Gitignore;
-use std::{ffi::OsStr, path::Path, sync::Arc};
+use std::{path::Path, sync::Arc};
 
 pub enum IgnoreStack {
     None,
@@ -34,24 +34,4 @@ impl IgnoreStack {
             }),
         }
     }
-
-    pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
-        if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
-            return true;
-        }
-
-        match self {
-            Self::None => false,
-            Self::All => true,
-            Self::Some {
-                abs_base_path,
-                ignore,
-                parent: prev,
-            } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
-                ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
-                ignore::Match::Ignore(_) => true,
-                ignore::Match::Whitelist(_) => false,
-            },
-        }
-    }
 }

crates/project/src/project_settings.rs 🔗

@@ -10,6 +10,12 @@ pub struct ProjectSettings {
     pub lsp: HashMap<Arc<str>, LspSettings>,
     #[serde(default)]
     pub git: GitSettings,
+    // TODO kb better names and docs
+    // TODO kb how to react on their changes?
+    #[serde(default)]
+    pub scan_exclude_files: Vec<String>,
+    #[serde(default)]
+    pub scan_include_files: Vec<String>,
 }
 
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

crates/project/src/worktree.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
-    copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+    copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
+    ProjectEntryId, RemoveOptions,
 };
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
@@ -55,7 +56,10 @@ use std::{
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::{paths::HOME, ResultExt};
+use util::{
+    paths::{PathMatcher, HOME},
+    ResultExt,
+};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
 pub struct WorktreeId(usize);
@@ -216,6 +220,8 @@ pub struct LocalSnapshot {
     /// All of the git repositories in the worktree, indexed by the project entry
     /// id of their parent directory.
     git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+    scan_exclude_files: Vec<PathMatcher>,
+    scan_include_files: Vec<PathMatcher>,
 }
 
 struct BackgroundScannerState {
@@ -303,8 +309,34 @@ impl Worktree {
             let root_name = abs_path
                 .file_name()
                 .map_or(String::new(), |f| f.to_string_lossy().to_string());
-
+            let project_settings = settings::get::<ProjectSettings>(cx);
+            let scan_exclude_files = project_settings.scan_exclude_files.iter()
+            .filter_map(|pattern| {
+                PathMatcher::new(pattern)
+                    .map(Some)
+                    .unwrap_or_else(|e| {
+                        log::error!(
+                            "Skipping pattern {pattern} in `scan_exclude_files` project settings due to parsing error: {e:#}"
+                        );
+                        None
+                    })
+            })
+            .collect::<Vec<_>>();
+            let scan_include_files = project_settings.scan_include_files.iter()
+            .filter_map(|pattern| {
+                PathMatcher::new(pattern)
+                    .map(Some)
+                    .unwrap_or_else(|e| {
+                        log::error!(
+                            "Skipping pattern {pattern} in `scan_include_files` project settings due to parsing error: {e:#}"
+                        );
+                        None
+                    })
+            })
+            .collect::<Vec<_>>();
             let mut snapshot = LocalSnapshot {
+                scan_include_files,
+                scan_exclude_files,
                 ignores_by_parent_abs_path: Default::default(),
                 git_repositories: Default::default(),
                 snapshot: Snapshot {
@@ -2042,7 +2074,7 @@ impl LocalSnapshot {
 
         let mut ignore_stack = IgnoreStack::none();
         for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
-            if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
+            if self.is_abs_path_ignored(parent_abs_path, &ignore_stack, true) {
                 ignore_stack = IgnoreStack::all();
                 break;
             } else if let Some(ignore) = ignore {
@@ -2050,7 +2082,7 @@ impl LocalSnapshot {
             }
         }
 
-        if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
+        if self.is_abs_path_ignored(abs_path, &ignore_stack, is_dir) {
             ignore_stack = IgnoreStack::all();
         }
         ignore_stack
@@ -2145,6 +2177,45 @@ impl LocalSnapshot {
         paths.sort_by(|a, b| a.0.cmp(b.0));
         paths
     }
+
+    fn is_abs_path_ignored(
+        &self,
+        abs_path: &Path,
+        ignore_stack: &IgnoreStack,
+        is_dir: bool,
+    ) -> bool {
+        dbg!(&abs_path);
+        if self
+            .scan_include_files
+            .iter()
+            .any(|include_matcher| include_matcher.is_match(abs_path))
+        {
+            dbg!("included!!");
+            return false;
+        } else if self
+            .scan_exclude_files
+            .iter()
+            .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+        {
+            dbg!("excluded!!");
+            return true;
+        } else if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
+            return true;
+        }
+        match ignore_stack {
+            IgnoreStack::None => false,
+            IgnoreStack::All => true,
+            IgnoreStack::Some {
+                abs_base_path,
+                ignore,
+                parent: prev,
+            } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
+                ignore::Match::None => self.is_abs_path_ignored(abs_path, &prev, is_dir),
+                ignore::Match::Ignore(_) => true,
+                ignore::Match::Whitelist(_) => false,
+            },
+        }
+    }
 }
 
 impl BackgroundScannerState {
@@ -2767,7 +2838,7 @@ pub struct Entry {
     pub mtime: SystemTime,
     pub is_symlink: bool,
 
-    /// Whether this entry is ignored by Git.
+    /// Whether this entry is ignored by Zed.
     ///
     /// We only scan ignored entries once the directory is expanded and
     /// exclude them from searches.
@@ -3464,7 +3535,7 @@ impl BackgroundScanner {
                 for entry in &mut new_entries {
                     let entry_abs_path = root_abs_path.join(&entry.path);
                     entry.is_ignored =
-                        ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
+                        self.is_abs_path_ignored(&entry_abs_path, &ignore_stack, entry.is_dir());
 
                     if entry.is_dir() {
                         if let Some(job) = new_jobs.next().expect("missing scan job for entry") {
@@ -3523,7 +3594,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_ignored =
+                    self.is_abs_path_ignored(&child_abs_path, &ignore_stack, true);
 
                 // Avoid recursing until crash in the case of a recursive symlink
                 if !job.ancestor_inodes.contains(&child_entry.inode) {
@@ -3547,7 +3619,8 @@ impl BackgroundScanner {
                     new_jobs.push(None);
                 }
             } else {
-                child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
+                child_entry.is_ignored =
+                    self.is_abs_path_ignored(&child_abs_path, &ignore_stack, false);
                 if !child_entry.is_ignored {
                     if let Some((repository_dir, repository, staged_statuses)) =
                         &job.containing_repository
@@ -3825,7 +3898,7 @@ impl BackgroundScanner {
         for mut entry in snapshot.child_entries(path).cloned() {
             let was_ignored = entry.is_ignored;
             let abs_path: Arc<Path> = snapshot.abs_path().join(&entry.path).into();
-            entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
+            entry.is_ignored = self.is_abs_path_ignored(&abs_path, &ignore_stack, entry.is_dir());
             if entry.is_dir() {
                 let child_ignore_stack = if entry.is_ignored {
                     IgnoreStack::all()
@@ -4008,6 +4081,18 @@ impl BackgroundScanner {
 
         smol::Timer::after(Duration::from_millis(100)).await;
     }
+
+    fn is_abs_path_ignored(
+        &self,
+        abs_path: &Path,
+        ignore_stack: &IgnoreStack,
+        is_dir: bool,
+    ) -> bool {
+        self.state
+            .lock()
+            .snapshot
+            .is_abs_path_ignored(abs_path, ignore_stack, is_dir)
+    }
 }
 
 fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {

crates/project/src/worktree_tests.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
+    project_settings::ProjectSettings,
     worktree::{Event, Snapshot, WorktreeModelHandle},
-    Entry, EntryKind, PathChange, Worktree,
+    Entry, EntryKind, PathChange, Project, Worktree,
 };
 use anyhow::Result;
 use client::Client;
@@ -12,6 +13,7 @@ use postage::stream::Stream;
 use pretty_assertions::assert_eq;
 use rand::prelude::*;
 use serde_json::json;
+use settings::SettingsStore;
 use std::{
     env,
     fmt::Write,
@@ -877,6 +879,87 @@ async fn test_write_file(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_ignore_inclusions_and_exclusions(cx: &mut TestAppContext) {
+    let dir = temp_tree(json!({
+        ".git": {},
+        ".gitignore": "**/target\n/node_modules\n",
+        "target": {},
+        "node_modules": {
+            ".DS_Store": "",
+            "prettier": {
+                "package.json": "{}",
+            },
+        },
+        "src": {
+            ".DS_Store": "",
+            "foo": {
+                "foo.rs": "mod another;\n",
+                "another.rs": "// another",
+            },
+            "bar": {
+                "bar.rs": "// bar",
+            },
+            "lib.rs": "mod foo;\nmod bar;\n",
+        },
+        ".DS_Store": "",
+    }));
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        Project::init_settings(cx);
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                project_settings.scan_exclude_files =
+                    vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()];
+                project_settings.scan_include_files = vec!["**/node_modules".to_string()];
+            });
+        });
+    });
+
+    let tree = Worktree::local(
+        build_client(cx),
+        dir.path(),
+        true,
+        Arc::new(RealFs),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    tree.flush_fs_events(cx).await;
+
+    // tree.update(cx, |tree, cx| {
+    //     tree.as_local().unwrap().write_file(
+    //         Path::new("tracked-dir/file.txt"),
+    //         "hello".into(),
+    //         Default::default(),
+    //         cx,
+    //     )
+    // })
+    // .await
+    // .unwrap();
+    // tree.update(cx, |tree, cx| {
+    //     tree.as_local().unwrap().write_file(
+    //         Path::new("ignored-dir/file.txt"),
+    //         "world".into(),
+    //         Default::default(),
+    //         cx,
+    //     )
+    // })
+    // .await
+    // .unwrap();
+
+    // tree.read_with(cx, |tree, _| {
+    //     let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
+    //     let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+    //     assert!(!tracked.is_ignored);
+    //     assert!(ignored.is_ignored);
+    // });
+    dbg!("!!!!!!!!!!!!");
+}
+
 #[gpui::test(iterations = 30)]
 async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
     let fs = FakeFs::new(cx.background());