diff --git a/crates/project/src/ignore.rs b/crates/project/src/ignore.rs index 8bac08b96c..4f28160e3a 100644 --- a/crates/project/src/ignore.rs +++ b/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, - }, - } - } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 607b284813..b2bafe228e 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -10,6 +10,12 @@ pub struct ProjectSettings { pub lsp: HashMap, 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, + #[serde(default)] + pub scan_include_files: Vec, } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d59885225a..d1633b828d 100644 --- a/crates/project/src/worktree.rs +++ b/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, + scan_exclude_files: Vec, + scan_include_files: Vec, } 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::(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::>(); + 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::>(); 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 = 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 { diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 4253f45b0c..fff23a36b4 100644 --- a/crates/project/src/worktree_tests.rs +++ b/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::(|store, cx| { + store.update_user_settings::(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());