@@ -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,
- },
- }
- }
}
@@ -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 {
@@ -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());