From ca225d0765f4fdedee5a157543058edeac3951f3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 08:50:21 +0200 Subject: [PATCH 1/6] Make `build_gitignore` async --- crates/project/src/worktree.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0735d3e1febb4329a8988fe2a247f666f7b0ccb4..d48df1b53fd982c0334b50036a64d455404897ef 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1331,7 +1331,7 @@ impl LocalSnapshot { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); - match build_gitignore(&abs_path, fs) { + match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { let ignore_dir_path = entry.path.parent().unwrap(); self.ignores @@ -1506,8 +1506,8 @@ impl LocalSnapshot { } } -fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { - let contents = smol::block_on(fs.load(&abs_path))?; +async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { + let contents = fs.load(&abs_path).await?; let parent = abs_path.parent().unwrap_or(Path::new("/")); let mut builder = GitignoreBuilder::new(parent); for line in contents.lines() { @@ -2117,7 +2117,7 @@ impl BackgroundScanner { // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored if child_name == *GITIGNORE { - match build_gitignore(&child_abs_path, self.fs.as_ref()) { + match build_gitignore(&child_abs_path, self.fs.as_ref()).await { Ok(ignore) => { let ignore = Arc::new(ignore); ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); From 9328ab121a398711e2b590f4c6d5ec74fc43f1f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 10:56:58 +0200 Subject: [PATCH 2/6] Use absolute paths to compute ignored status This lays the groundwork for harvesting gitignores up above the worktree. --- crates/project/src/ignore.rs | 16 ++--- crates/project/src/worktree.rs | 125 +++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 54 deletions(-) diff --git a/crates/project/src/ignore.rs b/crates/project/src/ignore.rs index 9eb605eaf87b062d43b2de7b5027aba747bbc51b..8bac08b96c3a7b920328d946723ae423404b529e 100644 --- a/crates/project/src/ignore.rs +++ b/crates/project/src/ignore.rs @@ -4,7 +4,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; pub enum IgnoreStack { None, Some { - base: Arc, + abs_base_path: Arc, ignore: Arc, parent: Arc, }, @@ -24,19 +24,19 @@ impl IgnoreStack { matches!(self, IgnoreStack::All) } - pub fn append(self: Arc, base: Arc, ignore: Arc) -> Arc { + pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { match self.as_ref() { IgnoreStack::All => self, _ => Arc::new(Self::Some { - base, + abs_base_path, ignore, parent: self, }), } } - pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool { - if is_dir && path.file_name() == Some(OsStr::new(".git")) { + 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; } @@ -44,11 +44,11 @@ impl IgnoreStack { Self::None => false, Self::All => true, Self::Some { - base, + abs_base_path, ignore, parent: prev, - } => match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) { - ignore::Match::None => prev.is_path_ignored(path, is_dir), + } => 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/worktree.rs b/crates/project/src/worktree.rs index d48df1b53fd982c0334b50036a64d455404897ef..c81d9d5637e4d6372bf3be83daa185f0dd48c10e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -102,7 +102,7 @@ pub struct Snapshot { #[derive(Clone)] pub struct LocalSnapshot { abs_path: Arc, - ignores: HashMap, (Arc, usize)>, + ignores_by_abs_path: HashMap, (Arc, usize)>, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, @@ -370,7 +370,7 @@ impl LocalWorktree { let tree = cx.add_model(move |cx: &mut ModelContext| { let mut snapshot = LocalSnapshot { abs_path, - ignores: Default::default(), + ignores_by_abs_path: Default::default(), removed_entry_ids: Default::default(), next_entry_id, snapshot: Snapshot { @@ -819,8 +819,8 @@ impl LocalWorktree { { let mut snapshot = this.background_snapshot.lock(); entry.is_ignored = snapshot - .ignore_stack_for_path(&path, entry.is_dir()) - .is_path_ignored(&path, entry.is_dir()); + .ignore_stack_for_abs_path(&abs_path, entry.is_dir()) + .is_abs_path_ignored(&abs_path, entry.is_dir()); if let Some(old_path) = old_path { snapshot.remove_path(&old_path); } @@ -1333,9 +1333,10 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { - let ignore_dir_path = entry.path.parent().unwrap(); - self.ignores - .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); + self.ignores_by_abs_path.insert( + abs_path.parent().unwrap().into(), + (Arc::new(ignore), self.scan_id), + ); } Err(error) => { log::error!( @@ -1387,7 +1388,10 @@ impl LocalSnapshot { }; if let Some(ignore) = ignore { - self.ignores.insert(parent_path, (ignore, self.scan_id)); + self.ignores_by_abs_path.insert( + self.abs_path.join(&parent_path).into(), + (ignore, self.scan_id), + ); } if matches!(parent_entry.kind, EntryKind::PendingDir) { parent_entry.kind = EntryKind::Dir; @@ -1472,16 +1476,18 @@ impl LocalSnapshot { self.entries_by_id.edit(entries_by_id_edits, &()); if path.file_name() == Some(&GITIGNORE) { - if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { + let abs_parent_path = self.abs_path.join(path.parent().unwrap()); + if let Some((_, scan_id)) = self.ignores_by_abs_path.get_mut(abs_parent_path.as_path()) + { *scan_id = self.snapshot.scan_id; } } } - fn ignore_stack_for_path(&self, path: &Path, is_dir: bool) -> Arc { + fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { let mut new_ignores = Vec::new(); - for ancestor in path.ancestors().skip(1) { - if let Some((ignore, _)) = self.ignores.get(ancestor) { + for ancestor in abs_path.ancestors().skip(1) { + if let Some((ignore, _)) = self.ignores_by_abs_path.get(ancestor) { new_ignores.push((ancestor, Some(ignore.clone()))); } else { new_ignores.push((ancestor, None)); @@ -1489,16 +1495,16 @@ impl LocalSnapshot { } let mut ignore_stack = IgnoreStack::none(); - for (parent_path, ignore) in new_ignores.into_iter().rev() { - if ignore_stack.is_path_ignored(&parent_path, true) { + for (parent_abs_path, ignore) in new_ignores.into_iter().rev() { + if ignore_stack.is_abs_path_ignored(&parent_abs_path, true) { ignore_stack = IgnoreStack::all(); break; } else if let Some(ignore) = ignore { - ignore_stack = ignore_stack.append(Arc::from(parent_path), ignore); + ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore); } } - if ignore_stack.is_path_ignored(path, is_dir) { + if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { ignore_stack = IgnoreStack::all(); } @@ -2120,7 +2126,8 @@ impl BackgroundScanner { match build_gitignore(&child_abs_path, self.fs.as_ref()).await { Ok(ignore) => { let ignore = Arc::new(ignore); - ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); + ignore_stack = + ignore_stack.append(job.abs_path.as_path().into(), ignore.clone()); new_ignore = Some(ignore); } Err(error) => { @@ -2138,7 +2145,9 @@ impl BackgroundScanner { // new jobs as well. let mut new_jobs = new_jobs.iter_mut(); for entry in &mut new_entries { - entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir()); + let entry_abs_path = self.abs_path().join(&entry.path); + entry.is_ignored = + ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir()); if entry.is_dir() { new_jobs.next().unwrap().ignore_stack = if entry.is_ignored { IgnoreStack::all() @@ -2157,7 +2166,7 @@ impl BackgroundScanner { ); if child_metadata.is_dir { - let is_ignored = ignore_stack.is_path_ignored(&child_path, true); + let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); child_entry.is_ignored = is_ignored; new_entries.push(child_entry); new_jobs.push(ScanJob { @@ -2171,7 +2180,7 @@ impl BackgroundScanner { scan_queue: job.scan_queue.clone(), }); } else { - child_entry.is_ignored = ignore_stack.is_path_ignored(&child_path, false); + child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); new_entries.push(child_entry); }; } @@ -2200,8 +2209,8 @@ impl BackgroundScanner { next_entry_id = snapshot.next_entry_id.clone(); } - let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await { - abs_path + let root_canonical_path = if let Ok(path) = self.fs.canonicalize(&root_abs_path).await { + path } else { return false; }; @@ -2221,27 +2230,29 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock(); snapshot.scan_id += 1; for event in &events { - if let Ok(path) = event.path.strip_prefix(&root_abs_path) { + if let Ok(path) = event.path.strip_prefix(&root_canonical_path) { snapshot.remove_path(&path); } } for (event, metadata) in events.into_iter().zip(metadata.into_iter()) { - let path: Arc = match event.path.strip_prefix(&root_abs_path) { + let path: Arc = match event.path.strip_prefix(&root_canonical_path) { Ok(path) => Arc::from(path.to_path_buf()), Err(_) => { log::error!( "unexpected event {:?} for root path {:?}", event.path, - root_abs_path + root_canonical_path ); continue; } }; + let abs_path = root_abs_path.join(&path); match metadata { Ok(Some(metadata)) => { - let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); + let ignore_stack = + snapshot.ignore_stack_for_abs_path(&abs_path, metadata.is_dir); let mut fs_entry = Entry::new( path.clone(), &metadata, @@ -2253,7 +2264,7 @@ impl BackgroundScanner { if metadata.is_dir { self.executor .block(scan_queue_tx.send(ScanJob { - abs_path: event.path, + abs_path, path, ignore_stack, scan_queue: scan_queue_tx.clone(), @@ -2301,37 +2312,41 @@ impl BackgroundScanner { let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); - for (parent_path, (_, scan_id)) in &snapshot.ignores { + for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_abs_path { + let parent_path = parent_abs_path.strip_prefix(&snapshot.abs_path).unwrap(); if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() { - ignores_to_update.push(parent_path.clone()); + ignores_to_update.push(parent_abs_path.clone()); } let ignore_path = parent_path.join(&*GITIGNORE); if snapshot.entry_for_path(ignore_path).is_none() { - ignores_to_delete.push(parent_path.clone()); + ignores_to_delete.push(parent_abs_path.clone()); } } - for parent_path in ignores_to_delete { - snapshot.ignores.remove(&parent_path); - self.snapshot.lock().ignores.remove(&parent_path); + for parent_abs_path in ignores_to_delete { + snapshot.ignores_by_abs_path.remove(&parent_abs_path); + self.snapshot + .lock() + .ignores_by_abs_path + .remove(&parent_abs_path); } let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); ignores_to_update.sort_unstable(); let mut ignores_to_update = ignores_to_update.into_iter().peekable(); - while let Some(parent_path) = ignores_to_update.next() { + while let Some(parent_abs_path) = ignores_to_update.next() { while ignores_to_update .peek() - .map_or(false, |p| p.starts_with(&parent_path)) + .map_or(false, |p| p.starts_with(&parent_abs_path)) { ignores_to_update.next().unwrap(); } - let ignore_stack = snapshot.ignore_stack_for_path(&parent_path, true); + let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); ignore_queue_tx .send(UpdateIgnoreStatusJob { - path: parent_path, + abs_path: parent_abs_path, ignore_stack, ignore_queue: ignore_queue_tx.clone(), }) @@ -2355,15 +2370,17 @@ impl BackgroundScanner { async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { let mut ignore_stack = job.ignore_stack; - if let Some((ignore, _)) = snapshot.ignores.get(&job.path) { - ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); + if let Some((ignore, _)) = snapshot.ignores_by_abs_path.get(&job.abs_path) { + ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); } let mut entries_by_id_edits = Vec::new(); let mut entries_by_path_edits = Vec::new(); - for mut entry in snapshot.child_entries(&job.path).cloned() { + let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap(); + for mut entry in snapshot.child_entries(path).cloned() { let was_ignored = entry.is_ignored; - entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir()); + let abs_path = self.abs_path().join(&entry.path); + entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir()); if entry.is_dir() { let child_ignore_stack = if entry.is_ignored { IgnoreStack::all() @@ -2372,7 +2389,7 @@ impl BackgroundScanner { }; job.ignore_queue .send(UpdateIgnoreStatusJob { - path: entry.path.clone(), + abs_path: abs_path.into(), ignore_stack: child_ignore_stack, ignore_queue: job.ignore_queue.clone(), }) @@ -2393,6 +2410,20 @@ impl BackgroundScanner { snapshot.entries_by_path.edit(entries_by_path_edits, &()); snapshot.entries_by_id.edit(entries_by_id_edits, &()); } + + async fn build_root_ignore_stack(&self) { + // let parent_abs_path = if let Some(path) = self.abs_path().parent() { + // path + // } else { + // return IgnoreStack::none() + // } + + // let mut cur_path = PathBuf::new(); + // for component in self.abs_path().components() { + // // self.snapshot.lock().ignores.insert(parent_path, (ignore, self.scan_id)); + // cur_path.push(compo) + // } + } } fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { @@ -2413,7 +2444,7 @@ struct ScanJob { } struct UpdateIgnoreStatusJob { - path: Arc, + abs_path: Arc, ignore_stack: Arc, ignore_queue: Sender, } @@ -2891,7 +2922,7 @@ mod tests { let mut initial_snapshot = LocalSnapshot { abs_path: root_dir.path().into(), removed_entry_ids: Default::default(), - ignores: Default::default(), + ignores_by_abs_path: Default::default(), next_entry_id: next_entry_id.clone(), snapshot: Snapshot { id: WorktreeId::from_usize(0), @@ -3176,8 +3207,10 @@ mod tests { .collect::>(); assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); - for (ignore_parent_path, _) in &self.ignores { - assert!(self.entry_for_path(ignore_parent_path).is_some()); + for (ignore_parent_abs_path, _) in &self.ignores_by_abs_path { + let ignore_parent_path = + ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); + assert!(self.entry_for_path(&ignore_parent_path).is_some()); assert!(self .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) .is_some()); From e66144104fc77e14d4937514856563cf07ac48d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 11:19:46 +0200 Subject: [PATCH 3/6] Honor gitignores above worktree root --- crates/project/src/worktree.rs | 123 +++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c81d9d5637e4d6372bf3be83daa185f0dd48c10e..5f8739fd88a87ebbf3af2ecf516e97afb613cea4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2046,24 +2046,41 @@ impl BackgroundScanner { async fn scan_dirs(&mut self) -> Result<()> { let root_char_bag; + let root_abs_path; let next_entry_id; let is_dir; { let snapshot = self.snapshot.lock(); root_char_bag = snapshot.root_char_bag; + root_abs_path = snapshot.abs_path.clone(); next_entry_id = snapshot.next_entry_id.clone(); is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir()) }; + // Populate ignores above the root. + for ancestor in root_abs_path.ancestors().skip(1) { + if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await + { + self.snapshot + .lock() + .ignores_by_abs_path + .insert(ancestor.into(), (ignore.into(), 0)); + } + } + if is_dir { let path: Arc = Arc::from(Path::new("")); - let abs_path = self.abs_path(); + let ignore_stack = self + .snapshot + .lock() + .ignore_stack_for_abs_path(&root_abs_path, true); + let (tx, rx) = channel::unbounded(); self.executor .block(tx.send(ScanJob { - abs_path: abs_path.to_path_buf(), + abs_path: root_abs_path.to_path_buf(), path, - ignore_stack: IgnoreStack::none(), + ignore_stack, scan_queue: tx.clone(), })) .unwrap(); @@ -2313,14 +2330,15 @@ impl BackgroundScanner { let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_abs_path { - let parent_path = parent_abs_path.strip_prefix(&snapshot.abs_path).unwrap(); - if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() { - ignores_to_update.push(parent_abs_path.clone()); - } + if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) { + if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() { + ignores_to_update.push(parent_abs_path.clone()); + } - let ignore_path = parent_path.join(&*GITIGNORE); - if snapshot.entry_for_path(ignore_path).is_none() { - ignores_to_delete.push(parent_abs_path.clone()); + let ignore_path = parent_path.join(&*GITIGNORE); + if snapshot.entry_for_path(ignore_path).is_none() { + ignores_to_delete.push(parent_abs_path.clone()); + } } } @@ -2410,20 +2428,6 @@ impl BackgroundScanner { snapshot.entries_by_path.edit(entries_by_path_edits, &()); snapshot.entries_by_id.edit(entries_by_id_edits, &()); } - - async fn build_root_ignore_stack(&self) { - // let parent_abs_path = if let Some(path) = self.abs_path().parent() { - // path - // } else { - // return IgnoreStack::none() - // } - - // let mut cur_path = PathBuf::new(); - // for component in self.abs_path().components() { - // // self.snapshot.lock().ignores.insert(parent_path, (ignore, self.scan_id)); - // cur_path.push(compo) - // } - } } fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { @@ -2797,23 +2801,28 @@ mod tests { #[gpui::test] async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { - let dir = temp_tree(json!({ - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": { - "tracked-file1": "tracked contents", - }, - "ignored-dir": { - "ignored-file1": "ignored contents", + let parent_dir = temp_tree(json!({ + ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", + "tree": { + ".git": {}, + ".gitignore": "ignored-dir\n", + "tracked-dir": { + "tracked-file1": "", + "ancestor-ignored-file1": "", + }, + "ignored-dir": { + "ignored-file1": "" + } } })); + let dir = parent_dir.path().join("tree"); let http_client = FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); let tree = Worktree::local( client, - dir.path(), + dir.as_path(), true, Arc::new(RealFs), Default::default(), @@ -2826,23 +2835,47 @@ mod tests { tree.flush_fs_events(&cx).await; cx.read(|cx| { let tree = tree.read(cx); - 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, false); - assert_eq!(ignored.is_ignored, true); + assert!( + !tree + .entry_for_path("tracked-dir/tracked-file1") + .unwrap() + .is_ignored + ); + assert!( + tree.entry_for_path("tracked-dir/ancestor-ignored-file1") + .unwrap() + .is_ignored + ); + assert!( + tree.entry_for_path("ignored-dir/ignored-file1") + .unwrap() + .is_ignored + ); }); - std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); - std::fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); + std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap(); + std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap(); + std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap(); tree.flush_fs_events(&cx).await; cx.read(|cx| { let tree = tree.read(cx); - let dot_git = tree.entry_for_path(".git").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, false); - assert_eq!(ignored.is_ignored, true); - assert_eq!(dot_git.is_ignored, true); + assert!( + !tree + .entry_for_path("tracked-dir/tracked-file2") + .unwrap() + .is_ignored + ); + assert!( + tree.entry_for_path("tracked-dir/ancestor-ignored-file2") + .unwrap() + .is_ignored + ); + assert!( + tree.entry_for_path("ignored-dir/ignored-file2") + .unwrap() + .is_ignored + ); + assert!(tree.entry_for_path(".git").unwrap().is_ignored); }); } From 32c6ae3188ebd1db206dfa8749540b4f5d285f63 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 11:24:32 +0200 Subject: [PATCH 4/6] :art: --- crates/project/src/worktree.rs | 45 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5f8739fd88a87ebbf3af2ecf516e97afb613cea4..dbd4b443a1b0b4bac0a66409f6fc203760cd940b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -102,7 +102,7 @@ pub struct Snapshot { #[derive(Clone)] pub struct LocalSnapshot { abs_path: Arc, - ignores_by_abs_path: HashMap, (Arc, usize)>, + ignores_by_parent_abs_path: HashMap, (Arc, usize)>, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, @@ -370,7 +370,7 @@ impl LocalWorktree { let tree = cx.add_model(move |cx: &mut ModelContext| { let mut snapshot = LocalSnapshot { abs_path, - ignores_by_abs_path: Default::default(), + ignores_by_parent_abs_path: Default::default(), removed_entry_ids: Default::default(), next_entry_id, snapshot: Snapshot { @@ -1333,7 +1333,7 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { - self.ignores_by_abs_path.insert( + self.ignores_by_parent_abs_path.insert( abs_path.parent().unwrap().into(), (Arc::new(ignore), self.scan_id), ); @@ -1388,7 +1388,7 @@ impl LocalSnapshot { }; if let Some(ignore) = ignore { - self.ignores_by_abs_path.insert( + self.ignores_by_parent_abs_path.insert( self.abs_path.join(&parent_path).into(), (ignore, self.scan_id), ); @@ -1477,7 +1477,9 @@ impl LocalSnapshot { if path.file_name() == Some(&GITIGNORE) { let abs_parent_path = self.abs_path.join(path.parent().unwrap()); - if let Some((_, scan_id)) = self.ignores_by_abs_path.get_mut(abs_parent_path.as_path()) + if let Some((_, scan_id)) = self + .ignores_by_parent_abs_path + .get_mut(abs_parent_path.as_path()) { *scan_id = self.snapshot.scan_id; } @@ -1487,7 +1489,7 @@ impl LocalSnapshot { fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { let mut new_ignores = Vec::new(); for ancestor in abs_path.ancestors().skip(1) { - if let Some((ignore, _)) = self.ignores_by_abs_path.get(ancestor) { + if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { new_ignores.push((ancestor, Some(ignore.clone()))); } else { new_ignores.push((ancestor, None)); @@ -2063,18 +2065,25 @@ impl BackgroundScanner { { self.snapshot .lock() - .ignores_by_abs_path + .ignores_by_parent_abs_path .insert(ancestor.into(), (ignore.into(), 0)); } } + let ignore_stack = { + let mut snapshot = self.snapshot.lock(); + let ignore_stack = snapshot.ignore_stack_for_abs_path(&root_abs_path, true); + if ignore_stack.is_all() { + if let Some(mut root_entry) = snapshot.root_entry().cloned() { + root_entry.is_ignored = true; + snapshot.insert_entry(root_entry, self.fs.as_ref()); + } + } + ignore_stack + }; + if is_dir { let path: Arc = Arc::from(Path::new("")); - let ignore_stack = self - .snapshot - .lock() - .ignore_stack_for_abs_path(&root_abs_path, true); - let (tx, rx) = channel::unbounded(); self.executor .block(tx.send(ScanJob { @@ -2329,7 +2338,7 @@ impl BackgroundScanner { let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); - for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_abs_path { + for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path { if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) { if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() { ignores_to_update.push(parent_abs_path.clone()); @@ -2343,10 +2352,10 @@ impl BackgroundScanner { } for parent_abs_path in ignores_to_delete { - snapshot.ignores_by_abs_path.remove(&parent_abs_path); + snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path); self.snapshot .lock() - .ignores_by_abs_path + .ignores_by_parent_abs_path .remove(&parent_abs_path); } @@ -2388,7 +2397,7 @@ impl BackgroundScanner { async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { let mut ignore_stack = job.ignore_stack; - if let Some((ignore, _)) = snapshot.ignores_by_abs_path.get(&job.abs_path) { + if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); } @@ -2955,7 +2964,7 @@ mod tests { let mut initial_snapshot = LocalSnapshot { abs_path: root_dir.path().into(), removed_entry_ids: Default::default(), - ignores_by_abs_path: Default::default(), + ignores_by_parent_abs_path: Default::default(), next_entry_id: next_entry_id.clone(), snapshot: Snapshot { id: WorktreeId::from_usize(0), @@ -3240,7 +3249,7 @@ mod tests { .collect::>(); assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); - for (ignore_parent_abs_path, _) in &self.ignores_by_abs_path { + for (ignore_parent_abs_path, _) in &self.ignores_by_parent_abs_path { let ignore_parent_path = ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); assert!(self.entry_for_path(&ignore_parent_path).is_some()); From 56f9c7bc1be9bf04a2a60f15bd0282b1ae23ca2a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 11:54:45 +0200 Subject: [PATCH 5/6] Include ignored files in fuzzy search when root entry is ignored --- crates/file_finder/src/file_finder.rs | 67 ++++++++++++++++++++++++--- crates/project/src/project.rs | 62 +++++-------------------- crates/project/src/project_tests.rs | 44 +----------------- 3 files changed, 74 insertions(+), 99 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index cdefa6b593a61c84e83d104a182d8525e61a43d2..67077f24ea8cf5fa1614a8f460fc99663c7f41f2 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -4,7 +4,7 @@ use gpui::{ RenderContext, Task, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; -use project::{Project, ProjectPath, WorktreeId}; +use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use settings::Settings; use std::{ path::Path, @@ -134,17 +134,40 @@ impl FileFinder { } fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + let worktrees = self + .project + .read(cx) + .visible_worktrees(cx) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() + .map(|worktree| { + let worktree = worktree.read(cx); + PathMatchCandidateSet { + snapshot: worktree.snapshot(), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), + include_root_name, + } + }) + .collect::>(); + let search_id = util::post_inc(&mut self.search_count); self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); - let project = self.project.clone(); cx.spawn(|this, mut cx| async move { - let matches = project - .read_with(&cx, |project, cx| { - project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx) - }) - .await; + let matches = fuzzy::match_paths( + candidate_sets.as_slice(), + &query, + false, + 100, + &cancel_flag, + cx.background(), + ) + .await; let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); this.update(&mut cx, |this, cx| { this.set_matches(search_id, did_cancel, query, matches, cx) @@ -475,4 +498,34 @@ mod tests { assert_eq!(f.selected_index(), 0); }); } + + #[gpui::test] + async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { + let app_state = cx.update(AppState::test); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "dir1": {}, + "dir2": { + "dir3": {} + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); + let (_, finder) = + cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); + finder + .update(cx, |f, cx| f.spawn_search("dir".into(), cx)) + .await; + cx.read(|cx| { + let finder = finder.read(cx); + assert_eq!(finder.matches.len(), 0); + }); + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0ac3064e56b33840130d0e43a98d12d8908f4476..5bfb3a9453de0331eca73d44afe43e3e0a715b3c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -13,7 +13,6 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; -use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, @@ -58,7 +57,7 @@ use std::{ rc::Rc, str, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, + atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, time::Instant, @@ -5678,43 +5677,6 @@ impl Project { }) } - pub fn match_paths<'a>( - &self, - query: &'a str, - include_ignored: bool, - smart_case: bool, - max_results: usize, - cancel_flag: &'a AtomicBool, - cx: &AppContext, - ) -> impl 'a + Future> { - let worktrees = self - .worktrees(cx) - .filter(|worktree| worktree.read(cx).is_visible()) - .collect::>(); - let include_root_name = worktrees.len() > 1; - let candidate_sets = worktrees - .into_iter() - .map(|worktree| CandidateSet { - snapshot: worktree.read(cx).snapshot(), - include_ignored, - include_root_name, - }) - .collect::>(); - - let background = cx.background().clone(); - async move { - fuzzy::match_paths( - candidate_sets.as_slice(), - query, - smart_case, - max_results, - cancel_flag, - background, - ) - .await - } - } - fn edits_from_lsp( &mut self, buffer: &ModelHandle, @@ -5942,14 +5904,14 @@ impl OpenBuffer { } } -struct CandidateSet { - snapshot: Snapshot, - include_ignored: bool, - include_root_name: bool, +pub struct PathMatchCandidateSet { + pub snapshot: Snapshot, + pub include_ignored: bool, + pub include_root_name: bool, } -impl<'a> PathMatchCandidateSet<'a> for CandidateSet { - type Candidates = CandidateSetIter<'a>; +impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { + type Candidates = PathMatchCandidateSetIter<'a>; fn id(&self) -> usize { self.snapshot.id().to_usize() @@ -5974,23 +5936,23 @@ impl<'a> PathMatchCandidateSet<'a> for CandidateSet { } fn candidates(&'a self, start: usize) -> Self::Candidates { - CandidateSetIter { + PathMatchCandidateSetIter { traversal: self.snapshot.files(self.include_ignored, start), } } } -struct CandidateSetIter<'a> { +pub struct PathMatchCandidateSetIter<'a> { traversal: Traversal<'a>, } -impl<'a> Iterator for CandidateSetIter<'a> { - type Item = PathMatchCandidate<'a>; +impl<'a> Iterator for PathMatchCandidateSetIter<'a> { + type Item = fuzzy::PathMatchCandidate<'a>; fn next(&mut self) -> Option { self.traversal.next().map(|entry| { if let EntryKind::File(char_bag) = entry.kind { - PathMatchCandidate { + fuzzy::PathMatchCandidate { path: &entry.path, char_bag, } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 00f7bb8c9463c32079f6c7057cb0c57f1c2ea46e..9a65bab0c00b8a25a9f34f7c61403631ae459037 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -8,12 +8,12 @@ use language::{ }; use lsp::Url; use serde_json::json; -use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll}; +use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; use unindent::Unindent as _; use util::{assert_set_eq, test::temp_tree}; #[gpui::test] -async fn test_populate_and_search(cx: &mut gpui::TestAppContext) { +async fn test_symlinks(cx: &mut gpui::TestAppContext) { let dir = temp_tree(json!({ "root": { "apple": "", @@ -38,7 +38,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) { .unwrap(); let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await; - project.read_with(cx, |project, cx| { let tree = project.worktrees(cx).next().unwrap().read(cx); assert_eq!(tree.file_count(), 5); @@ -47,23 +46,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) { tree.inode_for_path("finnochio/grape") ); }); - - let cancel_flag = Default::default(); - let results = project - .read_with(cx, |project, cx| { - project.match_paths("bna", false, false, 10, &cancel_flag, cx) - }) - .await; - assert_eq!( - results - .into_iter() - .map(|result| result.path) - .collect::>>(), - vec![ - PathBuf::from("banana/carrot/date").into(), - PathBuf::from("banana/carrot/endive").into(), - ] - ); } #[gpui::test] @@ -1645,28 +1627,6 @@ fn chunks_with_diagnostics( chunks } -#[gpui::test] -async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { - let dir = temp_tree(json!({ - "root": { - "dir1": {}, - "dir2": { - "dir3": {} - } - } - })); - - let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; - let cancel_flag = Default::default(); - let results = project - .read_with(cx, |project, cx| { - project.match_paths("dir", false, false, 10, &cancel_flag, cx) - }) - .await; - - assert!(results.is_empty()); -} - #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { let mut language = Language::new( From 540aa1748a7abc2b7a0e69f6712c3c0cbd07cc8e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Jul 2022 12:16:42 +0200 Subject: [PATCH 6/6] Add unit test for ignored files in file finder --- crates/file_finder/src/file_finder.rs | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 67077f24ea8cf5fa1614a8f460fc99663c7f41f2..720e0142beb35684fc4afad10fd7e7f5e0545e00 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -412,6 +412,51 @@ mod tests { }); } + #[gpui::test] + async fn test_ignored_files(cx: &mut gpui::TestAppContext) { + let app_state = cx.update(AppState::test); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + "/ancestor/tracked-root".as_ref(), + "/ancestor/ignored-root".as_ref(), + ], + cx, + ) + .await; + let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); + let (_, finder) = + cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); + finder + .update(cx, |f, cx| f.spawn_search("hi".into(), cx)) + .await; + finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7)); + } + #[gpui::test] async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) { let app_state = cx.update(AppState::test);