Recompute ignore status when .gitignore changes or for new entries

Antonio Scandurra created

Change summary

zed/src/worktree.rs       | 202 +++++++++++++++++++++++++++++++++++++---
zed/src/worktree/fuzzy.rs |   8 +
2 files changed, 189 insertions(+), 21 deletions(-)

Detailed changes

zed/src/worktree.rs 🔗

@@ -17,11 +17,12 @@ use postage::{
 };
 use smol::{channel::Sender, Timer};
 use std::{
-    collections::{HashMap, HashSet},
+    collections::{BTreeMap, HashMap, HashSet},
     ffi::OsStr,
     fmt, fs,
     future::Future,
     io::{self, Read, Write},
+    mem,
     ops::{AddAssign, Deref},
     os::unix::fs::MetadataExt,
     path::{Path, PathBuf},
@@ -30,6 +31,7 @@ use std::{
 };
 
 const GITIGNORE: &'static str = ".gitignore";
+const EDIT_BATCH_LEN: usize = 2000;
 
 #[derive(Clone, Debug)]
 enum ScanState {
@@ -58,6 +60,7 @@ impl Worktree {
         let id = ctx.model_id();
         let snapshot = Snapshot {
             id,
+            scan_id: 0,
             path: path.into(),
             root_inode: None,
             ignores: Default::default(),
@@ -230,9 +233,10 @@ impl fmt::Debug for Worktree {
 #[derive(Clone)]
 pub struct Snapshot {
     id: usize,
+    scan_id: usize,
     path: Arc<Path>,
     root_inode: Option<u64>,
-    ignores: HashMap<u64, Gitignore>,
+    ignores: HashMap<u64, (Arc<Gitignore>, usize)>,
     entries: SumTree<Entry>,
 }
 
@@ -272,15 +276,7 @@ impl Snapshot {
         })
     }
 
-    pub fn is_path_ignored(&self, path: impl AsRef<Path>) -> Result<bool> {
-        if let Some(inode) = self.inode_for_path(path.as_ref()) {
-            self.is_inode_ignored(inode)
-        } else {
-            Ok(false)
-        }
-    }
-
-    pub fn is_inode_ignored(&self, mut inode: u64) -> Result<bool> {
+    fn is_inode_ignored(&self, mut inode: u64) -> Result<bool> {
         let mut components = Vec::new();
         let mut relative_path = PathBuf::new();
         let mut entry = self
@@ -297,7 +293,7 @@ impl Snapshot {
                 components.push(child_name.as_ref());
                 inode = parent;
 
-                if let Some(ignore) = self.ignores.get(&inode) {
+                if let Some((ignore, _)) = self.ignores.get(&inode) {
                     relative_path.clear();
                     relative_path.extend(components.iter().rev());
                     match ignore.matched_path_or_any_parents(&relative_path, entry.is_dir()) {
@@ -506,7 +502,8 @@ impl Snapshot {
             log::info!("error in ignore file {:?} - {:?}", path, err);
         }
 
-        self.ignores.insert(dir_inode, ignore);
+        self.ignores
+            .insert(dir_inode, (Arc::new(ignore), self.scan_id));
     }
 
     fn remove_ignore_file(&mut self, dir_inode: u64) {
@@ -585,16 +582,37 @@ pub enum Entry {
         is_symlink: bool,
         children: Arc<[(u64, Arc<OsStr>)]>,
         pending: bool,
+        is_ignored: Option<bool>,
     },
     File {
         inode: u64,
         parent: Option<u64>,
         is_symlink: bool,
         path: PathEntry,
+        is_ignored: Option<bool>,
     },
 }
 
 impl Entry {
+    fn is_ignored(&self) -> Option<bool> {
+        match self {
+            Entry::Dir { is_ignored, .. } => *is_ignored,
+            Entry::File { is_ignored, .. } => *is_ignored,
+        }
+    }
+
+    fn set_ignored(&mut self, ignored: bool) {
+        match self {
+            Entry::Dir { is_ignored, .. } => *is_ignored = Some(ignored),
+            Entry::File {
+                is_ignored, path, ..
+            } => {
+                *is_ignored = Some(ignored);
+                path.is_ignored = Some(ignored);
+            }
+        }
+    }
+
     fn inode(&self) -> u64 {
         match self {
             Entry::Dir { inode, .. } => *inode,
@@ -625,6 +643,7 @@ impl sum_tree::Item for Entry {
             } else {
                 0
             },
+            recompute_is_ignored: self.is_ignored().is_none(),
         }
     }
 }
@@ -641,12 +660,14 @@ impl sum_tree::KeyedItem for Entry {
 pub struct EntrySummary {
     max_ino: u64,
     file_count: usize,
+    recompute_is_ignored: bool,
 }
 
 impl<'a> AddAssign<&'a EntrySummary> for EntrySummary {
     fn add_assign(&mut self, rhs: &'a EntrySummary) {
         self.max_ino = rhs.max_ino;
         self.file_count += rhs.file_count;
+        self.recompute_is_ignored |= rhs.recompute_is_ignored;
     }
 }
 
@@ -721,6 +742,8 @@ impl BackgroundScanner {
     }
 
     fn scan_dirs(&self) -> io::Result<()> {
+        self.snapshot.lock().scan_id += 1;
+
         let path = self.path();
         let metadata = fs::metadata(&path)?;
         let inode = metadata.ino();
@@ -735,6 +758,7 @@ impl BackgroundScanner {
                 is_symlink,
                 children: Arc::from([]),
                 pending: true,
+                is_ignored: None,
             };
 
             {
@@ -776,14 +800,17 @@ impl BackgroundScanner {
                 None,
                 Entry::File {
                     parent: None,
-                    path: PathEntry::new(inode, &relative_path),
+                    path: PathEntry::new(inode, &relative_path, None),
                     inode,
                     is_symlink,
+                    is_ignored: None,
                 },
             );
             snapshot.root_inode = Some(inode);
         }
 
+        self.recompute_ignore_statuses();
+
         Ok(())
     }
 
@@ -811,6 +838,7 @@ impl BackgroundScanner {
                         is_symlink: child_is_symlink,
                         children: Arc::from([]),
                         pending: true,
+                        is_ignored: None,
                     },
                 ));
                 new_jobs.push(ScanJob {
@@ -824,9 +852,10 @@ impl BackgroundScanner {
                     child_name,
                     Entry::File {
                         parent: Some(job.inode),
-                        path: PathEntry::new(child_inode, &child_relative_path),
+                        path: PathEntry::new(child_inode, &child_relative_path, None),
                         inode: child_inode,
                         is_symlink: child_is_symlink,
+                        is_ignored: None,
                     },
                 ));
             };
@@ -842,6 +871,8 @@ impl BackgroundScanner {
 
     fn process_events(&self, mut events: Vec<fsevent::Event>) -> bool {
         let mut snapshot = self.snapshot();
+        snapshot.scan_id += 1;
+
         let root_path = if let Ok(path) = snapshot.path.canonicalize() {
             path
         } else {
@@ -910,9 +941,133 @@ impl BackgroundScanner {
             }
         });
 
+        self.recompute_ignore_statuses();
+
         true
     }
 
+    fn recompute_ignore_statuses(&self) {
+        self.compute_ignore_status_for_new_ignores();
+        self.compute_ignore_status_for_new_entries();
+    }
+
+    fn compute_ignore_status_for_new_ignores(&self) {
+        struct IgnoreJob {
+            inode: u64,
+            ignore_queue_tx: crossbeam_channel::Sender<IgnoreJob>,
+        }
+
+        let snapshot = self.snapshot.lock().clone();
+
+        let mut new_ignore_parents = BTreeMap::new();
+        for (parent_inode, (_, scan_id)) in &snapshot.ignores {
+            if *scan_id == snapshot.scan_id {
+                let parent_path = snapshot.path_for_inode(*parent_inode, false).unwrap();
+                new_ignore_parents.insert(parent_path, *parent_inode);
+            }
+        }
+        let mut new_ignores = new_ignore_parents.into_iter().peekable();
+
+        let (ignore_queue_tx, ignore_queue_rx) = crossbeam_channel::unbounded();
+        while let Some((parent_path, parent_inode)) = new_ignores.next() {
+            while new_ignores
+                .peek()
+                .map_or(false, |(p, _)| p.starts_with(&parent_path))
+            {
+                new_ignores.next().unwrap();
+            }
+
+            ignore_queue_tx
+                .send(IgnoreJob {
+                    inode: parent_inode,
+                    ignore_queue_tx: ignore_queue_tx.clone(),
+                })
+                .unwrap();
+        }
+        drop(ignore_queue_tx);
+
+        self.thread_pool.scoped(|scope| {
+            let (edits_tx, edits_rx) = crossbeam_channel::unbounded();
+
+            scope.execute(move || {
+                let mut edits = Vec::new();
+                while let Ok(edit) = edits_rx.recv() {
+                    edits.push(edit);
+                    while let Ok(edit) = edits_rx.try_recv() {
+                        edits.push(edit);
+                        if edits.len() == EDIT_BATCH_LEN {
+                            break;
+                        }
+                    }
+                    self.snapshot.lock().entries.edit(mem::take(&mut edits));
+                }
+            });
+
+            for _ in 0..self.thread_pool.thread_count() - 1 {
+                let edits_tx = edits_tx.clone();
+                scope.execute(|| {
+                    let edits_tx = edits_tx;
+                    while let Ok(job) = ignore_queue_rx.recv() {
+                        let mut entry = snapshot.entries.get(&job.inode).unwrap().clone();
+                        entry.set_ignored(snapshot.is_inode_ignored(job.inode).unwrap());
+                        if let Entry::Dir { children, .. } = &entry {
+                            for (child_inode, _) in children.as_ref() {
+                                job.ignore_queue_tx
+                                    .send(IgnoreJob {
+                                        inode: *child_inode,
+                                        ignore_queue_tx: job.ignore_queue_tx.clone(),
+                                    })
+                                    .unwrap();
+                            }
+                        }
+
+                        edits_tx.send(Edit::Insert(entry)).unwrap();
+                    }
+                });
+            }
+        });
+    }
+
+    fn compute_ignore_status_for_new_entries(&self) {
+        let snapshot = self.snapshot.lock().clone();
+
+        let (entries_tx, entries_rx) = crossbeam_channel::unbounded();
+        self.thread_pool.scoped(|scope| {
+            let (edits_tx, edits_rx) = crossbeam_channel::unbounded();
+            scope.execute(move || {
+                let mut edits = Vec::new();
+                while let Ok(edit) = edits_rx.recv() {
+                    edits.push(edit);
+                    while let Ok(edit) = edits_rx.try_recv() {
+                        edits.push(edit);
+                        if edits.len() == EDIT_BATCH_LEN {
+                            break;
+                        }
+                    }
+                    self.snapshot.lock().entries.edit(mem::take(&mut edits));
+                }
+            });
+
+            scope.execute(|| {
+                let entries_tx = entries_tx;
+                for entry in snapshot.entries.filter::<_, ()>(|e| e.recompute_is_ignored) {
+                    entries_tx.send(entry.clone()).unwrap();
+                }
+            });
+
+            for _ in 0..self.thread_pool.thread_count() - 2 {
+                let edits_tx = edits_tx.clone();
+                scope.execute(|| {
+                    let edits_tx = edits_tx;
+                    while let Ok(mut entry) = entries_rx.recv() {
+                        entry.set_ignored(snapshot.is_inode_ignored(entry.inode()).unwrap());
+                        edits_tx.send(Edit::Insert(entry)).unwrap();
+                    }
+                });
+            }
+        });
+    }
+
     fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result<Option<Entry>> {
         let metadata = match fs::metadata(&path) {
             Err(err) => {
@@ -947,6 +1102,7 @@ impl BackgroundScanner {
                 is_symlink,
                 pending: true,
                 children: Arc::from([]),
+                is_ignored: None,
             }
         } else {
             Entry::File {
@@ -958,7 +1114,9 @@ impl BackgroundScanner {
                     root_path
                         .parent()
                         .map_or(path, |parent| path.strip_prefix(parent).unwrap()),
+                    None,
                 ),
+                is_ignored: None,
             }
         };
 
@@ -1143,8 +1301,10 @@ mod tests {
             app.read(|ctx| tree.read(ctx).scan_complete()).await;
             app.read(|ctx| {
                 let tree = tree.read(ctx);
-                assert!(!tree.is_path_ignored("tracked-dir/tracked-file1").unwrap());
-                assert!(tree.is_path_ignored("ignored-dir/ignored-file1").unwrap());
+                let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
+                let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
+                assert_eq!(tracked.is_ignored(), Some(false));
+                assert_eq!(ignored.is_ignored(), Some(true));
             });
 
             fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
@@ -1152,8 +1312,10 @@ mod tests {
             app.read(|ctx| tree.read(ctx).next_scan_complete()).await;
             app.read(|ctx| {
                 let tree = tree.read(ctx);
-                assert!(!tree.is_path_ignored("tracked-dir/tracked-file2").unwrap());
-                assert!(tree.is_path_ignored("ignored-dir/ignored-file2").unwrap());
+                let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
+                let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
+                assert_eq!(tracked.is_ignored(), Some(false));
+                assert_eq!(ignored.is_ignored(), Some(true));
             });
         });
     }
@@ -1189,6 +1351,7 @@ mod tests {
             let scanner = BackgroundScanner::new(
                 Arc::new(Mutex::new(Snapshot {
                     id: 0,
+                    scan_id: 0,
                     path: root_dir.path().into(),
                     root_inode: None,
                     entries: Default::default(),
@@ -1219,6 +1382,7 @@ mod tests {
             let new_scanner = BackgroundScanner::new(
                 Arc::new(Mutex::new(Snapshot {
                     id: 0,
+                    scan_id: 0,
                     path: root_dir.path().into(),
                     root_inode: None,
                     entries: Default::default(),

zed/src/worktree/fuzzy.rs 🔗

@@ -21,10 +21,11 @@ pub struct PathEntry {
     pub path_chars: CharBag,
     pub path: Arc<[char]>,
     pub lowercase_path: Arc<[char]>,
+    pub is_ignored: Option<bool>,
 }
 
 impl PathEntry {
-    pub fn new(ino: u64, path: &Path) -> Self {
+    pub fn new(ino: u64, path: &Path, is_ignored: Option<bool>) -> Self {
         let path = path.to_string_lossy();
         let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>().into();
         let path: Arc<[char]> = path.chars().collect::<Vec<_>>().into();
@@ -35,6 +36,7 @@ impl PathEntry {
             path_chars,
             path,
             lowercase_path,
+            is_ignored,
         }
     }
 }
@@ -196,7 +198,7 @@ fn match_single_tree_paths<'a>(
             continue;
         }
 
-        if !include_ignored && snapshot.is_inode_ignored(path_entry.ino).unwrap_or(true) {
+        if !include_ignored && path_entry.is_ignored.unwrap_or(false) {
             continue;
         }
 
@@ -500,6 +502,7 @@ mod tests {
                 path_chars,
                 path,
                 lowercase_path,
+                is_ignored: Some(false),
             });
         }
 
@@ -512,6 +515,7 @@ mod tests {
         match_single_tree_paths(
             &Snapshot {
                 id: 0,
+                scan_id: 0,
                 path: PathBuf::new().into(),
                 root_inode: None,
                 ignores: Default::default(),