From 0cc5e8f7429a9930dbae3dc7f13fa1dd0563ef60 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 19:15:08 -0600 Subject: [PATCH 001/102] Replace easy-parallel with scoped-pool for path searches The easy-parallel crate spawned new threads on each call, which was resulting in way too many threads. Co-Authored-By: Brooks Swinnerton <934497+bswinnerton@users.noreply.github.com> --- Cargo.lock | 32 +++++++++++++++++++++++++++++++- gpui/Cargo.toml | 1 + gpui/src/app.rs | 6 ++++++ gpui/src/lib.rs | 1 + zed/src/file_finder.rs | 3 ++- zed/src/worktree/fuzzy.rs | 16 ++++++++-------- zed/src/worktree/worktree.rs | 6 ++++-- 7 files changed, 53 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c327163bce..68b6460eab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -924,6 +930,7 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", + "scoped-pool", "serde", "serde_json", "simplelog", @@ -1065,7 +1072,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" dependencies = [ - "scopeguard", + "scopeguard 1.1.0", ] [[package]] @@ -1674,12 +1681,29 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-pool" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a3a15e704545ce59ed2b5c60a5d32bda4d7869befb8b36667b658a6c00b43" +dependencies = [ + "crossbeam", + "scopeguard 0.1.2", + "variance", +] + [[package]] name = "scoped-tls" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +[[package]] +name = "scopeguard" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a076157c1e2dc561d8de585151ee6965d910dd4dcb5dabb7ae3e83981a6c57" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2058,6 +2082,12 @@ dependencies = [ "ctor", ] +[[package]] +name = "variance" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abfc2be1fb59663871379ea884fd81de80c496f2274e021c01d6fe56cd77b05" + [[package]] name = "vec-arena" version = "1.0.0" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 7bf1913372..9fd7ce036a 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,6 +18,7 @@ pathfinder_geometry = "0.5" rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" +scoped-pool = "1.0.0" serde = "1.0.125" serde_json = "1.0.64" smallvec = "1.6.1" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b35c5ba0b4..6515b67105 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -413,6 +413,7 @@ impl MutableAppContext { windows: HashMap::new(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background: Arc::new(executor::Background::new()), + scoped_pool: scoped_pool::Pool::new(num_cpus::get()), }, actions: HashMap::new(), global_actions: HashMap::new(), @@ -1315,6 +1316,7 @@ pub struct AppContext { windows: HashMap, background: Arc, ref_counts: Arc>, + scoped_pool: scoped_pool::Pool, } impl AppContext { @@ -1353,6 +1355,10 @@ impl AppContext { pub fn background_executor(&self) -> &Arc { &self.background } + + pub fn scoped_pool(&self) -> &scoped_pool::Pool { + &self.scoped_pool + } } impl ReadModel for AppContext { diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index b60ce9b92d..342a6bf7ee 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -27,3 +27,4 @@ pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; +pub use scoped_pool; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index b27189814c..654017556d 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -347,8 +347,9 @@ impl FileFinder { fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { let worktrees = self.worktrees(ctx.as_ref()); let search_id = util::post_inc(&mut self.search_count); + let pool = ctx.app().scoped_pool().clone(); let task = ctx.background_executor().spawn(async move { - let matches = match_paths(worktrees.as_slice(), &query, false, false, 100); + let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); (search_id, matches) }); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index e6baeec962..c4a3d451ed 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -1,4 +1,4 @@ -use easy_parallel::Parallel; +use gpui::scoped_pool; use super::char_bag::CharBag; @@ -54,6 +54,7 @@ pub fn match_paths( include_ignored: bool, smart_case: bool, max_results: usize, + pool: scoped_pool::Pool, ) -> Vec { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -68,10 +69,9 @@ pub fn match_paths( let segment_size = (path_count + cpus - 1) / cpus; let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); - Parallel::new() - .each( - segment_results.iter_mut().enumerate(), - |(segment_idx, results)| { + pool.scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + scope.execute(move || { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; @@ -115,9 +115,9 @@ pub fn match_paths( } tree_start = tree_end; } - }, - ) - .run(); + }) + } + }); let mut results = segment_results .into_iter() diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 2e63ad117f..60fab4c5a9 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -11,7 +11,7 @@ use crate::{ use anyhow::{anyhow, Result}; use crossbeam_channel as channel; use easy_parallel::Parallel; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::RwLock; use smol::prelude::*; @@ -606,6 +606,7 @@ pub fn match_paths( include_ignored: bool, smart_case: bool, max_results: usize, + pool: scoped_pool::Pool, ) -> Vec { let tree_states = trees.iter().map(|tree| tree.0.read()).collect::>(); fuzzy::match_paths( @@ -634,6 +635,7 @@ pub fn match_paths( include_ignored, smart_case, max_results, + pool, ) } @@ -674,7 +676,7 @@ mod test { app.read(|ctx| { let tree = tree.read(ctx); assert_eq!(tree.file_count(), 4); - let results = match_paths(&[tree.clone()], "bna", false, false, 10) + let results = match_paths(&[tree.clone()], "bna", false, false, 10, ctx.scoped_pool().clone()) .iter() .map(|result| tree.entry_path(result.entry_id)) .collect::, _>>() From 3803eb85a5914eb2c1698d9107523d1c0bfbba76 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 Apr 2021 15:38:54 -0600 Subject: [PATCH 002/102] Inline clone --- zed/src/worktree/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 60fab4c5a9..261487bd2f 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -105,13 +105,13 @@ impl Worktree { let id = self.push_dir(None, name, ino, is_symlink, is_ignored); let (tx, rx) = channel::unbounded(); - let tx_ = tx.clone(); tx.send(Ok(DirToScan { id, path, relative_path, ignore: Some(ignore), dirs_to_scan: tx_, + dirs_to_scan: tx.clone(), })) .unwrap(); drop(tx); From cdfd61369eb83186ff39f5195debd1827e8618c7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 Apr 2021 15:56:57 -0600 Subject: [PATCH 003/102] Switch worktree entries to HashMap --- zed/src/worktree/worktree.rs | 68 ++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 261487bd2f..dea9c1bcdb 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -33,7 +33,7 @@ pub struct Worktree(Arc>); struct WorktreeState { id: usize, path: PathBuf, - entries: Vec, + entries: HashMap, file_paths: Vec, histories: HashMap, scanning: bool, @@ -55,7 +55,7 @@ impl Worktree { let tree = Self(Arc::new(RwLock::new(WorktreeState { id, path: path.into(), - entries: Vec::new(), + entries: HashMap::new(), file_paths: Vec::new(), histories: HashMap::new(), scanning: ctx.is_some(), @@ -102,7 +102,7 @@ impl Worktree { if metadata.file_type().is_dir() { let is_ignored = is_ignored || name == ".git"; - let id = self.push_dir(None, name, ino, is_symlink, is_ignored); + let id = self.insert_dir(None, name, ino, is_symlink, is_ignored); let (tx, rx) = channel::unbounded(); tx.send(Ok(DirToScan { @@ -110,7 +110,6 @@ impl Worktree { path, relative_path, ignore: Some(ignore), - dirs_to_scan: tx_, dirs_to_scan: tx.clone(), })) .unwrap(); @@ -127,7 +126,7 @@ impl Worktree { .into_iter() .collect::>()?; } else { - self.push_file(None, name, ino, is_symlink, is_ignored, relative_path); + self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path); } Ok(()) @@ -157,7 +156,7 @@ impl Worktree { } } - let id = self.push_dir(Some(to_scan.id), name, ino, is_symlink, is_ignored); + let id = self.insert_dir(Some(to_scan.id), name, ino, is_symlink, is_ignored); new_children.push(id); let dirs_to_scan = to_scan.dirs_to_scan.clone(); @@ -173,7 +172,7 @@ impl Worktree { i.matched(to_scan.path.join(&name), false).is_ignore() }); - new_children.push(self.push_file( + new_children.push(self.insert_file( Some(to_scan.id), name, ino, @@ -184,14 +183,15 @@ impl Worktree { }; } - if let Entry::Dir { children, .. } = &mut self.0.write().entries[to_scan.id] { + if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.id) + { *children = new_children.clone(); } Ok(()) } - fn push_dir( + fn insert_dir( &self, parent: Option, name: OsString, @@ -201,18 +201,21 @@ impl Worktree { ) -> usize { let entries = &mut self.0.write().entries; let dir_id = entries.len(); - entries.push(Entry::Dir { - parent, - name, - ino, - is_symlink, - is_ignored, - children: Vec::new(), - }); + entries.insert( + dir_id, + Entry::Dir { + parent, + name, + ino, + is_symlink, + is_ignored, + children: Vec::new(), + }, + ); dir_id } - fn push_file( + fn insert_file( &self, parent: Option, name: OsString, @@ -228,13 +231,16 @@ impl Worktree { let mut state = self.0.write(); let entry_id = state.entries.len(); - state.entries.push(Entry::File { - parent, - name, - ino, - is_symlink, - is_ignored, - }); + state.entries.insert( + entry_id, + Entry::File { + parent, + name, + ino, + is_symlink, + is_ignored, + }, + ); state.file_paths.push(PathEntry { entry_id, path_chars, @@ -254,7 +260,7 @@ impl Worktree { let mut entries = Vec::new(); loop { - let entry = &state.entries[entry_id]; + let entry = &state.entries[&entry_id]; entries.push(entry); if let Some(parent_id) = entry.parent() { entry_id = parent_id; @@ -277,7 +283,7 @@ impl Worktree { } fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: usize, indent: usize) -> fmt::Result { - match &self.0.read().entries[entry_id] { + match &self.0.read().entries[&entry_id] { Entry::Dir { name, children, .. } => { write!( f, @@ -498,7 +504,7 @@ impl Iterator for Iter { if !self.started { self.started = true; - return if let Some(entry) = state.entries.first().cloned() { + return if let Some(entry) = state.entries.get(&0).cloned() { self.stack.push(IterStackEntry { entry_id: 0, child_idx: 0, @@ -511,7 +517,7 @@ impl Iterator for Iter { } while let Some(parent) = self.stack.last_mut() { - if let Entry::Dir { children, .. } = &state.entries[parent.entry_id] { + if let Entry::Dir { children, .. } = &state.entries[&parent.entry_id] { if parent.child_idx < children.len() { let child_id = children[post_inc(&mut parent.child_idx)]; @@ -522,7 +528,7 @@ impl Iterator for Iter { return Some(Traversal::Push { entry_id: child_id, - entry: state.entries[child_id].clone(), + entry: state.entries[&child_id].clone(), }); } else { self.stack.pop(); @@ -614,7 +620,7 @@ pub fn match_paths( .iter() .map(|tree| { let skip_prefix = if trees.len() == 1 { - if let Some(Entry::Dir { name, .. }) = tree.entries.get(0) { + if let Some(Entry::Dir { name, .. }) = tree.entries.get(&0) { let name = name.to_string_lossy(); if name == "/" { 1 From 3c0bbe5eb5995d71f1af927bf1262ba3123ea8b6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 Apr 2021 16:03:10 -0600 Subject: [PATCH 004/102] Store root entry id --- zed/src/worktree/worktree.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index dea9c1bcdb..a00b0d55c5 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -33,6 +33,7 @@ pub struct Worktree(Arc>); struct WorktreeState { id: usize, path: PathBuf, + root_ino: Option, entries: HashMap, file_paths: Vec, histories: HashMap, @@ -55,6 +56,7 @@ impl Worktree { let tree = Self(Arc::new(RwLock::new(WorktreeState { id, path: path.into(), + root_ino: None, entries: HashMap::new(), file_paths: Vec::new(), histories: HashMap::new(), @@ -100,6 +102,7 @@ impl Worktree { } let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); + self.0.write().root_ino = Some(0); if metadata.file_type().is_dir() { let is_ignored = is_ignored || name == ".git"; let id = self.insert_dir(None, name, ino, is_symlink, is_ignored); @@ -409,6 +412,13 @@ impl Entity for Worktree { type Event = (); } +impl WorktreeState { + fn root_entry(&self) -> Option<&Entry> { + self.root_ino + .and_then(|root_ino| self.entries.get(&root_ino)) + } +} + pub trait WorktreeHandle { fn file(&self, entry_id: usize, app: &AppContext) -> Result; } @@ -504,7 +514,7 @@ impl Iterator for Iter { if !self.started { self.started = true; - return if let Some(entry) = state.entries.get(&0).cloned() { + return if let Some(entry) = state.root_entry().cloned() { self.stack.push(IterStackEntry { entry_id: 0, child_idx: 0, @@ -620,7 +630,7 @@ pub fn match_paths( .iter() .map(|tree| { let skip_prefix = if trees.len() == 1 { - if let Some(Entry::Dir { name, .. }) = tree.entries.get(&0) { + if let Some(Entry::Dir { name, .. }) = tree.root_entry() { let name = name.to_string_lossy(); if name == "/" { 1 From 24cdfd24711a68828db3490d1a60a2aa4803d191 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 Apr 2021 16:42:58 -0600 Subject: [PATCH 005/102] Identify Worktree entries by their inode This will allow us to re-parent elements when re-scanning when the file system changes. --- zed/src/editor/buffer/mod.rs | 2 +- zed/src/editor/buffer_view.rs | 2 +- zed/src/file_finder.rs | 6 +- zed/src/workspace/pane.rs | 15 ++-- zed/src/workspace/workspace.rs | 8 +- zed/src/workspace/workspace_view.rs | 10 +-- zed/src/worktree/fuzzy.rs | 15 ++-- zed/src/worktree/worktree.rs | 126 ++++++++++++++-------------- 8 files changed, 95 insertions(+), 89 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index d69e1d96e2..642dc1fc7f 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -433,7 +433,7 @@ impl Buffer { self.file.as_ref().map(|file| file.path(app)) } - pub fn entry_id(&self) -> Option<(usize, usize)> { + pub fn entry_id(&self) -> Option<(usize, u64)> { self.file.as_ref().map(|file| file.entry_id()) } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index f1855c9128..e366c72bf3 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -1225,7 +1225,7 @@ impl workspace::ItemView for BufferView { } } - fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> { + fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> { self.buffer.read(app).entry_id() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 654017556d..beb49d503e 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -44,7 +44,7 @@ pub fn init(app: &mut MutableAppContext) { } pub enum Event { - Selected(usize, usize), + Selected(usize, u64), Dismissed, } @@ -339,7 +339,7 @@ impl FileFinder { } } - fn select(&mut self, entry: &(usize, usize), ctx: &mut ViewContext) { + fn select(&mut self, entry: &(usize, u64), ctx: &mut ViewContext) { let (tree_id, entry_id) = *entry; ctx.emit(Event::Selected(tree_id, entry_id)); } @@ -347,7 +347,7 @@ impl FileFinder { fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { let worktrees = self.worktrees(ctx.as_ref()); let search_id = util::post_inc(&mut self.search_count); - let pool = ctx.app().scoped_pool().clone(); + let pool = ctx.as_ref().scoped_pool().clone(); let task = ctx.background_executor().spawn(async move { let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); (search_id, matches) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index e0b0109ea4..bb52a6da5a 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -105,15 +105,12 @@ impl Pane { self.items.get(self.active_item).cloned() } - pub fn activate_entry( - &mut self, - entry_id: (usize, usize), - ctx: &mut ViewContext, - ) -> bool { - if let Some(index) = self.items.iter().position(|item| { - item.entry_id(ctx.as_ref()) - .map_or(false, |id| id == entry_id) - }) { + pub fn activate_entry(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext) -> bool { + if let Some(index) = self + .items + .iter() + .position(|item| item.entry_id(ctx.as_ref()).map_or(false, |id| id == entry_id)) + { self.activate_item(index, ctx); true } else { diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index a3a2d79a0e..125f4b8bc4 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -76,7 +76,7 @@ enum OpenedItem { pub struct Workspace { replica_id: ReplicaId, worktrees: HashSet>, - items: HashMap<(usize, usize), OpenedItem>, + items: HashMap<(usize, u64), OpenedItem>, } impl Workspace { @@ -125,7 +125,7 @@ impl Workspace { pub fn open_entry( &mut self, - entry: (usize, usize), + entry: (usize, u64), ctx: &mut ModelContext<'_, Self>, ) -> anyhow::Result + Send>>> { if let Some(item) = self.items.get(&entry).cloned() { @@ -200,12 +200,12 @@ impl Entity for Workspace { #[cfg(test)] pub trait WorkspaceHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, usize)>; + fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)>; } #[cfg(test)] impl WorkspaceHandle for ModelHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, usize)> { + fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)> { self.read(app) .worktrees() .iter() diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 23612116c3..ac3a7b4307 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -19,7 +19,7 @@ pub fn init(app: &mut MutableAppContext) { pub trait ItemView: View { fn title(&self, app: &AppContext) -> String; - fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>; + fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>; fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, @@ -42,7 +42,7 @@ pub trait ItemView: View { pub trait ItemViewHandle: Send + Sync { fn title(&self, app: &AppContext) -> String; - fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>; + fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, app: &mut MutableAppContext) -> Option>; fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext); @@ -57,7 +57,7 @@ impl ItemViewHandle for ViewHandle { self.read(app).title(app) } - fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> { + fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> { self.read(app).entry_id(app) } @@ -124,7 +124,7 @@ pub struct WorkspaceView { center: PaneGroup, panes: Vec>, active_pane: ViewHandle, - loading_entries: HashSet<(usize, usize)>, + loading_entries: HashSet<(usize, u64)>, } impl WorkspaceView { @@ -189,7 +189,7 @@ impl WorkspaceView { } } - pub fn open_entry(&mut self, entry: (usize, usize), ctx: &mut ViewContext) { + pub fn open_entry(&mut self, entry: (usize, u64), ctx: &mut ViewContext) { if self.loading_entries.contains(&entry) { return; } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index c4a3d451ed..8bdb7d7eea 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -12,7 +12,7 @@ const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; pub struct PathEntry { - pub entry_id: usize, + pub ino: u64, pub path_chars: CharBag, pub path: Vec, pub lowercase_path: Vec, @@ -24,7 +24,7 @@ pub struct PathMatch { pub score: f64, pub positions: Vec, pub tree_id: usize, - pub entry_id: usize, + pub entry_id: u64, pub skipped_prefix_len: usize, } @@ -191,7 +191,7 @@ fn match_single_tree_paths( if score > 0.0 { results.push(Reverse(PathMatch { tree_id, - entry_id: path_entry.entry_id, + entry_id: path_entry.ino, score, positions: match_positions.clone(), skipped_prefix_len, @@ -453,7 +453,7 @@ mod tests { let path_chars = CharBag::from(&lowercase_path[..]); let path = path.chars().collect(); path_entries.push(PathEntry { - entry_id: i, + ino: i as u64, path_chars, path, lowercase_path, @@ -490,7 +490,12 @@ mod tests { results .into_iter() .rev() - .map(|result| (paths[result.0.entry_id].clone(), result.0.positions)) + .map(|result| { + ( + paths[result.0.entry_id as usize].clone(), + result.0.positions, + ) + }) .collect() } } diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index a00b0d55c5..b7c0765693 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -33,15 +33,15 @@ pub struct Worktree(Arc>); struct WorktreeState { id: usize, path: PathBuf, - root_ino: Option, - entries: HashMap, + root_ino: Option, + entries: HashMap, file_paths: Vec, - histories: HashMap, + histories: HashMap, scanning: bool, } struct DirToScan { - id: usize, + ino: u64, path: PathBuf, relative_path: PathBuf, ignore: Option, @@ -102,14 +102,13 @@ impl Worktree { } let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - self.0.write().root_ino = Some(0); if metadata.file_type().is_dir() { let is_ignored = is_ignored || name == ".git"; - let id = self.insert_dir(None, name, ino, is_symlink, is_ignored); + self.insert_dir(None, name, ino, is_symlink, is_ignored); let (tx, rx) = channel::unbounded(); tx.send(Ok(DirToScan { - id, + ino, path, relative_path, ignore: Some(ignore), @@ -131,6 +130,7 @@ impl Worktree { } else { self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path); } + self.0.write().root_ino = Some(ino); Ok(()) } @@ -159,12 +159,12 @@ impl Worktree { } } - let id = self.insert_dir(Some(to_scan.id), name, ino, is_symlink, is_ignored); - new_children.push(id); + self.insert_dir(Some(to_scan.ino), name, ino, is_symlink, is_ignored); + new_children.push(ino); let dirs_to_scan = to_scan.dirs_to_scan.clone(); let _ = to_scan.dirs_to_scan.send(Ok(DirToScan { - id, + ino, path, relative_path, ignore, @@ -175,18 +175,19 @@ impl Worktree { i.matched(to_scan.path.join(&name), false).is_ignore() }); - new_children.push(self.insert_file( - Some(to_scan.id), + self.insert_file( + Some(to_scan.ino), name, ino, is_symlink, is_ignored, relative_path, - )); + ); + new_children.push(ino); }; } - if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.id) + if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.ino) { *children = new_children.clone(); } @@ -196,16 +197,15 @@ impl Worktree { fn insert_dir( &self, - parent: Option, + parent: Option, name: OsString, ino: u64, is_symlink: bool, is_ignored: bool, - ) -> usize { + ) { let entries = &mut self.0.write().entries; - let dir_id = entries.len(); entries.insert( - dir_id, + ino, Entry::Dir { parent, name, @@ -215,27 +215,25 @@ impl Worktree { children: Vec::new(), }, ); - dir_id } fn insert_file( &self, - parent: Option, + parent: Option, name: OsString, ino: u64, is_symlink: bool, is_ignored: bool, path: PathBuf, - ) -> usize { + ) { let path = path.to_string_lossy(); let lowercase_path = path.to_lowercase().chars().collect::>(); let path = path.chars().collect::>(); let path_chars = CharBag::from(&path[..]); let mut state = self.0.write(); - let entry_id = state.entries.len(); state.entries.insert( - entry_id, + ino, Entry::File { parent, name, @@ -245,25 +243,23 @@ impl Worktree { }, ); state.file_paths.push(PathEntry { - entry_id, + ino, path_chars, path, lowercase_path, is_ignored, }); - entry_id } - pub fn entry_path(&self, mut entry_id: usize) -> Result { + pub fn entry_path(&self, mut entry_id: u64) -> Result { let state = self.0.read(); - if entry_id >= state.entries.len() { - return Err(anyhow!("Entry does not exist in tree")); - } - let mut entries = Vec::new(); loop { - let entry = &state.entries[&entry_id]; + let entry = state + .entries + .get(&entry_id) + .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; entries.push(entry); if let Some(parent_id) = entry.parent() { entry_id = parent_id; @@ -279,13 +275,13 @@ impl Worktree { Ok(path) } - pub fn abs_entry_path(&self, entry_id: usize) -> Result { + pub fn abs_entry_path(&self, entry_id: u64) -> Result { let mut path = self.0.read().path.clone(); path.pop(); Ok(path.join(self.entry_path(entry_id)?)) } - fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: usize, indent: usize) -> fmt::Result { + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: u64, indent: usize) -> fmt::Result { match &self.0.read().entries[&entry_id] { Entry::Dir { name, children, .. } => { write!( @@ -333,6 +329,10 @@ impl Worktree { } } + pub fn has_entry(&self, entry_id: u64) -> bool { + self.0.read().entries.contains_key(&entry_id) + } + pub fn entry_count(&self) -> usize { self.0.read().entries.len() } @@ -341,7 +341,7 @@ impl Worktree { self.0.read().file_paths.len() } - pub fn load_history(&self, entry_id: usize) -> impl Future> { + pub fn load_history(&self, entry_id: u64) -> impl Future> { let tree = self.clone(); async move { @@ -360,12 +360,7 @@ impl Worktree { } } - pub fn save<'a>( - &self, - entry_id: usize, - content: Snapshot, - ctx: &AppContext, - ) -> Task> { + pub fn save<'a>(&self, entry_id: u64, content: Snapshot, ctx: &AppContext) -> Task> { let path = self.abs_entry_path(entry_id); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); @@ -420,34 +415,34 @@ impl WorktreeState { } pub trait WorktreeHandle { - fn file(&self, entry_id: usize, app: &AppContext) -> Result; + fn file(&self, entry_id: u64, app: &AppContext) -> Result; } impl WorktreeHandle for ModelHandle { - fn file(&self, entry_id: usize, app: &AppContext) -> Result { - if entry_id >= self.read(app).entry_count() { - return Err(anyhow!("Entry does not exist in tree")); + fn file(&self, entry_id: u64, app: &AppContext) -> Result { + if self.read(app).has_entry(entry_id) { + Err(anyhow!("entry does not exist in tree")) + } else { + Ok(FileHandle { + worktree: self.clone(), + entry_id, + }) } - - Ok(FileHandle { - worktree: self.clone(), - entry_id, - }) } } #[derive(Clone, Debug)] pub enum Entry { Dir { - parent: Option, + parent: Option, name: OsString, ino: u64, is_symlink: bool, is_ignored: bool, - children: Vec, + children: Vec, }, File { - parent: Option, + parent: Option, name: OsString, ino: u64, is_symlink: bool, @@ -456,12 +451,18 @@ pub enum Entry { } impl Entry { - fn parent(&self) -> Option { + fn parent(&self) -> Option { match self { Entry::Dir { parent, .. } | Entry::File { parent, .. } => *parent, } } + fn ino(&self) -> u64 { + match self { + Entry::Dir { ino, .. } | Entry::File { ino, .. } => *ino, + } + } + fn name(&self) -> &OsStr { match self { Entry::Dir { name, .. } | Entry::File { name, .. } => name, @@ -472,7 +473,7 @@ impl Entry { #[derive(Clone)] pub struct FileHandle { worktree: ModelHandle, - entry_id: usize, + entry_id: u64, } impl FileHandle { @@ -489,13 +490,13 @@ impl FileHandle { worktree.save(self.entry_id, content, ctx) } - pub fn entry_id(&self) -> (usize, usize) { + pub fn entry_id(&self) -> (usize, u64) { (self.worktree.id(), self.entry_id) } } struct IterStackEntry { - entry_id: usize, + entry_id: u64, child_idx: usize, } @@ -516,18 +517,21 @@ impl Iterator for Iter { return if let Some(entry) = state.root_entry().cloned() { self.stack.push(IterStackEntry { - entry_id: 0, + entry_id: entry.ino(), child_idx: 0, }); - Some(Traversal::Push { entry_id: 0, entry }) + Some(Traversal::Push { + entry_id: entry.ino(), + entry, + }) } else { None }; } while let Some(parent) = self.stack.last_mut() { - if let Entry::Dir { children, .. } = &state.entries[&parent.entry_id] { + if let Some(Entry::Dir { children, .. }) = &state.entries.get(&parent.entry_id) { if parent.child_idx < children.len() { let child_id = children[post_inc(&mut parent.child_idx)]; @@ -558,7 +562,7 @@ impl Iterator for Iter { #[derive(Debug)] pub enum Traversal { - Push { entry_id: usize, entry: Entry }, + Push { entry_id: u64, entry: Entry }, Pop, } @@ -568,7 +572,7 @@ pub struct FilesIter { } pub struct FilesIterItem { - pub entry_id: usize, + pub entry_id: u64, pub path: PathBuf, } From 41f50cdb615ccd9e2eb79d26961a47a075cd4732 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 Apr 2021 18:42:19 -0600 Subject: [PATCH 006/102] Require a context when constructing a worktree --- zed/src/workspace/workspace.rs | 2 +- zed/src/worktree/worktree.rs | 32 +++++++++++++++----------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index 125f4b8bc4..f4518d2704 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -117,7 +117,7 @@ impl Workspace { } } - let worktree = ctx.add_model(|ctx| Worktree::new(ctx.model_id(), path, Some(ctx))); + let worktree = ctx.add_model(|ctx| Worktree::new(ctx.model_id(), path, ctx)); ctx.observe(&worktree, Self::on_worktree_updated); self.worktrees.insert(worktree); ctx.notify(); diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index b7c0765693..b789a9d139 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -49,7 +49,7 @@ struct DirToScan { } impl Worktree { - pub fn new(id: usize, path: T, ctx: Option<&mut ModelContext>) -> Self + pub fn new(id: usize, path: T, ctx: &mut ModelContext) -> Self where T: Into, { @@ -60,27 +60,25 @@ impl Worktree { entries: HashMap::new(), file_paths: Vec::new(), histories: HashMap::new(), - scanning: ctx.is_some(), + scanning: true, }))); - if let Some(ctx) = ctx { - tree.0.write().scanning = true; - + let done_scanning = { let tree = tree.clone(); - let task = ctx.background_executor().spawn(async move { + ctx.background_executor().spawn(async move { tree.scan_dirs()?; Ok(()) - }); + }) + }; - ctx.spawn(task, Self::done_scanning).detach(); + ctx.spawn(done_scanning, Self::done_scanning).detach(); - ctx.spawn_stream( - timer::repeat(Duration::from_millis(100)).map(|_| ()), - Self::scanning, - |_, _| {}, - ) - .detach(); - } + ctx.spawn_stream( + timer::repeat(Duration::from_millis(100)).map(|_| ()), + Self::scanning, + |_, _| {}, + ) + .detach(); tree } @@ -690,7 +688,7 @@ mod test { let root_link_path = dir.path().join("root_link"); unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); - let tree = app.add_model(|ctx| Worktree::new(1, root_link_path, Some(ctx))); + let tree = app.add_model(|ctx| Worktree::new(1, root_link_path, ctx)); app.finish_pending_tasks().await; app.read(|ctx| { @@ -719,7 +717,7 @@ mod test { "file1": "the old contents", })); - let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), Some(ctx))); + let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), ctx)); app.finish_pending_tasks().await; let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); From bc34ff54fea7aef9510a676a26bca5f8ecac25ce Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 Apr 2021 19:34:43 -0600 Subject: [PATCH 007/102] Add a failing test for detecting a file move --- zed/src/test.rs | 16 ++++++++++ zed/src/worktree/worktree.rs | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/zed/src/test.rs b/zed/src/test.rs index 7e2d80efe0..da965b257b 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -2,6 +2,7 @@ use rand::Rng; use std::{ collections::BTreeMap, path::{Path, PathBuf}, + time::{Duration, Instant}, }; use tempdir::TempDir; @@ -136,3 +137,18 @@ fn write_tree(path: &Path, tree: serde_json::Value) { panic!("You must pass a JSON object to this helper") } } + +pub async fn assert_condition(poll_interval: u64, timeout: u64, mut f: impl FnMut() -> bool) { + let poll_interval = Duration::from_millis(poll_interval); + let timeout = Duration::from_millis(timeout); + let start = Instant::now(); + loop { + if f() { + return; + } else if Instant::now().duration_since(start) < timeout { + smol::Timer::after(poll_interval).await; + } else { + panic!("timed out waiting on condition"); + } + } +} diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index b789a9d139..e7f1714427 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -279,6 +279,28 @@ impl Worktree { Ok(path.join(self.entry_path(entry_id)?)) } + #[cfg(test)] + fn entry_for_path(&self, path: impl AsRef) -> Option { + let path = path.as_ref(); + let state = self.0.read(); + state.root_ino.and_then(|mut ino| { + 'components: for component in path { + if let Entry::Dir { children, .. } = &state.entries[&ino] { + for child in children { + if state.entries[child].name() == component { + ino = *child; + continue 'components; + } + } + return None; + } else { + return None; + } + } + Some(ino) + }) + } + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: u64, indent: usize) -> fmt::Result { match &self.0.read().entries[&entry_id] { Entry::Dir { name, children, .. } => { @@ -740,4 +762,40 @@ mod test { assert_eq!(history.base_text.as_ref(), buffer.text()); }); } + + #[test] + fn test_rescan() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + "dir1": { + "file": "contents" + }, + "dir2": { + } + })); + + let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), ctx)); + app.finish_pending_tasks().await; + + let file_entry = app.read(|ctx| tree.read(ctx).entry_for_path("dir1/file").unwrap()); + + app.read(|ctx| { + let tree = tree.read(ctx); + assert_eq!( + tree.abs_entry_path(file_entry).unwrap(), + tree.path().join("dir1/file") + ); + }); + + std::fs::rename(dir.path().join("dir1/file"), dir.path().join("dir2/file")).unwrap(); + + assert_condition(1, 300, || { + app.read(|ctx| { + let tree = tree.read(ctx); + tree.abs_entry_path(file_entry).unwrap() == tree.path().join("dir2/file") + }) + }) + .await + }); + } } From f3a0a11fc5f45a1267315389f0ac3507cebf5f75 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 14:51:49 -0600 Subject: [PATCH 008/102] WIP --- Cargo.lock | 128 +++++++++++++++++++++++++++++++++++ zed/Cargo.toml | 1 + zed/src/lib.rs | 1 + zed/src/worktree/worktree.rs | 63 +++++++++-------- 4 files changed, 163 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68b6460eab..15558eaa3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,15 @@ name = "async-task" version = "4.0.3" source = "git+https://github.com/zed-industries/async-task?rev=341b57d6de98cdfd7b418567b8de2022ca993a6e#341b57d6de98cdfd7b418567b8de2022ca993a6e" +[[package]] +name = "atomic" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" +dependencies = [ + "autocfg", +] + [[package]] name = "atomic-waker" version = "1.0.0" @@ -524,6 +533,16 @@ dependencies = [ "crossbeam-utils 0.8.2", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.2", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -799,6 +818,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "futures" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.12" @@ -806,6 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -835,6 +869,31 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-sink" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23" + +[[package]] +name = "futures-task" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" + +[[package]] +name = "futures-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "generator" version = "0.6.23" @@ -1328,6 +1387,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d70072c20945e1ab871c472a285fc772aefd4f5407723c206242f2c6f94595d6" +[[package]] +name = "pin-project" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.4" @@ -1371,6 +1450,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "pollster" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cce106fd2646acbe31a0e4006f75779d535c26a44f153ada196e9edcfc6d944" + +[[package]] +name = "postage" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a63d25391d04a097954b76aba742b6b5b74f213dfe3dbaeeb36e8ddc1c657f0b" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "pin-project", + "pollster", + "static_assertions", + "thiserror", +] + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -1862,6 +1963,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" @@ -1933,6 +2040,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.3" @@ -2298,6 +2425,7 @@ dependencies = [ "log", "num_cpus", "parking_lot", + "postage", "rand 0.8.3", "rust-embed", "seahash", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 4ed0d1f1d5..1f6cec6e9b 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -26,6 +26,7 @@ libc = "0.2" log = "0.4" num_cpus = "1.13.0" parking_lot = "0.11.1" +postage = {version = "0.4.1", features = ["futures-traits"]} rand = "0.8.3" rust-embed = "5.9.0" seahash = "4.1" diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 14c2369258..066f07a20f 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -7,6 +7,7 @@ pub mod settings; mod sum_tree; #[cfg(test)] mod test; +mod throttle; mod time; mod timer; mod util; diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index e7f1714427..7d1d769593 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ editor::{History, Snapshot}, - timer, + throttle::throttled, util::post_inc, }; use anyhow::{anyhow, Result}; @@ -14,6 +14,7 @@ use easy_parallel::Parallel; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::RwLock; +use postage::watch; use smol::prelude::*; use std::{ collections::HashMap, @@ -37,7 +38,13 @@ struct WorktreeState { entries: HashMap, file_paths: Vec, histories: HashMap, - scanning: bool, + scan_state: watch::Sender, +} + +#[derive(Clone)] +enum ScanState { + Scanning, + Idle, } struct DirToScan { @@ -53,6 +60,8 @@ impl Worktree { where T: Into, { + let scan_state = watch::channel_with(ScanState::Scanning); + let tree = Self(Arc::new(RwLock::new(WorktreeState { id, path: path.into(), @@ -60,22 +69,22 @@ impl Worktree { entries: HashMap::new(), file_paths: Vec::new(), histories: HashMap::new(), - scanning: true, + scan_state: scan_state.0, }))); - let done_scanning = { + { let tree = tree.clone(); - ctx.background_executor().spawn(async move { - tree.scan_dirs()?; - Ok(()) - }) - }; - - ctx.spawn(done_scanning, Self::done_scanning).detach(); + std::thread::spawn(move || { + if let Err(error) = tree.scan_dirs() { + log::error!("error scanning worktree: {}", error); + } + tree.set_scan_state(ScanState::Idle); + }); + } ctx.spawn_stream( - timer::repeat(Duration::from_millis(100)).map(|_| ()), - Self::scanning, + throttled(Duration::from_millis(100), scan_state.1), + Self::observe_scan_state, |_, _| {}, ) .detach(); @@ -83,6 +92,10 @@ impl Worktree { tree } + fn set_scan_state(&self, state: ScanState) { + *self.0.write().scan_state.borrow_mut() = state; + } + fn scan_dirs(&self) -> io::Result<()> { let path = self.0.read().path.clone(); let metadata = fs::metadata(&path)?; @@ -201,7 +214,8 @@ impl Worktree { is_symlink: bool, is_ignored: bool, ) { - let entries = &mut self.0.write().entries; + let mut state = self.0.write(); + let entries = &mut state.entries; entries.insert( ino, Entry::Dir { @@ -213,6 +227,7 @@ impl Worktree { children: Vec::new(), }, ); + *state.scan_state.borrow_mut() = ScanState::Scanning; } fn insert_file( @@ -247,6 +262,7 @@ impl Worktree { lowercase_path, is_ignored, }); + *state.scan_state.borrow_mut() = ScanState::Scanning; } pub fn entry_path(&self, mut entry_id: u64) -> Result { @@ -394,22 +410,9 @@ impl Worktree { }) } - fn scanning(&mut self, _: (), ctx: &mut ModelContext) { - if self.0.read().scanning { - ctx.notify(); - } else { - ctx.halt_stream(); - } - } - - fn done_scanning(&mut self, result: io::Result<()>, ctx: &mut ModelContext) { - log::info!("done scanning"); - self.0.write().scanning = false; - if let Err(error) = result { - log::error!("error populating worktree: {}", error); - } else { - ctx.notify(); - } + fn observe_scan_state(&mut self, _: ScanState, ctx: &mut ModelContext) { + // log::info!("observe {:?}", std::time::Instant::now()); + ctx.notify() } } From e3fbb97ecc64d98a57b7794fc939dab259985b38 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 17:51:43 -0600 Subject: [PATCH 009/102] Throttle worktree state updates on the main thread Co-Authored-By: Max Brunsfeld --- zed/src/throttle.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 zed/src/throttle.rs diff --git a/zed/src/throttle.rs b/zed/src/throttle.rs new file mode 100644 index 0000000000..2fcd028496 --- /dev/null +++ b/zed/src/throttle.rs @@ -0,0 +1,68 @@ +use core::time; +use futures_core::{Future, Stream}; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; +use time::Duration; + +pub struct Throttled { + period: Duration, + stream: S, + timer: Option, +} + +pub fn throttled(period: Duration, stream: S) -> impl Stream { + Throttled { + period, + stream, + timer: None, + } +} + +impl Stream for Throttled { + type Item = S::Item; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Some(timer) = self.as_mut().timer() { + if let Poll::Pending = timer.poll(cx) { + return Poll::Pending; + } else { + self.as_mut().get_mut().timer = None; + } + } + + let mut stream = self.as_mut().stream(); + let mut last_item = None; + loop { + match stream.as_mut().poll_next(cx) { + Poll::Ready(None) => { + return Poll::Ready(None); + } + Poll::Ready(Some(item)) => last_item = Some(item), + Poll::Pending => break, + } + } + + if let Some(last_item) = last_item { + self.get_mut().timer = Some(smol::Timer::after(self.period)); + Poll::Ready(Some(last_item)) + } else { + Poll::Pending + } + } +} + +impl Throttled { + fn stream(self: Pin<&mut Self>) -> Pin<&mut S> { + unsafe { self.map_unchecked_mut(|s| &mut s.stream) } + } + + fn timer(self: Pin<&mut Self>) -> Option> { + if self.timer.is_some() { + Some(unsafe { self.map_unchecked_mut(|s| s.timer.as_mut().unwrap()) }) + } else { + None + } + } +} From e4f41de7bf4608db9d35cfaabd1376141a689cc3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 19:41:38 -0600 Subject: [PATCH 010/102] Invert condition when opening entries Co-Authored-By: Brooks Swinnerton <934497+bswinnerton@users.noreply.github.com> --- .vscode/launch.json | 8 ++++---- zed/src/worktree/worktree.rs | 21 ++++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 68fd885cf2..aea73eeaa2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,15 +7,15 @@ { "type": "lldb", "request": "launch", - "name": "Debug executable 'zed'", + "name": "Debug executable 'Zed'", "cargo": { "args": [ "build", - "--bin=zed", + "--bin=Zed", "--package=zed" ], "filter": { - "name": "zed", + "name": "Zed", "kind": "bin" } }, @@ -63,4 +63,4 @@ "cwd": "${workspaceFolder}" } ] -} +} \ No newline at end of file diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 7d1d769593..1ddc85ac93 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -444,12 +444,12 @@ pub trait WorktreeHandle { impl WorktreeHandle for ModelHandle { fn file(&self, entry_id: u64, app: &AppContext) -> Result { if self.read(app).has_entry(entry_id) { - Err(anyhow!("entry does not exist in tree")) - } else { Ok(FileHandle { worktree: self.clone(), entry_id, }) + } else { + Err(anyhow!("entry does not exist in tree")) } } } @@ -719,11 +719,18 @@ mod test { app.read(|ctx| { let tree = tree.read(ctx); assert_eq!(tree.file_count(), 4); - let results = match_paths(&[tree.clone()], "bna", false, false, 10, ctx.scoped_pool().clone()) - .iter() - .map(|result| tree.entry_path(result.entry_id)) - .collect::, _>>() - .unwrap(); + let results = match_paths( + &[tree.clone()], + "bna", + false, + false, + 10, + ctx.scoped_pool().clone(), + ) + .iter() + .map(|result| tree.entry_path(result.entry_id)) + .collect::, _>>() + .unwrap(); assert_eq!( results, vec![ From 26f9127e834ae9496fa795fa0f1b703b01e97893 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 19:45:02 -0600 Subject: [PATCH 011/102] Spawn worktree scanning on the scoped pool Co-Authored-By: Brooks Swinnerton <934497+bswinnerton@users.noreply.github.com> --- zed/src/worktree/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 1ddc85ac93..9ff9a0481f 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -74,7 +74,7 @@ impl Worktree { { let tree = tree.clone(); - std::thread::spawn(move || { + ctx.app().scoped_pool().spawn(move || { if let Err(error) = tree.scan_dirs() { log::error!("error scanning worktree: {}", error); } From cbc1d830676aede99201c14775b00747a5584ce6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 Apr 2021 11:09:06 -0600 Subject: [PATCH 012/102] Get worktree out of the way so we can try again Co-Authored-By: Antonio Scandurra --- zed/src/editor/buffer/mod.rs | 2 +- zed/src/file_finder.rs | 2 +- zed/src/lib.rs | 2 +- zed/src/workspace/workspace.rs | 2 +- zed/src/{worktree => worktree_old}/char_bag.rs | 0 zed/src/{worktree => worktree_old}/fuzzy.rs | 0 zed/src/{worktree => worktree_old}/mod.rs | 0 zed/src/{worktree => worktree_old}/worktree.rs | 2 +- 8 files changed, 5 insertions(+), 5 deletions(-) rename zed/src/{worktree => worktree_old}/char_bag.rs (100%) rename zed/src/{worktree => worktree_old}/fuzzy.rs (100%) rename zed/src/{worktree => worktree_old}/mod.rs (100%) rename zed/src/{worktree => worktree_old}/worktree.rs (99%) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 642dc1fc7f..8c44f9768f 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -15,7 +15,7 @@ use crate::{ sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree}, time::{self, ReplicaId}, util::RandomCharIter, - worktree::FileHandle, + worktree_old::FileHandle, }; use anyhow::{anyhow, Result}; use gpui::{AppContext, Entity, ModelContext}; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index beb49d503e..87f43db2ee 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -3,7 +3,7 @@ use crate::{ settings::Settings, util, watch, workspace::{Workspace, WorkspaceView}, - worktree::{match_paths, PathMatch, Worktree}, + worktree_old::{match_paths, PathMatch, Worktree}, }; use gpui::{ color::{ColorF, ColorU}, diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 066f07a20f..71ae77ad36 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -13,4 +13,4 @@ mod timer; mod util; pub mod watch; pub mod workspace; -mod worktree; +mod worktree_old; diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index f4518d2704..595a9959ff 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -4,7 +4,7 @@ use crate::{ settings::Settings, time::ReplicaId, watch, - worktree::{Worktree, WorktreeHandle as _}, + worktree_old::{Worktree, WorktreeHandle as _}, }; use anyhow::anyhow; use gpui::{AppContext, Entity, Handle, ModelContext, ModelHandle, MutableAppContext, ViewContext}; diff --git a/zed/src/worktree/char_bag.rs b/zed/src/worktree_old/char_bag.rs similarity index 100% rename from zed/src/worktree/char_bag.rs rename to zed/src/worktree_old/char_bag.rs diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree_old/fuzzy.rs similarity index 100% rename from zed/src/worktree/fuzzy.rs rename to zed/src/worktree_old/fuzzy.rs diff --git a/zed/src/worktree/mod.rs b/zed/src/worktree_old/mod.rs similarity index 100% rename from zed/src/worktree/mod.rs rename to zed/src/worktree_old/mod.rs diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree_old/worktree.rs similarity index 99% rename from zed/src/worktree/worktree.rs rename to zed/src/worktree_old/worktree.rs index 9ff9a0481f..f45c22d1ec 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree_old/worktree.rs @@ -74,7 +74,7 @@ impl Worktree { { let tree = tree.clone(); - ctx.app().scoped_pool().spawn(move || { + ctx.as_ref().scoped_pool().spawn(move || { if let Err(error) = tree.scan_dirs() { log::error!("error scanning worktree: {}", error); } From 36e6ed3aefedc3d48b431c2b79430a01a0f8b66e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 Apr 2021 12:38:06 -0600 Subject: [PATCH 013/102] WIP --- gpui/src/app.rs | 12 +- zed/src/file_finder.rs | 2 +- zed/src/lib.rs | 1 + zed/src/sum_tree/mod.rs | 6 + zed/src/worktree.rs | 311 +++++++++++++++++++++++++++++++ zed/src/worktree_old/worktree.rs | 4 +- 6 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 zed/src/worktree.rs diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 6515b67105..7a9975096d 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -413,7 +413,7 @@ impl MutableAppContext { windows: HashMap::new(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background: Arc::new(executor::Background::new()), - scoped_pool: scoped_pool::Pool::new(num_cpus::get()), + thread_pool: scoped_pool::Pool::new(num_cpus::get()), }, actions: HashMap::new(), global_actions: HashMap::new(), @@ -1316,7 +1316,7 @@ pub struct AppContext { windows: HashMap, background: Arc, ref_counts: Arc>, - scoped_pool: scoped_pool::Pool, + thread_pool: scoped_pool::Pool, } impl AppContext { @@ -1356,8 +1356,8 @@ impl AppContext { &self.background } - pub fn scoped_pool(&self) -> &scoped_pool::Pool { - &self.scoped_pool + pub fn thread_pool(&self) -> &scoped_pool::Pool { + &self.thread_pool } } @@ -1505,6 +1505,10 @@ impl<'a, T: Entity> ModelContext<'a, T> { &self.app.ctx.background } + pub fn thread_pool(&self) -> &scoped_pool::Pool { + &self.app.ctx.thread_pool + } + pub fn halt_stream(&mut self) { self.halt_stream = true; } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 87f43db2ee..36f182086d 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -347,7 +347,7 @@ impl FileFinder { fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { let worktrees = self.worktrees(ctx.as_ref()); let search_id = util::post_inc(&mut self.search_count); - let pool = ctx.as_ref().scoped_pool().clone(); + let pool = ctx.as_ref().thread_pool().clone(); let task = ctx.background_executor().spawn(async move { let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); (search_id, matches) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 71ae77ad36..16307bc19a 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -13,4 +13,5 @@ mod timer; mod util; pub mod watch; pub mod workspace; +mod worktree; mod worktree_old; diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index 723b625475..e690556d30 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -375,6 +375,12 @@ impl SumTree { } } +impl Default for SumTree { + fn default() -> Self { + Self::new() + } +} + #[derive(Clone, Debug)] pub enum Node { Internal { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs new file mode 100644 index 0000000000..2b1ee86d56 --- /dev/null +++ b/zed/src/worktree.rs @@ -0,0 +1,311 @@ +use crate::sum_tree::{self, Edit, SumTree}; +use gpui::{Entity, ModelContext}; +use ignore::dir::{Ignore, IgnoreBuilder}; +use parking_lot::Mutex; +use smol::channel::Sender; +use std::{ + ffi::{OsStr, OsString}, + fs, io, + ops::AddAssign, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, + sync::Arc, +}; + +enum ScanState { + Idle, + Scanning, +} + +pub struct Worktree { + path: Arc, + entries: SumTree, + scanner: BackgroundScanner, + scan_state: ScanState, +} + +impl Worktree { + fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { + let path = path.into(); + let scan_state = smol::channel::unbounded(); + let scanner = BackgroundScanner::new(path.clone(), scan_state.0); + let tree = Self { + path, + entries: Default::default(), + scanner, + scan_state: ScanState::Idle, + }; + + { + let scanner = tree.scanner.clone(); + std::thread::spawn(move || scanner.run()); + } + + tree + } +} + +impl Entity for Worktree { + type Event = (); +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Entry { + Dir { + parent: Option, + name: Arc, + ino: u64, + is_symlink: bool, + is_ignored: bool, + children: Arc<[u64]>, + pending: bool, + }, + File { + parent: Option, + name: Arc, + ino: u64, + is_symlink: bool, + is_ignored: bool, + }, +} + +impl Entry { + fn ino(&self) -> u64 { + match self { + Entry::Dir { ino, .. } => *ino, + Entry::File { ino, .. } => *ino, + } + } +} + +impl sum_tree::Item for Entry { + type Summary = EntrySummary; + + fn summary(&self) -> Self::Summary { + EntrySummary { + max_ino: self.ino(), + file_count: if matches!(self, Self::File { .. }) { + 1 + } else { + 0 + }, + } + } +} + +impl sum_tree::KeyedItem for Entry { + type Key = u64; + + fn key(&self) -> Self::Key { + self.ino() + } +} + +#[derive(Clone, Debug, Default)] +pub struct EntrySummary { + max_ino: u64, + file_count: usize, +} + +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; + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for u64 { + fn add_summary(&mut self, summary: &'a EntrySummary) { + *self = summary.max_ino; + } +} + +#[derive(Clone)] +struct BackgroundScanner { + path: Arc, + entries: Arc>>, + notify: Sender, +} + +impl BackgroundScanner { + fn new(path: Arc, notify: Sender) -> Self { + Self { + path, + entries: Default::default(), + notify, + } + } + + fn run(&self) { + if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { + return; + } + + self.scan_dirs(); + + if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { + return; + } + + // TODO: Update when dir changes + } + + fn scan_dirs(&self) -> io::Result<()> { + let metadata = fs::metadata(&self.path)?; + let ino = metadata.ino(); + let is_symlink = fs::symlink_metadata(&self.path)?.file_type().is_symlink(); + let name = self.path.file_name().unwrap_or(OsStr::new("/")).into(); + let relative_path = PathBuf::from(&name); + + let mut ignore = IgnoreBuilder::new() + .build() + .add_parents(&self.path) + .unwrap(); + if metadata.is_dir() { + ignore = ignore.add_child(&self.path).unwrap(); + } + let is_ignored = ignore.matched(&self.path, metadata.is_dir()).is_ignore(); + + if metadata.file_type().is_dir() { + let is_ignored = is_ignored || name == ".git"; + + self.insert_entries(Some(Entry::Dir { + parent: None, + name, + ino, + is_symlink, + is_ignored, + children: Arc::from([]), + pending: true, + })); + + let (tx, rx) = crossbeam_channel::unbounded(); + + tx.send(Ok(ScanJob { + ino, + path: path.into(), + relative_path, + ignore: Some(ignore), + scan_queue: tx.clone(), + })) + .unwrap(); + drop(tx); + + Parallel::>::new() + .each(0..16, |_| { + while let Ok(result) = rx.recv() { + self.scan_dir(result?)?; + } + Ok(()) + }) + .run() + .into_iter() + .collect::>()?; + } else { + self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path); + } + self.0.write().root_ino = Some(ino); + + Ok(()) + } + + fn scan_dir(&self, to_scan: ScanJob) -> io::Result<()> { + let mut new_children = Vec::new(); + + for child_entry in fs::read_dir(&to_scan.path)? { + let child_entry = child_entry?; + let name = child_entry.file_name(); + let relative_path = to_scan.relative_path.join(&name); + let metadata = child_entry.metadata()?; + let ino = metadata.ino(); + let is_symlink = metadata.file_type().is_symlink(); + + if metadata.is_dir() { + let path = to_scan.path.join(&name); + let mut is_ignored = true; + let mut ignore = None; + + if let Some(parent_ignore) = to_scan.ignore.as_ref() { + let child_ignore = parent_ignore.add_child(&path).unwrap(); + is_ignored = child_ignore.matched(&path, true).is_ignore() || name == ".git"; + if !is_ignored { + ignore = Some(child_ignore); + } + } + + self.insert_entries( + Some(Entry::Dir { + parent: (), + name: (), + ino: (), + is_symlink: (), + is_ignored: (), + children: (), + pending: (), + }) + .into_iter(), + ); + + self.insert_dir(Some(to_scan.ino), name, ino, is_symlink, is_ignored); + new_children.push(ino); + + let dirs_to_scan = to_scan.scan_queue.clone(); + let _ = to_scan.scan_queue.send(Ok(ScanJob { + ino, + path, + relative_path, + ignore, + scan_queue: dirs_to_scan, + })); + } else { + let is_ignored = to_scan.ignore.as_ref().map_or(true, |i| { + i.matched(to_scan.path.join(&name), false).is_ignore() + }); + + self.insert_file( + Some(to_scan.ino), + name, + ino, + is_symlink, + is_ignored, + relative_path, + ); + new_children.push(ino); + }; + } + + if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.ino) + { + *children = new_children.clone(); + } + + Ok(()) + } + + fn insert_entries(&self, entries: impl IntoIterator) { + self.entries + .lock() + .edit(&mut entries.into_iter().map(Edit::Insert).collect::>()); + } +} + +struct ScanJob { + ino: u64, + path: Arc, + relative_path: PathBuf, + ignore: Option, + scan_queue: crossbeam_channel::Sender>, +} + +trait UnwrapIgnoreTuple { + fn unwrap(self) -> Ignore; +} + +impl UnwrapIgnoreTuple for (Ignore, Option) { + fn unwrap(self) -> Ignore { + if let Some(error) = self.1 { + log::error!("error loading gitignore data: {}", error); + } + self.0 + } +} diff --git a/zed/src/worktree_old/worktree.rs b/zed/src/worktree_old/worktree.rs index f45c22d1ec..cb3536d2af 100644 --- a/zed/src/worktree_old/worktree.rs +++ b/zed/src/worktree_old/worktree.rs @@ -74,7 +74,7 @@ impl Worktree { { let tree = tree.clone(); - ctx.as_ref().scoped_pool().spawn(move || { + ctx.as_ref().thread_pool().spawn(move || { if let Err(error) = tree.scan_dirs() { log::error!("error scanning worktree: {}", error); } @@ -725,7 +725,7 @@ mod test { false, false, 10, - ctx.scoped_pool().clone(), + ctx.thread_pool().clone(), ) .iter() .map(|result| tree.entry_path(result.entry_id)) From 0bbff090f0765768901aa7a086c7f34982238a77 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 15 Apr 2021 12:10:37 +0200 Subject: [PATCH 014/102] WIP --- zed/src/sum_tree/mod.rs | 9 ++ zed/src/worktree.rs | 324 +++++++++++++++++++++++++++++++--------- 2 files changed, 261 insertions(+), 72 deletions(-) diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index e690556d30..0b7b398420 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -373,6 +373,15 @@ impl SumTree { new_tree }; } + + pub fn get(&self, key: &T::Key) -> Option<&T> { + let mut cursor = self.cursor::(); + if cursor.seek(key, SeekBias::Left) { + cursor.item() + } else { + None + } + } } impl Default for SumTree { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 2b1ee86d56..3e47a3c80e 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,20 +1,26 @@ use crate::sum_tree::{self, Edit, SumTree}; -use gpui::{Entity, ModelContext}; +use gpui::{scoped_pool, Entity, ModelContext}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; -use smol::channel::Sender; +use smol::{channel::Sender, Timer}; use std::{ ffi::{OsStr, OsString}, - fs, io, + fmt, fs, io, ops::AddAssign, os::unix::fs::MetadataExt, path::{Path, PathBuf}, - sync::Arc, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, + time::{Duration, Instant}, }; +#[derive(Debug)] enum ScanState { Idle, Scanning, + Err(io::Error), } pub struct Worktree { @@ -22,33 +28,109 @@ pub struct Worktree { entries: SumTree, scanner: BackgroundScanner, scan_state: ScanState, + will_poll_entries: bool, } impl Worktree { fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let path = path.into(); let scan_state = smol::channel::unbounded(); - let scanner = BackgroundScanner::new(path.clone(), scan_state.0); + let scanner = BackgroundScanner::new(path.clone(), scan_state.0, ctx.thread_pool().clone()); let tree = Self { path, entries: Default::default(), scanner, scan_state: ScanState::Idle, + will_poll_entries: false, }; - { - let scanner = tree.scanner.clone(); - std::thread::spawn(move || scanner.run()); - } + let scanner = tree.scanner.clone(); + std::thread::spawn(move || scanner.run()); + + ctx.spawn_stream(scan_state.1, Self::observe_scan_state, |_, _| {}) + .detach(); tree } + + fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { + self.scan_state = scan_state; + self.poll_entries(ctx); + ctx.notify(); + } + + fn poll_entries(&mut self, ctx: &mut ModelContext) { + self.entries = self.scanner.snapshot(); + if self.is_scanning() && !self.will_poll_entries { + self.will_poll_entries = true; + ctx.spawn(Timer::after(Duration::from_millis(100)), |this, _, ctx| { + this.will_poll_entries = false; + this.poll_entries(ctx); + }) + .detach(); + } + } + + fn is_scanning(&self) -> bool { + if let ScanState::Scanning = self.scan_state { + true + } else { + false + } + } + + fn is_empty(&self) -> bool { + self.root_ino() == 0 + } + + fn root_ino(&self) -> u64 { + self.scanner.root_ino.load(atomic::Ordering::SeqCst) + } + + fn file_count(&self) -> usize { + self.entries.summary().file_count + } + + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { + match self.entries.get(&ino).unwrap() { + Entry::Dir { name, children, .. } => { + write!( + f, + "{}{}/ ({})\n", + " ".repeat(indent), + name.to_string_lossy(), + ino + )?; + for child_id in children.iter() { + self.fmt_entry(f, *child_id, indent + 2)?; + } + Ok(()) + } + Entry::File { name, .. } => write!( + f, + "{}{} ({})\n", + " ".repeat(indent), + name.to_string_lossy(), + ino + ), + } + } } impl Entity for Worktree { type Event = (); } +impl fmt::Debug for Worktree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_empty() { + write!(f, "Empty tree\n") + } else { + self.fmt_entry(f, self.root_ino(), 0) + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Entry { Dir { @@ -123,25 +205,37 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for u64 { #[derive(Clone)] struct BackgroundScanner { path: Arc, + root_ino: Arc, entries: Arc>>, notify: Sender, + thread_pool: scoped_pool::Pool, } impl BackgroundScanner { - fn new(path: Arc, notify: Sender) -> Self { + fn new(path: Arc, notify: Sender, thread_pool: scoped_pool::Pool) -> Self { Self { path, + root_ino: Arc::new(AtomicU64::new(0)), entries: Default::default(), notify, + thread_pool, } } + fn snapshot(&self) -> SumTree { + self.entries.lock().clone() + } + fn run(&self) { if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return; } - self.scan_dirs(); + if let Err(err) = self.scan_dirs() { + if smol::block_on(self.notify.send(ScanState::Err(err))).is_err() { + return; + } + } if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { return; @@ -151,10 +245,11 @@ impl BackgroundScanner { } fn scan_dirs(&self) -> io::Result<()> { + println!("Scanning dirs ;)"); let metadata = fs::metadata(&self.path)?; let ino = metadata.ino(); let is_symlink = fs::symlink_metadata(&self.path)?.file_type().is_symlink(); - let name = self.path.file_name().unwrap_or(OsStr::new("/")).into(); + let name = Arc::from(self.path.file_name().unwrap_or(OsStr::new("/"))); let relative_path = PathBuf::from(&name); let mut ignore = IgnoreBuilder::new() @@ -167,9 +262,8 @@ impl BackgroundScanner { let is_ignored = ignore.matched(&self.path, metadata.is_dir()).is_ignore(); if metadata.file_type().is_dir() { - let is_ignored = is_ignored || name == ".git"; - - self.insert_entries(Some(Entry::Dir { + let is_ignored = is_ignored || name.as_ref() == ".git"; + let dir_entry = Entry::Dir { parent: None, name, ino, @@ -177,106 +271,131 @@ impl BackgroundScanner { is_ignored, children: Arc::from([]), pending: true, - })); + }; + self.insert_entries(Some(dir_entry.clone())); let (tx, rx) = crossbeam_channel::unbounded(); tx.send(Ok(ScanJob { ino, - path: path.into(), + path: self.path.clone(), relative_path, + dir_entry, ignore: Some(ignore), scan_queue: tx.clone(), })) .unwrap(); drop(tx); - Parallel::>::new() - .each(0..16, |_| { - while let Ok(result) = rx.recv() { - self.scan_dir(result?)?; - } - Ok(()) - }) - .run() - .into_iter() - .collect::>()?; + let mut results = Vec::new(); + results.resize_with(16, || Ok(())); + self.thread_pool.scoped(|pool| { + for result in &mut results { + pool.execute(|| { + let result = result; + while let Ok(job) = rx.recv() { + if let Err(err) = job.and_then(|job| self.scan_dir(job)) { + *result = Err(err); + break; + } + } + }); + } + }); + results.into_iter().collect::>()?; } else { - self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path); + self.insert_entries(Some(Entry::File { + parent: None, + name, + ino, + is_symlink, + is_ignored, + })); } - self.0.write().root_ino = Some(ino); + + self.root_ino.store(ino, atomic::Ordering::SeqCst); Ok(()) } - fn scan_dir(&self, to_scan: ScanJob) -> io::Result<()> { - let mut new_children = Vec::new(); + fn scan_dir(&self, job: ScanJob) -> io::Result<()> { + let scan_queue = job.scan_queue; + let mut dir_entry = job.dir_entry; - for child_entry in fs::read_dir(&to_scan.path)? { + let mut new_children = Vec::new(); + let mut new_entries = Vec::new(); + let mut new_jobs = Vec::new(); + + for child_entry in fs::read_dir(&job.path)? { let child_entry = child_entry?; - let name = child_entry.file_name(); - let relative_path = to_scan.relative_path.join(&name); + let name: Arc = child_entry.file_name().into(); + let relative_path = job.relative_path.join(name.as_ref()); let metadata = child_entry.metadata()?; let ino = metadata.ino(); let is_symlink = metadata.file_type().is_symlink(); + let path = job.path.join(name.as_ref()); + new_children.push(ino); if metadata.is_dir() { - let path = to_scan.path.join(&name); let mut is_ignored = true; let mut ignore = None; - if let Some(parent_ignore) = to_scan.ignore.as_ref() { + if let Some(parent_ignore) = job.ignore.as_ref() { let child_ignore = parent_ignore.add_child(&path).unwrap(); - is_ignored = child_ignore.matched(&path, true).is_ignore() || name == ".git"; + is_ignored = + child_ignore.matched(&path, true).is_ignore() || name.as_ref() == ".git"; if !is_ignored { ignore = Some(child_ignore); } } - self.insert_entries( - Some(Entry::Dir { - parent: (), - name: (), - ino: (), - is_symlink: (), - is_ignored: (), - children: (), - pending: (), - }) - .into_iter(), - ); - - self.insert_dir(Some(to_scan.ino), name, ino, is_symlink, is_ignored); - new_children.push(ino); - - let dirs_to_scan = to_scan.scan_queue.clone(); - let _ = to_scan.scan_queue.send(Ok(ScanJob { - ino, - path, - relative_path, - ignore, - scan_queue: dirs_to_scan, - })); - } else { - let is_ignored = to_scan.ignore.as_ref().map_or(true, |i| { - i.matched(to_scan.path.join(&name), false).is_ignore() - }); - - self.insert_file( - Some(to_scan.ino), + let dir_entry = Entry::Dir { + parent: Some(job.ino), name, ino, is_symlink, is_ignored, + children: Arc::from([]), + pending: true, + }; + new_entries.push(dir_entry.clone()); + new_jobs.push(ScanJob { + ino, + path: Arc::from(path), relative_path, - ); - new_children.push(ino); + dir_entry, + ignore, + scan_queue: scan_queue.clone(), + }); + } else { + let is_ignored = job + .ignore + .as_ref() + .map_or(true, |i| i.matched(path, false).is_ignore()); + new_entries.push(Entry::File { + parent: Some(job.ino), + name, + ino, + is_symlink, + is_ignored, + }); }; } - if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.ino) + if let Entry::Dir { + children, pending, .. + } = &mut dir_entry { - *children = new_children.clone(); + *children = Arc::from(new_children); + *pending = false; + } else { + unreachable!() + } + new_entries.push(dir_entry); + + self.insert_entries(new_entries); + for new_job in new_jobs { + let _ = scan_queue.send(Ok(new_job)); } Ok(()) @@ -293,6 +412,7 @@ struct ScanJob { ino: u64, path: Arc, relative_path: PathBuf, + dir_entry: Entry, ignore: Option, scan_queue: crossbeam_channel::Sender>, } @@ -309,3 +429,63 @@ impl UnwrapIgnoreTuple for (Ignore, Option) { self.0 } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::editor::Buffer; + use crate::test::*; + use anyhow::Result; + use gpui::App; + use serde_json::json; + use std::os::unix; + + #[test] + fn test_populate_and_search() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + "root": { + "apple": "", + "banana": { + "carrot": { + "date": "", + "endive": "", + } + }, + "fennel": { + "grape": "", + } + } + })); + + let root_link_path = dir.path().join("root_link"); + unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); + + let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); + assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 4)).await; + // app.read(|ctx| { + // let tree = tree.read(ctx); + // assert_eq!(tree.file_count(), 4); + // let results = match_paths( + // &[tree.clone()], + // "bna", + // false, + // false, + // 10, + // ctx.thread_pool().clone(), + // ) + // .iter() + // .map(|result| tree.entry_path(result.entry_id)) + // .collect::, _>>() + // .unwrap(); + // assert_eq!( + // results, + // vec![ + // PathBuf::from("root_link/banana/carrot/date"), + // PathBuf::from("root_link/banana/carrot/endive"), + // ] + // ); + // }) + }); + } +} From 070069d65e3b0d29e7a6db45310c900819d7d430 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 15 Apr 2021 18:12:56 +0200 Subject: [PATCH 015/102] Ensure top of cursor stack is either empty or a leaf node Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- zed/src/sum_tree/cursor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zed/src/sum_tree/cursor.rs b/zed/src/sum_tree/cursor.rs index 440e99ba90..4f6ec81055 100644 --- a/zed/src/sum_tree/cursor.rs +++ b/zed/src/sum_tree/cursor.rs @@ -271,6 +271,7 @@ where } self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); } pub fn descend_to_first_item(&mut self, mut subtree: &'a SumTree, filter_node: F) @@ -656,6 +657,7 @@ where } self.at_end = self.stack.is_empty(); + debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); if bias == SeekBias::Left { let mut end = self.seek_dimension.clone(); if let Some(summary) = self.item_summary() { From cefc753123ae1fdf180c0011219c18d6aef2fd30 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 15 Apr 2021 18:18:53 +0200 Subject: [PATCH 016/102] Re-introduce fuzzy-matching on the new `WorkTree` implementation Co-Authored-By: Nathan Sobo --- zed/src/sum_tree/mod.rs | 2 +- zed/src/worktree.rs | 158 ++++++---- zed/src/worktree/char_bag.rs | 44 +++ zed/src/worktree/fuzzy.rs | 543 +++++++++++++++++++++++++++++++++++ 4 files changed, 698 insertions(+), 49 deletions(-) create mode 100644 zed/src/worktree/char_bag.rs create mode 100644 zed/src/worktree/fuzzy.rs diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index 0b7b398420..7bd164c519 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -10,7 +10,7 @@ const TREE_BASE: usize = 2; #[cfg(not(test))] const TREE_BASE: usize = 6; -pub trait Item: Clone + Eq + fmt::Debug { +pub trait Item: Clone + fmt::Debug { type Summary: for<'a> AddAssign<&'a Self::Summary> + Default + Clone + fmt::Debug; fn summary(&self) -> Self::Summary; diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 3e47a3c80e..a4a50d8eb1 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,10 +1,16 @@ +mod char_bag; +mod fuzzy; + use crate::sum_tree::{self, Edit, SumTree}; +use anyhow::{anyhow, Result}; +pub use fuzzy::match_paths; +use fuzzy::PathEntry; use gpui::{scoped_pool, Entity, ModelContext}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; use std::{ - ffi::{OsStr, OsString}, + ffi::OsStr, fmt, fs, io, ops::AddAssign, os::unix::fs::MetadataExt, @@ -13,7 +19,7 @@ use std::{ atomic::{self, AtomicU64}, Arc, }, - time::{Duration, Instant}, + time::Duration, }; #[derive(Debug)] @@ -24,24 +30,26 @@ enum ScanState { } pub struct Worktree { + id: usize, path: Arc, entries: SumTree, scanner: BackgroundScanner, scan_state: ScanState, - will_poll_entries: bool, + poll_scheduled: bool, } impl Worktree { fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let path = path.into(); let scan_state = smol::channel::unbounded(); - let scanner = BackgroundScanner::new(path.clone(), scan_state.0, ctx.thread_pool().clone()); + let scanner = BackgroundScanner::new(path.clone(), scan_state.0); let tree = Self { + id: ctx.model_id(), path, entries: Default::default(), scanner, scan_state: ScanState::Idle, - will_poll_entries: false, + poll_scheduled: false, }; let scanner = tree.scanner.clone(); @@ -56,18 +64,19 @@ impl Worktree { fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { self.scan_state = scan_state; self.poll_entries(ctx); - ctx.notify(); } fn poll_entries(&mut self, ctx: &mut ModelContext) { self.entries = self.scanner.snapshot(); - if self.is_scanning() && !self.will_poll_entries { - self.will_poll_entries = true; + ctx.notify(); + + if self.is_scanning() && !self.poll_scheduled { ctx.spawn(Timer::after(Duration::from_millis(100)), |this, _, ctx| { - this.will_poll_entries = false; + this.poll_scheduled = false; this.poll_entries(ctx); }) .detach(); + self.poll_scheduled = true; } } @@ -79,18 +88,46 @@ impl Worktree { } } - fn is_empty(&self) -> bool { - self.root_ino() == 0 + fn root_ino(&self) -> Option { + let ino = self.scanner.root_ino.load(atomic::Ordering::SeqCst); + if ino == 0 { + None + } else { + Some(ino) + } } - fn root_ino(&self) -> u64 { - self.scanner.root_ino.load(atomic::Ordering::SeqCst) + fn root_entry(&self) -> Option<&Entry> { + self.root_ino().and_then(|ino| self.entries.get(&ino)) } fn file_count(&self) -> usize { self.entries.summary().file_count } + fn abs_entry_path(&self, ino: u64) -> Result { + Ok(self.path.join(self.entry_path(ino)?)) + } + + fn entry_path(&self, ino: u64) -> Result { + let mut components = Vec::new(); + let mut entry = self + .entries + .get(&ino) + .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; + components.push(entry.name()); + while let Some(parent) = entry.parent() { + entry = self.entries.get(&parent).unwrap(); + components.push(entry.name()); + } + + let mut path = PathBuf::new(); + for component in components.into_iter().rev() { + path.push(component); + } + Ok(path) + } + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { match self.entries.get(&ino).unwrap() { Entry::Dir { name, children, .. } => { @@ -123,15 +160,15 @@ impl Entity for Worktree { impl fmt::Debug for Worktree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.is_empty() { - write!(f, "Empty tree\n") + if let Some(root_ino) = self.root_ino() { + self.fmt_entry(f, root_ino, 0) } else { - self.fmt_entry(f, self.root_ino(), 0) + write!(f, "Empty tree\n") } } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug)] pub enum Entry { Dir { parent: Option, @@ -145,6 +182,7 @@ pub enum Entry { File { parent: Option, name: Arc, + path: PathEntry, ino: u64, is_symlink: bool, is_ignored: bool, @@ -158,6 +196,20 @@ impl Entry { Entry::File { ino, .. } => *ino, } } + + fn parent(&self) -> Option { + match self { + Entry::Dir { parent, .. } => *parent, + Entry::File { parent, .. } => *parent, + } + } + + fn name(&self) -> &OsStr { + match self { + Entry::Dir { name, .. } => name, + Entry::File { name, .. } => name, + } + } } impl sum_tree::Item for Entry { @@ -202,6 +254,15 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for u64 { } } +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +struct FileCount(usize); + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { + fn add_summary(&mut self, summary: &'a EntrySummary) { + self.0 += summary.file_count; + } +} + #[derive(Clone)] struct BackgroundScanner { path: Arc, @@ -212,13 +273,13 @@ struct BackgroundScanner { } impl BackgroundScanner { - fn new(path: Arc, notify: Sender, thread_pool: scoped_pool::Pool) -> Self { + fn new(path: Arc, notify: Sender) -> Self { Self { path, root_ino: Arc::new(AtomicU64::new(0)), entries: Default::default(), notify, - thread_pool, + thread_pool: scoped_pool::Pool::new(16), } } @@ -245,7 +306,6 @@ impl BackgroundScanner { } fn scan_dirs(&self) -> io::Result<()> { - println!("Scanning dirs ;)"); let metadata = fs::metadata(&self.path)?; let ino = metadata.ino(); let is_symlink = fs::symlink_metadata(&self.path)?.file_type().is_symlink(); @@ -273,6 +333,7 @@ impl BackgroundScanner { pending: true, }; self.insert_entries(Some(dir_entry.clone())); + self.root_ino.store(ino, atomic::Ordering::SeqCst); let (tx, rx) = crossbeam_channel::unbounded(); @@ -288,7 +349,7 @@ impl BackgroundScanner { drop(tx); let mut results = Vec::new(); - results.resize_with(16, || Ok(())); + results.resize_with(self.thread_pool.workers(), || Ok(())); self.thread_pool.scoped(|pool| { for result in &mut results { pool.execute(|| { @@ -307,14 +368,14 @@ impl BackgroundScanner { self.insert_entries(Some(Entry::File { parent: None, name, + path: PathEntry::new(ino, &relative_path, is_ignored), ino, is_symlink, is_ignored, })); + self.root_ino.store(ino, atomic::Ordering::SeqCst); } - self.root_ino.store(ino, atomic::Ordering::SeqCst); - Ok(()) } @@ -371,10 +432,11 @@ impl BackgroundScanner { let is_ignored = job .ignore .as_ref() - .map_or(true, |i| i.matched(path, false).is_ignore()); + .map_or(true, |i| i.matched(&path, false).is_ignore()); new_entries.push(Entry::File { parent: Some(job.ino), name, + path: PathEntry::new(ino, &relative_path, is_ignored), ino, is_symlink, is_ignored, @@ -395,7 +457,7 @@ impl BackgroundScanner { self.insert_entries(new_entries); for new_job in new_jobs { - let _ = scan_queue.send(Ok(new_job)); + scan_queue.send(Ok(new_job)).unwrap(); } Ok(()) @@ -463,29 +525,29 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 4)).await; - // app.read(|ctx| { - // let tree = tree.read(ctx); - // assert_eq!(tree.file_count(), 4); - // let results = match_paths( - // &[tree.clone()], - // "bna", - // false, - // false, - // 10, - // ctx.thread_pool().clone(), - // ) - // .iter() - // .map(|result| tree.entry_path(result.entry_id)) - // .collect::, _>>() - // .unwrap(); - // assert_eq!( - // results, - // vec![ - // PathBuf::from("root_link/banana/carrot/date"), - // PathBuf::from("root_link/banana/carrot/endive"), - // ] - // ); - // }) + app.read(|ctx| { + let tree = tree.read(ctx); + let results = match_paths( + Some(tree).into_iter(), + "bna", + false, + false, + false, + 10, + ctx.thread_pool().clone(), + ) + .iter() + .map(|result| tree.entry_path(result.entry_id)) + .collect::, _>>() + .unwrap(); + assert_eq!( + results, + vec![ + PathBuf::from("root_link/banana/carrot/date"), + PathBuf::from("root_link/banana/carrot/endive"), + ] + ); + }) }); } } diff --git a/zed/src/worktree/char_bag.rs b/zed/src/worktree/char_bag.rs new file mode 100644 index 0000000000..9e3c5314e9 --- /dev/null +++ b/zed/src/worktree/char_bag.rs @@ -0,0 +1,44 @@ +#[derive(Copy, Clone, Debug)] +pub struct CharBag(u64); + +impl CharBag { + pub fn is_superset(self, other: CharBag) -> bool { + self.0 & other.0 == other.0 + } + + fn insert(&mut self, c: char) { + if c >= 'a' && c <= 'z' { + let mut count = self.0; + let idx = c as u8 - 'a' as u8; + count = count >> (idx * 2); + count = ((count << 1) | 1) & 3; + count = count << idx * 2; + self.0 |= count; + } else if c >= '0' && c <= '9' { + let idx = c as u8 - '0' as u8; + self.0 |= 1 << (idx + 52); + } else if c == '-' { + self.0 |= 1 << 62; + } + } +} + +impl From<&str> for CharBag { + fn from(s: &str) -> Self { + let mut bag = Self(0); + for c in s.chars() { + bag.insert(c); + } + bag + } +} + +impl From<&[char]> for CharBag { + fn from(chars: &[char]) -> Self { + let mut bag = Self(0); + for c in chars { + bag.insert(*c); + } + bag + } +} diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs new file mode 100644 index 0000000000..22f48a15c9 --- /dev/null +++ b/zed/src/worktree/fuzzy.rs @@ -0,0 +1,543 @@ +use gpui::scoped_pool; + +use crate::sum_tree::SeekBias; + +use super::{char_bag::CharBag, Entry, FileCount, Worktree}; + +use std::{ + cmp::{max, min, Ordering, Reverse}, + collections::BinaryHeap, + path::Path, + sync::Arc, +}; + +const BASE_DISTANCE_PENALTY: f64 = 0.6; +const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; +const MIN_DISTANCE_PENALTY: f64 = 0.2; + +#[derive(Clone, Debug)] +pub struct PathEntry { + pub ino: u64, + pub path_chars: CharBag, + pub path: Arc<[char]>, + pub lowercase_path: Arc<[char]>, + pub is_ignored: bool, +} + +impl PathEntry { + pub fn new(ino: u64, path: &Path, is_ignored: bool) -> Self { + let path = path.to_string_lossy(); + let lowercase_path = path.to_lowercase().chars().collect::>().into(); + let path: Arc<[char]> = path.chars().collect::>().into(); + let path_chars = CharBag::from(path.as_ref()); + + Self { + ino, + path_chars, + path, + lowercase_path, + is_ignored, + } + } +} + +#[derive(Clone, Debug)] +pub struct PathMatch { + pub score: f64, + pub positions: Vec, + pub tree_id: usize, + pub entry_id: u64, + pub skipped_prefix_len: usize, +} + +impl PartialEq for PathMatch { + fn eq(&self, other: &Self) -> bool { + self.score.eq(&other.score) + } +} + +impl Eq for PathMatch {} + +impl PartialOrd for PathMatch { + fn partial_cmp(&self, other: &Self) -> Option { + self.score.partial_cmp(&other.score) + } +} + +impl Ord for PathMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Equal) + } +} + +pub fn match_paths<'a, T>( + trees: T, + query: &str, + include_root_name: bool, + include_ignored: bool, + smart_case: bool, + max_results: usize, + pool: scoped_pool::Pool, +) -> Vec +where + T: Clone + Send + Iterator, +{ + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let lowercase_query = &lowercase_query; + let query = &query; + let query_chars = CharBag::from(&lowercase_query[..]); + + let cpus = num_cpus::get(); + let path_count: usize = trees.clone().map(Worktree::file_count).sum(); + let segment_size = (path_count + cpus - 1) / cpus; + let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); + + pool.scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let trees = trees.clone(); + scope.execute(move || { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + + let mut min_score = 0.0; + let mut last_positions = Vec::new(); + last_positions.resize(query.len(), 0); + let mut match_positions = Vec::new(); + match_positions.resize(query.len(), 0); + let mut score_matrix = Vec::new(); + let mut best_position_matrix = Vec::new(); + + let mut tree_start = 0; + for tree in trees { + let tree_end = tree_start + tree.file_count(); + if tree_start < segment_end && segment_start < tree_end { + let start = max(tree_start, segment_start) - tree_start; + let end = min(tree_end, segment_end) - tree_start; + let mut cursor = tree.entries.cursor::<_, ()>(); + cursor.seek(&FileCount(start), SeekBias::Right); + let path_entries = cursor + .filter_map(|e| { + if let Entry::File { path, .. } = e { + Some(path) + } else { + None + } + }) + .take(end - start); + + let skipped_prefix_len = if include_root_name { + 0 + } else if let Some(Entry::Dir { name, .. }) = tree.root_entry() { + let name = name.to_string_lossy(); + if name == "/" { + 1 + } else { + name.chars().count() + 1 + } + } else { + 0 + }; + + match_single_tree_paths( + tree.id, + skipped_prefix_len, + path_entries, + query, + lowercase_query, + query_chars.clone(), + include_ignored, + smart_case, + results, + max_results, + &mut min_score, + &mut match_positions, + &mut last_positions, + &mut score_matrix, + &mut best_position_matrix, + ); + } + if tree_end >= segment_end { + break; + } + tree_start = tree_end; + } + }) + } + }); + + let mut results = segment_results + .into_iter() + .flatten() + .map(|r| r.0) + .collect::>(); + results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); + results.truncate(max_results); + results +} + +fn match_single_tree_paths<'a>( + tree_id: usize, + skipped_prefix_len: usize, + path_entries: impl Iterator, + query: &[char], + lowercase_query: &[char], + query_chars: CharBag, + include_ignored: bool, + smart_case: bool, + results: &mut BinaryHeap>, + max_results: usize, + min_score: &mut f64, + match_positions: &mut Vec, + last_positions: &mut Vec, + score_matrix: &mut Vec>, + best_position_matrix: &mut Vec, +) { + for path_entry in path_entries { + if !include_ignored && path_entry.is_ignored { + continue; + } + + if !path_entry.path_chars.is_superset(query_chars.clone()) { + continue; + } + + if !find_last_positions( + last_positions, + skipped_prefix_len, + &path_entry.lowercase_path, + &lowercase_query[..], + ) { + continue; + } + + let matrix_len = query.len() * (path_entry.path.len() - skipped_prefix_len); + score_matrix.clear(); + score_matrix.resize(matrix_len, None); + best_position_matrix.clear(); + best_position_matrix.resize(matrix_len, skipped_prefix_len); + + let score = score_match( + &query[..], + &lowercase_query[..], + &path_entry.path, + &path_entry.lowercase_path, + skipped_prefix_len, + smart_case, + &last_positions, + score_matrix, + best_position_matrix, + match_positions, + *min_score, + ); + + if score > 0.0 { + results.push(Reverse(PathMatch { + tree_id, + entry_id: path_entry.ino, + score, + positions: match_positions.clone(), + skipped_prefix_len, + })); + if results.len() == max_results { + *min_score = results.peek().unwrap().0.score; + } + } + } +} + +fn find_last_positions( + last_positions: &mut Vec, + skipped_prefix_len: usize, + path: &[char], + query: &[char], +) -> bool { + let mut path = path.iter(); + for (i, char) in query.iter().enumerate().rev() { + if let Some(j) = path.rposition(|c| c == char) { + if j >= skipped_prefix_len { + last_positions[i] = j; + } else { + return false; + } + } else { + return false; + } + } + true +} + +fn score_match( + query: &[char], + query_cased: &[char], + path: &[char], + path_cased: &[char], + skipped_prefix_len: usize, + smart_case: bool, + last_positions: &[usize], + score_matrix: &mut [Option], + best_position_matrix: &mut [usize], + match_positions: &mut [usize], + min_score: f64, +) -> f64 { + let score = recursive_score_match( + query, + query_cased, + path, + path_cased, + skipped_prefix_len, + smart_case, + last_positions, + score_matrix, + best_position_matrix, + min_score, + 0, + skipped_prefix_len, + query.len() as f64, + ) * query.len() as f64; + + if score <= 0.0 { + return 0.0; + } + + let path_len = path.len() - skipped_prefix_len; + let mut cur_start = 0; + for i in 0..query.len() { + match_positions[i] = best_position_matrix[i * path_len + cur_start] - skipped_prefix_len; + cur_start = match_positions[i] + 1; + } + + score +} + +fn recursive_score_match( + query: &[char], + query_cased: &[char], + path: &[char], + path_cased: &[char], + skipped_prefix_len: usize, + smart_case: bool, + last_positions: &[usize], + score_matrix: &mut [Option], + best_position_matrix: &mut [usize], + min_score: f64, + query_idx: usize, + path_idx: usize, + cur_score: f64, +) -> f64 { + if query_idx == query.len() { + return 1.0; + } + + let path_len = path.len() - skipped_prefix_len; + + if let Some(memoized) = score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] { + return memoized; + } + + let mut score = 0.0; + let mut best_position = 0; + + let query_char = query_cased[query_idx]; + let limit = last_positions[query_idx]; + + let mut last_slash = 0; + for j in path_idx..=limit { + let path_char = path_cased[j]; + let is_path_sep = path_char == '/' || path_char == '\\'; + + if query_idx == 0 && is_path_sep { + last_slash = j; + } + + if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let mut char_score = 1.0; + if j > path_idx { + let last = path[j - 1]; + let curr = path[j]; + + if last == '/' { + char_score = 0.9; + } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { + char_score = 0.8; + } else if last.is_lowercase() && curr.is_uppercase() { + char_score = 0.8; + } else if last == '.' { + char_score = 0.7; + } else if query_idx == 0 { + char_score = BASE_DISTANCE_PENALTY; + } else { + char_score = MIN_DISTANCE_PENALTY.max( + BASE_DISTANCE_PENALTY + - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, + ); + } + } + + // Apply a severe penalty if the case doesn't match. + // This will make the exact matches have higher score than the case-insensitive and the + // path insensitive matches. + if (smart_case || path[j] == '/') && query[query_idx] != path[j] { + char_score *= 0.001; + } + + let mut multiplier = char_score; + + // Scale the score based on how deep within the patch we found the match. + if query_idx == 0 { + multiplier /= (path.len() - last_slash) as f64; + } + + let mut next_score = 1.0; + if min_score > 0.0 { + next_score = cur_score * multiplier; + // Scores only decrease. If we can't pass the previous best, bail + if next_score < min_score { + // Ensure that score is non-zero so we use it in the memo table. + if score == 0.0 { + score = 1e-18; + } + continue; + } + } + + let new_score = recursive_score_match( + query, + query_cased, + path, + path_cased, + skipped_prefix_len, + smart_case, + last_positions, + score_matrix, + best_position_matrix, + min_score, + query_idx + 1, + j + 1, + next_score, + ) * multiplier; + + if new_score > score { + score = new_score; + best_position = j; + // Optimization: can't score better than 1. + if new_score == 1.0 { + break; + } + } + } + } + + if best_position != 0 { + best_position_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = best_position; + } + + score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = Some(score); + score +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_match_path_entries() { + let paths = vec![ + "", + "a", + "ab", + "abC", + "abcd", + "alphabravocharlie", + "AlphaBravoCharlie", + "thisisatestdir", + "/////ThisIsATestDir", + "/this/is/a/test/dir", + "/test/tiatd", + ]; + + assert_eq!( + match_query("abc", false, &paths), + vec![ + ("abC", vec![0, 1, 2]), + ("abcd", vec![0, 1, 2]), + ("AlphaBravoCharlie", vec![0, 5, 10]), + ("alphabravocharlie", vec![4, 5, 10]), + ] + ); + assert_eq!( + match_query("t/i/a/t/d", false, &paths), + vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),] + ); + + assert_eq!( + match_query("tiatd", false, &paths), + vec![ + ("/test/tiatd", vec![6, 7, 8, 9, 10]), + ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]), + ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]), + ("thisisatestdir", vec![0, 2, 6, 7, 11]), + ] + ); + } + + fn match_query<'a>( + query: &str, + smart_case: bool, + paths: &Vec<&'a str>, + ) -> Vec<(&'a str, Vec)> { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_chars = CharBag::from(&lowercase_query[..]); + + let mut path_entries = Vec::new(); + for (i, path) in paths.iter().enumerate() { + let lowercase_path: Arc<[char]> = + path.to_lowercase().chars().collect::>().into(); + let path_chars = CharBag::from(lowercase_path.as_ref()); + let path = path.chars().collect(); + path_entries.push(PathEntry { + ino: i as u64, + path_chars, + path, + lowercase_path, + is_ignored: false, + }); + } + + let mut match_positions = Vec::new(); + let mut last_positions = Vec::new(); + match_positions.resize(query.len(), 0); + last_positions.resize(query.len(), 0); + + let mut results = BinaryHeap::new(); + match_single_tree_paths( + 0, + 0, + path_entries.iter(), + &query[..], + &lowercase_query[..], + query_chars, + true, + smart_case, + &mut results, + 100, + &mut 0.0, + &mut match_positions, + &mut last_positions, + &mut Vec::new(), + &mut Vec::new(), + ); + + results + .into_iter() + .rev() + .map(|result| { + ( + paths[result.0.entry_id as usize].clone(), + result.0.positions, + ) + }) + .collect() + } +} From 497dedbb84f9105751f62a8ebdd54d1f634d1a54 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Apr 2021 11:22:00 -0700 Subject: [PATCH 017/102] Get file IO test passing on new worktree Co-Authored-By: Antonio Scandurra --- zed/src/worktree.rs | 90 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index a4a50d8eb1..d9a8d1e028 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,17 +1,22 @@ mod char_bag; mod fuzzy; -use crate::sum_tree::{self, Edit, SumTree}; +use crate::{ + editor::Snapshot, + sum_tree::{self, Edit, SumTree}, +}; use anyhow::{anyhow, Result}; pub use fuzzy::match_paths; use fuzzy::PathEntry; -use gpui::{scoped_pool, Entity, ModelContext}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; +use std::future::Future; use std::{ ffi::OsStr, - fmt, fs, io, + fmt, fs, + io::{self, Read, Write}, ops::AddAssign, os::unix::fs::MetadataExt, path::{Path, PathBuf}, @@ -29,6 +34,11 @@ enum ScanState { Err(io::Error), } +pub struct FilesIterItem { + pub ino: u64, + pub path: PathBuf, +} + pub struct Worktree { id: usize, path: Arc, @@ -97,6 +107,19 @@ impl Worktree { } } + fn files<'a>(&'a self) -> impl Iterator + 'a { + self.entries.cursor::<(), ()>().filter_map(|e| { + if let Entry::File { path, ino, .. } = e { + Some(FilesIterItem { + ino: *ino, + path: PathBuf::from(path.path.iter().collect::()), + }) + } else { + None + } + }) + } + fn root_entry(&self) -> Option<&Entry> { self.root_ino().and_then(|ino| self.entries.get(&ino)) } @@ -106,7 +129,10 @@ impl Worktree { } fn abs_entry_path(&self, ino: u64) -> Result { - Ok(self.path.join(self.entry_path(ino)?)) + let mut result = self.path.to_path_buf(); + result.pop(); + result.push(self.entry_path(ino)?); + Ok(result) } fn entry_path(&self, ino: u64) -> Result { @@ -128,6 +154,31 @@ impl Worktree { Ok(path) } + pub fn load_file(&self, ino: u64, ctx: &AppContext) -> impl Future> { + let path = self.abs_entry_path(ino); + ctx.background_executor().spawn(async move { + let mut file = std::fs::File::open(&path?)?; + let mut base_text = String::new(); + file.read_to_string(&mut base_text)?; + Ok(base_text) + }) + } + + pub fn save<'a>(&self, ino: u64, content: Snapshot, ctx: &AppContext) -> Task> { + let path = self.abs_entry_path(ino); + eprintln!("save to path: {:?}", path); + ctx.background_executor().spawn(async move { + let buffer_size = content.text_summary().bytes.min(10 * 1024); + let file = std::fs::File::create(&path?)?; + let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); + for chunk in content.fragments() { + writer.write(chunk.as_bytes())?; + } + writer.flush()?; + Ok(()) + }) + } + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { match self.entries.get(&ino).unwrap() { Entry::Dir { name, children, .. } => { @@ -550,4 +601,35 @@ mod tests { }) }); } + + #[test] + fn test_save_file() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); + + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; + + let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); + + let entry = app.read(|ctx| { + let entry = tree.read(ctx).files().next().unwrap(); + assert_eq!(entry.path.file_name().unwrap(), "file1"); + entry + }); + let file_ino = entry.ino; + + tree.update(&mut app, |tree, ctx| { + smol::block_on(tree.save(file_ino, buffer.snapshot(), ctx.as_ref())).unwrap() + }); + + let loaded_text = app + .read(|ctx| tree.read(ctx).load_file(file_ino, ctx)) + .await + .unwrap(); + assert_eq!(loaded_text, buffer.text()); + }); + } } From fbaab121f61781c7166f401ce3e30bddf1f9a81a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Apr 2021 16:29:55 -0700 Subject: [PATCH 018/102] Add fsevent crate to workspace Co-Authored-By: Nathan Sobo --- Cargo.lock | 19 ++++ Cargo.toml | 2 +- fsevent/Cargo.toml | 15 +++ fsevent/src/lib.rs | 266 +++++++++++++++++++++++++++++++++++++++++++++ zed/Cargo.toml | 1 + 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 fsevent/Cargo.toml create mode 100644 fsevent/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5b6551603a..e51e1aaaf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,24 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fsevent" +version = "2.0.2" +dependencies = [ + "bitflags", + "fsevent-sys", + "tempdir", +] + +[[package]] +name = "fsevent-sys" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a29c77f1ca394c3e73a9a5d24cfcabb734682d9634fc398f2204a63c994120" +dependencies = [ + "libc", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2433,6 +2451,7 @@ dependencies = [ "crossbeam-channel 0.5.0", "dirs", "easy-parallel", + "fsevent", "futures-core", "gpui", "ignore", diff --git a/Cargo.toml b/Cargo.toml index b60e33d042..96be78508c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["zed", "gpui"] +members = ["zed", "gpui", "fsevent"] [patch.crates-io] async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"} diff --git a/fsevent/Cargo.toml b/fsevent/Cargo.toml new file mode 100644 index 0000000000..cd7b2f0e7f --- /dev/null +++ b/fsevent/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fsevent" +version = "2.0.2" +license = "MIT" +edition = "2018" + +[dependencies] +bitflags = "1" +fsevent-sys = "3.0.2" + +[dev-dependencies] +tempdir = "0.3.7" + +[package.metadata.docs.rs] +targets = ["x86_64-apple-darwin"] diff --git a/fsevent/src/lib.rs b/fsevent/src/lib.rs new file mode 100644 index 0000000000..09ef812679 --- /dev/null +++ b/fsevent/src/lib.rs @@ -0,0 +1,266 @@ +#![cfg(target_os = "macos")] + +use bitflags::bitflags; +use fsevent_sys::{self as fs, core_foundation as cf}; +use std::{ + convert::AsRef, + ffi::{c_void, CStr, OsStr}, + os::unix::ffi::OsStrExt, + path::{Path, PathBuf}, + slice, + sync::mpsc::Sender, + time::Duration, +}; + +#[derive(Debug)] +pub struct Event { + pub event_id: u64, + pub flags: StreamFlags, + pub path: PathBuf, +} + +pub struct EventStream { + stream: fs::FSEventStreamRef, + _sender: Box>>, +} + +unsafe impl Send for EventStream {} + +impl EventStream { + pub fn new(paths: &[&Path], latency: Duration, event_sender: Sender>) -> Self { + unsafe { + let sender = Box::new(event_sender); + let stream_context = fs::FSEventStreamContext { + version: 0, + info: sender.as_ref() as *const _ as *mut c_void, + retain: None, + release: None, + copy_description: None, + }; + + let cf_paths = + cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks); + assert!(!cf_paths.is_null()); + + for path in paths { + let path_bytes = path.as_os_str().as_bytes(); + let cf_url = cf::CFURLCreateFromFileSystemRepresentation( + cf::kCFAllocatorDefault, + path_bytes.as_ptr() as *const i8, + path_bytes.len() as cf::CFIndex, + false, + ); + let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle); + cf::CFArrayAppendValue(cf_paths, cf_path); + cf::CFRelease(cf_path); + cf::CFRelease(cf_url); + } + + let stream = fs::FSEventStreamCreate( + cf::kCFAllocatorDefault, + callback, + &stream_context, + cf_paths, + fs::kFSEventStreamEventIdSinceNow, + latency.as_secs_f64(), + fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, + ); + cf::CFRelease(cf_paths); + + EventStream { + stream, + _sender: sender, + } + } + } + + pub fn run(self) { + unsafe { + fs::FSEventStreamScheduleWithRunLoop( + self.stream, + cf::CFRunLoopGetCurrent(), + cf::kCFRunLoopDefaultMode, + ); + + fs::FSEventStreamStart(self.stream); + cf::CFRunLoopRun(); + + fs::FSEventStreamFlushSync(self.stream); + fs::FSEventStreamStop(self.stream); + fs::FSEventStreamRelease(self.stream); + } + } +} + +extern "C" fn callback( + stream_ref: fs::FSEventStreamRef, + info: *mut ::std::os::raw::c_void, + num: usize, // size_t numEvents + event_paths: *mut ::std::os::raw::c_void, // void *eventPaths + event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[] + event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[] +) { + unsafe { + let event_paths = event_paths as *const *const ::std::os::raw::c_char; + let e_ptr = event_flags as *mut u32; + let i_ptr = event_ids as *mut u64; + let sender = (info as *mut Sender>).as_mut().unwrap(); + + let paths = slice::from_raw_parts(event_paths, num); + let flags = slice::from_raw_parts_mut(e_ptr, num); + let ids = slice::from_raw_parts_mut(i_ptr, num); + + let mut events = Vec::with_capacity(num); + for p in 0..num { + let path_c_str = CStr::from_ptr(paths[p]); + let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes())); + if let Some(flag) = StreamFlags::from_bits(flags[p]) { + events.push(Event { + event_id: ids[p], + flags: flag, + path, + }); + } else { + debug_assert!(false, "unknown flag set for fs event: {}", flags[p]); + } + } + + if sender.send(events).is_err() { + fs::FSEventStreamStop(stream_ref); + cf::CFRunLoopStop(cf::CFRunLoopGetCurrent()); + } + } +} + +// Synchronize with +// /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h +bitflags! { + #[repr(C)] + pub struct StreamFlags: u32 { + const NONE = 0x00000000; + const MUST_SCAN_SUBDIRS = 0x00000001; + const USER_DROPPED = 0x00000002; + const KERNEL_DROPPED = 0x00000004; + const IDS_WRAPPED = 0x00000008; + const HISTORY_DONE = 0x00000010; + const ROOT_CHANGED = 0x00000020; + const MOUNT = 0x00000040; + const UNMOUNT = 0x00000080; + const ITEM_CREATED = 0x00000100; + const ITEM_REMOVED = 0x00000200; + const INODE_META_MOD = 0x00000400; + const ITEM_RENAMED = 0x00000800; + const ITEM_MODIFIED = 0x00001000; + const FINDER_INFO_MOD = 0x00002000; + const ITEM_CHANGE_OWNER = 0x00004000; + const ITEM_XATTR_MOD = 0x00008000; + const IS_FILE = 0x00010000; + const IS_DIR = 0x00020000; + const IS_SYMLINK = 0x00040000; + const OWN_EVENT = 0x00080000; + const IS_HARDLINK = 0x00100000; + const IS_LAST_HARDLINK = 0x00200000; + const ITEM_CLONED = 0x400000; + } +} + +impl std::fmt::Display for StreamFlags { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.contains(StreamFlags::MUST_SCAN_SUBDIRS) { + let _d = write!(f, "MUST_SCAN_SUBDIRS "); + } + if self.contains(StreamFlags::USER_DROPPED) { + let _d = write!(f, "USER_DROPPED "); + } + if self.contains(StreamFlags::KERNEL_DROPPED) { + let _d = write!(f, "KERNEL_DROPPED "); + } + if self.contains(StreamFlags::IDS_WRAPPED) { + let _d = write!(f, "IDS_WRAPPED "); + } + if self.contains(StreamFlags::HISTORY_DONE) { + let _d = write!(f, "HISTORY_DONE "); + } + if self.contains(StreamFlags::ROOT_CHANGED) { + let _d = write!(f, "ROOT_CHANGED "); + } + if self.contains(StreamFlags::MOUNT) { + let _d = write!(f, "MOUNT "); + } + if self.contains(StreamFlags::UNMOUNT) { + let _d = write!(f, "UNMOUNT "); + } + if self.contains(StreamFlags::ITEM_CREATED) { + let _d = write!(f, "ITEM_CREATED "); + } + if self.contains(StreamFlags::ITEM_REMOVED) { + let _d = write!(f, "ITEM_REMOVED "); + } + if self.contains(StreamFlags::INODE_META_MOD) { + let _d = write!(f, "INODE_META_MOD "); + } + if self.contains(StreamFlags::ITEM_RENAMED) { + let _d = write!(f, "ITEM_RENAMED "); + } + if self.contains(StreamFlags::ITEM_MODIFIED) { + let _d = write!(f, "ITEM_MODIFIED "); + } + if self.contains(StreamFlags::FINDER_INFO_MOD) { + let _d = write!(f, "FINDER_INFO_MOD "); + } + if self.contains(StreamFlags::ITEM_CHANGE_OWNER) { + let _d = write!(f, "ITEM_CHANGE_OWNER "); + } + if self.contains(StreamFlags::ITEM_XATTR_MOD) { + let _d = write!(f, "ITEM_XATTR_MOD "); + } + if self.contains(StreamFlags::IS_FILE) { + let _d = write!(f, "IS_FILE "); + } + if self.contains(StreamFlags::IS_DIR) { + let _d = write!(f, "IS_DIR "); + } + if self.contains(StreamFlags::IS_SYMLINK) { + let _d = write!(f, "IS_SYMLINK "); + } + if self.contains(StreamFlags::OWN_EVENT) { + let _d = write!(f, "OWN_EVENT "); + } + if self.contains(StreamFlags::IS_LAST_HARDLINK) { + let _d = write!(f, "IS_LAST_HARDLINK "); + } + if self.contains(StreamFlags::IS_HARDLINK) { + let _d = write!(f, "IS_HARDLINK "); + } + if self.contains(StreamFlags::ITEM_CLONED) { + let _d = write!(f, "ITEM_CLONED "); + } + write!(f, "") + } +} + +#[test] +fn test_observe() { + use std::{fs, sync::mpsc, time::Duration}; + use tempdir::TempDir; + + let dir = TempDir::new("test_observe").unwrap(); + let path = dir.path().canonicalize().unwrap(); + fs::write(path.join("a"), "a contents").unwrap(); + + let (tx, rx) = mpsc::channel(); + let stream = EventStream::new(&[&path], Duration::from_millis(50), tx); + std::thread::spawn(move || stream.run()); + + fs::write(path.join("b"), "b contents").unwrap(); + let events = rx.recv_timeout(Duration::from_millis(500)).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.path, path.join("b")); + assert!(event.flags.contains(StreamFlags::ITEM_CREATED)); + + fs::remove_file(path.join("a")).unwrap(); + let events = rx.recv_timeout(Duration::from_millis(500)).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.path, path.join("a")); + assert!(event.flags.contains(StreamFlags::ITEM_REMOVED)); +} diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 846d6d729e..981f09c2b8 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -19,6 +19,7 @@ crossbeam-channel = "0.5.0" dirs = "3.0" easy-parallel = "3.1.0" futures-core = "0.3" +fsevent = {path = "../fsevent"} gpui = {path = "../gpui"} ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"} lazy_static = "1.4.0" From 6d3dc85dadc60b18873ed580d8a6d869177d4084 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Apr 2021 16:46:34 -0700 Subject: [PATCH 019/102] Tweak fsevent flags Co-Authored-By: Nathan Sobo --- fsevent/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fsevent/src/lib.rs b/fsevent/src/lib.rs index 09ef812679..d4b5fb5b25 100644 --- a/fsevent/src/lib.rs +++ b/fsevent/src/lib.rs @@ -63,7 +63,9 @@ impl EventStream { cf_paths, fs::kFSEventStreamEventIdSinceNow, latency.as_secs_f64(), - fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, + fs::kFSEventStreamCreateFlagFileEvents + | fs::kFSEventStreamCreateFlagNoDefer + | fs::kFSEventStreamCreateFlagWatchRoot, ); cf::CFRelease(cf_paths); @@ -263,4 +265,8 @@ fn test_observe() { let event = events.last().unwrap(); assert_eq!(event.path, path.join("a")); assert!(event.flags.contains(StreamFlags::ITEM_REMOVED)); + + let dir2 = TempDir::new("test_observe2").unwrap(); + fs::rename(path, dir2.path().join("something")).unwrap(); + let events = rx.recv_timeout(Duration::from_millis(500)).unwrap(); } From 4878bf82ff57e21fc1c3e64c284a6fcef31d3c45 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Apr 2021 17:38:52 -0700 Subject: [PATCH 020/102] Make EventStream interface more flexible Take a callback instead of an mpsc Sender. The run method blocks and invokes the callback for each batch of events. The caller controls the threading. The callback can return false to terminate the event stream. Co-Authored-By: Nathan Sobo --- fsevent/src/lib.rs | 100 ++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/fsevent/src/lib.rs b/fsevent/src/lib.rs index d4b5fb5b25..8f81b2e016 100644 --- a/fsevent/src/lib.rs +++ b/fsevent/src/lib.rs @@ -8,31 +8,33 @@ use std::{ os::unix::ffi::OsStrExt, path::{Path, PathBuf}, slice, - sync::mpsc::Sender, time::Duration, }; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Event { pub event_id: u64, pub flags: StreamFlags, pub path: PathBuf, } -pub struct EventStream { +pub struct EventStream { stream: fs::FSEventStreamRef, - _sender: Box>>, + _callback: Box, } -unsafe impl Send for EventStream {} +unsafe impl Send for EventStream {} -impl EventStream { - pub fn new(paths: &[&Path], latency: Duration, event_sender: Sender>) -> Self { +impl EventStream +where + F: FnMut(&[Event]) -> bool, +{ + pub fn new(paths: &[&Path], latency: Duration, callback: F) -> Self { unsafe { - let sender = Box::new(event_sender); + let callback = Box::new(callback); let stream_context = fs::FSEventStreamContext { version: 0, - info: sender.as_ref() as *const _ as *mut c_void, + info: callback.as_ref() as *const _ as *mut c_void, retain: None, release: None, copy_description: None, @@ -58,7 +60,7 @@ impl EventStream { let stream = fs::FSEventStreamCreate( cf::kCFAllocatorDefault, - callback, + Self::trampoline, &stream_context, cf_paths, fs::kFSEventStreamEventIdSinceNow, @@ -71,7 +73,7 @@ impl EventStream { EventStream { stream, - _sender: sender, + _callback: callback, } } } @@ -92,44 +94,44 @@ impl EventStream { fs::FSEventStreamRelease(self.stream); } } -} -extern "C" fn callback( - stream_ref: fs::FSEventStreamRef, - info: *mut ::std::os::raw::c_void, - num: usize, // size_t numEvents - event_paths: *mut ::std::os::raw::c_void, // void *eventPaths - event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[] - event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[] -) { - unsafe { - let event_paths = event_paths as *const *const ::std::os::raw::c_char; - let e_ptr = event_flags as *mut u32; - let i_ptr = event_ids as *mut u64; - let sender = (info as *mut Sender>).as_mut().unwrap(); + extern "C" fn trampoline( + stream_ref: fs::FSEventStreamRef, + info: *mut ::std::os::raw::c_void, + num: usize, // size_t numEvents + event_paths: *mut ::std::os::raw::c_void, // void *eventPaths + event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[] + event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[] + ) { + unsafe { + let event_paths = event_paths as *const *const ::std::os::raw::c_char; + let e_ptr = event_flags as *mut u32; + let i_ptr = event_ids as *mut u64; + let callback = (info as *mut F).as_mut().unwrap(); - let paths = slice::from_raw_parts(event_paths, num); - let flags = slice::from_raw_parts_mut(e_ptr, num); - let ids = slice::from_raw_parts_mut(i_ptr, num); + let paths = slice::from_raw_parts(event_paths, num); + let flags = slice::from_raw_parts_mut(e_ptr, num); + let ids = slice::from_raw_parts_mut(i_ptr, num); - let mut events = Vec::with_capacity(num); - for p in 0..num { - let path_c_str = CStr::from_ptr(paths[p]); - let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes())); - if let Some(flag) = StreamFlags::from_bits(flags[p]) { - events.push(Event { - event_id: ids[p], - flags: flag, - path, - }); - } else { - debug_assert!(false, "unknown flag set for fs event: {}", flags[p]); + let mut events = Vec::with_capacity(num); + for p in 0..num { + let path_c_str = CStr::from_ptr(paths[p]); + let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes())); + if let Some(flag) = StreamFlags::from_bits(flags[p]) { + events.push(Event { + event_id: ids[p], + flags: flag, + path, + }); + } else { + debug_assert!(false, "unknown flag set for fs event: {}", flags[p]); + } } - } - if sender.send(events).is_err() { - fs::FSEventStreamStop(stream_ref); - cf::CFRunLoopStop(cf::CFRunLoopGetCurrent()); + if !callback(&events) { + fs::FSEventStreamStop(stream_ref); + cf::CFRunLoopStop(cf::CFRunLoopGetCurrent()); + } } } } @@ -242,7 +244,7 @@ impl std::fmt::Display for StreamFlags { } #[test] -fn test_observe() { +fn test_event_stream() { use std::{fs, sync::mpsc, time::Duration}; use tempdir::TempDir; @@ -251,7 +253,9 @@ fn test_observe() { fs::write(path.join("a"), "a contents").unwrap(); let (tx, rx) = mpsc::channel(); - let stream = EventStream::new(&[&path], Duration::from_millis(50), tx); + let stream = EventStream::new(&[&path], Duration::from_millis(50), move |events| { + tx.send(events.to_vec()).is_ok() + }); std::thread::spawn(move || stream.run()); fs::write(path.join("b"), "b contents").unwrap(); @@ -265,8 +269,4 @@ fn test_observe() { let event = events.last().unwrap(); assert_eq!(event.path, path.join("a")); assert!(event.flags.contains(StreamFlags::ITEM_REMOVED)); - - let dir2 = TempDir::new("test_observe2").unwrap(); - fs::rename(path, dir2.path().join("something")).unwrap(); - let events = rx.recv_timeout(Duration::from_millis(500)).unwrap(); } From 3fa4e5acee94e2e87bc263b8fea2532014bfd963 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 15 Apr 2021 17:39:32 -0700 Subject: [PATCH 021/102] Fill out some missing parts of the new worktree module Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 128 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 13 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d9a8d1e028..db45fb79c3 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::{anyhow, Result}; pub use fuzzy::match_paths; use fuzzy::PathEntry; -use gpui::{scoped_pool, AppContext, Entity, ModelContext, Task}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; @@ -27,6 +27,8 @@ use std::{ time::Duration, }; +pub use fuzzy::PathMatch; + #[derive(Debug)] enum ScanState { Idle, @@ -48,6 +50,12 @@ pub struct Worktree { poll_scheduled: bool, } +#[derive(Clone)] +pub struct FileHandle { + worktree: ModelHandle, + inode: u64, +} + impl Worktree { fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let path = path.into(); @@ -98,7 +106,7 @@ impl Worktree { } } - fn root_ino(&self) -> Option { + fn root_inode(&self) -> Option { let ino = self.scanner.root_ino.load(atomic::Ordering::SeqCst); if ino == 0 { None @@ -121,21 +129,20 @@ impl Worktree { } fn root_entry(&self) -> Option<&Entry> { - self.root_ino().and_then(|ino| self.entries.get(&ino)) + self.root_inode().and_then(|ino| self.entries.get(&ino)) } fn file_count(&self) -> usize { self.entries.summary().file_count } - fn abs_entry_path(&self, ino: u64) -> Result { + fn abs_path_for_inode(&self, ino: u64) -> Result { let mut result = self.path.to_path_buf(); - result.pop(); - result.push(self.entry_path(ino)?); + result.push(self.path_for_inode(ino, false)?); Ok(result) } - fn entry_path(&self, ino: u64) -> Result { + pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result { let mut components = Vec::new(); let mut entry = self .entries @@ -147,15 +154,39 @@ impl Worktree { components.push(entry.name()); } + let mut components = components.into_iter().rev(); + if !include_root { + components.next(); + } + let mut path = PathBuf::new(); - for component in components.into_iter().rev() { + for component in components { path.push(component); } Ok(path) } + fn inode_for_path(&self, path: impl AsRef) -> Option { + let path = path.as_ref(); + self.root_inode().and_then(|mut inode| { + 'components: for path_component in path { + if let Some(Entry::Dir { children, .. }) = &self.entries.get(&inode) { + for child in children.as_ref() { + if self.entries.get(child).map(|entry| entry.name()) == Some(path_component) + { + inode = *child; + continue 'components; + } + } + } + return None; + } + Some(inode) + }) + } + pub fn load_file(&self, ino: u64, ctx: &AppContext) -> impl Future> { - let path = self.abs_entry_path(ino); + let path = self.abs_path_for_inode(ino); ctx.background_executor().spawn(async move { let mut file = std::fs::File::open(&path?)?; let mut base_text = String::new(); @@ -165,7 +196,7 @@ impl Worktree { } pub fn save<'a>(&self, ino: u64, content: Snapshot, ctx: &AppContext) -> Task> { - let path = self.abs_entry_path(ino); + let path = self.abs_path_for_inode(ino); eprintln!("save to path: {:?}", path); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); @@ -211,7 +242,7 @@ impl Entity for Worktree { impl fmt::Debug for Worktree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(root_ino) = self.root_ino() { + if let Some(root_ino) = self.root_inode() { self.fmt_entry(f, root_ino, 0) } else { write!(f, "Empty tree\n") @@ -219,6 +250,28 @@ impl fmt::Debug for Worktree { } } +impl FileHandle { + pub fn path(&self, ctx: &AppContext) -> PathBuf { + self.worktree + .read(ctx) + .path_for_inode(self.inode, false) + .unwrap() + } + + pub fn load(&self, ctx: &AppContext) -> impl Future> { + self.worktree.read(ctx).load_file(self.inode, ctx) + } + + pub fn save<'a>(&self, content: Snapshot, ctx: &AppContext) -> Task> { + let worktree = self.worktree.read(ctx); + worktree.save(self.inode, content, ctx) + } + + pub fn entry_id(&self) -> (usize, u64) { + (self.worktree.id(), self.inode) + } +} + #[derive(Clone, Debug)] pub enum Entry { Dir { @@ -339,6 +392,14 @@ impl BackgroundScanner { } fn run(&self) { + let event_stream = fsevent::EventStream::new( + &[self.path.as_ref()], + Duration::from_millis(100), + |events| { + eprintln!("events: {:?}", events); + }, + ); + if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return; } @@ -353,7 +414,7 @@ impl BackgroundScanner { return; } - // TODO: Update when dir changes + event_stream.run(); } fn scan_dirs(&self) -> io::Result<()> { @@ -588,7 +649,7 @@ mod tests { ctx.thread_pool().clone(), ) .iter() - .map(|result| tree.entry_path(result.entry_id)) + .map(|result| tree.path_for_inode(result.entry_id, true)) .collect::, _>>() .unwrap(); assert_eq!( @@ -632,4 +693,45 @@ mod tests { assert_eq!(loaded_text, buffer.text()); }); } + + #[test] + fn test_rescan() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + "dir1": { + "file": "contents" + }, + "dir2": { + } + })); + + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; + + let file_entry = app.read(|ctx| tree.read(ctx).inode_for_path("dir1/file").unwrap()); + app.read(|ctx| { + let tree = tree.read(ctx); + assert_eq!( + tree.path_for_inode(file_entry, false) + .unwrap() + .to_str() + .unwrap(), + "dir1/file" + ); + }); + + std::fs::rename(dir.path().join("dir1/file"), dir.path().join("dir2/file")).unwrap(); + assert_condition(1, 300, || { + app.read(|ctx| { + let tree = tree.read(ctx); + tree.path_for_inode(file_entry, false) + .unwrap() + .to_str() + .unwrap() + == "dir2/file" + }) + }) + .await + }); + } } From 0caf908c7827f6ab4423cb0888c90a3332719b86 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 20:28:10 -0600 Subject: [PATCH 022/102] Remove unused timer module --- zed/src/lib.rs | 1 - zed/src/timer.rs | 42 ------------------------------------------ 2 files changed, 43 deletions(-) delete mode 100644 zed/src/timer.rs diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 16307bc19a..9bf663d076 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -9,7 +9,6 @@ mod sum_tree; mod test; mod throttle; mod time; -mod timer; mod util; pub mod watch; pub mod workspace; diff --git a/zed/src/timer.rs b/zed/src/timer.rs deleted file mode 100644 index de3f9e17b0..0000000000 --- a/zed/src/timer.rs +++ /dev/null @@ -1,42 +0,0 @@ -use smol::prelude::*; -use std::{ - pin::Pin, - task::Poll, - time::{Duration, Instant}, -}; - -pub struct Repeat { - timer: smol::Timer, - period: Duration, -} - -impl Stream for Repeat { - type Item = Instant; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - match self.as_mut().timer().poll(cx) { - Poll::Ready(instant) => { - let period = self.as_ref().period; - self.as_mut().timer().set_after(period); - Poll::Ready(Some(instant)) - } - Poll::Pending => Poll::Pending, - } - } -} - -impl Repeat { - fn timer(self: std::pin::Pin<&mut Self>) -> Pin<&mut smol::Timer> { - unsafe { self.map_unchecked_mut(|s| &mut s.timer) } - } -} - -pub fn repeat(period: Duration) -> Repeat { - Repeat { - timer: smol::Timer::after(period), - period, - } -} From e55abc42205288544589b230781d0624245fdbfa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 20:28:20 -0600 Subject: [PATCH 023/102] Silence warning about binary name --- zed/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zed/src/main.rs b/zed/src/main.rs index 3aaddff884..773acf147e 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -1,3 +1,6 @@ +// Allow binary to be called Zed for a nice application menu when running executable direcly +#![allow(non_snake_case)] + use fs::OpenOptions; use log::LevelFilter; use simplelog::SimpleLogger; From 5648c67d5437558aad8667fcfc5c70bd276c6446 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 20:29:45 -0600 Subject: [PATCH 024/102] Perform path matching on Worktree snapshots We're going to need something that can be moved to a background thread. Worktree used to be easy to clone, but that's no longer really true. Instead we can take a snapshot. --- zed/src/worktree.rs | 40 ++++++++++++++++++++++++++++++++++----- zed/src/worktree/fuzzy.rs | 20 ++++++++++---------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index db45fb79c3..6e5191b760 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -2,7 +2,7 @@ mod char_bag; mod fuzzy; use crate::{ - editor::Snapshot, + editor::Snapshot as BufferSnapshot, sum_tree::{self, Edit, SumTree}, }; use anyhow::{anyhow, Result}; @@ -50,6 +50,12 @@ pub struct Worktree { poll_scheduled: bool, } +pub struct Snapshot { + id: usize, + root_inode: Option, + entries: SumTree, +} + #[derive(Clone)] pub struct FileHandle { worktree: ModelHandle, @@ -57,7 +63,7 @@ pub struct FileHandle { } impl Worktree { - fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { + pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let path = path.into(); let scan_state = smol::channel::unbounded(); let scanner = BackgroundScanner::new(path.clone(), scan_state.0); @@ -79,6 +85,14 @@ impl Worktree { tree } + pub fn snapshot(&self) -> Snapshot { + Snapshot { + id: self.id, + root_inode: self.root_inode(), + entries: self.entries.clone(), + } + } + fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { self.scan_state = scan_state; self.poll_entries(ctx); @@ -195,7 +209,12 @@ impl Worktree { }) } - pub fn save<'a>(&self, ino: u64, content: Snapshot, ctx: &AppContext) -> Task> { + pub fn save<'a>( + &self, + ino: u64, + content: BufferSnapshot, + ctx: &AppContext, + ) -> Task> { let path = self.abs_path_for_inode(ino); eprintln!("save to path: {:?}", path); ctx.background_executor().spawn(async move { @@ -250,6 +269,16 @@ impl fmt::Debug for Worktree { } } +impl Snapshot { + pub fn file_count(&self) -> usize { + self.entries.summary().file_count + } + + pub fn root_entry(&self) -> Option<&Entry> { + self.root_inode.and_then(|inode| self.entries.get(&inode)) + } +} + impl FileHandle { pub fn path(&self, ctx: &AppContext) -> PathBuf { self.worktree @@ -262,7 +291,7 @@ impl FileHandle { self.worktree.read(ctx).load_file(self.inode, ctx) } - pub fn save<'a>(&self, content: Snapshot, ctx: &AppContext) -> Task> { + pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task> { let worktree = self.worktree.read(ctx); worktree.save(self.inode, content, ctx) } @@ -397,6 +426,7 @@ impl BackgroundScanner { Duration::from_millis(100), |events| { eprintln!("events: {:?}", events); + true }, ); @@ -640,7 +670,7 @@ mod tests { app.read(|ctx| { let tree = tree.read(ctx); let results = match_paths( - Some(tree).into_iter(), + Some(tree.snapshot()).iter(), "bna", false, false, diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 22f48a15c9..1b03fbb769 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -2,7 +2,7 @@ use gpui::scoped_pool; use crate::sum_tree::SeekBias; -use super::{char_bag::CharBag, Entry, FileCount, Worktree}; +use super::{char_bag::CharBag, Entry, FileCount, Snapshot, Worktree}; use std::{ cmp::{max, min, Ordering, Reverse}, @@ -71,7 +71,7 @@ impl Ord for PathMatch { } pub fn match_paths<'a, T>( - trees: T, + snapshots: T, query: &str, include_root_name: bool, include_ignored: bool, @@ -80,7 +80,7 @@ pub fn match_paths<'a, T>( pool: scoped_pool::Pool, ) -> Vec where - T: Clone + Send + Iterator, + T: Clone + Send + Iterator, { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -89,13 +89,13 @@ where let query_chars = CharBag::from(&lowercase_query[..]); let cpus = num_cpus::get(); - let path_count: usize = trees.clone().map(Worktree::file_count).sum(); + let path_count: usize = snapshots.clone().map(Snapshot::file_count).sum(); let segment_size = (path_count + cpus - 1) / cpus; let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); pool.scoped(|scope| { for (segment_idx, results) in segment_results.iter_mut().enumerate() { - let trees = trees.clone(); + let trees = snapshots.clone(); scope.execute(move || { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; @@ -109,12 +109,12 @@ where let mut best_position_matrix = Vec::new(); let mut tree_start = 0; - for tree in trees { - let tree_end = tree_start + tree.file_count(); + for snapshot in trees { + let tree_end = tree_start + snapshot.file_count(); if tree_start < segment_end && segment_start < tree_end { let start = max(tree_start, segment_start) - tree_start; let end = min(tree_end, segment_end) - tree_start; - let mut cursor = tree.entries.cursor::<_, ()>(); + let mut cursor = snapshot.entries.cursor::<_, ()>(); cursor.seek(&FileCount(start), SeekBias::Right); let path_entries = cursor .filter_map(|e| { @@ -128,7 +128,7 @@ where let skipped_prefix_len = if include_root_name { 0 - } else if let Some(Entry::Dir { name, .. }) = tree.root_entry() { + } else if let Some(Entry::Dir { name, .. }) = snapshot.root_entry() { let name = name.to_string_lossy(); if name == "/" { 1 @@ -140,7 +140,7 @@ where }; match_single_tree_paths( - tree.id, + snapshot.id, skipped_prefix_len, path_entries, query, From 358fad8242f6124ba0285e3eea8d73aa33969764 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 21:02:30 -0600 Subject: [PATCH 025/102] Replace the old worktree with the new one --- zed/src/editor/buffer/mod.rs | 2 +- zed/src/file_finder.rs | 40 +- zed/src/lib.rs | 1 - zed/src/workspace/workspace.rs | 12 +- zed/src/workspace/workspace_view.rs | 15 +- zed/src/worktree.rs | 124 +++-- zed/src/worktree/fuzzy.rs | 6 +- zed/src/worktree_old/char_bag.rs | 44 -- zed/src/worktree_old/fuzzy.rs | 501 ----------------- zed/src/worktree_old/mod.rs | 5 - zed/src/worktree_old/worktree.rs | 811 ---------------------------- 11 files changed, 105 insertions(+), 1456 deletions(-) delete mode 100644 zed/src/worktree_old/char_bag.rs delete mode 100644 zed/src/worktree_old/fuzzy.rs delete mode 100644 zed/src/worktree_old/mod.rs delete mode 100644 zed/src/worktree_old/worktree.rs diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 22dc32d33f..14553c9119 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -15,7 +15,7 @@ use crate::{ sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree}, time::{self, ReplicaId}, util::RandomCharIter, - worktree_old::FileHandle, + worktree::FileHandle, }; use anyhow::{anyhow, Result}; use gpui::{AppContext, Entity, ModelContext}; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index b95832d668..6f59035550 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -3,7 +3,7 @@ use crate::{ settings::Settings, util, watch, workspace::{Workspace, WorkspaceView}, - worktree_old::{match_paths, PathMatch, Worktree}, + worktree::{match_paths, PathMatch, Worktree}, }; use gpui::{ color::{ColorF, ColorU}, @@ -140,19 +140,16 @@ impl FileFinder { let entry_id = path_match.entry_id; self.worktree(tree_id, app).map(|tree| { - let path = tree.entry_path(entry_id).unwrap(); + let path = tree + .path_for_inode(entry_id, path_match.include_root) + .unwrap(); let file_name = path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); - let mut path = path.to_string_lossy().to_string(); - if path_match.skipped_prefix_len > 0 { - let mut i = 0; - path.retain(|_| util::post_inc(&mut i) >= path_match.skipped_prefix_len) - } - + let path = path.to_string_lossy().to_string(); let path_positions = path_match.positions.clone(); let file_name_start = path.chars().count() - file_name.chars().count(); let mut file_name_positions = Vec::new(); @@ -345,11 +342,25 @@ impl FileFinder { } fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { - let worktrees = self.worktrees(ctx.as_ref()); + let snapshots = self + .workspace + .read(ctx) + .worktrees() + .iter() + .map(|tree| tree.read(ctx).snapshot()) + .collect::>(); let search_id = util::post_inc(&mut self.search_count); let pool = ctx.as_ref().thread_pool().clone(); let task = ctx.background_executor().spawn(async move { - let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); + let matches = match_paths( + snapshots.iter(), + &query, + snapshots.len() > 1, + false, + false, + 100, + pool, + ); (search_id, matches) }); @@ -377,15 +388,6 @@ impl FileFinder { .get(&tree_id) .map(|worktree| worktree.read(app)) } - - fn worktrees(&self, app: &AppContext) -> Vec { - self.workspace - .read(app) - .worktrees() - .iter() - .map(|worktree| worktree.read(app).clone()) - .collect() - } } #[cfg(test)] diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 9bf663d076..f58314265c 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -13,4 +13,3 @@ mod util; pub mod watch; pub mod workspace; mod worktree; -mod worktree_old; diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index 595a9959ff..21b51327c0 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -4,7 +4,7 @@ use crate::{ settings::Settings, time::ReplicaId, watch, - worktree_old::{Worktree, WorktreeHandle as _}, + worktree::{Worktree, WorktreeHandle as _}, }; use anyhow::anyhow; use gpui::{AppContext, Entity, Handle, ModelContext, ModelHandle, MutableAppContext, ViewContext}; @@ -117,7 +117,7 @@ impl Workspace { } } - let worktree = ctx.add_model(|ctx| Worktree::new(ctx.model_id(), path, ctx)); + let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx)); ctx.observe(&worktree, Self::on_worktree_updated); self.worktrees.insert(worktree); ctx.notify(); @@ -211,9 +211,7 @@ impl WorkspaceHandle for ModelHandle { .iter() .flat_map(|tree| { let tree_id = tree.id(); - tree.read(app) - .files() - .map(move |file| (tree_id, file.entry_id)) + tree.read(app).files().map(move |inode| (tree_id, inode)) }) .collect::>() } @@ -241,8 +239,8 @@ mod tests { // Get the first file entry. let tree = app.read(|ctx| workspace.read(ctx).worktrees.iter().next().unwrap().clone()); - let entry_id = app.read(|ctx| tree.read(ctx).files().next().unwrap().entry_id); - let entry = (tree.id(), entry_id); + let file_inode = app.read(|ctx| tree.read(ctx).files().next().unwrap()); + let entry = (tree.id(), file_inode); // Open the same entry twice before it finishes loading. let (future_1, future_2) = workspace.update(&mut app, |w, app| { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 11b5856d75..1496ddef76 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -5,7 +5,7 @@ use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; -use log::{error, info}; +use log::error; use std::{collections::HashSet, path::PathBuf}; pub fn init(app: &mut MutableAppContext) { @@ -227,19 +227,6 @@ impl WorkspaceView { } } - pub fn open_example_entry(&mut self, ctx: &mut ViewContext) { - if let Some(tree) = self.workspace.read(ctx).worktrees().iter().next() { - if let Some(file) = tree.read(ctx).files().next() { - info!("open_entry ({}, {})", tree.id(), file.entry_id); - self.open_entry((tree.id(), file.entry_id), ctx); - } else { - error!("No example file found for worktree {}", tree.id()); - } - } else { - error!("No worktree found while opening example entry"); - } - } - pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { self.active_pane.update(ctx, |pane, ctx| { if let Some(item) = pane.active_item() { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6e5191b760..2c4a0850ed 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -2,7 +2,7 @@ mod char_bag; mod fuzzy; use crate::{ - editor::Snapshot as BufferSnapshot, + editor::{History, Snapshot as BufferSnapshot}, sum_tree::{self, Edit, SumTree}, }; use anyhow::{anyhow, Result}; @@ -36,11 +36,6 @@ enum ScanState { Err(io::Error), } -pub struct FilesIterItem { - pub ino: u64, - pub path: PathBuf, -} - pub struct Worktree { id: usize, path: Arc, @@ -85,14 +80,6 @@ impl Worktree { tree } - pub fn snapshot(&self) -> Snapshot { - Snapshot { - id: self.id, - root_inode: self.root_inode(), - entries: self.entries.clone(), - } - } - fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { self.scan_state = scan_state; self.poll_entries(ctx); @@ -129,28 +116,27 @@ impl Worktree { } } - fn files<'a>(&'a self) -> impl Iterator + 'a { - self.entries.cursor::<(), ()>().filter_map(|e| { - if let Entry::File { path, ino, .. } = e { - Some(FilesIterItem { - ino: *ino, - path: PathBuf::from(path.path.iter().collect::()), - }) - } else { - None - } - }) + pub fn snapshot(&self) -> Snapshot { + Snapshot { + id: self.id, + root_inode: self.root_inode(), + entries: self.entries.clone(), + } } - fn root_entry(&self) -> Option<&Entry> { - self.root_inode().and_then(|ino| self.entries.get(&ino)) + pub fn contains_path(&self, path: &Path) -> bool { + path.starts_with(&self.path) } - fn file_count(&self) -> usize { + pub fn has_inode(&self, inode: u64) -> bool { + self.entries.get(&inode).is_some() + } + + pub fn file_count(&self) -> usize { self.entries.summary().file_count } - fn abs_path_for_inode(&self, ino: u64) -> Result { + pub fn abs_path_for_inode(&self, ino: u64) -> Result { let mut result = self.path.to_path_buf(); result.push(self.path_for_inode(ino, false)?); Ok(result) @@ -199,13 +185,17 @@ impl Worktree { }) } - pub fn load_file(&self, ino: u64, ctx: &AppContext) -> impl Future> { + pub fn load_history( + &self, + ino: u64, + ctx: &AppContext, + ) -> impl Future> { let path = self.abs_path_for_inode(ino); ctx.background_executor().spawn(async move { let mut file = std::fs::File::open(&path?)?; let mut base_text = String::new(); file.read_to_string(&mut base_text)?; - Ok(base_text) + Ok(History::new(Arc::from(base_text))) }) } @@ -253,6 +243,17 @@ impl Worktree { ), } } + + #[cfg(test)] + pub fn files<'a>(&'a self) -> impl Iterator + 'a { + self.entries.cursor::<(), ()>().filter_map(|entry| { + if let Entry::File { inode, .. } = entry { + Some(*inode) + } else { + None + } + }) + } } impl Entity for Worktree { @@ -287,8 +288,8 @@ impl FileHandle { .unwrap() } - pub fn load(&self, ctx: &AppContext) -> impl Future> { - self.worktree.read(ctx).load_file(self.inode, ctx) + pub fn load_history(&self, ctx: &AppContext) -> impl Future> { + self.worktree.read(ctx).load_history(self.inode, ctx) } pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task> { @@ -306,7 +307,7 @@ pub enum Entry { Dir { parent: Option, name: Arc, - ino: u64, + inode: u64, is_symlink: bool, is_ignored: bool, children: Arc<[u64]>, @@ -316,7 +317,7 @@ pub enum Entry { parent: Option, name: Arc, path: PathEntry, - ino: u64, + inode: u64, is_symlink: bool, is_ignored: bool, }, @@ -325,8 +326,8 @@ pub enum Entry { impl Entry { fn ino(&self) -> u64 { match self { - Entry::Dir { ino, .. } => *ino, - Entry::File { ino, .. } => *ino, + Entry::Dir { inode: ino, .. } => *ino, + Entry::File { inode: ino, .. } => *ino, } } @@ -468,7 +469,7 @@ impl BackgroundScanner { let dir_entry = Entry::Dir { parent: None, name, - ino, + inode: ino, is_symlink, is_ignored, children: Arc::from([]), @@ -511,7 +512,7 @@ impl BackgroundScanner { parent: None, name, path: PathEntry::new(ino, &relative_path, is_ignored), - ino, + inode: ino, is_symlink, is_ignored, })); @@ -555,7 +556,7 @@ impl BackgroundScanner { let dir_entry = Entry::Dir { parent: Some(job.ino), name, - ino, + inode: ino, is_symlink, is_ignored, children: Arc::from([]), @@ -579,7 +580,7 @@ impl BackgroundScanner { parent: Some(job.ino), name, path: PathEntry::new(ino, &relative_path, is_ignored), - ino, + inode: ino, is_symlink, is_ignored, }); @@ -621,6 +622,23 @@ struct ScanJob { scan_queue: crossbeam_channel::Sender>, } +pub trait WorktreeHandle { + fn file(&self, entry_id: u64, app: &AppContext) -> Result; +} + +impl WorktreeHandle for ModelHandle { + fn file(&self, inode: u64, app: &AppContext) -> Result { + if self.read(app).has_inode(inode) { + Ok(FileHandle { + worktree: self.clone(), + inode, + }) + } else { + Err(anyhow!("entry does not exist in tree")) + } + } +} + trait UnwrapIgnoreTuple { fn unwrap(self) -> Ignore; } @@ -705,22 +723,28 @@ mod tests { let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); - let entry = app.read(|ctx| { - let entry = tree.read(ctx).files().next().unwrap(); - assert_eq!(entry.path.file_name().unwrap(), "file1"); - entry + let file_inode = app.read(|ctx| { + let tree = tree.read(ctx); + let inode = tree.files().next().unwrap(); + assert_eq!( + tree.path_for_inode(inode, false) + .unwrap() + .file_name() + .unwrap(), + "file1" + ); + inode }); - let file_ino = entry.ino; tree.update(&mut app, |tree, ctx| { - smol::block_on(tree.save(file_ino, buffer.snapshot(), ctx.as_ref())).unwrap() + smol::block_on(tree.save(file_inode, buffer.snapshot(), ctx.as_ref())).unwrap() }); - let loaded_text = app - .read(|ctx| tree.read(ctx).load_file(file_ino, ctx)) + let loaded_history = app + .read(|ctx| tree.read(ctx).load_history(file_inode, ctx)) .await .unwrap(); - assert_eq!(loaded_text, buffer.text()); + assert_eq!(loaded_history.base_text.as_ref(), buffer.text()); }); } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 1b03fbb769..d9f6bab8f8 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -2,7 +2,7 @@ use gpui::scoped_pool; use crate::sum_tree::SeekBias; -use super::{char_bag::CharBag, Entry, FileCount, Snapshot, Worktree}; +use super::{char_bag::CharBag, Entry, FileCount, Snapshot}; use std::{ cmp::{max, min, Ordering, Reverse}, @@ -47,7 +47,7 @@ pub struct PathMatch { pub positions: Vec, pub tree_id: usize, pub entry_id: u64, - pub skipped_prefix_len: usize, + pub include_root: bool, } impl PartialEq for PathMatch { @@ -237,7 +237,7 @@ fn match_single_tree_paths<'a>( entry_id: path_entry.ino, score, positions: match_positions.clone(), - skipped_prefix_len, + include_root: skipped_prefix_len == 0, })); if results.len() == max_results { *min_score = results.peek().unwrap().0.score; diff --git a/zed/src/worktree_old/char_bag.rs b/zed/src/worktree_old/char_bag.rs deleted file mode 100644 index 9e3c5314e9..0000000000 --- a/zed/src/worktree_old/char_bag.rs +++ /dev/null @@ -1,44 +0,0 @@ -#[derive(Copy, Clone, Debug)] -pub struct CharBag(u64); - -impl CharBag { - pub fn is_superset(self, other: CharBag) -> bool { - self.0 & other.0 == other.0 - } - - fn insert(&mut self, c: char) { - if c >= 'a' && c <= 'z' { - let mut count = self.0; - let idx = c as u8 - 'a' as u8; - count = count >> (idx * 2); - count = ((count << 1) | 1) & 3; - count = count << idx * 2; - self.0 |= count; - } else if c >= '0' && c <= '9' { - let idx = c as u8 - '0' as u8; - self.0 |= 1 << (idx + 52); - } else if c == '-' { - self.0 |= 1 << 62; - } - } -} - -impl From<&str> for CharBag { - fn from(s: &str) -> Self { - let mut bag = Self(0); - for c in s.chars() { - bag.insert(c); - } - bag - } -} - -impl From<&[char]> for CharBag { - fn from(chars: &[char]) -> Self { - let mut bag = Self(0); - for c in chars { - bag.insert(*c); - } - bag - } -} diff --git a/zed/src/worktree_old/fuzzy.rs b/zed/src/worktree_old/fuzzy.rs deleted file mode 100644 index 8bdb7d7eea..0000000000 --- a/zed/src/worktree_old/fuzzy.rs +++ /dev/null @@ -1,501 +0,0 @@ -use gpui::scoped_pool; - -use super::char_bag::CharBag; - -use std::{ - cmp::{max, min, Ordering, Reverse}, - collections::BinaryHeap, -}; - -const BASE_DISTANCE_PENALTY: f64 = 0.6; -const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; -const MIN_DISTANCE_PENALTY: f64 = 0.2; - -pub struct PathEntry { - pub ino: u64, - pub path_chars: CharBag, - pub path: Vec, - pub lowercase_path: Vec, - pub is_ignored: bool, -} - -#[derive(Clone, Debug)] -pub struct PathMatch { - pub score: f64, - pub positions: Vec, - pub tree_id: usize, - pub entry_id: u64, - pub skipped_prefix_len: usize, -} - -impl PartialEq for PathMatch { - fn eq(&self, other: &Self) -> bool { - self.score.eq(&other.score) - } -} - -impl Eq for PathMatch {} - -impl PartialOrd for PathMatch { - fn partial_cmp(&self, other: &Self) -> Option { - self.score.partial_cmp(&other.score) - } -} - -impl Ord for PathMatch { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap_or(Ordering::Equal) - } -} - -pub fn match_paths( - paths_by_tree_id: &[(usize, usize, &[PathEntry])], - query: &str, - include_ignored: bool, - smart_case: bool, - max_results: usize, - pool: scoped_pool::Pool, -) -> Vec { - let lowercase_query = query.to_lowercase().chars().collect::>(); - let query = query.chars().collect::>(); - let lowercase_query = &lowercase_query; - let query = &query; - let query_chars = CharBag::from(&lowercase_query[..]); - - let cpus = num_cpus::get(); - let path_count = paths_by_tree_id - .iter() - .fold(0, |sum, (_, _, paths)| sum + paths.len()); - let segment_size = (path_count + cpus - 1) / cpus; - let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); - - pool.scoped(|scope| { - for (segment_idx, results) in segment_results.iter_mut().enumerate() { - scope.execute(move || { - let segment_start = segment_idx * segment_size; - let segment_end = segment_start + segment_size; - - let mut min_score = 0.0; - let mut last_positions = Vec::new(); - last_positions.resize(query.len(), 0); - let mut match_positions = Vec::new(); - match_positions.resize(query.len(), 0); - let mut score_matrix = Vec::new(); - let mut best_position_matrix = Vec::new(); - - let mut tree_start = 0; - for (tree_id, skipped_prefix_len, paths) in paths_by_tree_id { - let tree_end = tree_start + paths.len(); - if tree_start < segment_end && segment_start < tree_end { - let start = max(tree_start, segment_start) - tree_start; - let end = min(tree_end, segment_end) - tree_start; - - match_single_tree_paths( - *tree_id, - *skipped_prefix_len, - paths, - start, - end, - query, - lowercase_query, - query_chars, - include_ignored, - smart_case, - results, - max_results, - &mut min_score, - &mut match_positions, - &mut last_positions, - &mut score_matrix, - &mut best_position_matrix, - ); - } - if tree_end >= segment_end { - break; - } - tree_start = tree_end; - } - }) - } - }); - - let mut results = segment_results - .into_iter() - .flatten() - .map(|r| r.0) - .collect::>(); - results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); - results.truncate(max_results); - results -} - -fn match_single_tree_paths( - tree_id: usize, - skipped_prefix_len: usize, - path_entries: &[PathEntry], - start: usize, - end: usize, - query: &[char], - lowercase_query: &[char], - query_chars: CharBag, - include_ignored: bool, - smart_case: bool, - results: &mut BinaryHeap>, - max_results: usize, - min_score: &mut f64, - match_positions: &mut Vec, - last_positions: &mut Vec, - score_matrix: &mut Vec>, - best_position_matrix: &mut Vec, -) { - for i in start..end { - let path_entry = unsafe { &path_entries.get_unchecked(i) }; - - if !include_ignored && path_entry.is_ignored { - continue; - } - - if !path_entry.path_chars.is_superset(query_chars) { - continue; - } - - if !find_last_positions( - last_positions, - skipped_prefix_len, - &path_entry.lowercase_path, - &lowercase_query[..], - ) { - continue; - } - - let matrix_len = query.len() * (path_entry.path.len() - skipped_prefix_len); - score_matrix.clear(); - score_matrix.resize(matrix_len, None); - best_position_matrix.clear(); - best_position_matrix.resize(matrix_len, skipped_prefix_len); - - let score = score_match( - &query[..], - &lowercase_query[..], - &path_entry.path, - &path_entry.lowercase_path, - skipped_prefix_len, - smart_case, - &last_positions, - score_matrix, - best_position_matrix, - match_positions, - *min_score, - ); - - if score > 0.0 { - results.push(Reverse(PathMatch { - tree_id, - entry_id: path_entry.ino, - score, - positions: match_positions.clone(), - skipped_prefix_len, - })); - if results.len() == max_results { - *min_score = results.peek().unwrap().0.score; - } - } - } -} - -fn find_last_positions( - last_positions: &mut Vec, - skipped_prefix_len: usize, - path: &[char], - query: &[char], -) -> bool { - let mut path = path.iter(); - for (i, char) in query.iter().enumerate().rev() { - if let Some(j) = path.rposition(|c| c == char) { - if j >= skipped_prefix_len { - last_positions[i] = j; - } else { - return false; - } - } else { - return false; - } - } - true -} - -fn score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - skipped_prefix_len: usize, - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - match_positions: &mut [usize], - min_score: f64, -) -> f64 { - let score = recursive_score_match( - query, - query_cased, - path, - path_cased, - skipped_prefix_len, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - 0, - skipped_prefix_len, - query.len() as f64, - ) * query.len() as f64; - - if score <= 0.0 { - return 0.0; - } - - let path_len = path.len() - skipped_prefix_len; - let mut cur_start = 0; - for i in 0..query.len() { - match_positions[i] = best_position_matrix[i * path_len + cur_start] - skipped_prefix_len; - cur_start = match_positions[i] + 1; - } - - score -} - -fn recursive_score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - skipped_prefix_len: usize, - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - min_score: f64, - query_idx: usize, - path_idx: usize, - cur_score: f64, -) -> f64 { - if query_idx == query.len() { - return 1.0; - } - - let path_len = path.len() - skipped_prefix_len; - - if let Some(memoized) = score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] { - return memoized; - } - - let mut score = 0.0; - let mut best_position = 0; - - let query_char = query_cased[query_idx]; - let limit = last_positions[query_idx]; - - let mut last_slash = 0; - for j in path_idx..=limit { - let path_char = path_cased[j]; - let is_path_sep = path_char == '/' || path_char == '\\'; - - if query_idx == 0 && is_path_sep { - last_slash = j; - } - - if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { - let mut char_score = 1.0; - if j > path_idx { - let last = path[j - 1]; - let curr = path[j]; - - if last == '/' { - char_score = 0.9; - } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { - char_score = 0.8; - } else if last.is_lowercase() && curr.is_uppercase() { - char_score = 0.8; - } else if last == '.' { - char_score = 0.7; - } else if query_idx == 0 { - char_score = BASE_DISTANCE_PENALTY; - } else { - char_score = MIN_DISTANCE_PENALTY.max( - BASE_DISTANCE_PENALTY - - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, - ); - } - } - - // Apply a severe penalty if the case doesn't match. - // This will make the exact matches have higher score than the case-insensitive and the - // path insensitive matches. - if (smart_case || path[j] == '/') && query[query_idx] != path[j] { - char_score *= 0.001; - } - - let mut multiplier = char_score; - - // Scale the score based on how deep within the patch we found the match. - if query_idx == 0 { - multiplier /= (path.len() - last_slash) as f64; - } - - let mut next_score = 1.0; - if min_score > 0.0 { - next_score = cur_score * multiplier; - // Scores only decrease. If we can't pass the previous best, bail - if next_score < min_score { - // Ensure that score is non-zero so we use it in the memo table. - if score == 0.0 { - score = 1e-18; - } - continue; - } - } - - let new_score = recursive_score_match( - query, - query_cased, - path, - path_cased, - skipped_prefix_len, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - query_idx + 1, - j + 1, - next_score, - ) * multiplier; - - if new_score > score { - score = new_score; - best_position = j; - // Optimization: can't score better than 1. - if new_score == 1.0 { - break; - } - } - } - } - - if best_position != 0 { - best_position_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = best_position; - } - - score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = Some(score); - score -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_match_path_entries() { - let paths = vec![ - "", - "a", - "ab", - "abC", - "abcd", - "alphabravocharlie", - "AlphaBravoCharlie", - "thisisatestdir", - "/////ThisIsATestDir", - "/this/is/a/test/dir", - "/test/tiatd", - ]; - - assert_eq!( - match_query("abc", false, &paths), - vec![ - ("abC", vec![0, 1, 2]), - ("abcd", vec![0, 1, 2]), - ("AlphaBravoCharlie", vec![0, 5, 10]), - ("alphabravocharlie", vec![4, 5, 10]), - ] - ); - assert_eq!( - match_query("t/i/a/t/d", false, &paths), - vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),] - ); - - assert_eq!( - match_query("tiatd", false, &paths), - vec![ - ("/test/tiatd", vec![6, 7, 8, 9, 10]), - ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]), - ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]), - ("thisisatestdir", vec![0, 2, 6, 7, 11]), - ] - ); - } - - fn match_query<'a>( - query: &str, - smart_case: bool, - paths: &Vec<&'a str>, - ) -> Vec<(&'a str, Vec)> { - let lowercase_query = query.to_lowercase().chars().collect::>(); - let query = query.chars().collect::>(); - let query_chars = CharBag::from(&lowercase_query[..]); - - let mut path_entries = Vec::new(); - for (i, path) in paths.iter().enumerate() { - let lowercase_path = path.to_lowercase().chars().collect::>(); - let path_chars = CharBag::from(&lowercase_path[..]); - let path = path.chars().collect(); - path_entries.push(PathEntry { - ino: i as u64, - path_chars, - path, - lowercase_path, - is_ignored: false, - }); - } - - let mut match_positions = Vec::new(); - let mut last_positions = Vec::new(); - match_positions.resize(query.len(), 0); - last_positions.resize(query.len(), 0); - - let mut results = BinaryHeap::new(); - match_single_tree_paths( - 0, - 0, - &path_entries, - 0, - path_entries.len(), - &query[..], - &lowercase_query[..], - query_chars, - true, - smart_case, - &mut results, - 100, - &mut 0.0, - &mut match_positions, - &mut last_positions, - &mut Vec::new(), - &mut Vec::new(), - ); - - results - .into_iter() - .rev() - .map(|result| { - ( - paths[result.0.entry_id as usize].clone(), - result.0.positions, - ) - }) - .collect() - } -} diff --git a/zed/src/worktree_old/mod.rs b/zed/src/worktree_old/mod.rs deleted file mode 100644 index 3ece82b454..0000000000 --- a/zed/src/worktree_old/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod char_bag; -mod fuzzy; -mod worktree; - -pub use worktree::{match_paths, FileHandle, PathMatch, Worktree, WorktreeHandle}; diff --git a/zed/src/worktree_old/worktree.rs b/zed/src/worktree_old/worktree.rs deleted file mode 100644 index cb3536d2af..0000000000 --- a/zed/src/worktree_old/worktree.rs +++ /dev/null @@ -1,811 +0,0 @@ -pub use super::fuzzy::PathMatch; -use super::{ - char_bag::CharBag, - fuzzy::{self, PathEntry}, -}; -use crate::{ - editor::{History, Snapshot}, - throttle::throttled, - util::post_inc, -}; -use anyhow::{anyhow, Result}; -use crossbeam_channel as channel; -use easy_parallel::Parallel; -use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; -use ignore::dir::{Ignore, IgnoreBuilder}; -use parking_lot::RwLock; -use postage::watch; -use smol::prelude::*; -use std::{ - collections::HashMap, - ffi::{OsStr, OsString}, - fmt, fs, - io::{self, Write}, - os::unix::fs::MetadataExt, - path::Path, - path::PathBuf, - sync::Arc, - time::Duration, -}; - -#[derive(Clone)] -pub struct Worktree(Arc>); - -struct WorktreeState { - id: usize, - path: PathBuf, - root_ino: Option, - entries: HashMap, - file_paths: Vec, - histories: HashMap, - scan_state: watch::Sender, -} - -#[derive(Clone)] -enum ScanState { - Scanning, - Idle, -} - -struct DirToScan { - ino: u64, - path: PathBuf, - relative_path: PathBuf, - ignore: Option, - dirs_to_scan: channel::Sender>, -} - -impl Worktree { - pub fn new(id: usize, path: T, ctx: &mut ModelContext) -> Self - where - T: Into, - { - let scan_state = watch::channel_with(ScanState::Scanning); - - let tree = Self(Arc::new(RwLock::new(WorktreeState { - id, - path: path.into(), - root_ino: None, - entries: HashMap::new(), - file_paths: Vec::new(), - histories: HashMap::new(), - scan_state: scan_state.0, - }))); - - { - let tree = tree.clone(); - ctx.as_ref().thread_pool().spawn(move || { - if let Err(error) = tree.scan_dirs() { - log::error!("error scanning worktree: {}", error); - } - tree.set_scan_state(ScanState::Idle); - }); - } - - ctx.spawn_stream( - throttled(Duration::from_millis(100), scan_state.1), - Self::observe_scan_state, - |_, _| {}, - ) - .detach(); - - tree - } - - fn set_scan_state(&self, state: ScanState) { - *self.0.write().scan_state.borrow_mut() = state; - } - - fn scan_dirs(&self) -> io::Result<()> { - let path = self.0.read().path.clone(); - let metadata = fs::metadata(&path)?; - let ino = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); - let name = path - .file_name() - .map(|name| OsString::from(name)) - .unwrap_or(OsString::from("/")); - let relative_path = PathBuf::from(&name); - - let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - if metadata.is_dir() { - ignore = ignore.add_child(&path).unwrap(); - } - let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - - if metadata.file_type().is_dir() { - let is_ignored = is_ignored || name == ".git"; - self.insert_dir(None, name, ino, is_symlink, is_ignored); - let (tx, rx) = channel::unbounded(); - - tx.send(Ok(DirToScan { - ino, - path, - relative_path, - ignore: Some(ignore), - dirs_to_scan: tx.clone(), - })) - .unwrap(); - drop(tx); - - Parallel::>::new() - .each(0..16, |_| { - while let Ok(result) = rx.recv() { - self.scan_dir(result?)?; - } - Ok(()) - }) - .run() - .into_iter() - .collect::>()?; - } else { - self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path); - } - self.0.write().root_ino = Some(ino); - - Ok(()) - } - - fn scan_dir(&self, to_scan: DirToScan) -> io::Result<()> { - let mut new_children = Vec::new(); - - for child_entry in fs::read_dir(&to_scan.path)? { - let child_entry = child_entry?; - let name = child_entry.file_name(); - let relative_path = to_scan.relative_path.join(&name); - let metadata = child_entry.metadata()?; - let ino = metadata.ino(); - let is_symlink = metadata.file_type().is_symlink(); - - if metadata.is_dir() { - let path = to_scan.path.join(&name); - let mut is_ignored = true; - let mut ignore = None; - - if let Some(parent_ignore) = to_scan.ignore.as_ref() { - let child_ignore = parent_ignore.add_child(&path).unwrap(); - is_ignored = child_ignore.matched(&path, true).is_ignore() || name == ".git"; - if !is_ignored { - ignore = Some(child_ignore); - } - } - - self.insert_dir(Some(to_scan.ino), name, ino, is_symlink, is_ignored); - new_children.push(ino); - - let dirs_to_scan = to_scan.dirs_to_scan.clone(); - let _ = to_scan.dirs_to_scan.send(Ok(DirToScan { - ino, - path, - relative_path, - ignore, - dirs_to_scan, - })); - } else { - let is_ignored = to_scan.ignore.as_ref().map_or(true, |i| { - i.matched(to_scan.path.join(&name), false).is_ignore() - }); - - self.insert_file( - Some(to_scan.ino), - name, - ino, - is_symlink, - is_ignored, - relative_path, - ); - new_children.push(ino); - }; - } - - if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.ino) - { - *children = new_children.clone(); - } - - Ok(()) - } - - fn insert_dir( - &self, - parent: Option, - name: OsString, - ino: u64, - is_symlink: bool, - is_ignored: bool, - ) { - let mut state = self.0.write(); - let entries = &mut state.entries; - entries.insert( - ino, - Entry::Dir { - parent, - name, - ino, - is_symlink, - is_ignored, - children: Vec::new(), - }, - ); - *state.scan_state.borrow_mut() = ScanState::Scanning; - } - - fn insert_file( - &self, - parent: Option, - name: OsString, - ino: u64, - is_symlink: bool, - is_ignored: bool, - path: PathBuf, - ) { - let path = path.to_string_lossy(); - let lowercase_path = path.to_lowercase().chars().collect::>(); - let path = path.chars().collect::>(); - let path_chars = CharBag::from(&path[..]); - - let mut state = self.0.write(); - state.entries.insert( - ino, - Entry::File { - parent, - name, - ino, - is_symlink, - is_ignored, - }, - ); - state.file_paths.push(PathEntry { - ino, - path_chars, - path, - lowercase_path, - is_ignored, - }); - *state.scan_state.borrow_mut() = ScanState::Scanning; - } - - pub fn entry_path(&self, mut entry_id: u64) -> Result { - let state = self.0.read(); - - let mut entries = Vec::new(); - loop { - let entry = state - .entries - .get(&entry_id) - .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - entries.push(entry); - if let Some(parent_id) = entry.parent() { - entry_id = parent_id; - } else { - break; - } - } - - let mut path = PathBuf::new(); - for entry in entries.into_iter().rev() { - path.push(entry.name()); - } - Ok(path) - } - - pub fn abs_entry_path(&self, entry_id: u64) -> Result { - let mut path = self.0.read().path.clone(); - path.pop(); - Ok(path.join(self.entry_path(entry_id)?)) - } - - #[cfg(test)] - fn entry_for_path(&self, path: impl AsRef) -> Option { - let path = path.as_ref(); - let state = self.0.read(); - state.root_ino.and_then(|mut ino| { - 'components: for component in path { - if let Entry::Dir { children, .. } = &state.entries[&ino] { - for child in children { - if state.entries[child].name() == component { - ino = *child; - continue 'components; - } - } - return None; - } else { - return None; - } - } - Some(ino) - }) - } - - fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: u64, indent: usize) -> fmt::Result { - match &self.0.read().entries[&entry_id] { - Entry::Dir { name, children, .. } => { - write!( - f, - "{}{}/ ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - entry_id - )?; - for child_id in children.iter() { - self.fmt_entry(f, *child_id, indent + 2)?; - } - Ok(()) - } - Entry::File { name, .. } => write!( - f, - "{}{} ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - entry_id - ), - } - } - - pub fn path(&self) -> PathBuf { - PathBuf::from(&self.0.read().path) - } - - pub fn contains_path(&self, path: &Path) -> bool { - path.starts_with(self.path()) - } - - pub fn iter(&self) -> Iter { - Iter { - tree: self.clone(), - stack: Vec::new(), - started: false, - } - } - - pub fn files(&self) -> FilesIter { - FilesIter { - iter: self.iter(), - path: PathBuf::new(), - } - } - - pub fn has_entry(&self, entry_id: u64) -> bool { - self.0.read().entries.contains_key(&entry_id) - } - - pub fn entry_count(&self) -> usize { - self.0.read().entries.len() - } - - pub fn file_count(&self) -> usize { - self.0.read().file_paths.len() - } - - pub fn load_history(&self, entry_id: u64) -> impl Future> { - let tree = self.clone(); - - async move { - if let Some(history) = tree.0.read().histories.get(&entry_id) { - return Ok(history.clone()); - } - - let path = tree.abs_entry_path(entry_id)?; - - let mut file = smol::fs::File::open(&path).await?; - let mut base_text = String::new(); - file.read_to_string(&mut base_text).await?; - let history = History::new(Arc::from(base_text)); - tree.0.write().histories.insert(entry_id, history.clone()); - Ok(history) - } - } - - pub fn save<'a>(&self, entry_id: u64, content: Snapshot, ctx: &AppContext) -> Task> { - let path = self.abs_entry_path(entry_id); - ctx.background_executor().spawn(async move { - let buffer_size = content.text_summary().bytes.min(10 * 1024); - let file = std::fs::File::create(&path?)?; - let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); - for chunk in content.fragments() { - writer.write(chunk.as_bytes())?; - } - writer.flush()?; - Ok(()) - }) - } - - fn observe_scan_state(&mut self, _: ScanState, ctx: &mut ModelContext) { - // log::info!("observe {:?}", std::time::Instant::now()); - ctx.notify() - } -} - -impl fmt::Debug for Worktree { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.entry_count() == 0 { - write!(f, "Empty tree\n") - } else { - self.fmt_entry(f, 0, 0) - } - } -} - -impl Entity for Worktree { - type Event = (); -} - -impl WorktreeState { - fn root_entry(&self) -> Option<&Entry> { - self.root_ino - .and_then(|root_ino| self.entries.get(&root_ino)) - } -} - -pub trait WorktreeHandle { - fn file(&self, entry_id: u64, app: &AppContext) -> Result; -} - -impl WorktreeHandle for ModelHandle { - fn file(&self, entry_id: u64, app: &AppContext) -> Result { - if self.read(app).has_entry(entry_id) { - Ok(FileHandle { - worktree: self.clone(), - entry_id, - }) - } else { - Err(anyhow!("entry does not exist in tree")) - } - } -} - -#[derive(Clone, Debug)] -pub enum Entry { - Dir { - parent: Option, - name: OsString, - ino: u64, - is_symlink: bool, - is_ignored: bool, - children: Vec, - }, - File { - parent: Option, - name: OsString, - ino: u64, - is_symlink: bool, - is_ignored: bool, - }, -} - -impl Entry { - fn parent(&self) -> Option { - match self { - Entry::Dir { parent, .. } | Entry::File { parent, .. } => *parent, - } - } - - fn ino(&self) -> u64 { - match self { - Entry::Dir { ino, .. } | Entry::File { ino, .. } => *ino, - } - } - - fn name(&self) -> &OsStr { - match self { - Entry::Dir { name, .. } | Entry::File { name, .. } => name, - } - } -} - -#[derive(Clone)] -pub struct FileHandle { - worktree: ModelHandle, - entry_id: u64, -} - -impl FileHandle { - pub fn path(&self, app: &AppContext) -> PathBuf { - self.worktree.read(app).entry_path(self.entry_id).unwrap() - } - - pub fn load_history(&self, app: &AppContext) -> impl Future> { - self.worktree.read(app).load_history(self.entry_id) - } - - pub fn save<'a>(&self, content: Snapshot, ctx: &AppContext) -> Task> { - let worktree = self.worktree.read(ctx); - worktree.save(self.entry_id, content, ctx) - } - - pub fn entry_id(&self) -> (usize, u64) { - (self.worktree.id(), self.entry_id) - } -} - -struct IterStackEntry { - entry_id: u64, - child_idx: usize, -} - -pub struct Iter { - tree: Worktree, - stack: Vec, - started: bool, -} - -impl Iterator for Iter { - type Item = Traversal; - - fn next(&mut self) -> Option { - let state = self.tree.0.read(); - - if !self.started { - self.started = true; - - return if let Some(entry) = state.root_entry().cloned() { - self.stack.push(IterStackEntry { - entry_id: entry.ino(), - child_idx: 0, - }); - - Some(Traversal::Push { - entry_id: entry.ino(), - entry, - }) - } else { - None - }; - } - - while let Some(parent) = self.stack.last_mut() { - if let Some(Entry::Dir { children, .. }) = &state.entries.get(&parent.entry_id) { - if parent.child_idx < children.len() { - let child_id = children[post_inc(&mut parent.child_idx)]; - - self.stack.push(IterStackEntry { - entry_id: child_id, - child_idx: 0, - }); - - return Some(Traversal::Push { - entry_id: child_id, - entry: state.entries[&child_id].clone(), - }); - } else { - self.stack.pop(); - - return Some(Traversal::Pop); - } - } else { - self.stack.pop(); - - return Some(Traversal::Pop); - } - } - - None - } -} - -#[derive(Debug)] -pub enum Traversal { - Push { entry_id: u64, entry: Entry }, - Pop, -} - -pub struct FilesIter { - iter: Iter, - path: PathBuf, -} - -pub struct FilesIterItem { - pub entry_id: u64, - pub path: PathBuf, -} - -impl Iterator for FilesIter { - type Item = FilesIterItem; - - fn next(&mut self) -> Option { - loop { - match self.iter.next() { - Some(Traversal::Push { - entry_id, entry, .. - }) => match entry { - Entry::Dir { name, .. } => { - self.path.push(name); - } - Entry::File { name, .. } => { - self.path.push(name); - return Some(FilesIterItem { - entry_id, - path: self.path.clone(), - }); - } - }, - Some(Traversal::Pop) => { - self.path.pop(); - } - None => { - return None; - } - } - } - } -} - -trait UnwrapIgnoreTuple { - fn unwrap(self) -> Ignore; -} - -impl UnwrapIgnoreTuple for (Ignore, Option) { - fn unwrap(self) -> Ignore { - if let Some(error) = self.1 { - log::error!("error loading gitignore data: {}", error); - } - self.0 - } -} - -pub fn match_paths( - trees: &[Worktree], - query: &str, - include_ignored: bool, - smart_case: bool, - max_results: usize, - pool: scoped_pool::Pool, -) -> Vec { - let tree_states = trees.iter().map(|tree| tree.0.read()).collect::>(); - fuzzy::match_paths( - &tree_states - .iter() - .map(|tree| { - let skip_prefix = if trees.len() == 1 { - if let Some(Entry::Dir { name, .. }) = tree.root_entry() { - let name = name.to_string_lossy(); - if name == "/" { - 1 - } else { - name.chars().count() + 1 - } - } else { - 0 - } - } else { - 0 - }; - - (tree.id, skip_prefix, &tree.file_paths[..]) - }) - .collect::>()[..], - query, - include_ignored, - smart_case, - max_results, - pool, - ) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::editor::Buffer; - use crate::test::*; - use anyhow::Result; - use gpui::App; - use serde_json::json; - use std::os::unix; - - #[test] - fn test_populate_and_search() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "root": { - "apple": "", - "banana": { - "carrot": { - "date": "", - "endive": "", - } - }, - "fennel": { - "grape": "", - } - } - })); - - let root_link_path = dir.path().join("root_link"); - unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); - - let tree = app.add_model(|ctx| Worktree::new(1, root_link_path, ctx)); - app.finish_pending_tasks().await; - - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!(tree.file_count(), 4); - let results = match_paths( - &[tree.clone()], - "bna", - false, - false, - 10, - ctx.thread_pool().clone(), - ) - .iter() - .map(|result| tree.entry_path(result.entry_id)) - .collect::, _>>() - .unwrap(); - assert_eq!( - results, - vec![ - PathBuf::from("root_link/banana/carrot/date"), - PathBuf::from("root_link/banana/carrot/endive"), - ] - ); - }) - }); - } - - #[test] - fn test_save_file() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "file1": "the old contents", - })); - - let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), ctx)); - app.finish_pending_tasks().await; - - let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); - - let entry = app.read(|ctx| { - let entry = tree.read(ctx).files().next().unwrap(); - assert_eq!(entry.path.file_name().unwrap(), "file1"); - entry - }); - let file_id = entry.entry_id; - - tree.update(&mut app, |tree, ctx| { - smol::block_on(tree.save(file_id, buffer.snapshot(), ctx.as_ref())).unwrap() - }); - - let history = app - .read(|ctx| tree.read(ctx).load_history(file_id)) - .await - .unwrap(); - assert_eq!(history.base_text.as_ref(), buffer.text()); - }); - } - - #[test] - fn test_rescan() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "dir1": { - "file": "contents" - }, - "dir2": { - } - })); - - let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), ctx)); - app.finish_pending_tasks().await; - - let file_entry = app.read(|ctx| tree.read(ctx).entry_for_path("dir1/file").unwrap()); - - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!( - tree.abs_entry_path(file_entry).unwrap(), - tree.path().join("dir1/file") - ); - }); - - std::fs::rename(dir.path().join("dir1/file"), dir.path().join("dir2/file")).unwrap(); - - assert_condition(1, 300, || { - app.read(|ctx| { - let tree = tree.read(ctx); - tree.abs_entry_path(file_entry).unwrap() == tree.path().join("dir2/file") - }) - }) - .await - }); - } -} From af47ef94d542365f5646d946bcb138ea05a067b0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 21:04:30 -0600 Subject: [PATCH 026/102] Remove unused throttle module --- zed/src/lib.rs | 1 - zed/src/throttle.rs | 68 --------------------------------------------- 2 files changed, 69 deletions(-) delete mode 100644 zed/src/throttle.rs diff --git a/zed/src/lib.rs b/zed/src/lib.rs index f58314265c..7c6155f2d1 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -7,7 +7,6 @@ pub mod settings; mod sum_tree; #[cfg(test)] mod test; -mod throttle; mod time; mod util; pub mod watch; diff --git a/zed/src/throttle.rs b/zed/src/throttle.rs deleted file mode 100644 index 2fcd028496..0000000000 --- a/zed/src/throttle.rs +++ /dev/null @@ -1,68 +0,0 @@ -use core::time; -use futures_core::{Future, Stream}; -use std::{ - pin::Pin, - task::{Context, Poll}, -}; -use time::Duration; - -pub struct Throttled { - period: Duration, - stream: S, - timer: Option, -} - -pub fn throttled(period: Duration, stream: S) -> impl Stream { - Throttled { - period, - stream, - timer: None, - } -} - -impl Stream for Throttled { - type Item = S::Item; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if let Some(timer) = self.as_mut().timer() { - if let Poll::Pending = timer.poll(cx) { - return Poll::Pending; - } else { - self.as_mut().get_mut().timer = None; - } - } - - let mut stream = self.as_mut().stream(); - let mut last_item = None; - loop { - match stream.as_mut().poll_next(cx) { - Poll::Ready(None) => { - return Poll::Ready(None); - } - Poll::Ready(Some(item)) => last_item = Some(item), - Poll::Pending => break, - } - } - - if let Some(last_item) = last_item { - self.get_mut().timer = Some(smol::Timer::after(self.period)); - Poll::Ready(Some(last_item)) - } else { - Poll::Pending - } - } -} - -impl Throttled { - fn stream(self: Pin<&mut Self>) -> Pin<&mut S> { - unsafe { self.map_unchecked_mut(|s| &mut s.stream) } - } - - fn timer(self: Pin<&mut Self>) -> Option> { - if self.timer.is_some() { - Some(unsafe { self.map_unchecked_mut(|s| s.timer.as_mut().unwrap()) }) - } else { - None - } - } -} From b55acb63f0d018bdafe4ce68a6819efb3a87c7cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 Apr 2021 21:05:18 -0600 Subject: [PATCH 027/102] Fix warning --- zed/src/worktree.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 2c4a0850ed..3ff789b8c5 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -166,6 +166,7 @@ impl Worktree { Ok(path) } + #[cfg(test)] fn inode_for_path(&self, path: impl AsRef) -> Option { let path = path.as_ref(); self.root_inode().and_then(|mut inode| { From fd121172881cd2a49034f342e73a9b9cbd97ebc3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 15:25:43 +0200 Subject: [PATCH 028/102] Start on rescanning Worktree --- zed/src/sum_tree/mod.rs | 19 ++- zed/src/worktree.rs | 288 ++++++++++++++++++++++++++++++++++------ 2 files changed, 260 insertions(+), 47 deletions(-) diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index 7bd164c519..a78de88f80 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -332,11 +332,12 @@ impl SumTree { }; } - pub fn edit(&mut self, edits: &mut [Edit]) { + pub fn edit(&mut self, edits: &mut [Edit]) -> Vec { if edits.is_empty() { - return; + return Vec::new(); } + let mut replaced = Vec::new(); edits.sort_unstable_by_key(|item| item.key()); *self = { @@ -358,13 +359,19 @@ impl SumTree { new_tree.push_tree(slice); old_item = cursor.item(); } - if old_item.map_or(false, |old_item| old_item.key() == new_key) { - cursor.next(); + + if let Some(old_item) = old_item { + if old_item.key() == new_key { + replaced.push(old_item.clone()); + cursor.next(); + } } + match edit { Edit::Insert(item) => { buffered_items.push(item.clone()); } + Edit::Remove(_) => {} } } @@ -372,6 +379,8 @@ impl SumTree { new_tree.push_tree(cursor.suffix()); new_tree }; + + replaced } pub fn get(&self, key: &T::Key) -> Option<&T> { @@ -461,12 +470,14 @@ impl Node { #[derive(Debug)] pub enum Edit { Insert(T), + Remove(T::Key), } impl Edit { fn key(&self) -> T::Key { match self { Edit::Insert(item) => item.key(), + Edit::Remove(key) => key.clone(), } } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 3ff789b8c5..0b4df17f91 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -12,7 +12,10 @@ use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; -use std::future::Future; +use std::{ + collections::{HashMap, HashSet}, + future::Future, +}; use std::{ ffi::OsStr, fmt, fs, @@ -59,11 +62,12 @@ pub struct FileHandle { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { + let id = ctx.model_id(); let path = path.into(); let scan_state = smol::channel::unbounded(); - let scanner = BackgroundScanner::new(path.clone(), scan_state.0); + let scanner = BackgroundScanner::new(id, path.clone(), scan_state.0); let tree = Self { - id: ctx.model_id(), + id, path, entries: Default::default(), scanner, @@ -107,19 +111,10 @@ impl Worktree { } } - fn root_inode(&self) -> Option { - let ino = self.scanner.root_ino.load(atomic::Ordering::SeqCst); - if ino == 0 { - None - } else { - Some(ino) - } - } - pub fn snapshot(&self) -> Snapshot { Snapshot { id: self.id, - root_inode: self.root_inode(), + root_inode: self.scanner.root_inode(), entries: self.entries.clone(), } } @@ -166,26 +161,6 @@ impl Worktree { Ok(path) } - #[cfg(test)] - fn inode_for_path(&self, path: impl AsRef) -> Option { - let path = path.as_ref(); - self.root_inode().and_then(|mut inode| { - 'components: for path_component in path { - if let Some(Entry::Dir { children, .. }) = &self.entries.get(&inode) { - for child in children.as_ref() { - if self.entries.get(child).map(|entry| entry.name()) == Some(path_component) - { - inode = *child; - continue 'components; - } - } - } - return None; - } - Some(inode) - }) - } - pub fn load_history( &self, ino: u64, @@ -263,7 +238,7 @@ impl Entity for Worktree { impl fmt::Debug for Worktree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(root_ino) = self.root_inode() { + if let Some(root_ino) = self.scanner.root_inode() { self.fmt_entry(f, root_ino, 0) } else { write!(f, "Empty tree\n") @@ -279,6 +254,25 @@ impl Snapshot { pub fn root_entry(&self) -> Option<&Entry> { self.root_inode.and_then(|inode| self.entries.get(&inode)) } + + fn inode_for_path(&self, path: impl AsRef) -> Option { + let path = path.as_ref(); + self.root_inode.and_then(|mut inode| { + 'components: for path_component in path { + if let Some(Entry::Dir { children, .. }) = &self.entries.get(&inode) { + for child in children.as_ref() { + if self.entries.get(child).map(|entry| entry.name()) == Some(path_component) + { + inode = *child; + continue 'components; + } + } + } + return None; + } + Some(inode) + }) + } } impl FileHandle { @@ -400,6 +394,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { #[derive(Clone)] struct BackgroundScanner { + id: usize, path: Arc, root_ino: Arc, entries: Arc>>, @@ -408,8 +403,9 @@ struct BackgroundScanner { } impl BackgroundScanner { - fn new(path: Arc, notify: Sender) -> Self { + fn new(id: usize, path: Arc, notify: Sender) -> Self { Self { + id, path, root_ino: Arc::new(AtomicU64::new(0)), entries: Default::default(), @@ -418,17 +414,32 @@ impl BackgroundScanner { } } + fn root_inode(&self) -> Option { + let ino = self.root_ino.load(atomic::Ordering::SeqCst); + if ino == 0 { + None + } else { + Some(ino) + } + } + fn snapshot(&self) -> SumTree { self.entries.lock().clone() } fn run(&self) { + let scanner = self.clone(); let event_stream = fsevent::EventStream::new( &[self.path.as_ref()], Duration::from_millis(100), |events| { - eprintln!("events: {:?}", events); - true + if let Err(err) = scanner.process_events(events) { + dbg!(err); + // TODO: handle errors + false + } else { + true + } }, ); @@ -499,7 +510,7 @@ impl BackgroundScanner { pool.execute(|| { let result = result; while let Ok(job) = rx.recv() { - if let Err(err) = job.and_then(|job| self.scan_dir(job)) { + if let Err(err) = job.and_then(|job| self.scan_dir(job, None)) { *result = Err(err); break; } @@ -523,7 +534,7 @@ impl BackgroundScanner { Ok(()) } - fn scan_dir(&self, job: ScanJob) -> io::Result<()> { + fn scan_dir(&self, job: ScanJob, mut children: Option<&mut Vec>) -> io::Result<()> { let scan_queue = job.scan_queue; let mut dir_entry = job.dir_entry; @@ -541,6 +552,9 @@ impl BackgroundScanner { let path = job.path.join(name.as_ref()); new_children.push(ino); + if let Some(children) = children.as_mut() { + children.push(ino); + } if metadata.is_dir() { let mut is_ignored = true; let mut ignore = None; @@ -607,10 +621,193 @@ impl BackgroundScanner { Ok(()) } + fn process_events(&self, events: &[fsevent::Event]) -> Result { + if self.notify.receiver_count() == 0 { + return Ok(false); + } + + // TODO: should we canonicalize this at the start? + let root_path = self.path.canonicalize()?; + let snapshot = Snapshot { + id: self.id, + entries: self.entries.lock().clone(), + root_inode: self.root_inode(), + }; + let mut removed = HashSet::new(); + let mut paths = events.into_iter().map(|e| &*e.path).collect::>(); + paths.sort_unstable(); + + let mut paths = paths.into_iter().peekable(); + while let Some(path) = paths.next() { + let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); + + // Don't scan descendants of this path. + while paths.peek().map_or(false, |p| p.starts_with(path)) { + paths.next(); + } + + let mut stack = Vec::new(); + stack.extend(snapshot.inode_for_path(&relative_path)); + while let Some(inode) = stack.pop() { + removed.insert(inode); + if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) { + stack.extend(children.iter().copied()) + } + } + + match fs::metadata(path) { + Ok(metadata) => { + let inode = metadata.ino(); + let is_symlink = fs::symlink_metadata(path)?.file_type().is_symlink(); + let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); + let mut ignore = IgnoreBuilder::new().build().add_parents(path).unwrap(); + if metadata.is_dir() { + ignore = ignore.add_child(path).unwrap(); + } + let is_ignored = ignore.matched(path, metadata.is_dir()).is_ignore(); + let parent = if path == root_path { + None + } else { + Some(fs::metadata(path.parent().unwrap())?.ino()) + }; + + removed.remove(&inode); + if metadata.file_type().is_dir() { + let is_ignored = is_ignored || name.as_ref() == ".git"; + let dir_entry = Entry::Dir { + parent, + name, + inode, + is_symlink, + is_ignored, + children: Arc::from([]), + pending: true, + }; + self.insert_entries(Some(dir_entry.clone())); + + let (tx, rx) = crossbeam_channel::unbounded(); + + tx.send(Ok(ScanJob { + ino: inode, + path: Arc::from(path), + relative_path, + dir_entry, + ignore: Some(ignore), + scan_queue: tx.clone(), + })) + .unwrap(); + drop(tx); + + let mut inodes = Vec::new(); + inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); + self.thread_pool.scoped(|pool| { + for worker_inodes in &mut inodes { + pool.execute(|| { + let worker_inodes = worker_inodes; + while let Ok(job) = rx.recv() { + if let Err(err) = job.and_then(|job| { + self.scan_dir( + job, + Some(worker_inodes.as_mut().unwrap()), + ) + }) { + *worker_inodes = Err(err); + break; + } + } + }); + } + }); + + for worker_inodes in inodes { + for inode in worker_inodes? { + removed.remove(&inode); + } + } + } else { + self.insert_entries(Some(Entry::File { + parent, + name, + path: PathEntry::new(inode, &relative_path, is_ignored), + inode, + is_symlink, + is_ignored, + })); + } + } + Err(err) => { + if err.kind() != io::ErrorKind::NotFound { + return Err(anyhow::Error::new(err)); + } + } + } + } + + self.remove_entries(removed); + + Ok(self.notify.receiver_count() != 0) + } + fn insert_entries(&self, entries: impl IntoIterator) { - self.entries - .lock() - .edit(&mut entries.into_iter().map(Edit::Insert).collect::>()); + let mut edits = Vec::new(); + let mut new_parents = HashMap::new(); + for entry in entries { + new_parents.insert(entry.ino(), entry.parent()); + edits.push(Edit::Insert(entry)); + } + + let mut entries = self.entries.lock(); + let prev_entries = entries.edit(&mut edits); + Self::remove_stale_children(&mut *entries, prev_entries, new_parents); + } + + fn remove_entries(&self, inodes: impl IntoIterator) { + let mut entries = self.entries.lock(); + let prev_entries = + entries.edit(&mut inodes.into_iter().map(Edit::Remove).collect::>()); + Self::remove_stale_children(&mut *entries, prev_entries, HashMap::new()); + } + + fn remove_stale_children( + tree: &mut SumTree, + prev_entries: Vec, + new_parents: HashMap>, + ) { + let mut new_parent_entries = HashMap::new(); + + for prev_entry in prev_entries { + let new_parent = new_parents.get(&prev_entry.ino()).copied().flatten(); + if new_parent != prev_entry.parent() { + if let Some(prev_parent) = prev_entry.parent() { + let (_, new_children) = + new_parent_entries.entry(prev_parent).or_insert_with(|| { + let prev_parent_entry = tree.get(&prev_parent).unwrap(); + if let Entry::Dir { children, .. } = prev_parent_entry { + (prev_parent_entry.clone(), children.to_vec()) + } else { + unreachable!() + } + }); + + if let Some(ix) = new_children.iter().position(|ino| *ino == prev_entry.ino()) { + new_children.swap_remove(ix); + } + } + } + } + + let mut parent_edits = new_parent_entries + .into_iter() + .map(|(_, (mut parent_entry, new_children))| { + if let Entry::Dir { children, .. } = &mut parent_entry { + *children = Arc::from(new_children); + } else { + unreachable!() + } + Edit::Insert(parent_entry) + }) + .collect::>(); + tree.edit(&mut parent_edits); } } @@ -763,7 +960,12 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; - let file_entry = app.read(|ctx| tree.read(ctx).inode_for_path("dir1/file").unwrap()); + let file_entry = app.read(|ctx| { + tree.read(ctx) + .snapshot() + .inode_for_path("dir1/file") + .unwrap() + }); app.read(|ctx| { let tree = tree.read(ctx); assert_eq!( From fbd5fbd703272caa7a3afdccf7843d21e479f2b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 16:11:55 +0200 Subject: [PATCH 029/102] Parallelize scanning of changed directories --- zed/src/worktree.rs | 75 +++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 0b4df17f91..ba2a3e771c 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -434,7 +434,6 @@ impl BackgroundScanner { Duration::from_millis(100), |events| { if let Err(err) = scanner.process_events(events) { - dbg!(err); // TODO: handle errors false } else { @@ -637,6 +636,7 @@ impl BackgroundScanner { let mut paths = events.into_iter().map(|e| &*e.path).collect::>(); paths.sort_unstable(); + let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); let mut paths = paths.into_iter().peekable(); while let Some(path) = paths.next() { let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); @@ -685,45 +685,16 @@ impl BackgroundScanner { }; self.insert_entries(Some(dir_entry.clone())); - let (tx, rx) = crossbeam_channel::unbounded(); - - tx.send(Ok(ScanJob { - ino: inode, - path: Arc::from(path), - relative_path, - dir_entry, - ignore: Some(ignore), - scan_queue: tx.clone(), - })) - .unwrap(); - drop(tx); - - let mut inodes = Vec::new(); - inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); - self.thread_pool.scoped(|pool| { - for worker_inodes in &mut inodes { - pool.execute(|| { - let worker_inodes = worker_inodes; - while let Ok(job) = rx.recv() { - if let Err(err) = job.and_then(|job| { - self.scan_dir( - job, - Some(worker_inodes.as_mut().unwrap()), - ) - }) { - *worker_inodes = Err(err); - break; - } - } - }); - } - }); - - for worker_inodes in inodes { - for inode in worker_inodes? { - removed.remove(&inode); - } - } + scan_queue_tx + .send(Ok(ScanJob { + ino: inode, + path: Arc::from(path), + relative_path, + dir_entry, + ignore: Some(ignore), + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); } else { self.insert_entries(Some(Entry::File { parent, @@ -742,7 +713,31 @@ impl BackgroundScanner { } } } + drop(scan_queue_tx); + let mut inodes = Vec::new(); + inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); + self.thread_pool.scoped(|pool| { + for worker_inodes in &mut inodes { + pool.execute(|| { + let worker_inodes = worker_inodes; + while let Ok(job) = scan_queue_rx.recv() { + if let Err(err) = job.and_then(|job| { + self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) + }) { + *worker_inodes = Err(err); + break; + } + } + }); + } + }); + + for worker_inodes in inodes { + for inode in worker_inodes? { + removed.remove(&inode); + } + } self.remove_entries(removed); Ok(self.notify.receiver_count() != 0) From 09c4d651948e81eefd31910b7c9cc8e4f11fa64a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 16:13:28 +0200 Subject: [PATCH 030/102] :lipstick: --- zed/src/worktree.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index ba2a3e771c..e26469083f 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -715,10 +715,10 @@ impl BackgroundScanner { } drop(scan_queue_tx); - let mut inodes = Vec::new(); - inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); + let mut scanned_inodes = Vec::new(); + scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); self.thread_pool.scoped(|pool| { - for worker_inodes in &mut inodes { + for worker_inodes in &mut scanned_inodes { pool.execute(|| { let worker_inodes = worker_inodes; while let Ok(job) = scan_queue_rx.recv() { @@ -733,7 +733,7 @@ impl BackgroundScanner { } }); - for worker_inodes in inodes { + for worker_inodes in scanned_inodes { for inode in worker_inodes? { removed.remove(&inode); } From 457d9453762acf6a8a872728dc09ffe305612cb6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 16:16:46 +0200 Subject: [PATCH 031/102] Avoid unnecessary cloning of items when calling `SumTree::edit` --- zed/src/operation_queue.rs | 6 +----- zed/src/sum_tree/mod.rs | 4 ++-- zed/src/worktree.rs | 9 ++++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/zed/src/operation_queue.rs b/zed/src/operation_queue.rs index baad04b0bf..0603d5b9ff 100644 --- a/zed/src/operation_queue.rs +++ b/zed/src/operation_queue.rs @@ -35,11 +35,7 @@ impl OperationQueue { pub fn insert(&mut self, mut ops: Vec) { ops.sort_by_key(|op| op.timestamp()); ops.dedup_by_key(|op| op.timestamp()); - let mut edits = ops - .into_iter() - .map(|op| Edit::Insert(op)) - .collect::>(); - self.0.edit(&mut edits); + self.0.edit(ops.into_iter().map(Edit::Insert).collect()); } pub fn drain(&mut self) -> Self { diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index a78de88f80..413f0e7ddc 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -332,7 +332,7 @@ impl SumTree { }; } - pub fn edit(&mut self, edits: &mut [Edit]) -> Vec { + pub fn edit(&mut self, mut edits: Vec>) -> Vec { if edits.is_empty() { return Vec::new(); } @@ -369,7 +369,7 @@ impl SumTree { match edit { Edit::Insert(item) => { - buffered_items.push(item.clone()); + buffered_items.push(item); } Edit::Remove(_) => {} } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index e26469083f..be2cb88fbc 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -752,14 +752,13 @@ impl BackgroundScanner { } let mut entries = self.entries.lock(); - let prev_entries = entries.edit(&mut edits); + let prev_entries = entries.edit(edits); Self::remove_stale_children(&mut *entries, prev_entries, new_parents); } fn remove_entries(&self, inodes: impl IntoIterator) { let mut entries = self.entries.lock(); - let prev_entries = - entries.edit(&mut inodes.into_iter().map(Edit::Remove).collect::>()); + let prev_entries = entries.edit(inodes.into_iter().map(Edit::Remove).collect()); Self::remove_stale_children(&mut *entries, prev_entries, HashMap::new()); } @@ -791,7 +790,7 @@ impl BackgroundScanner { } } - let mut parent_edits = new_parent_entries + let parent_edits = new_parent_entries .into_iter() .map(|(_, (mut parent_entry, new_children))| { if let Entry::Dir { children, .. } = &mut parent_entry { @@ -802,7 +801,7 @@ impl BackgroundScanner { Edit::Insert(parent_entry) }) .collect::>(); - tree.edit(&mut parent_edits); + tree.edit(parent_edits); } } From b68b0fce56988c0cfedb9b6b4181b97a5e72b6f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 16:26:40 +0200 Subject: [PATCH 032/102] Add simple unit test for `SumTree::{edit,get}` --- zed/src/sum_tree/mod.rs | 54 ++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index 413f0e7ddc..03a9430ef5 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -337,7 +337,7 @@ impl SumTree { return Vec::new(); } - let mut replaced = Vec::new(); + let mut removed = Vec::new(); edits.sort_unstable_by_key(|item| item.key()); *self = { @@ -362,7 +362,7 @@ impl SumTree { if let Some(old_item) = old_item { if old_item.key() == new_key { - replaced.push(old_item.clone()); + removed.push(old_item.clone()); cursor.next(); } } @@ -380,7 +380,7 @@ impl SumTree { new_tree }; - replaced + removed } pub fn get(&self, key: &T::Key) -> Option<&T> { @@ -497,6 +497,7 @@ where #[cfg(test)] mod tests { use super::*; + use std::cmp; use std::ops::Add; #[test] @@ -780,11 +781,33 @@ mod tests { assert_eq!(cursor.slice(&Count(6), SeekBias::Right).items(), vec![6]); } + #[test] + fn test_edit() { + let mut tree = SumTree::::new(); + + let removed = tree.edit(vec![Edit::Insert(1), Edit::Insert(2), Edit::Insert(0)]); + assert_eq!(tree.items(), vec![0, 1, 2]); + assert_eq!(removed, Vec::::new()); + assert_eq!(tree.get(&0), Some(&0)); + assert_eq!(tree.get(&1), Some(&1)); + assert_eq!(tree.get(&2), Some(&2)); + assert_eq!(tree.get(&4), None); + + let removed = tree.edit(vec![Edit::Insert(2), Edit::Insert(4), Edit::Remove(0)]); + assert_eq!(tree.items(), vec![1, 2, 4]); + assert_eq!(removed, vec![0, 2]); + assert_eq!(tree.get(&0), None); + assert_eq!(tree.get(&1), Some(&1)); + assert_eq!(tree.get(&2), Some(&2)); + assert_eq!(tree.get(&4), Some(&4)); + } + #[derive(Clone, Default, Debug)] pub struct IntegersSummary { count: Count, sum: Sum, contains_even: bool, + max: u8, } #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] @@ -801,15 +824,31 @@ mod tests { count: Count(1), sum: Sum(*self as usize), contains_even: (*self & 1) == 0, + max: *self, } } } + impl KeyedItem for u8 { + type Key = u8; + + fn key(&self) -> Self::Key { + *self + } + } + + impl<'a> Dimension<'a, IntegersSummary> for u8 { + fn add_summary(&mut self, summary: &IntegersSummary) { + *self = summary.max; + } + } + impl<'a> AddAssign<&'a Self> for IntegersSummary { fn add_assign(&mut self, other: &Self) { self.count.0 += &other.count.0; self.sum.0 += &other.sum.0; self.contains_even |= other.contains_even; + self.max = cmp::max(self.max, other.max); } } @@ -819,15 +858,6 @@ mod tests { } } - // impl<'a> Add<&'a Self> for Count { - // type Output = Self; - // - // fn add(mut self, other: &Self) -> Self { - // self.0 += other.0; - // self - // } - // } - impl<'a> Dimension<'a, IntegersSummary> for Sum { fn add_summary(&mut self, summary: &IntegersSummary) { self.0 += summary.sum.0; From ce5fbbb46b9ace5312169119b08fbf3e0cad9d32 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 17:22:30 +0200 Subject: [PATCH 033/102] WIP --- zed/src/worktree.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index be2cb88fbc..5afa708495 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -428,16 +428,16 @@ impl BackgroundScanner { } fn run(&self) { - let scanner = self.clone(); let event_stream = fsevent::EventStream::new( &[self.path.as_ref()], Duration::from_millis(100), |events| { - if let Err(err) = scanner.process_events(events) { - // TODO: handle errors - false - } else { - true + match self.process_events(events) { + Ok(alive) => alive, + Err(err) => { + // TODO: handle errors + false + } } }, ); @@ -954,7 +954,7 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; - let file_entry = app.read(|ctx| { + let file_inode = app.read(|ctx| { tree.read(ctx) .snapshot() .inode_for_path("dir1/file") @@ -963,7 +963,7 @@ mod tests { app.read(|ctx| { let tree = tree.read(ctx); assert_eq!( - tree.path_for_inode(file_entry, false) + tree.path_for_inode(file_inode, false) .unwrap() .to_str() .unwrap(), @@ -975,14 +975,21 @@ mod tests { assert_condition(1, 300, || { app.read(|ctx| { let tree = tree.read(ctx); - tree.path_for_inode(file_entry, false) + tree.path_for_inode(file_inode, false) .unwrap() .to_str() .unwrap() == "dir2/file" }) }) - .await + .await; + app.read(|ctx| { + let tree = tree.read(ctx); + assert_eq!( + tree.snapshot().inode_for_path("dir2/file"), + Some(file_inode) + ); + }); }); } } From e19a56c36642494d054613447cf8ee065729d269 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 16 Apr 2021 19:54:46 +0200 Subject: [PATCH 034/102] WIP --- fsevent/src/lib.rs | 4 +- zed/src/worktree.rs | 94 ++++++++++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/fsevent/src/lib.rs b/fsevent/src/lib.rs index 8f81b2e016..38baaf2c84 100644 --- a/fsevent/src/lib.rs +++ b/fsevent/src/lib.rs @@ -27,7 +27,7 @@ unsafe impl Send for EventStream {} impl EventStream where - F: FnMut(&[Event]) -> bool, + F: FnMut(Vec) -> bool, { pub fn new(paths: &[&Path], latency: Duration, callback: F) -> Self { unsafe { @@ -128,7 +128,7 @@ where } } - if !callback(&events) { + if !callback(events) { fs::FSEventStreamStop(stream_ref); cf::CFRunLoopStop(cf::CFRunLoopGetCurrent()); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 5afa708495..9a8752b87e 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -620,7 +620,7 @@ impl BackgroundScanner { Ok(()) } - fn process_events(&self, events: &[fsevent::Event]) -> Result { + fn process_events(&self, mut events: Vec) -> Result { if self.notify.receiver_count() == 0 { return Ok(false); } @@ -633,45 +633,40 @@ impl BackgroundScanner { root_inode: self.root_inode(), }; let mut removed = HashSet::new(); - let mut paths = events.into_iter().map(|e| &*e.path).collect::>(); - paths.sort_unstable(); + let mut observed = HashSet::new(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); - let mut paths = paths.into_iter().peekable(); + + events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + let mut paths = events.into_iter().map(|e| e.path).peekable(); while let Some(path) = paths.next() { let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); - - // Don't scan descendants of this path. - while paths.peek().map_or(false, |p| p.starts_with(path)) { - paths.next(); - } - - let mut stack = Vec::new(); - stack.extend(snapshot.inode_for_path(&relative_path)); - while let Some(inode) = stack.pop() { - removed.insert(inode); - if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) { - stack.extend(children.iter().copied()) - } - } - - match fs::metadata(path) { + match fs::metadata(&path) { Ok(metadata) => { let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(path)?.file_type().is_symlink(); + let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); - let mut ignore = IgnoreBuilder::new().build().add_parents(path).unwrap(); + let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); if metadata.is_dir() { - ignore = ignore.add_child(path).unwrap(); + ignore = ignore.add_child(&path).unwrap(); } - let is_ignored = ignore.matched(path, metadata.is_dir()).is_ignore(); + let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); let parent = if path == root_path { None } else { Some(fs::metadata(path.parent().unwrap())?.ino()) }; - removed.remove(&inode); + let prev_entry = snapshot.entries.get(&inode); + // If we haven't seen this inode yet, we are going to recursively scan it, so + // ignore event involving a descendant. + if prev_entry.is_none() { + while paths.peek().map_or(false, |p| p.starts_with(&path)) { + paths.next(); + } + } + + observed.insert(inode); if metadata.file_type().is_dir() { let is_ignored = is_ignored || name.as_ref() == ".git"; let dir_entry = Entry::Dir { @@ -707,7 +702,18 @@ impl BackgroundScanner { } } Err(err) => { - if err.kind() != io::ErrorKind::NotFound { + if err.kind() == io::ErrorKind::NotFound { + // Fill removed with the inodes of all descendants of this path. + let mut stack = Vec::new(); + stack.extend(snapshot.inode_for_path(&relative_path)); + while let Some(inode) = stack.pop() { + removed.insert(inode); + if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) + { + stack.extend(children.iter().copied()) + } + } + } else { return Err(anyhow::Error::new(err)); } } @@ -735,10 +741,10 @@ impl BackgroundScanner { for worker_inodes in scanned_inodes { for inode in worker_inodes? { - removed.remove(&inode); + remove_counts.remove(&inode); } } - self.remove_entries(removed); + self.remove_entries(remove_counts); Ok(self.notify.receiver_count() != 0) } @@ -943,9 +949,20 @@ mod tests { #[test] fn test_rescan() { App::test_async((), |mut app| async move { + let dir2 = temp_tree(json!({ + "dir1": { + "dir3": { + "file": "contents", + } + }, + "dir2": { + } + })); let dir = temp_tree(json!({ "dir1": { - "file": "contents" + "dir3": { + "file": "contents", + } }, "dir2": { } @@ -954,41 +971,38 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; - let file_inode = app.read(|ctx| { + let dir_inode = app.read(|ctx| { tree.read(ctx) .snapshot() - .inode_for_path("dir1/file") + .inode_for_path("dir1/dir3") .unwrap() }); app.read(|ctx| { let tree = tree.read(ctx); assert_eq!( - tree.path_for_inode(file_inode, false) + tree.path_for_inode(dir_inode, false) .unwrap() .to_str() .unwrap(), - "dir1/file" + "dir1/dir3" ); }); - std::fs::rename(dir.path().join("dir1/file"), dir.path().join("dir2/file")).unwrap(); + std::fs::rename(dir2.path(), dir.path().join("foo")).unwrap(); assert_condition(1, 300, || { app.read(|ctx| { let tree = tree.read(ctx); - tree.path_for_inode(file_inode, false) + tree.path_for_inode(dir_inode, false) .unwrap() .to_str() .unwrap() - == "dir2/file" + == "dir2/dir3" }) }) .await; app.read(|ctx| { let tree = tree.read(ctx); - assert_eq!( - tree.snapshot().inode_for_path("dir2/file"), - Some(file_inode) - ); + assert_eq!(tree.snapshot().inode_for_path("dir2/dir3"), Some(dir_inode)); }); }); } From 6a549727ce0919c43b1b23306d21ef796506f738 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 Apr 2021 12:53:07 -0600 Subject: [PATCH 035/102] WIP: Lay down a skeleton for another attempt at rescan Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 566 +++++++++++++++++++++++++------------------- 1 file changed, 317 insertions(+), 249 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 9a8752b87e..81f62f2a06 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -23,10 +23,7 @@ use std::{ ops::AddAssign, os::unix::fs::MetadataExt, path::{Path, PathBuf}, - sync::{ - atomic::{self, AtomicU64}, - Arc, - }, + sync::Arc, time::Duration, }; @@ -40,16 +37,16 @@ enum ScanState { } pub struct Worktree { - id: usize, - path: Arc, - entries: SumTree, - scanner: BackgroundScanner, + snapshot: Snapshot, + scanner: Arc, scan_state: ScanState, poll_scheduled: bool, } +#[derive(Clone)] pub struct Snapshot { id: usize, + path: Arc, root_inode: Option, entries: SumTree, } @@ -62,14 +59,16 @@ pub struct FileHandle { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { - let id = ctx.model_id(); - let path = path.into(); let scan_state = smol::channel::unbounded(); - let scanner = BackgroundScanner::new(id, path.clone(), scan_state.0); - let tree = Self { - id, - path, + let snapshot = Snapshot { + id: ctx.model_id(), + path: path.into(), + root_inode: None, entries: Default::default(), + }; + let scanner = Arc::new(BackgroundScanner::new(snapshot.clone(), scan_state.0)); + let tree = Self { + snapshot, scanner, scan_state: ScanState::Idle, poll_scheduled: false, @@ -90,7 +89,7 @@ impl Worktree { } fn poll_entries(&mut self, ctx: &mut ModelContext) { - self.entries = self.scanner.snapshot(); + self.snapshot = self.scanner.snapshot(); ctx.notify(); if self.is_scanning() && !self.poll_scheduled { @@ -112,27 +111,23 @@ impl Worktree { } pub fn snapshot(&self) -> Snapshot { - Snapshot { - id: self.id, - root_inode: self.scanner.root_inode(), - entries: self.entries.clone(), - } + self.snapshot.clone() } pub fn contains_path(&self, path: &Path) -> bool { - path.starts_with(&self.path) + path.starts_with(&self.snapshot.path) } pub fn has_inode(&self, inode: u64) -> bool { - self.entries.get(&inode).is_some() + self.snapshot.entries.get(&inode).is_some() } pub fn file_count(&self) -> usize { - self.entries.summary().file_count + self.snapshot.entries.summary().file_count } pub fn abs_path_for_inode(&self, ino: u64) -> Result { - let mut result = self.path.to_path_buf(); + let mut result = self.snapshot.path.to_path_buf(); result.push(self.path_for_inode(ino, false)?); Ok(result) } @@ -140,12 +135,13 @@ impl Worktree { pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result { let mut components = Vec::new(); let mut entry = self + .snapshot .entries .get(&ino) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; components.push(entry.name()); while let Some(parent) = entry.parent() { - entry = self.entries.get(&parent).unwrap(); + entry = self.snapshot.entries.get(&parent).unwrap(); components.push(entry.name()); } @@ -196,7 +192,7 @@ impl Worktree { } fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { - match self.entries.get(&ino).unwrap() { + match self.snapshot.entries.get(&ino).unwrap() { Entry::Dir { name, children, .. } => { write!( f, @@ -222,13 +218,16 @@ impl Worktree { #[cfg(test)] pub fn files<'a>(&'a self) -> impl Iterator + 'a { - self.entries.cursor::<(), ()>().filter_map(|entry| { - if let Entry::File { inode, .. } = entry { - Some(*inode) - } else { - None - } - }) + self.snapshot + .entries + .cursor::<(), ()>() + .filter_map(|entry| { + if let Entry::File { inode, .. } = entry { + Some(*inode) + } else { + None + } + }) } } @@ -238,7 +237,7 @@ impl Entity for Worktree { impl fmt::Debug for Worktree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(root_ino) = self.scanner.root_inode() { + if let Some(root_ino) = self.snapshot.root_inode { self.fmt_entry(f, root_ino, 0) } else { write!(f, "Empty tree\n") @@ -273,6 +272,11 @@ impl Snapshot { Some(inode) }) } + + fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { + self.inode_for_path(path) + .and_then(|inode| self.entries.get(&inode)) + } } impl FileHandle { @@ -392,55 +396,60 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { } } -#[derive(Clone)] struct BackgroundScanner { - id: usize, - path: Arc, - root_ino: Arc, - entries: Arc>>, + snapshot: Mutex, notify: Sender, thread_pool: scoped_pool::Pool, } impl BackgroundScanner { - fn new(id: usize, path: Arc, notify: Sender) -> Self { + fn new(snapshot: Snapshot, notify: Sender) -> Self { Self { - id, - path, - root_ino: Arc::new(AtomicU64::new(0)), - entries: Default::default(), + snapshot: Mutex::new(snapshot), notify, thread_pool: scoped_pool::Pool::new(16), } } - fn root_inode(&self) -> Option { - let ino = self.root_ino.load(atomic::Ordering::SeqCst); - if ino == 0 { - None - } else { - Some(ino) - } + fn path(&self) -> Arc { + self.snapshot.lock().path.clone() } - fn snapshot(&self) -> SumTree { - self.entries.lock().clone() + fn snapshot(&self) -> Snapshot { + self.snapshot.lock().clone() } fn run(&self) { - let event_stream = fsevent::EventStream::new( - &[self.path.as_ref()], - Duration::from_millis(100), - |events| { - match self.process_events(events) { - Ok(alive) => alive, - Err(err) => { - // TODO: handle errors - false - } + let path = { + let mut snapshot = self.snapshot.lock(); + let canonical_path = snapshot + .path + .canonicalize() + .map(Arc::from) + .unwrap_or_else(|_| snapshot.path.clone()); + snapshot.path = canonical_path.clone(); + canonical_path + }; + + // Create the event stream before we start scanning to ensure we receive events for changes + // that occur in the middle of the scan. + let event_stream = + fsevent::EventStream::new(&[path.as_ref()], Duration::from_millis(100), |events| { + if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { + return false; } - }, - ); + + if let Err(error) = self.process_events(events) { + log::error!("error handling events: {}", error); + return false; + } + + if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { + return false; + } + + true + }); if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return; @@ -460,40 +469,38 @@ impl BackgroundScanner { } fn scan_dirs(&self) -> io::Result<()> { - let metadata = fs::metadata(&self.path)?; - let ino = metadata.ino(); - let is_symlink = fs::symlink_metadata(&self.path)?.file_type().is_symlink(); - let name = Arc::from(self.path.file_name().unwrap_or(OsStr::new("/"))); + let path = self.path(); + let metadata = fs::metadata(&path)?; + let inode = metadata.ino(); + let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); + let name = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); let relative_path = PathBuf::from(&name); - let mut ignore = IgnoreBuilder::new() - .build() - .add_parents(&self.path) - .unwrap(); + let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); if metadata.is_dir() { - ignore = ignore.add_child(&self.path).unwrap(); + ignore = ignore.add_child(&path).unwrap(); } - let is_ignored = ignore.matched(&self.path, metadata.is_dir()).is_ignore(); + let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); if metadata.file_type().is_dir() { let is_ignored = is_ignored || name.as_ref() == ".git"; let dir_entry = Entry::Dir { parent: None, name, - inode: ino, + inode, is_symlink, is_ignored, children: Arc::from([]), pending: true, }; self.insert_entries(Some(dir_entry.clone())); - self.root_ino.store(ino, atomic::Ordering::SeqCst); + self.snapshot.lock().root_inode = Some(inode); let (tx, rx) = crossbeam_channel::unbounded(); tx.send(Ok(ScanJob { - ino, - path: self.path.clone(), + ino: inode, + path: path.clone(), relative_path, dir_entry, ignore: Some(ignore), @@ -522,12 +529,12 @@ impl BackgroundScanner { self.insert_entries(Some(Entry::File { parent: None, name, - path: PathEntry::new(ino, &relative_path, is_ignored), - inode: ino, + path: PathEntry::new(inode, &relative_path, is_ignored), + inode, is_symlink, is_ignored, })); - self.root_ino.store(ino, atomic::Ordering::SeqCst); + self.snapshot.lock().root_inode = Some(inode); } Ok(()) @@ -620,195 +627,256 @@ impl BackgroundScanner { Ok(()) } - fn process_events(&self, mut events: Vec) -> Result { - if self.notify.receiver_count() == 0 { - return Ok(false); - } - - // TODO: should we canonicalize this at the start? - let root_path = self.path.canonicalize()?; - let snapshot = Snapshot { - id: self.id, - entries: self.entries.lock().clone(), - root_inode: self.root_inode(), - }; - let mut removed = HashSet::new(); - let mut observed = HashSet::new(); - - let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); + fn process_events(&self, mut events: Vec) -> Result<()> { + let snapshot = self.snapshot(); events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut paths = events.into_iter().map(|e| e.path).peekable(); - while let Some(path) = paths.next() { - let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); - match fs::metadata(&path) { - Ok(metadata) => { - let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); - let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); - let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - if metadata.is_dir() { - ignore = ignore.add_child(&path).unwrap(); - } - let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - let parent = if path == root_path { - None - } else { - Some(fs::metadata(path.parent().unwrap())?.ino()) - }; - let prev_entry = snapshot.entries.get(&inode); - // If we haven't seen this inode yet, we are going to recursively scan it, so - // ignore event involving a descendant. - if prev_entry.is_none() { - while paths.peek().map_or(false, |p| p.starts_with(&path)) { - paths.next(); - } - } + for path in paths { + let relative_path = path.strip_prefix(&snapshot.path)?.to_path_buf(); + let snapshot_entry = snapshot.entry_for_path(relative_path); - observed.insert(inode); - if metadata.file_type().is_dir() { - let is_ignored = is_ignored || name.as_ref() == ".git"; - let dir_entry = Entry::Dir { - parent, - name, - inode, - is_symlink, - is_ignored, - children: Arc::from([]), - pending: true, - }; - self.insert_entries(Some(dir_entry.clone())); + if let Some(fs_entry) = self.fs_entry_for_path(&path) { + if let Some(snapshot_entry) = snapshot_entry { + // If the parent does not match: + // Remove snapshot entry from its parent. + // Set its parent to none. + // Add its inode to a set of inodes to potentially remove after the batch. - scan_queue_tx - .send(Ok(ScanJob { - ino: inode, - path: Arc::from(path), - relative_path, - dir_entry, - ignore: Some(ignore), - scan_queue: scan_queue_tx.clone(), - })) - .unwrap(); - } else { - self.insert_entries(Some(Entry::File { - parent, - name, - path: PathEntry::new(inode, &relative_path, is_ignored), - inode, - is_symlink, - is_ignored, - })); - } + // If the parent does match, continue to next path } - Err(err) => { - if err.kind() == io::ErrorKind::NotFound { - // Fill removed with the inodes of all descendants of this path. - let mut stack = Vec::new(); - stack.extend(snapshot.inode_for_path(&relative_path)); - while let Some(inode) = stack.pop() { - removed.insert(inode); - if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) - { - stack.extend(children.iter().copied()) - } - } - } else { - return Err(anyhow::Error::new(err)); - } + + // If we get here, we either had no snapshot entry at the path or we had the wrong + // entry (different inode) and removed it. + // In either case, we now need to add the entry for this path + + if let Some(existing_snapshot_entry) = snapshot.entry_for_inode(fs_entry.inode) { + // An entry already exists in the snapshot, but in the wrong spot. + // Set its parent to the correct parent + // Insert it in the children of its parent + } else { + // An entry doesn't exist in the snapshot, this is the first time we've seen it. + // If this is a directory, do a recursive scan and discard subsequent events that are contained by the current path + // Then set the parent of the result of that scan to the correct parent + // Insert it in the children of that parent. + } + } else { + if let Some(snapshot_entry) = snapshot_entry { + // Remove snapshot entry from its parent. + // Set its parent to none. + // Add its inode to a set of inodes to potentially remove after the batch. } } } - drop(scan_queue_tx); - let mut scanned_inodes = Vec::new(); - scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); - self.thread_pool.scoped(|pool| { - for worker_inodes in &mut scanned_inodes { - pool.execute(|| { - let worker_inodes = worker_inodes; - while let Ok(job) = scan_queue_rx.recv() { - if let Err(err) = job.and_then(|job| { - self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) - }) { - *worker_inodes = Err(err); - break; - } - } - }); - } - }); + // Check whether any entry whose parent was set to none is still an orphan. If so, remove it and all descedants. - for worker_inodes in scanned_inodes { - for inode in worker_inodes? { - remove_counts.remove(&inode); - } - } - self.remove_entries(remove_counts); + *self.snapshot.lock() = snapshot; - Ok(self.notify.receiver_count() != 0) + Ok(()) } + fn fs_entry_for_path(&self, path: &Path) -> Option { + todo!() + } + + // fn process_events2(&self, mut events: Vec) -> Result { + // if self.notify.receiver_count() == 0 { + // return Ok(false); + // } + + // // TODO: should we canonicalize this at the start? + // let root_path = self.path.canonicalize()?; + // let snapshot = Snapshot { + // id: self.id, + // entries: self.entries.lock().clone(), + // root_inode: self.root_inode(), + // }; + // let mut removed = HashSet::new(); + // let mut observed = HashSet::new(); + + // let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); + + // events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + // let mut paths = events.into_iter().map(|e| e.path).peekable(); + // while let Some(path) = paths.next() { + // let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); + // match fs::metadata(&path) { + // Ok(metadata) => { + // let inode = metadata.ino(); + // let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); + // let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); + // let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); + // if metadata.is_dir() { + // ignore = ignore.add_child(&path).unwrap(); + // } + // let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); + // let parent = if path == root_path { + // None + // } else { + // Some(fs::metadata(path.parent().unwrap())?.ino()) + // }; + + // let prev_entry = snapshot.entries.get(&inode); + // // If we haven't seen this inode yet, we are going to recursively scan it, so + // // ignore event involving a descendant. + // if prev_entry.is_none() { + // while paths.peek().map_or(false, |p| p.starts_with(&path)) { + // paths.next(); + // } + // } + + // observed.insert(inode); + // if metadata.file_type().is_dir() { + // let is_ignored = is_ignored || name.as_ref() == ".git"; + // let dir_entry = Entry::Dir { + // parent, + // name, + // inode, + // is_symlink, + // is_ignored, + // children: Arc::from([]), + // pending: true, + // }; + // self.insert_entries(Some(dir_entry.clone())); + + // scan_queue_tx + // .send(Ok(ScanJob { + // ino: inode, + // path: Arc::from(path), + // relative_path, + // dir_entry, + // ignore: Some(ignore), + // scan_queue: scan_queue_tx.clone(), + // })) + // .unwrap(); + // } else { + // self.insert_entries(Some(Entry::File { + // parent, + // name, + // path: PathEntry::new(inode, &relative_path, is_ignored), + // inode, + // is_symlink, + // is_ignored, + // })); + // } + // } + // Err(err) => { + // if err.kind() == io::ErrorKind::NotFound { + // // Fill removed with the inodes of all descendants of this path. + // let mut stack = Vec::new(); + // stack.extend(snapshot.inode_for_path(&relative_path)); + // while let Some(inode) = stack.pop() { + // removed.insert(inode); + // if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) + // { + // stack.extend(children.iter().copied()) + // } + // } + // } else { + // return Err(anyhow::Error::new(err)); + // } + // } + // } + // } + // drop(scan_queue_tx); + + // let mut scanned_inodes = Vec::new(); + // scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); + // self.thread_pool.scoped(|pool| { + // for worker_inodes in &mut scanned_inodes { + // pool.execute(|| { + // let worker_inodes = worker_inodes; + // while let Ok(job) = scan_queue_rx.recv() { + // if let Err(err) = job.and_then(|job| { + // self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) + // }) { + // *worker_inodes = Err(err); + // break; + // } + // } + // }); + // } + // }); + + // for worker_inodes in scanned_inodes { + // for inode in worker_inodes? { + // remove_counts.remove(&inode); + // } + // } + // self.remove_entries(remove_counts); + + // Ok(self.notify.receiver_count() != 0) + // } + fn insert_entries(&self, entries: impl IntoIterator) { - let mut edits = Vec::new(); - let mut new_parents = HashMap::new(); - for entry in entries { - new_parents.insert(entry.ino(), entry.parent()); - edits.push(Edit::Insert(entry)); - } - - let mut entries = self.entries.lock(); - let prev_entries = entries.edit(edits); - Self::remove_stale_children(&mut *entries, prev_entries, new_parents); + self.snapshot + .lock() + .entries + .edit(entries.into_iter().map(Edit::Insert).collect::>()); } - fn remove_entries(&self, inodes: impl IntoIterator) { - let mut entries = self.entries.lock(); - let prev_entries = entries.edit(inodes.into_iter().map(Edit::Remove).collect()); - Self::remove_stale_children(&mut *entries, prev_entries, HashMap::new()); - } + // fn insert_entries(&self, entries: impl IntoIterator) { + // let mut edits = Vec::new(); + // let mut new_parents = HashMap::new(); + // for entry in entries { + // new_parents.insert(entry.ino(), entry.parent()); + // edits.push(Edit::Insert(entry)); + // } - fn remove_stale_children( - tree: &mut SumTree, - prev_entries: Vec, - new_parents: HashMap>, - ) { - let mut new_parent_entries = HashMap::new(); + // let mut entries = self.snapshot.lock().entries; + // let prev_entries = entries.edit(edits); + // Self::remove_stale_children(&mut *entries, prev_entries, new_parents); + // } - for prev_entry in prev_entries { - let new_parent = new_parents.get(&prev_entry.ino()).copied().flatten(); - if new_parent != prev_entry.parent() { - if let Some(prev_parent) = prev_entry.parent() { - let (_, new_children) = - new_parent_entries.entry(prev_parent).or_insert_with(|| { - let prev_parent_entry = tree.get(&prev_parent).unwrap(); - if let Entry::Dir { children, .. } = prev_parent_entry { - (prev_parent_entry.clone(), children.to_vec()) - } else { - unreachable!() - } - }); + // fn remove_entries(&self, inodes: impl IntoIterator) { + // let mut entries = self.entries.lock(); + // let prev_entries = entries.edit(inodes.into_iter().map(Edit::Remove).collect()); + // Self::remove_stale_children(&mut *entries, prev_entries, HashMap::new()); + // } - if let Some(ix) = new_children.iter().position(|ino| *ino == prev_entry.ino()) { - new_children.swap_remove(ix); - } - } - } - } + // fn remove_stale_children( + // tree: &mut SumTree, + // prev_entries: Vec, + // new_parents: HashMap>, + // ) { + // let mut new_parent_entries = HashMap::new(); - let parent_edits = new_parent_entries - .into_iter() - .map(|(_, (mut parent_entry, new_children))| { - if let Entry::Dir { children, .. } = &mut parent_entry { - *children = Arc::from(new_children); - } else { - unreachable!() - } - Edit::Insert(parent_entry) - }) - .collect::>(); - tree.edit(parent_edits); - } + // for prev_entry in prev_entries { + // let new_parent = new_parents.get(&prev_entry.ino()).copied().flatten(); + // if new_parent != prev_entry.parent() { + // if let Some(prev_parent) = prev_entry.parent() { + // let (_, new_children) = + // new_parent_entries.entry(prev_parent).or_insert_with(|| { + // let prev_parent_entry = tree.get(&prev_parent).unwrap(); + // if let Entry::Dir { children, .. } = prev_parent_entry { + // (prev_parent_entry.clone(), children.to_vec()) + // } else { + // unreachable!() + // } + // }); + + // if let Some(ix) = new_children.iter().position(|ino| *ino == prev_entry.ino()) { + // new_children.swap_remove(ix); + // } + // } + // } + // } + + // let parent_edits = new_parent_entries + // .into_iter() + // .map(|(_, (mut parent_entry, new_children))| { + // if let Entry::Dir { children, .. } = &mut parent_entry { + // *children = Arc::from(new_children); + // } else { + // unreachable!() + // } + // Edit::Insert(parent_entry) + // }) + // .collect::>(); + // tree.edit(parent_edits); + // } } struct ScanJob { From 5b05e57441a06117c2acc7f6cc33d9a08534970e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 16 Apr 2021 15:22:35 -0700 Subject: [PATCH 036/102] Start filling out new structure for processing fs events --- zed/src/worktree.rs | 495 +++++++++++++++++++++++--------------------- 1 file changed, 261 insertions(+), 234 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 81f62f2a06..04b53e85a2 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -12,10 +12,7 @@ use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; -use std::{ - collections::{HashMap, HashSet}, - future::Future, -}; +use std::{collections::HashSet, future::Future}; use std::{ ffi::OsStr, fmt, fs, @@ -178,7 +175,6 @@ impl Worktree { ctx: &AppContext, ) -> Task> { let path = self.abs_path_for_inode(ino); - eprintln!("save to path: {:?}", path); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); let file = std::fs::File::create(&path?)?; @@ -277,6 +273,80 @@ impl Snapshot { self.inode_for_path(path) .and_then(|inode| self.entries.get(&inode)) } + + fn reparent_entry( + &mut self, + child_inode: u64, + old_parent_inode: Option, + new_parent_inode: Option, + ) { + let mut edits_len = 1; + if old_parent_inode.is_some() { + edits_len += 1; + } + if new_parent_inode.is_some() { + edits_len += 1; + } + let mut deletions = Vec::with_capacity(edits_len); + let mut insertions = Vec::with_capacity(edits_len); + + // Remove the entries from the sum tree. + deletions.push(Edit::Remove(child_inode)); + if let Some(old_parent_inode) = old_parent_inode { + deletions.push(Edit::Remove(old_parent_inode)); + } + if let Some(new_parent_inode) = new_parent_inode { + deletions.push(Edit::Remove(new_parent_inode)); + } + let removed_entries = self.entries.edit(deletions); + let mut child_entry = None; + let mut old_parent_entry = None; + let mut new_parent_entry = None; + for removed_entry in removed_entries { + if removed_entry.ino() == child_inode { + child_entry = Some(removed_entry); + } else if Some(removed_entry.ino()) == old_parent_inode { + old_parent_entry = Some(removed_entry); + } else if Some(removed_entry.ino()) == new_parent_inode { + new_parent_entry = Some(removed_entry); + } + } + + // Update the child entry's parent. + let mut child_entry = child_entry.expect("cannot reparent non-existent entry"); + child_entry.set_parent(new_parent_inode); + insertions.push(Edit::Insert(child_entry)); + + // Remove the child entry from it's old parent's children. + if let Some(mut old_parent_entry) = old_parent_entry { + if let Entry::Dir { children, .. } = &mut old_parent_entry { + *children = children + .into_iter() + .cloned() + .filter(|c| *c != child_inode) + .collect(); + insertions.push(Edit::Insert(old_parent_entry)); + } else { + panic!("snapshot entry's new parent was not a directory"); + } + } + + // Add the child entry to it's new parent's children. + if let Some(mut new_parent_entry) = new_parent_entry { + if let Entry::Dir { children, .. } = &mut new_parent_entry { + *children = children + .into_iter() + .cloned() + .chain(Some(child_inode)) + .collect(); + insertions.push(Edit::Insert(new_parent_entry)); + } else { + panic!("snapshot entry's new parent is not a directory"); + } + } + + self.entries.edit(insertions); + } } impl FileHandle { @@ -337,6 +407,13 @@ impl Entry { } } + fn set_parent(&mut self, new_parent: Option) { + match self { + Entry::Dir { parent, .. } => *parent = new_parent, + Entry::File { parent, .. } => *parent = new_parent, + } + } + fn name(&self) -> &OsStr { match self { Entry::Dir { name, .. } => name, @@ -439,10 +516,7 @@ impl BackgroundScanner { return false; } - if let Err(error) = self.process_events(events) { - log::error!("error handling events: {}", error); - return false; - } + self.process_events(events); if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { return false; @@ -627,256 +701,209 @@ impl BackgroundScanner { Ok(()) } - fn process_events(&self, mut events: Vec) -> Result<()> { - let snapshot = self.snapshot(); + fn process_events(&self, mut events: Vec) { + let mut snapshot = self.snapshot(); events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut paths = events.into_iter().map(|e| e.path).peekable(); + let mut possible_removed_inodes = HashSet::new(); + let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); - for path in paths { - let relative_path = path.strip_prefix(&snapshot.path)?.to_path_buf(); - let snapshot_entry = snapshot.entry_for_path(relative_path); + while let Some(path) = paths.next() { + let relative_path = match path.strip_prefix(&snapshot.path) { + Ok(relative_path) => relative_path.to_path_buf(), + Err(e) => { + log::error!("Unexpected event {:?}", e); + continue; + } + }; - if let Some(fs_entry) = self.fs_entry_for_path(&path) { - if let Some(snapshot_entry) = snapshot_entry { - // If the parent does not match: - // Remove snapshot entry from its parent. - // Set its parent to none. - // Add its inode to a set of inodes to potentially remove after the batch. + let snapshot_entry = snapshot.entry_for_path(&relative_path); + let fs_entry = self.fs_entry_for_path(&snapshot.path, &path); - // If the parent does match, continue to next path + match fs_entry { + // If this path currently exists on the filesystem, then ensure that the snapshot's + // entry for this path is up-to-date. + Ok(Some((fs_entry, ignore))) => { + let fs_inode = fs_entry.ino(); + let fs_parent_inode = fs_entry.parent(); + + // If the snapshot already contains an entry for this path, then ensure that the + // entry has the correct inode and parent. + if let Some(snapshot_entry) = snapshot_entry { + let snapshot_inode = snapshot_entry.ino(); + let snapshot_parent_inode = snapshot_entry.parent(); + + // If the snapshot entry already matches the filesystem, then skip to the + // next event path. + if snapshot_inode == fs_inode && snapshot_parent_inode == fs_parent_inode { + continue; + } + + // If it does not match, then detach this inode from its current parent, and + // record that it may have been removed from the worktree. + snapshot.reparent_entry(snapshot_inode, snapshot_parent_inode, None); + possible_removed_inodes.insert(snapshot_inode); + } + + // If the snapshot already contained an entry for the inode that is now located + // at this path in the filesystem, then move it to reflect its current parent on + // the filesystem. + if let Some(snapshot_entry_for_inode) = snapshot.entries.get(&fs_inode) { + let snapshot_parent_inode = snapshot_entry_for_inode.parent(); + snapshot.reparent_entry(fs_inode, snapshot_parent_inode, fs_parent_inode); + } + // If the snapshot has no entry for this inode, then scan the filesystem to find + // all descendents of this new inode. Discard any subsequent events that are + // contained by the current path, since the directory is already being scanned + // from scratch. + else { + while let Some(next_path) = paths.peek() { + if next_path.starts_with(&path) { + paths.next(); + } + } + scan_queue_tx + .send(Ok(ScanJob { + ino: fs_inode, + path: Arc::from(path), + relative_path, + dir_entry: fs_entry, + ignore: Some(ignore), + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); + } } - // If we get here, we either had no snapshot entry at the path or we had the wrong - // entry (different inode) and removed it. - // In either case, we now need to add the entry for this path - - if let Some(existing_snapshot_entry) = snapshot.entry_for_inode(fs_entry.inode) { - // An entry already exists in the snapshot, but in the wrong spot. - // Set its parent to the correct parent - // Insert it in the children of its parent - } else { - // An entry doesn't exist in the snapshot, this is the first time we've seen it. - // If this is a directory, do a recursive scan and discard subsequent events that are contained by the current path - // Then set the parent of the result of that scan to the correct parent - // Insert it in the children of that parent. + // If this path no longer exists on the filesystem, then remove it from the snapshot. + Ok(None) => { + if let Some(snapshot_entry) = snapshot_entry { + let snapshot_inode = snapshot_entry.ino(); + let snapshot_parent_inode = snapshot_entry.parent(); + snapshot.reparent_entry(snapshot_inode, snapshot_parent_inode, None); + possible_removed_inodes.insert(snapshot_inode); + } } - } else { - if let Some(snapshot_entry) = snapshot_entry { - // Remove snapshot entry from its parent. - // Set its parent to none. - // Add its inode to a set of inodes to potentially remove after the batch. + Err(e) => { + // TODO - create a special 'error' entry in the entries tree to mark this + log::error!("Error reading file on event {:?}", e); } } } - // Check whether any entry whose parent was set to none is still an orphan. If so, remove it and all descedants. - + // For now, update the locked snapshot at this point, because `scan_dir` uses that. *self.snapshot.lock() = snapshot; - Ok(()) + // Scan any directories that were moved into this worktree as part of this event batch. + drop(scan_queue_tx); + let mut scanned_inodes = Vec::new(); + scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); + self.thread_pool.scoped(|pool| { + for worker_inodes in &mut scanned_inodes { + pool.execute(|| { + let worker_inodes = worker_inodes; + while let Ok(job) = scan_queue_rx.recv() { + if let Err(err) = job.and_then(|job| { + self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) + }) { + *worker_inodes = Err(err); + break; + } + } + }); + } + }); + + // Remove any entries that became orphaned when processing this events batch. + let mut snapshot = self.snapshot(); + let mut deletions = Vec::new(); + let mut descendent_stack = Vec::new(); + for inode in possible_removed_inodes { + if let Some(entry) = snapshot.entries.get(&inode) { + if entry.parent().is_none() { + descendent_stack.push(inode); + } + } + + // Recursively remove the orphaned nodes' descendants. + while let Some(inode) = descendent_stack.pop() { + if let Some(entry) = snapshot.entries.get(&inode) { + deletions.push(Edit::Remove(inode)); + if let Entry::Dir { children, .. } = entry { + descendent_stack.extend_from_slice(children.as_ref()); + } + } + } + } + snapshot.entries.edit(deletions); + *self.snapshot.lock() = snapshot; } - fn fs_entry_for_path(&self, path: &Path) -> Option { - todo!() + fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { + match fs::metadata(&path) { + Ok(metadata) => { + let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); + if metadata.is_dir() { + ignore = ignore.add_child(&path).unwrap(); + } + let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); + + let inode = metadata.ino(); + let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); + let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); + let parent = if path == root_path { + None + } else { + Some(fs::metadata(path.parent().unwrap())?.ino()) + }; + if metadata.file_type().is_dir() { + Ok(Some(( + Entry::Dir { + parent, + name, + inode, + is_symlink, + is_ignored, + children: Arc::from([]), + pending: true, + }, + ignore, + ))) + } else { + Ok(Some(( + Entry::File { + parent, + name, + path: PathEntry::new( + inode, + &path.strip_prefix(root_path).unwrap(), + is_ignored, + ), + inode, + is_symlink, + is_ignored, + }, + ignore, + ))) + } + } + Err(err) => { + if err.kind() == io::ErrorKind::NotFound { + Ok(None) + } else { + Err(anyhow::Error::new(err)) + } + } + } } - // fn process_events2(&self, mut events: Vec) -> Result { - // if self.notify.receiver_count() == 0 { - // return Ok(false); - // } - - // // TODO: should we canonicalize this at the start? - // let root_path = self.path.canonicalize()?; - // let snapshot = Snapshot { - // id: self.id, - // entries: self.entries.lock().clone(), - // root_inode: self.root_inode(), - // }; - // let mut removed = HashSet::new(); - // let mut observed = HashSet::new(); - - // let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); - - // events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - // let mut paths = events.into_iter().map(|e| e.path).peekable(); - // while let Some(path) = paths.next() { - // let relative_path = path.strip_prefix(&root_path)?.to_path_buf(); - // match fs::metadata(&path) { - // Ok(metadata) => { - // let inode = metadata.ino(); - // let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); - // let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); - // let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - // if metadata.is_dir() { - // ignore = ignore.add_child(&path).unwrap(); - // } - // let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - // let parent = if path == root_path { - // None - // } else { - // Some(fs::metadata(path.parent().unwrap())?.ino()) - // }; - - // let prev_entry = snapshot.entries.get(&inode); - // // If we haven't seen this inode yet, we are going to recursively scan it, so - // // ignore event involving a descendant. - // if prev_entry.is_none() { - // while paths.peek().map_or(false, |p| p.starts_with(&path)) { - // paths.next(); - // } - // } - - // observed.insert(inode); - // if metadata.file_type().is_dir() { - // let is_ignored = is_ignored || name.as_ref() == ".git"; - // let dir_entry = Entry::Dir { - // parent, - // name, - // inode, - // is_symlink, - // is_ignored, - // children: Arc::from([]), - // pending: true, - // }; - // self.insert_entries(Some(dir_entry.clone())); - - // scan_queue_tx - // .send(Ok(ScanJob { - // ino: inode, - // path: Arc::from(path), - // relative_path, - // dir_entry, - // ignore: Some(ignore), - // scan_queue: scan_queue_tx.clone(), - // })) - // .unwrap(); - // } else { - // self.insert_entries(Some(Entry::File { - // parent, - // name, - // path: PathEntry::new(inode, &relative_path, is_ignored), - // inode, - // is_symlink, - // is_ignored, - // })); - // } - // } - // Err(err) => { - // if err.kind() == io::ErrorKind::NotFound { - // // Fill removed with the inodes of all descendants of this path. - // let mut stack = Vec::new(); - // stack.extend(snapshot.inode_for_path(&relative_path)); - // while let Some(inode) = stack.pop() { - // removed.insert(inode); - // if let Some(Entry::Dir { children, .. }) = snapshot.entries.get(&inode) - // { - // stack.extend(children.iter().copied()) - // } - // } - // } else { - // return Err(anyhow::Error::new(err)); - // } - // } - // } - // } - // drop(scan_queue_tx); - - // let mut scanned_inodes = Vec::new(); - // scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); - // self.thread_pool.scoped(|pool| { - // for worker_inodes in &mut scanned_inodes { - // pool.execute(|| { - // let worker_inodes = worker_inodes; - // while let Ok(job) = scan_queue_rx.recv() { - // if let Err(err) = job.and_then(|job| { - // self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) - // }) { - // *worker_inodes = Err(err); - // break; - // } - // } - // }); - // } - // }); - - // for worker_inodes in scanned_inodes { - // for inode in worker_inodes? { - // remove_counts.remove(&inode); - // } - // } - // self.remove_entries(remove_counts); - - // Ok(self.notify.receiver_count() != 0) - // } - fn insert_entries(&self, entries: impl IntoIterator) { self.snapshot .lock() .entries .edit(entries.into_iter().map(Edit::Insert).collect::>()); } - - // fn insert_entries(&self, entries: impl IntoIterator) { - // let mut edits = Vec::new(); - // let mut new_parents = HashMap::new(); - // for entry in entries { - // new_parents.insert(entry.ino(), entry.parent()); - // edits.push(Edit::Insert(entry)); - // } - - // let mut entries = self.snapshot.lock().entries; - // let prev_entries = entries.edit(edits); - // Self::remove_stale_children(&mut *entries, prev_entries, new_parents); - // } - - // fn remove_entries(&self, inodes: impl IntoIterator) { - // let mut entries = self.entries.lock(); - // let prev_entries = entries.edit(inodes.into_iter().map(Edit::Remove).collect()); - // Self::remove_stale_children(&mut *entries, prev_entries, HashMap::new()); - // } - - // fn remove_stale_children( - // tree: &mut SumTree, - // prev_entries: Vec, - // new_parents: HashMap>, - // ) { - // let mut new_parent_entries = HashMap::new(); - - // for prev_entry in prev_entries { - // let new_parent = new_parents.get(&prev_entry.ino()).copied().flatten(); - // if new_parent != prev_entry.parent() { - // if let Some(prev_parent) = prev_entry.parent() { - // let (_, new_children) = - // new_parent_entries.entry(prev_parent).or_insert_with(|| { - // let prev_parent_entry = tree.get(&prev_parent).unwrap(); - // if let Entry::Dir { children, .. } = prev_parent_entry { - // (prev_parent_entry.clone(), children.to_vec()) - // } else { - // unreachable!() - // } - // }); - - // if let Some(ix) = new_children.iter().position(|ino| *ino == prev_entry.ino()) { - // new_children.swap_remove(ix); - // } - // } - // } - // } - - // let parent_edits = new_parent_entries - // .into_iter() - // .map(|(_, (mut parent_entry, new_children))| { - // if let Entry::Dir { children, .. } = &mut parent_entry { - // *children = Arc::from(new_children); - // } else { - // unreachable!() - // } - // Edit::Insert(parent_entry) - // }) - // .collect::>(); - // tree.edit(parent_edits); - // } } struct ScanJob { From 17f2df3e7135d9b9eb16576cbd324df7cbc536ce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 16 Apr 2021 22:17:52 -0700 Subject: [PATCH 037/102] Update entries' filenames when handling move events --- zed/src/worktree.rs | 67 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 04b53e85a2..d525d5a5ad 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -277,6 +277,7 @@ impl Snapshot { fn reparent_entry( &mut self, child_inode: u64, + new_filename: Option<&OsStr>, old_parent_inode: Option, new_parent_inode: Option, ) { @@ -315,6 +316,9 @@ impl Snapshot { // Update the child entry's parent. let mut child_entry = child_entry.expect("cannot reparent non-existent entry"); child_entry.set_parent(new_parent_inode); + if let Some(new_filename) = new_filename { + child_entry.set_name(new_filename); + } insertions.push(Edit::Insert(child_entry)); // Remove the child entry from it's old parent's children. @@ -395,11 +399,15 @@ pub enum Entry { impl Entry { fn ino(&self) -> u64 { match self { - Entry::Dir { inode: ino, .. } => *ino, - Entry::File { inode: ino, .. } => *ino, + Entry::Dir { inode, .. } => *inode, + Entry::File { inode, .. } => *inode, } } + fn is_dir(&self) -> bool { + matches!(self, Entry::Dir { .. }) + } + fn parent(&self) -> Option { match self { Entry::Dir { parent, .. } => *parent, @@ -420,6 +428,13 @@ impl Entry { Entry::File { name, .. } => name, } } + + fn set_name(&mut self, new_name: &OsStr) { + match self { + Entry::Dir { name, .. } => *name = new_name.into(), + Entry::File { name, .. } => *name = new_name.into(), + } + } } impl sum_tree::Item for Entry { @@ -742,7 +757,7 @@ impl BackgroundScanner { // If it does not match, then detach this inode from its current parent, and // record that it may have been removed from the worktree. - snapshot.reparent_entry(snapshot_inode, snapshot_parent_inode, None); + snapshot.reparent_entry(snapshot_inode, None, snapshot_parent_inode, None); possible_removed_inodes.insert(snapshot_inode); } @@ -751,7 +766,12 @@ impl BackgroundScanner { // the filesystem. if let Some(snapshot_entry_for_inode) = snapshot.entries.get(&fs_inode) { let snapshot_parent_inode = snapshot_entry_for_inode.parent(); - snapshot.reparent_entry(fs_inode, snapshot_parent_inode, fs_parent_inode); + snapshot.reparent_entry( + fs_inode, + path.file_name(), + snapshot_parent_inode, + fs_parent_inode, + ); } // If the snapshot has no entry for this inode, then scan the filesystem to find // all descendents of this new inode. Discard any subsequent events that are @@ -761,18 +781,31 @@ impl BackgroundScanner { while let Some(next_path) = paths.peek() { if next_path.starts_with(&path) { paths.next(); + } else { + break; } } - scan_queue_tx - .send(Ok(ScanJob { - ino: fs_inode, - path: Arc::from(path), - relative_path, - dir_entry: fs_entry, - ignore: Some(ignore), - scan_queue: scan_queue_tx.clone(), - })) - .unwrap(); + + snapshot.entries.insert(fs_entry.clone()); + snapshot.reparent_entry(fs_inode, None, None, fs_parent_inode); + + if fs_entry.is_dir() { + let relative_path = snapshot + .path + .parent() + .map_or(path.as_path(), |parent| path.strip_prefix(parent).unwrap()) + .to_path_buf(); + scan_queue_tx + .send(Ok(ScanJob { + ino: fs_inode, + path: Arc::from(path), + relative_path, + dir_entry: fs_entry, + ignore: Some(ignore), + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); + } } } @@ -781,7 +814,7 @@ impl BackgroundScanner { if let Some(snapshot_entry) = snapshot_entry { let snapshot_inode = snapshot_entry.ino(); let snapshot_parent_inode = snapshot_entry.parent(); - snapshot.reparent_entry(snapshot_inode, snapshot_parent_inode, None); + snapshot.reparent_entry(snapshot_inode, None, snapshot_parent_inode, None); possible_removed_inodes.insert(snapshot_inode); } } @@ -877,7 +910,9 @@ impl BackgroundScanner { name, path: PathEntry::new( inode, - &path.strip_prefix(root_path).unwrap(), + root_path + .parent() + .map_or(path, |parent| path.strip_prefix(parent).unwrap()), is_ignored, ), inode, From ca62d01b5364dd6ef25a62c64612dca13b1beaeb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 12:01:33 +0200 Subject: [PATCH 038/102] Start on a randomized test for `Worktree` --- zed/src/worktree.rs | 344 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 280 insertions(+), 64 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d525d5a5ad..0093c6a213 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -17,7 +17,7 @@ use std::{ ffi::OsStr, fmt, fs, io::{self, Read, Write}, - ops::AddAssign, + ops::{AddAssign, Deref}, os::unix::fs::MetadataExt, path::{Path, PathBuf}, sync::Arc, @@ -40,14 +40,6 @@ pub struct Worktree { poll_scheduled: bool, } -#[derive(Clone)] -pub struct Snapshot { - id: usize, - path: Arc, - root_inode: Option, - entries: SumTree, -} - #[derive(Clone)] pub struct FileHandle { worktree: ModelHandle, @@ -129,31 +121,6 @@ impl Worktree { Ok(result) } - pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result { - let mut components = Vec::new(); - let mut entry = self - .snapshot - .entries - .get(&ino) - .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - components.push(entry.name()); - while let Some(parent) = entry.parent() { - entry = self.snapshot.entries.get(&parent).unwrap(); - components.push(entry.name()); - } - - let mut components = components.into_iter().rev(); - if !include_root { - components.next(); - } - - let mut path = PathBuf::new(); - for component in components { - path.push(component); - } - Ok(path) - } - pub fn load_history( &self, ino: u64, @@ -187,31 +154,6 @@ impl Worktree { }) } - fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { - match self.snapshot.entries.get(&ino).unwrap() { - Entry::Dir { name, children, .. } => { - write!( - f, - "{}{}/ ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - ino - )?; - for child_id in children.iter() { - self.fmt_entry(f, *child_id, indent + 2)?; - } - Ok(()) - } - Entry::File { name, .. } => write!( - f, - "{}{} ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - ino - ), - } - } - #[cfg(test)] pub fn files<'a>(&'a self) -> impl Iterator + 'a { self.snapshot @@ -231,16 +173,28 @@ impl Entity for Worktree { type Event = (); } +impl Deref for Worktree { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + impl fmt::Debug for Worktree { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(root_ino) = self.snapshot.root_inode { - self.fmt_entry(f, root_ino, 0) - } else { - write!(f, "Empty tree\n") - } + self.snapshot.fmt(f) } } +#[derive(Clone)] +pub struct Snapshot { + id: usize, + path: Arc, + root_inode: Option, + entries: SumTree, +} + impl Snapshot { pub fn file_count(&self) -> usize { self.entries.summary().file_count @@ -274,6 +228,30 @@ impl Snapshot { .and_then(|inode| self.entries.get(&inode)) } + pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result { + let mut components = Vec::new(); + let mut entry = self + .entries + .get(&ino) + .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; + components.push(entry.name()); + while let Some(parent) = entry.parent() { + entry = self.entries.get(&parent).unwrap(); + components.push(entry.name()); + } + + let mut components = components.into_iter().rev(); + if !include_root { + components.next(); + } + + let mut path = PathBuf::new(); + for component in components { + path.push(component); + } + Ok(path) + } + fn reparent_entry( &mut self, child_inode: u64, @@ -351,6 +329,41 @@ impl Snapshot { self.entries.edit(insertions); } + + fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { + match self.entries.get(&ino).unwrap() { + Entry::Dir { name, children, .. } => { + write!( + f, + "{}{}/ ({})\n", + " ".repeat(indent), + name.to_string_lossy(), + ino + )?; + for child_id in children.iter() { + self.fmt_entry(f, *child_id, indent + 2)?; + } + Ok(()) + } + Entry::File { name, .. } => write!( + f, + "{}{} ({})\n", + " ".repeat(indent), + name.to_string_lossy(), + ino + ), + } + } +} + +impl fmt::Debug for Snapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(root_ino) = self.root_inode { + self.fmt_entry(f, root_ino, 0) + } else { + write!(f, "Empty tree\n") + } + } } impl FileHandle { @@ -987,8 +1000,13 @@ mod tests { use crate::test::*; use anyhow::Result; use gpui::App; + use log::LevelFilter; + use rand::prelude::*; use serde_json::json; + use simplelog::SimpleLogger; + use std::env; use std::os::unix; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn test_populate_and_search() { @@ -1136,4 +1154,202 @@ mod tests { }); }); } + + #[test] + fn test_random() { + if let Ok(true) = env::var("LOG").map(|l| l.parse().unwrap()) { + SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap(); + } + + let iterations = env::var("ITERATIONS") + .map(|i| i.parse().unwrap()) + .unwrap_or(100); + let operations = env::var("OPERATIONS") + .map(|o| o.parse().unwrap()) + .unwrap_or(40); + let seeds = if let Ok(seed) = env::var("SEED").map(|s| s.parse().unwrap()) { + seed..seed + 1 + } else { + 0..iterations + }; + + for seed in seeds { + dbg!(seed); + let mut rng = StdRng::seed_from_u64(seed); + + let root_dir = tempdir::TempDir::new(&format!("test-{}", seed)).unwrap(); + for _ in 0..20 { + randomly_mutate_tree(root_dir.path(), 1.0, &mut rng).unwrap(); + } + log::info!("Generated initial tree"); + + let (notify_tx, _notify_rx) = smol::channel::unbounded(); + let scanner = BackgroundScanner::new( + Snapshot { + id: 0, + path: root_dir.path().into(), + root_inode: None, + entries: Default::default(), + }, + notify_tx, + ); + scanner.scan_dirs().unwrap(); + + let mut events = Vec::new(); + let mut mutations_len = operations; + while mutations_len > 1 { + if !events.is_empty() && rng.gen_bool(0.4) { + let len = rng.gen_range(0..=events.len()); + let to_deliver = events.drain(0..len).collect::>(); + scanner.process_events(to_deliver); + } else { + events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap()); + mutations_len -= 1; + } + } + scanner.process_events(events); + + let (notify_tx, _notify_rx) = smol::channel::unbounded(); + let new_scanner = BackgroundScanner::new( + Snapshot { + id: 0, + path: root_dir.path().into(), + root_inode: None, + entries: Default::default(), + }, + notify_tx, + ); + new_scanner.scan_dirs().unwrap(); + assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); + } + } + + fn randomly_mutate_tree( + root_path: &Path, + insertion_probability: f64, + rng: &mut impl Rng, + ) -> Result> { + let (dirs, files) = read_dir_recursive(root_path.to_path_buf()); + + let mut events = Vec::new(); + let mut record_event = |path: PathBuf| { + events.push(fsevent::Event { + event_id: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + flags: fsevent::StreamFlags::empty(), + path, + }); + }; + + if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) { + let path = dirs.choose(rng).unwrap(); + let new_path = path.join(gen_name(rng)); + + if rng.gen() { + log::info!("Creating dir {:?}", new_path.strip_prefix(root_path)?); + fs::create_dir(&new_path)?; + } else { + log::info!("Creating file {:?}", new_path.strip_prefix(root_path)?); + fs::write(&new_path, "")?; + } + record_event(new_path); + } else { + let old_path = { + let file_path = files.choose(rng); + let dir_path = dirs[1..].choose(rng); + file_path.into_iter().chain(dir_path).choose(rng).unwrap() + }; + + let is_rename = rng.gen(); + if is_rename { + let new_path_parent = dirs + .iter() + .filter(|d| !d.starts_with(old_path)) + .choose(rng) + .unwrap(); + let new_path = new_path_parent.join(gen_name(rng)); + + log::info!( + "Renaming {:?} to {:?}", + old_path.strip_prefix(&root_path)?, + new_path.strip_prefix(&root_path)? + ); + fs::rename(&old_path, &new_path)?; + record_event(old_path.clone()); + record_event(new_path); + } else if old_path.is_dir() { + let (dirs, files) = read_dir_recursive(old_path.clone()); + + log::info!("Deleting dir {:?}", old_path.strip_prefix(&root_path)?); + fs::remove_dir_all(&old_path).unwrap(); + for file in files { + record_event(file); + } + for dir in dirs { + record_event(dir); + } + } else { + log::info!("Deleting file {:?}", old_path.strip_prefix(&root_path)?); + fs::remove_file(old_path).unwrap(); + record_event(old_path.clone()); + } + } + + Ok(events) + } + + fn read_dir_recursive(path: PathBuf) -> (Vec, Vec) { + let child_entries = fs::read_dir(&path).unwrap(); + let mut dirs = vec![path]; + let mut files = Vec::new(); + for child_entry in child_entries { + let child_path = child_entry.unwrap().path(); + if child_path.is_dir() { + let (child_dirs, child_files) = read_dir_recursive(child_path); + dirs.extend(child_dirs); + files.extend(child_files); + } else { + files.push(child_path); + } + } + (dirs, files) + } + + fn gen_name(rng: &mut impl Rng) -> String { + (0..6) + .map(|_| rng.sample(rand::distributions::Alphanumeric)) + .map(char::from) + .collect() + } + + impl Snapshot { + fn to_vec(&self) -> Vec<(PathBuf, u64)> { + use std::iter::FromIterator; + + let mut paths = Vec::new(); + + let mut stack = Vec::new(); + stack.extend(self.root_inode); + while let Some(inode) = stack.pop() { + let computed_path = self.path_for_inode(inode, true).unwrap(); + match self.entries.get(&inode).unwrap() { + Entry::Dir { children, .. } => { + stack.extend_from_slice(children); + } + Entry::File { path, .. } => { + assert_eq!( + String::from_iter(path.path.iter()), + computed_path.to_str().unwrap() + ); + } + } + paths.push((computed_path, inode)); + } + + paths.sort_by(|a, b| a.0.cmp(&b.0)); + paths + } + } } From f29c42904ea081c9729b495019974e19043b4220 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 12:53:02 +0200 Subject: [PATCH 039/102] Recompute the PathEntry for each file under a re-parented subtree --- zed/src/worktree.rs | 76 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 0093c6a213..5515043b4f 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -255,7 +255,7 @@ impl Snapshot { fn reparent_entry( &mut self, child_inode: u64, - new_filename: Option<&OsStr>, + new_path: Option<&Path>, old_parent_inode: Option, new_parent_inode: Option, ) { @@ -282,11 +282,11 @@ impl Snapshot { let mut old_parent_entry = None; let mut new_parent_entry = None; for removed_entry in removed_entries { - if removed_entry.ino() == child_inode { + if removed_entry.inode() == child_inode { child_entry = Some(removed_entry); - } else if Some(removed_entry.ino()) == old_parent_inode { + } else if Some(removed_entry.inode()) == old_parent_inode { old_parent_entry = Some(removed_entry); - } else if Some(removed_entry.ino()) == new_parent_inode { + } else if Some(removed_entry.inode()) == new_parent_inode { new_parent_entry = Some(removed_entry); } } @@ -294,12 +294,47 @@ impl Snapshot { // Update the child entry's parent. let mut child_entry = child_entry.expect("cannot reparent non-existent entry"); child_entry.set_parent(new_parent_inode); - if let Some(new_filename) = new_filename { - child_entry.set_name(new_filename); - } - insertions.push(Edit::Insert(child_entry)); + if let Some(new_path) = new_path { + let new_path = new_path.strip_prefix(self.path.parent().unwrap()).unwrap(); + child_entry.set_name(new_path.file_name().unwrap()); - // Remove the child entry from it's old parent's children. + // Recompute the PathEntry for each file under this subtree. + let mut stack = Vec::new(); + stack.push((child_entry, new_path.parent().unwrap().to_path_buf())); + while let Some((mut entry, mut new_path)) = stack.pop() { + new_path.push(entry.name()); + match &mut entry { + Entry::Dir { + children, inode, .. + } => { + for child_inode in children.as_ref() { + let child_entry = self.entries.get(child_inode).unwrap(); + stack.push((child_entry.clone(), new_path.clone())); + } + + // Descendant directories don't need to be mutated because their properties + // haven't changed, so only re-insert this directory if it is the entry we + // were reparenting. + if *inode == child_inode { + insertions.push(Edit::Insert(entry)); + } + } + Entry::File { + inode, + is_ignored, + path, + .. + } => { + *path = PathEntry::new(*inode, &new_path, *is_ignored); + insertions.push(Edit::Insert(entry)); + } + } + } + } else { + insertions.push(Edit::Insert(child_entry)); + } + + // Remove the child entry from its old parent's children. if let Some(mut old_parent_entry) = old_parent_entry { if let Entry::Dir { children, .. } = &mut old_parent_entry { *children = children @@ -313,7 +348,7 @@ impl Snapshot { } } - // Add the child entry to it's new parent's children. + // Add the child entry to its new parent's children. if let Some(mut new_parent_entry) = new_parent_entry { if let Entry::Dir { children, .. } = &mut new_parent_entry { *children = children @@ -410,7 +445,7 @@ pub enum Entry { } impl Entry { - fn ino(&self) -> u64 { + fn inode(&self) -> u64 { match self { Entry::Dir { inode, .. } => *inode, Entry::File { inode, .. } => *inode, @@ -455,7 +490,7 @@ impl sum_tree::Item for Entry { fn summary(&self) -> Self::Summary { EntrySummary { - max_ino: self.ino(), + max_ino: self.inode(), file_count: if matches!(self, Self::File { .. }) { 1 } else { @@ -469,7 +504,7 @@ impl sum_tree::KeyedItem for Entry { type Key = u64; fn key(&self) -> Self::Key { - self.ino() + self.inode() } } @@ -753,13 +788,13 @@ impl BackgroundScanner { // If this path currently exists on the filesystem, then ensure that the snapshot's // entry for this path is up-to-date. Ok(Some((fs_entry, ignore))) => { - let fs_inode = fs_entry.ino(); + let fs_inode = fs_entry.inode(); let fs_parent_inode = fs_entry.parent(); // If the snapshot already contains an entry for this path, then ensure that the // entry has the correct inode and parent. if let Some(snapshot_entry) = snapshot_entry { - let snapshot_inode = snapshot_entry.ino(); + let snapshot_inode = snapshot_entry.inode(); let snapshot_parent_inode = snapshot_entry.parent(); // If the snapshot entry already matches the filesystem, then skip to the @@ -781,7 +816,7 @@ impl BackgroundScanner { let snapshot_parent_inode = snapshot_entry_for_inode.parent(); snapshot.reparent_entry( fs_inode, - path.file_name(), + Some(&path), snapshot_parent_inode, fs_parent_inode, ); @@ -825,7 +860,7 @@ impl BackgroundScanner { // If this path no longer exists on the filesystem, then remove it from the snapshot. Ok(None) => { if let Some(snapshot_entry) = snapshot_entry { - let snapshot_inode = snapshot_entry.ino(); + let snapshot_inode = snapshot_entry.inode(); let snapshot_parent_inode = snapshot_entry.parent(); snapshot.reparent_entry(snapshot_inode, None, snapshot_parent_inode, None); possible_removed_inodes.insert(snapshot_inode); @@ -1167,6 +1202,9 @@ mod tests { let operations = env::var("OPERATIONS") .map(|o| o.parse().unwrap()) .unwrap_or(40); + let initial_entries = env::var("INITIAL_ENTRIES") + .map(|o| o.parse().unwrap()) + .unwrap_or(20); let seeds = if let Ok(seed) = env::var("SEED").map(|s| s.parse().unwrap()) { seed..seed + 1 } else { @@ -1178,7 +1216,7 @@ mod tests { let mut rng = StdRng::seed_from_u64(seed); let root_dir = tempdir::TempDir::new(&format!("test-{}", seed)).unwrap(); - for _ in 0..20 { + for _ in 0..initial_entries { randomly_mutate_tree(root_dir.path(), 1.0, &mut rng).unwrap(); } log::info!("Generated initial tree"); @@ -1201,12 +1239,14 @@ mod tests { if !events.is_empty() && rng.gen_bool(0.4) { let len = rng.gen_range(0..=events.len()); let to_deliver = events.drain(0..len).collect::>(); + log::info!("Delivering events: {:#?}", to_deliver); scanner.process_events(to_deliver); } else { events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap()); mutations_len -= 1; } } + log::info!("Quiescing: {:#?}", events); scanner.process_events(events); let (notify_tx, _notify_rx) = smol::channel::unbounded(); From 2c72f9c14f977031c178b58e96aa0a5198bf5b58 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 13:00:13 +0200 Subject: [PATCH 040/102] Don't change parent entries for moves within the same directory --- zed/src/worktree.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 5515043b4f..86b593ceb9 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -252,7 +252,7 @@ impl Snapshot { Ok(path) } - fn reparent_entry( + fn move_entry( &mut self, child_inode: u64, new_path: Option<&Path>, @@ -271,11 +271,13 @@ impl Snapshot { // Remove the entries from the sum tree. deletions.push(Edit::Remove(child_inode)); - if let Some(old_parent_inode) = old_parent_inode { - deletions.push(Edit::Remove(old_parent_inode)); - } - if let Some(new_parent_inode) = new_parent_inode { - deletions.push(Edit::Remove(new_parent_inode)); + if old_parent_inode != new_parent_inode { + if let Some(old_parent_inode) = old_parent_inode { + deletions.push(Edit::Remove(old_parent_inode)); + } + if let Some(new_parent_inode) = new_parent_inode { + deletions.push(Edit::Remove(new_parent_inode)); + } } let removed_entries = self.entries.edit(deletions); let mut child_entry = None; @@ -292,7 +294,7 @@ impl Snapshot { } // Update the child entry's parent. - let mut child_entry = child_entry.expect("cannot reparent non-existent entry"); + let mut child_entry = child_entry.expect("cannot move non-existent entry"); child_entry.set_parent(new_parent_inode); if let Some(new_path) = new_path { let new_path = new_path.strip_prefix(self.path.parent().unwrap()).unwrap(); @@ -313,8 +315,8 @@ impl Snapshot { } // Descendant directories don't need to be mutated because their properties - // haven't changed, so only re-insert this directory if it is the entry we - // were reparenting. + // haven't changed, so only re-insert this directory if it is the top entry + // we were moving. if *inode == child_inode { insertions.push(Edit::Insert(entry)); } @@ -805,7 +807,7 @@ impl BackgroundScanner { // If it does not match, then detach this inode from its current parent, and // record that it may have been removed from the worktree. - snapshot.reparent_entry(snapshot_inode, None, snapshot_parent_inode, None); + snapshot.move_entry(snapshot_inode, None, snapshot_parent_inode, None); possible_removed_inodes.insert(snapshot_inode); } @@ -814,7 +816,7 @@ impl BackgroundScanner { // the filesystem. if let Some(snapshot_entry_for_inode) = snapshot.entries.get(&fs_inode) { let snapshot_parent_inode = snapshot_entry_for_inode.parent(); - snapshot.reparent_entry( + snapshot.move_entry( fs_inode, Some(&path), snapshot_parent_inode, @@ -835,7 +837,7 @@ impl BackgroundScanner { } snapshot.entries.insert(fs_entry.clone()); - snapshot.reparent_entry(fs_inode, None, None, fs_parent_inode); + snapshot.move_entry(fs_inode, None, None, fs_parent_inode); if fs_entry.is_dir() { let relative_path = snapshot @@ -862,7 +864,7 @@ impl BackgroundScanner { if let Some(snapshot_entry) = snapshot_entry { let snapshot_inode = snapshot_entry.inode(); let snapshot_parent_inode = snapshot_entry.parent(); - snapshot.reparent_entry(snapshot_inode, None, snapshot_parent_inode, None); + snapshot.move_entry(snapshot_inode, None, snapshot_parent_inode, None); possible_removed_inodes.insert(snapshot_inode); } } From 06858c023ce3d5a72489f4422d21af8cf98da862 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 13:01:13 +0200 Subject: [PATCH 041/102] Enable logging automatically when passing a SEED env variable --- zed/src/worktree.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 86b593ceb9..2dad9aa283 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1194,10 +1194,6 @@ mod tests { #[test] fn test_random() { - if let Ok(true) = env::var("LOG").map(|l| l.parse().unwrap()) { - SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap(); - } - let iterations = env::var("ITERATIONS") .map(|i| i.parse().unwrap()) .unwrap_or(100); @@ -1208,6 +1204,8 @@ mod tests { .map(|o| o.parse().unwrap()) .unwrap_or(20); let seeds = if let Ok(seed) = env::var("SEED").map(|s| s.parse().unwrap()) { + // Init logging so that we can debug the operations for this seed. + SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap(); seed..seed + 1 } else { 0..iterations From f8f6a85ab0814a8bc47f3a1dfbe61e0460d614cf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 19:41:37 +0200 Subject: [PATCH 042/102] WIP --- zed/src/worktree.rs | 221 +++++++++++++++++++------------------------- 1 file changed, 97 insertions(+), 124 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 2dad9aa283..050c05a639 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -252,6 +252,41 @@ impl Snapshot { Ok(path) } + fn remove_entry(&mut self, inode: u64) { + if let Some(entry) = self.entries.get(&inode).cloned() { + let mut edits = Vec::new(); + + if let Some(parent) = entry.parent() { + let mut parent_entry = self.entries.get(&parent).unwrap().clone(); + if let Entry::Dir { children, .. } = &mut parent_entry { + *children = children + .into_iter() + .copied() + .filter(|child| *child != entry.inode()) + .collect::>() + .into(); + edits.push(Edit::Insert(parent_entry)); + } else { + unreachable!(); + } + } + + // Recursively remove the orphaned nodes' descendants. + let mut descendant_stack = vec![entry.inode()]; + while let Some(inode) = descendant_stack.pop() { + if let Some(entry) = self.entries.get(&inode) { + edits.push(Edit::Remove(inode)); + if let Entry::Dir { children, .. } = entry { + descendant_stack.extend_from_slice(children.as_ref()); + } + } + } + + // dbg!(&edits); + self.entries.edit(edits); + } + } + fn move_entry( &mut self, child_inode: u64, @@ -638,7 +673,7 @@ impl BackgroundScanner { let (tx, rx) = crossbeam_channel::unbounded(); tx.send(Ok(ScanJob { - ino: inode, + inode, path: path.clone(), relative_path, dir_entry, @@ -655,7 +690,7 @@ impl BackgroundScanner { pool.execute(|| { let result = result; while let Ok(job) = rx.recv() { - if let Err(err) = job.and_then(|job| self.scan_dir(job, None)) { + if let Err(err) = job.and_then(|job| self.scan_dir(job)) { *result = Err(err); break; } @@ -679,7 +714,7 @@ impl BackgroundScanner { Ok(()) } - fn scan_dir(&self, job: ScanJob, mut children: Option<&mut Vec>) -> io::Result<()> { + fn scan_dir(&self, job: ScanJob) -> io::Result<()> { let scan_queue = job.scan_queue; let mut dir_entry = job.dir_entry; @@ -697,9 +732,6 @@ impl BackgroundScanner { let path = job.path.join(name.as_ref()); new_children.push(ino); - if let Some(children) = children.as_mut() { - children.push(ino); - } if metadata.is_dir() { let mut is_ignored = true; let mut ignore = None; @@ -714,7 +746,7 @@ impl BackgroundScanner { } let dir_entry = Entry::Dir { - parent: Some(job.ino), + parent: Some(job.inode), name, inode: ino, is_symlink, @@ -724,7 +756,7 @@ impl BackgroundScanner { }; new_entries.push(dir_entry.clone()); new_jobs.push(ScanJob { - ino, + inode: ino, path: Arc::from(path), relative_path, dir_entry, @@ -737,7 +769,7 @@ impl BackgroundScanner { .as_ref() .map_or(true, |i| i.matched(&path, false).is_ignore()); new_entries.push(Entry::File { - parent: Some(job.ino), + parent: Some(job.inode), name, path: PathEntry::new(ino, &relative_path, is_ignored), inode: ino, @@ -771,9 +803,7 @@ impl BackgroundScanner { events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut paths = events.into_iter().map(|e| e.path).peekable(); - let mut possible_removed_inodes = HashSet::new(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); - while let Some(path) = paths.next() { let relative_path = match path.strip_prefix(&snapshot.path) { Ok(relative_path) => relative_path.to_path_buf(), @@ -782,145 +812,81 @@ impl BackgroundScanner { continue; } }; + // dbg!(&path, &relative_path, snapshot.entries.items()); - let snapshot_entry = snapshot.entry_for_path(&relative_path); - let fs_entry = self.fs_entry_for_path(&snapshot.path, &path); + while paths.peek().map_or(false, |p| p.starts_with(&path)) { + paths.next(); + } - match fs_entry { - // If this path currently exists on the filesystem, then ensure that the snapshot's - // entry for this path is up-to-date. + if let Some(snapshot_inode) = snapshot.inode_for_path(&relative_path) { + snapshot.remove_entry(snapshot_inode); + } + + match self.fs_entry_for_path(&snapshot.path, &path) { Ok(Some((fs_entry, ignore))) => { - let fs_inode = fs_entry.inode(); - let fs_parent_inode = fs_entry.parent(); + snapshot.remove_entry(fs_entry.inode()); - // If the snapshot already contains an entry for this path, then ensure that the - // entry has the correct inode and parent. - if let Some(snapshot_entry) = snapshot_entry { - let snapshot_inode = snapshot_entry.inode(); - let snapshot_parent_inode = snapshot_entry.parent(); - - // If the snapshot entry already matches the filesystem, then skip to the - // next event path. - if snapshot_inode == fs_inode && snapshot_parent_inode == fs_parent_inode { - continue; - } - - // If it does not match, then detach this inode from its current parent, and - // record that it may have been removed from the worktree. - snapshot.move_entry(snapshot_inode, None, snapshot_parent_inode, None); - possible_removed_inodes.insert(snapshot_inode); - } - - // If the snapshot already contained an entry for the inode that is now located - // at this path in the filesystem, then move it to reflect its current parent on - // the filesystem. - if let Some(snapshot_entry_for_inode) = snapshot.entries.get(&fs_inode) { - let snapshot_parent_inode = snapshot_entry_for_inode.parent(); - snapshot.move_entry( - fs_inode, - Some(&path), - snapshot_parent_inode, - fs_parent_inode, - ); - } - // If the snapshot has no entry for this inode, then scan the filesystem to find - // all descendents of this new inode. Discard any subsequent events that are - // contained by the current path, since the directory is already being scanned - // from scratch. - else { - while let Some(next_path) = paths.peek() { - if next_path.starts_with(&path) { - paths.next(); - } else { - break; + let mut edits = Vec::new(); + edits.push(Edit::Insert(fs_entry.clone())); + if let Some(parent) = fs_entry.parent() { + let mut parent_entry = snapshot.entries.get(&parent).unwrap().clone(); + if let Entry::Dir { children, .. } = &mut parent_entry { + if !children.contains(&fs_entry.inode()) { + *children = children + .into_iter() + .copied() + .chain(Some(fs_entry.inode())) + .collect::>() + .into(); + edits.push(Edit::Insert(parent_entry)); } - } - - snapshot.entries.insert(fs_entry.clone()); - snapshot.move_entry(fs_inode, None, None, fs_parent_inode); - - if fs_entry.is_dir() { - let relative_path = snapshot - .path - .parent() - .map_or(path.as_path(), |parent| path.strip_prefix(parent).unwrap()) - .to_path_buf(); - scan_queue_tx - .send(Ok(ScanJob { - ino: fs_inode, - path: Arc::from(path), - relative_path, - dir_entry: fs_entry, - ignore: Some(ignore), - scan_queue: scan_queue_tx.clone(), - })) - .unwrap(); + } else { + unreachable!(); } } - } + snapshot.entries.edit(edits); - // If this path no longer exists on the filesystem, then remove it from the snapshot. - Ok(None) => { - if let Some(snapshot_entry) = snapshot_entry { - let snapshot_inode = snapshot_entry.inode(); - let snapshot_parent_inode = snapshot_entry.parent(); - snapshot.move_entry(snapshot_inode, None, snapshot_parent_inode, None); - possible_removed_inodes.insert(snapshot_inode); + if fs_entry.is_dir() { + let relative_path = snapshot + .path + .parent() + .map_or(path.as_path(), |parent| path.strip_prefix(parent).unwrap()) + .to_path_buf(); + scan_queue_tx + .send(Ok(ScanJob { + inode: fs_entry.inode(), + path: Arc::from(path), + relative_path, + dir_entry: fs_entry, + ignore: Some(ignore), + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); } } - Err(e) => { + Ok(None) => {} + Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("Error reading file on event {:?}", e); + log::error!("Error reading file on event {:?}", err); } } } - // For now, update the locked snapshot at this point, because `scan_dir` uses that. *self.snapshot.lock() = snapshot; // Scan any directories that were moved into this worktree as part of this event batch. drop(scan_queue_tx); - let mut scanned_inodes = Vec::new(); - scanned_inodes.resize_with(self.thread_pool.workers(), || Ok(Vec::new())); self.thread_pool.scoped(|pool| { - for worker_inodes in &mut scanned_inodes { + for _ in 0..self.thread_pool.workers() { pool.execute(|| { - let worker_inodes = worker_inodes; while let Ok(job) = scan_queue_rx.recv() { - if let Err(err) = job.and_then(|job| { - self.scan_dir(job, Some(worker_inodes.as_mut().unwrap())) - }) { - *worker_inodes = Err(err); - break; + if let Err(err) = job.and_then(|job| self.scan_dir(job)) { + log::error!("Error scanning {:?}", err); } } }); } }); - - // Remove any entries that became orphaned when processing this events batch. - let mut snapshot = self.snapshot(); - let mut deletions = Vec::new(); - let mut descendent_stack = Vec::new(); - for inode in possible_removed_inodes { - if let Some(entry) = snapshot.entries.get(&inode) { - if entry.parent().is_none() { - descendent_stack.push(inode); - } - } - - // Recursively remove the orphaned nodes' descendants. - while let Some(inode) = descendent_stack.pop() { - if let Some(entry) = snapshot.entries.get(&inode) { - deletions.push(Edit::Remove(inode)); - if let Entry::Dir { children, .. } = entry { - descendent_stack.extend_from_slice(children.as_ref()); - } - } - } - } - snapshot.entries.edit(deletions); - *self.snapshot.lock() = snapshot; } fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { @@ -991,8 +957,14 @@ impl BackgroundScanner { } } +impl Drop for BackgroundScanner { + fn drop(&mut self) { + self.thread_pool.shutdown(); + } +} + struct ScanJob { - ino: u64, + inode: u64, path: Arc, relative_path: PathBuf, dir_entry: Entry, @@ -1260,6 +1232,7 @@ mod tests { notify_tx, ); new_scanner.scan_dirs().unwrap(); + dbg!(scanner.snapshot().entries.items()); assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); } } From 122926dcdefd70e5b70d0d955198df409b285452 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Apr 2021 20:16:54 +0200 Subject: [PATCH 043/102] WIP: Associate entry names with directory children Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 283 +++++++++++--------------------------- zed/src/worktree/fuzzy.rs | 9 +- 2 files changed, 85 insertions(+), 207 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 050c05a639..6ceaeac137 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -12,7 +12,7 @@ use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; use smol::{channel::Sender, Timer}; -use std::{collections::HashSet, future::Future}; +use std::future::Future; use std::{ ffi::OsStr, fmt, fs, @@ -204,15 +204,18 @@ impl Snapshot { self.root_inode.and_then(|inode| self.entries.get(&inode)) } + pub fn root_name(&self) -> Option<&OsStr> { + self.path.file_name() + } + fn inode_for_path(&self, path: impl AsRef) -> Option { let path = path.as_ref(); self.root_inode.and_then(|mut inode| { 'components: for path_component in path { if let Some(Entry::Dir { children, .. }) = &self.entries.get(&inode) { - for child in children.as_ref() { - if self.entries.get(child).map(|entry| entry.name()) == Some(path_component) - { - inode = *child; + for (child_inode, name) in children.as_ref() { + if name.as_ref() == path_component { + inode = *child_inode; continue 'components; } } @@ -228,183 +231,91 @@ impl Snapshot { .and_then(|inode| self.entries.get(&inode)) } - pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result { + pub fn path_for_inode(&self, mut ino: u64, include_root: bool) -> Result { let mut components = Vec::new(); let mut entry = self .entries .get(&ino) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - components.push(entry.name()); while let Some(parent) = entry.parent() { entry = self.entries.get(&parent).unwrap(); - components.push(entry.name()); + if let Entry::Dir { children, .. } = entry { + let (_, child_name) = children + .iter() + .find(|(child_inode, _)| *child_inode == ino) + .unwrap(); + components.push(child_name.as_ref()); + ino = parent; + } else { + unreachable!(); + } + } + if include_root { + components.push(self.path.file_name().unwrap()); } - let mut components = components.into_iter().rev(); - if !include_root { - components.next(); - } - - let mut path = PathBuf::new(); - for component in components { - path.push(component); - } - Ok(path) + Ok(components.into_iter().rev().collect()) } - fn remove_entry(&mut self, inode: u64) { - if let Some(entry) = self.entries.get(&inode).cloned() { + fn remove_path(&mut self, path: &Path) { + if let Some(parent_path) = path.parent() { let mut edits = Vec::new(); - if let Some(parent) = entry.parent() { - let mut parent_entry = self.entries.get(&parent).unwrap().clone(); - if let Entry::Dir { children, .. } = &mut parent_entry { - *children = children - .into_iter() - .copied() - .filter(|child| *child != entry.inode()) - .collect::>() - .into(); - edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); + let mut parent_entry = self.entry_for_path(parent_path).unwrap().clone(); + let parent_inode = parent_entry.inode(); + let mut entry_inode = None; + if let Entry::Dir { children, .. } = &mut parent_entry { + let mut new_children = Vec::new(); + for (child_inode, child_name) in children.as_ref() { + if Some(child_name.as_ref()) == path.file_name() { + entry_inode = Some(*child_inode); + } else { + new_children.push((*child_inode, child_name.clone())); + } } + + if new_children.iter().any(|c| Some(c.0) == entry_inode) { + entry_inode = None; + } + + dbg!(&children, &new_children, entry_inode, parent_inode); + *children = new_children.into(); + edits.push(Edit::Insert(parent_entry)); + } else { + unreachable!(); } - // Recursively remove the orphaned nodes' descendants. - let mut descendant_stack = vec![entry.inode()]; - while let Some(inode) = descendant_stack.pop() { - if let Some(entry) = self.entries.get(&inode) { - edits.push(Edit::Remove(inode)); - if let Entry::Dir { children, .. } = entry { - descendant_stack.extend_from_slice(children.as_ref()); + if let Some(entry_inode) = entry_inode { + let entry = self.entries.get(&entry_inode).unwrap(); + + // Recursively remove the orphaned nodes' descendants. + let mut descendant_stack = Vec::new(); + if entry.parent() == Some(parent_inode) { + descendant_stack.push(entry_inode); + while let Some(inode) = descendant_stack.pop() { + if let Some(entry) = self.entries.get(&inode) { + edits.push(Edit::Remove(inode)); + if let Entry::Dir { children, .. } = entry { + descendant_stack.extend(children.iter().map(|c| c.0)); + } + } } } } - // dbg!(&edits); self.entries.edit(edits); } } - fn move_entry( - &mut self, - child_inode: u64, - new_path: Option<&Path>, - old_parent_inode: Option, - new_parent_inode: Option, - ) { - let mut edits_len = 1; - if old_parent_inode.is_some() { - edits_len += 1; - } - if new_parent_inode.is_some() { - edits_len += 1; - } - let mut deletions = Vec::with_capacity(edits_len); - let mut insertions = Vec::with_capacity(edits_len); - - // Remove the entries from the sum tree. - deletions.push(Edit::Remove(child_inode)); - if old_parent_inode != new_parent_inode { - if let Some(old_parent_inode) = old_parent_inode { - deletions.push(Edit::Remove(old_parent_inode)); - } - if let Some(new_parent_inode) = new_parent_inode { - deletions.push(Edit::Remove(new_parent_inode)); - } - } - let removed_entries = self.entries.edit(deletions); - let mut child_entry = None; - let mut old_parent_entry = None; - let mut new_parent_entry = None; - for removed_entry in removed_entries { - if removed_entry.inode() == child_inode { - child_entry = Some(removed_entry); - } else if Some(removed_entry.inode()) == old_parent_inode { - old_parent_entry = Some(removed_entry); - } else if Some(removed_entry.inode()) == new_parent_inode { - new_parent_entry = Some(removed_entry); - } - } - - // Update the child entry's parent. - let mut child_entry = child_entry.expect("cannot move non-existent entry"); - child_entry.set_parent(new_parent_inode); - if let Some(new_path) = new_path { - let new_path = new_path.strip_prefix(self.path.parent().unwrap()).unwrap(); - child_entry.set_name(new_path.file_name().unwrap()); - - // Recompute the PathEntry for each file under this subtree. - let mut stack = Vec::new(); - stack.push((child_entry, new_path.parent().unwrap().to_path_buf())); - while let Some((mut entry, mut new_path)) = stack.pop() { - new_path.push(entry.name()); - match &mut entry { - Entry::Dir { - children, inode, .. - } => { - for child_inode in children.as_ref() { - let child_entry = self.entries.get(child_inode).unwrap(); - stack.push((child_entry.clone(), new_path.clone())); - } - - // Descendant directories don't need to be mutated because their properties - // haven't changed, so only re-insert this directory if it is the top entry - // we were moving. - if *inode == child_inode { - insertions.push(Edit::Insert(entry)); - } - } - Entry::File { - inode, - is_ignored, - path, - .. - } => { - *path = PathEntry::new(*inode, &new_path, *is_ignored); - insertions.push(Edit::Insert(entry)); - } - } - } - } else { - insertions.push(Edit::Insert(child_entry)); - } - - // Remove the child entry from its old parent's children. - if let Some(mut old_parent_entry) = old_parent_entry { - if let Entry::Dir { children, .. } = &mut old_parent_entry { - *children = children - .into_iter() - .cloned() - .filter(|c| *c != child_inode) - .collect(); - insertions.push(Edit::Insert(old_parent_entry)); - } else { - panic!("snapshot entry's new parent was not a directory"); - } - } - - // Add the child entry to its new parent's children. - if let Some(mut new_parent_entry) = new_parent_entry { - if let Entry::Dir { children, .. } = &mut new_parent_entry { - *children = children - .into_iter() - .cloned() - .chain(Some(child_inode)) - .collect(); - insertions.push(Edit::Insert(new_parent_entry)); - } else { - panic!("snapshot entry's new parent is not a directory"); - } - } - - self.entries.edit(insertions); - } - - fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result { + fn fmt_entry( + &self, + f: &mut fmt::Formatter<'_>, + ino: u64, + name: &OsStr, + indent: usize, + ) -> fmt::Result { match self.entries.get(&ino).unwrap() { - Entry::Dir { name, children, .. } => { + Entry::Dir { children, .. } => { write!( f, "{}{}/ ({})\n", @@ -412,12 +323,12 @@ impl Snapshot { name.to_string_lossy(), ino )?; - for child_id in children.iter() { - self.fmt_entry(f, *child_id, indent + 2)?; + for (child_inode, child_name) in children.iter() { + self.fmt_entry(f, *child_inode, child_name, indent + 2)?; } Ok(()) } - Entry::File { name, .. } => write!( + Entry::File { .. } => write!( f, "{}{} ({})\n", " ".repeat(indent), @@ -431,7 +342,7 @@ impl Snapshot { impl fmt::Debug for Snapshot { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(root_ino) = self.root_inode { - self.fmt_entry(f, root_ino, 0) + self.fmt_entry(f, root_ino, self.path.file_name().unwrap(), 0) } else { write!(f, "Empty tree\n") } @@ -464,16 +375,14 @@ impl FileHandle { pub enum Entry { Dir { parent: Option, - name: Arc, inode: u64, is_symlink: bool, is_ignored: bool, - children: Arc<[u64]>, + children: Arc<[(u64, Arc)]>, pending: bool, }, File { parent: Option, - name: Arc, path: PathEntry, inode: u64, is_symlink: bool, @@ -499,27 +408,6 @@ impl Entry { Entry::File { parent, .. } => *parent, } } - - fn set_parent(&mut self, new_parent: Option) { - match self { - Entry::Dir { parent, .. } => *parent = new_parent, - Entry::File { parent, .. } => *parent = new_parent, - } - } - - fn name(&self) -> &OsStr { - match self { - Entry::Dir { name, .. } => name, - Entry::File { name, .. } => name, - } - } - - fn set_name(&mut self, new_name: &OsStr) { - match self { - Entry::Dir { name, .. } => *name = new_name.into(), - Entry::File { name, .. } => *name = new_name.into(), - } - } } impl sum_tree::Item for Entry { @@ -660,7 +548,6 @@ impl BackgroundScanner { let is_ignored = is_ignored || name.as_ref() == ".git"; let dir_entry = Entry::Dir { parent: None, - name, inode, is_symlink, is_ignored, @@ -702,7 +589,6 @@ impl BackgroundScanner { } else { self.insert_entries(Some(Entry::File { parent: None, - name, path: PathEntry::new(inode, &relative_path, is_ignored), inode, is_symlink, @@ -731,7 +617,7 @@ impl BackgroundScanner { let is_symlink = metadata.file_type().is_symlink(); let path = job.path.join(name.as_ref()); - new_children.push(ino); + new_children.push((ino, name.clone())); if metadata.is_dir() { let mut is_ignored = true; let mut ignore = None; @@ -747,7 +633,6 @@ impl BackgroundScanner { let dir_entry = Entry::Dir { parent: Some(job.inode), - name, inode: ino, is_symlink, is_ignored, @@ -770,7 +655,6 @@ impl BackgroundScanner { .map_or(true, |i| i.matched(&path, false).is_ignore()); new_entries.push(Entry::File { parent: Some(job.inode), - name, path: PathEntry::new(ino, &relative_path, is_ignored), inode: ino, is_symlink, @@ -818,24 +702,22 @@ impl BackgroundScanner { paths.next(); } - if let Some(snapshot_inode) = snapshot.inode_for_path(&relative_path) { - snapshot.remove_entry(snapshot_inode); - } + snapshot.remove_path(&relative_path); match self.fs_entry_for_path(&snapshot.path, &path) { Ok(Some((fs_entry, ignore))) => { - snapshot.remove_entry(fs_entry.inode()); - + // snapshot.remove_entry(fs_entry.inode()); let mut edits = Vec::new(); edits.push(Edit::Insert(fs_entry.clone())); if let Some(parent) = fs_entry.parent() { let mut parent_entry = snapshot.entries.get(&parent).unwrap().clone(); if let Entry::Dir { children, .. } = &mut parent_entry { - if !children.contains(&fs_entry.inode()) { + if !children.iter().any(|c| c.0 == fs_entry.inode()) { + let name = Arc::from(path.file_name().unwrap()); *children = children .into_iter() - .copied() - .chain(Some(fs_entry.inode())) + .cloned() + .chain(Some((fs_entry.inode(), name))) .collect::>() .into(); edits.push(Edit::Insert(parent_entry)); @@ -899,7 +781,6 @@ impl BackgroundScanner { let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); let inode = metadata.ino(); - let name: Arc = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); let parent = if path == root_path { None @@ -910,7 +791,6 @@ impl BackgroundScanner { Ok(Some(( Entry::Dir { parent, - name, inode, is_symlink, is_ignored, @@ -923,7 +803,6 @@ impl BackgroundScanner { Ok(Some(( Entry::File { parent, - name, path: PathEntry::new( inode, root_path @@ -1349,7 +1228,7 @@ mod tests { let computed_path = self.path_for_inode(inode, true).unwrap(); match self.entries.get(&inode).unwrap() { Entry::Dir { children, .. } => { - stack.extend_from_slice(children); + stack.extend(children.iter().map(|c| c.0)); } Entry::File { path, .. } => { assert_eq!( diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index d9f6bab8f8..55efdfe737 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -128,12 +128,11 @@ where let skipped_prefix_len = if include_root_name { 0 - } else if let Some(Entry::Dir { name, .. }) = snapshot.root_entry() { - let name = name.to_string_lossy(); - if name == "/" { - 1 + } else if let Some(Entry::Dir { .. }) = snapshot.root_entry() { + if let Some(name) = snapshot.root_name() { + name.to_string_lossy().chars().count() + 1 } else { - name.chars().count() + 1 + 1 } } else { 0 From 3e93fb945928a886674465066f4c7375832c5e53 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 11:59:22 -0700 Subject: [PATCH 044/102] Get worktree randomized test passing Co-Authored-By: Antonio Scandurra --- zed/src/worktree.rs | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6ceaeac137..25ade0bca4 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -258,10 +258,10 @@ impl Snapshot { } fn remove_path(&mut self, path: &Path) { - if let Some(parent_path) = path.parent() { + if let Some(mut parent_entry) = path.parent().and_then(|p| self.entry_for_path(p).cloned()) + { let mut edits = Vec::new(); - let mut parent_entry = self.entry_for_path(parent_path).unwrap().clone(); let parent_inode = parent_entry.inode(); let mut entry_inode = None; if let Entry::Dir { children, .. } = &mut parent_entry { @@ -278,7 +278,6 @@ impl Snapshot { entry_inode = None; } - dbg!(&children, &new_children, entry_inode, parent_inode); *children = new_children.into(); edits.push(Edit::Insert(parent_entry)); } else { @@ -286,17 +285,16 @@ impl Snapshot { } if let Some(entry_inode) = entry_inode { - let entry = self.entries.get(&entry_inode).unwrap(); - // Recursively remove the orphaned nodes' descendants. let mut descendant_stack = Vec::new(); - if entry.parent() == Some(parent_inode) { - descendant_stack.push(entry_inode); - while let Some(inode) = descendant_stack.pop() { - if let Some(entry) = self.entries.get(&inode) { + descendant_stack.push((entry_inode, parent_inode)); + while let Some((inode, parent_inode)) = descendant_stack.pop() { + if let Some(entry) = self.entries.get(&inode) { + if entry.parent() == Some(parent_inode) { edits.push(Edit::Remove(inode)); if let Entry::Dir { children, .. } = entry { - descendant_stack.extend(children.iter().map(|c| c.0)); + descendant_stack + .extend(children.iter().map(|c| (c.0, entry.inode()))); } } } @@ -696,7 +694,6 @@ impl BackgroundScanner { continue; } }; - // dbg!(&path, &relative_path, snapshot.entries.items()); while paths.peek().map_or(false, |p| p.starts_with(&path)) { paths.next(); @@ -706,22 +703,19 @@ impl BackgroundScanner { match self.fs_entry_for_path(&snapshot.path, &path) { Ok(Some((fs_entry, ignore))) => { - // snapshot.remove_entry(fs_entry.inode()); let mut edits = Vec::new(); edits.push(Edit::Insert(fs_entry.clone())); if let Some(parent) = fs_entry.parent() { let mut parent_entry = snapshot.entries.get(&parent).unwrap().clone(); if let Entry::Dir { children, .. } = &mut parent_entry { - if !children.iter().any(|c| c.0 == fs_entry.inode()) { - let name = Arc::from(path.file_name().unwrap()); - *children = children - .into_iter() - .cloned() - .chain(Some((fs_entry.inode(), name))) - .collect::>() - .into(); - edits.push(Edit::Insert(parent_entry)); - } + let name = Arc::from(path.file_name().unwrap()); + *children = children + .into_iter() + .cloned() + .chain(Some((fs_entry.inode(), name))) + .collect::>() + .into(); + edits.push(Edit::Insert(parent_entry)); } else { unreachable!(); } @@ -1111,7 +1105,6 @@ mod tests { notify_tx, ); new_scanner.scan_dirs().unwrap(); - dbg!(scanner.snapshot().entries.items()); assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); } } From 0fd3f55730a323fa89dbb76da7d5ee5e03c2dc7f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 12:15:24 -0700 Subject: [PATCH 045/102] :lipstick: Reduce nesting in Snapshot::remove_path --- zed/src/worktree.rs | 77 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 25ade0bca4..c62f60ea20 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -258,51 +258,50 @@ impl Snapshot { } fn remove_path(&mut self, path: &Path) { - if let Some(mut parent_entry) = path.parent().and_then(|p| self.entry_for_path(p).cloned()) - { - let mut edits = Vec::new(); + let mut parent_entry = match path.parent().and_then(|p| self.entry_for_path(p).cloned()) { + Some(e) => e, + None => return, + }; - let parent_inode = parent_entry.inode(); - let mut entry_inode = None; - if let Entry::Dir { children, .. } = &mut parent_entry { - let mut new_children = Vec::new(); - for (child_inode, child_name) in children.as_ref() { - if Some(child_name.as_ref()) == path.file_name() { - entry_inode = Some(*child_inode); - } else { - new_children.push((*child_inode, child_name.clone())); - } + let mut edits = Vec::new(); + let parent_inode = parent_entry.inode(); + let mut entry_inode = None; + if let Entry::Dir { children, .. } = &mut parent_entry { + let mut new_children = Vec::new(); + for (child_inode, child_name) in children.as_ref() { + if Some(child_name.as_ref()) == path.file_name() { + entry_inode = Some(*child_inode); + } else { + new_children.push((*child_inode, child_name.clone())); } - - if new_children.iter().any(|c| Some(c.0) == entry_inode) { - entry_inode = None; - } - - *children = new_children.into(); - edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); } - if let Some(entry_inode) = entry_inode { - // Recursively remove the orphaned nodes' descendants. - let mut descendant_stack = Vec::new(); - descendant_stack.push((entry_inode, parent_inode)); - while let Some((inode, parent_inode)) = descendant_stack.pop() { - if let Some(entry) = self.entries.get(&inode) { - if entry.parent() == Some(parent_inode) { - edits.push(Edit::Remove(inode)); - if let Entry::Dir { children, .. } = entry { - descendant_stack - .extend(children.iter().map(|c| (c.0, entry.inode()))); - } + if new_children.iter().any(|c| Some(c.0) == entry_inode) { + entry_inode = None; + } + + *children = new_children.into(); + edits.push(Edit::Insert(parent_entry)); + } else { + unreachable!(); + } + + if let Some(entry_inode) = entry_inode { + let mut descendant_stack = Vec::new(); + descendant_stack.push((entry_inode, parent_inode)); + while let Some((inode, parent_inode)) = descendant_stack.pop() { + if let Some(entry) = self.entries.get(&inode) { + if entry.parent() == Some(parent_inode) { + edits.push(Edit::Remove(inode)); + if let Entry::Dir { children, .. } = entry { + descendant_stack.extend(children.iter().map(|c| (c.0, entry.inode()))); } } } } - - self.entries.edit(edits); } + + self.entries.edit(edits); } fn fmt_entry( @@ -372,19 +371,19 @@ impl FileHandle { #[derive(Clone, Debug)] pub enum Entry { Dir { - parent: Option, inode: u64, + parent: Option, is_symlink: bool, is_ignored: bool, children: Arc<[(u64, Arc)]>, pending: bool, }, File { - parent: Option, - path: PathEntry, inode: u64, + parent: Option, is_symlink: bool, is_ignored: bool, + path: PathEntry, }, } From 47f0b94d0c6f5b03d85e55ea19bf2cd5e91a420c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 13:05:34 -0700 Subject: [PATCH 046/102] Add an example executable for fsevents --- fsevent/examples/events.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 fsevent/examples/events.rs diff --git a/fsevent/examples/events.rs b/fsevent/examples/events.rs new file mode 100644 index 0000000000..7f064d9050 --- /dev/null +++ b/fsevent/examples/events.rs @@ -0,0 +1,16 @@ +use fsevent::EventStream; +use std::{env::args, path::Path, time::Duration}; + +fn main() { + let paths = args().skip(1).collect::>(); + let paths = paths.iter().map(Path::new).collect::>(); + assert!(paths.len() > 0, "Must pass 1 or more paths as arguments"); + let stream = EventStream::new(&paths, Duration::from_millis(100), |events| { + eprintln!("event batch"); + for event in events { + eprintln!(" {:?}", event); + } + true + }); + stream.run(); +} From e44a59dc7d40298fe1e7caac1469e41bf10c423f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 13:07:32 -0700 Subject: [PATCH 047/102] Worktree: handle files being renamed to overwrite dirs --- zed/src/worktree.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index c62f60ea20..91b1234233 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -282,8 +282,6 @@ impl Snapshot { *children = new_children.into(); edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); } if let Some(entry_inode) = entry_inode { @@ -1153,11 +1151,24 @@ mod tests { .filter(|d| !d.starts_with(old_path)) .choose(rng) .unwrap(); - let new_path = new_path_parent.join(gen_name(rng)); + + let overwrite_existing_dir = + !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3); + let new_path = if overwrite_existing_dir { + fs::remove_dir_all(&new_path_parent).ok(); + new_path_parent.to_path_buf() + } else { + new_path_parent.join(gen_name(rng)) + }; log::info!( - "Renaming {:?} to {:?}", + "Renaming {:?} to {}{:?}", old_path.strip_prefix(&root_path)?, + if overwrite_existing_dir { + "overwrite " + } else { + "" + }, new_path.strip_prefix(&root_path)? ); fs::rename(&old_path, &new_path)?; From f06164ade9fee3ad1d2ae8cd6fc82286227d17db Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 13:19:34 -0700 Subject: [PATCH 048/102] :lipstick: process_events --- zed/src/worktree.rs | 81 +++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 91b1234233..0731a08f73 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -208,6 +208,11 @@ impl Snapshot { self.path.file_name() } + fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { + self.inode_for_path(path) + .and_then(|inode| self.entries.get(&inode)) + } + fn inode_for_path(&self, path: impl AsRef) -> Option { let path = path.as_ref(); self.root_inode.and_then(|mut inode| { @@ -226,38 +231,53 @@ impl Snapshot { }) } - fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { - self.inode_for_path(path) - .and_then(|inode| self.entries.get(&inode)) - } - - pub fn path_for_inode(&self, mut ino: u64, include_root: bool) -> Result { + pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result { let mut components = Vec::new(); let mut entry = self .entries - .get(&ino) + .get(&inode) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; while let Some(parent) = entry.parent() { entry = self.entries.get(&parent).unwrap(); if let Entry::Dir { children, .. } = entry { let (_, child_name) = children .iter() - .find(|(child_inode, _)| *child_inode == ino) + .find(|(child_inode, _)| *child_inode == inode) .unwrap(); components.push(child_name.as_ref()); - ino = parent; + inode = parent; } else { unreachable!(); } } if include_root { - components.push(self.path.file_name().unwrap()); + components.push(self.root_name().unwrap()); } - Ok(components.into_iter().rev().collect()) } - fn remove_path(&mut self, path: &Path) { + fn insert_entry(&mut self, path: &Path, entry: Entry) { + let mut edits = Vec::new(); + edits.push(Edit::Insert(entry.clone())); + if let Some(parent) = entry.parent() { + let mut parent_entry = self.entries.get(&parent).unwrap().clone(); + if let Entry::Dir { children, .. } = &mut parent_entry { + let name = Arc::from(path.file_name().unwrap()); + *children = children + .into_iter() + .cloned() + .chain(Some((entry.inode(), name))) + .collect::>() + .into(); + edits.push(Edit::Insert(parent_entry)); + } else { + unreachable!(); + } + } + self.entries.edit(edits); + } + + fn remove_entry(&mut self, path: &Path) { let mut parent_entry = match path.parent().and_then(|p| self.entry_for_path(p).cloned()) { Some(e) => e, None => return, @@ -687,7 +707,7 @@ impl BackgroundScanner { let relative_path = match path.strip_prefix(&snapshot.path) { Ok(relative_path) => relative_path.to_path_buf(), Err(e) => { - log::error!("Unexpected event {:?}", e); + log::error!("unexpected event {:?}", e); continue; } }; @@ -696,40 +716,21 @@ impl BackgroundScanner { paths.next(); } - snapshot.remove_path(&relative_path); + snapshot.remove_entry(&relative_path); match self.fs_entry_for_path(&snapshot.path, &path) { Ok(Some((fs_entry, ignore))) => { - let mut edits = Vec::new(); - edits.push(Edit::Insert(fs_entry.clone())); - if let Some(parent) = fs_entry.parent() { - let mut parent_entry = snapshot.entries.get(&parent).unwrap().clone(); - if let Entry::Dir { children, .. } = &mut parent_entry { - let name = Arc::from(path.file_name().unwrap()); - *children = children - .into_iter() - .cloned() - .chain(Some((fs_entry.inode(), name))) - .collect::>() - .into(); - edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); - } - } - snapshot.entries.edit(edits); + snapshot.insert_entry(&path, fs_entry.clone()); if fs_entry.is_dir() { - let relative_path = snapshot - .path - .parent() - .map_or(path.as_path(), |parent| path.strip_prefix(parent).unwrap()) - .to_path_buf(); scan_queue_tx .send(Ok(ScanJob { inode: fs_entry.inode(), path: Arc::from(path), - relative_path, + relative_path: snapshot + .root_name() + .map_or(PathBuf::new(), PathBuf::from) + .join(relative_path), dir_entry: fs_entry, ignore: Some(ignore), scan_queue: scan_queue_tx.clone(), @@ -740,14 +741,14 @@ impl BackgroundScanner { Ok(None) => {} Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("Error reading file on event {:?}", err); + log::error!("error reading file on event {:?}", err); } } } *self.snapshot.lock() = snapshot; - // Scan any directories that were moved into this worktree as part of this event batch. + // Scan any directories that were created as part of this event batch. drop(scan_queue_tx); self.thread_pool.scoped(|pool| { for _ in 0..self.thread_pool.workers() { From f27b01fcd562f45a70a62ae3dc8e4b7ed6a6033f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 14:11:55 -0700 Subject: [PATCH 049/102] Get all worktree tests passing * Store non-canocalized path on worktree, but canonicalize it for the purpose of processing events * Simplify rescan unit test --- zed/src/worktree.rs | 87 +++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 0731a08f73..c55dadc743 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -260,18 +260,19 @@ impl Snapshot { let mut edits = Vec::new(); edits.push(Edit::Insert(entry.clone())); if let Some(parent) = entry.parent() { - let mut parent_entry = self.entries.get(&parent).unwrap().clone(); - if let Entry::Dir { children, .. } = &mut parent_entry { - let name = Arc::from(path.file_name().unwrap()); - *children = children - .into_iter() - .cloned() - .chain(Some((entry.inode(), name))) - .collect::>() - .into(); - edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); + if let Some(mut parent_entry) = self.entries.get(&parent).cloned() { + if let Entry::Dir { children, .. } = &mut parent_entry { + let name = Arc::from(path.file_name().unwrap()); + *children = children + .into_iter() + .cloned() + .chain(Some((entry.inode(), name))) + .collect::>() + .into(); + edits.push(Edit::Insert(parent_entry)); + } else { + unreachable!(); + } } } self.entries.edit(edits); @@ -500,16 +501,7 @@ impl BackgroundScanner { } fn run(&self) { - let path = { - let mut snapshot = self.snapshot.lock(); - let canonical_path = snapshot - .path - .canonicalize() - .map(Arc::from) - .unwrap_or_else(|_| snapshot.path.clone()); - snapshot.path = canonical_path.clone(); - canonical_path - }; + let path = self.snapshot.lock().path.clone(); // Create the event stream before we start scanning to ensure we receive events for changes // that occur in the middle of the scan. @@ -699,15 +691,19 @@ impl BackgroundScanner { fn process_events(&self, mut events: Vec) { let mut snapshot = self.snapshot(); + let root_path = snapshot + .path + .canonicalize() + .unwrap_or_else(|_| snapshot.path.to_path_buf()); events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut paths = events.into_iter().map(|e| e.path).peekable(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); while let Some(path) = paths.next() { - let relative_path = match path.strip_prefix(&snapshot.path) { + let relative_path = match path.strip_prefix(&root_path) { Ok(relative_path) => relative_path.to_path_buf(), - Err(e) => { - log::error!("unexpected event {:?}", e); + Err(_) => { + log::error!("unexpected event {:?} for root path {:?}", path, root_path); continue; } }; @@ -718,7 +714,7 @@ impl BackgroundScanner { snapshot.remove_entry(&relative_path); - match self.fs_entry_for_path(&snapshot.path, &path) { + match self.fs_entry_for_path(&root_path, &path) { Ok(Some((fs_entry, ignore))) => { snapshot.insert_entry(&path, fs_entry.clone()); @@ -977,61 +973,49 @@ mod tests { #[test] fn test_rescan() { App::test_async((), |mut app| async move { - let dir2 = temp_tree(json!({ - "dir1": { - "dir3": { - "file": "contents", - } - }, - "dir2": { - } - })); let dir = temp_tree(json!({ "dir1": { - "dir3": { - "file": "contents", - } + "file1": "contents 1", }, "dir2": { + "dir3": { + "file2": "contents 2", + } } })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; + assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 2)).await; - let dir_inode = app.read(|ctx| { + let file2_inode = app.read(|ctx| { tree.read(ctx) .snapshot() - .inode_for_path("dir1/dir3") + .inode_for_path("dir2/dir3/file2") .unwrap() }); app.read(|ctx| { let tree = tree.read(ctx); assert_eq!( - tree.path_for_inode(dir_inode, false) + tree.path_for_inode(file2_inode, false) .unwrap() .to_str() .unwrap(), - "dir1/dir3" + "dir2/dir3/file2" ); }); - std::fs::rename(dir2.path(), dir.path().join("foo")).unwrap(); + std::fs::rename(dir.path().join("dir2/dir3"), dir.path().join("dir4")).unwrap(); assert_condition(1, 300, || { app.read(|ctx| { let tree = tree.read(ctx); - tree.path_for_inode(dir_inode, false) + tree.path_for_inode(file2_inode, false) .unwrap() .to_str() .unwrap() - == "dir2/dir3" + == "dir4/file2" }) }) .await; - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!(tree.snapshot().inode_for_path("dir2/dir3"), Some(dir_inode)); - }); }); } @@ -1112,7 +1096,8 @@ mod tests { insertion_probability: f64, rng: &mut impl Rng, ) -> Result> { - let (dirs, files) = read_dir_recursive(root_path.to_path_buf()); + let root_path = root_path.canonicalize().unwrap(); + let (dirs, files) = read_dir_recursive(root_path.clone()); let mut events = Vec::new(); let mut record_event = |path: PathBuf| { From 8fb79a30941e7cc8713e1c7a85aecc7254fbf53a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 15:00:10 -0700 Subject: [PATCH 050/102] Enable split debuginfo for faster compiles --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 96be78508c..1ca7597d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,6 @@ cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d5 cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"} + +[profile.dev] +split-debuginfo = "unpacked" From 69a43afcbde5993a57b55b14a1a61abfe1c2bfb4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Apr 2021 17:45:01 -0700 Subject: [PATCH 051/102] Add a method for waiting until a worktree's current scan is complete Start removing usages of finish_pending_tasks in tests --- zed/src/file_finder.rs | 4 ++-- zed/src/workspace/workspace.rs | 16 ++++++++++++- zed/src/workspace/workspace_view.rs | 3 ++- zed/src/worktree.rs | 37 ++++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 6f59035550..065b32ebac 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -421,7 +421,8 @@ mod tests { let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx)); let (window_id, workspace_view) = app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); - app.finish_pending_tasks().await; // Open and populate worktree. + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; app.dispatch_action( window_id, vec![workspace_view.id()], @@ -444,7 +445,6 @@ mod tests { app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); - app.finish_pending_tasks().await; // Complete path search. // let view_state = finder.state(&app); // assert!(view_state.matches.len() > 1); diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index 21b51327c0..e8fc21c819 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -94,6 +94,19 @@ impl Workspace { &self.worktrees } + pub fn worktree_scans_complete(&self, ctx: &AppContext) -> impl Future + 'static { + let futures = self + .worktrees + .iter() + .map(|worktree| worktree.read(ctx).scan_complete()) + .collect::>(); + async move { + for future in futures { + future.await; + } + } + } + pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool { paths.iter().all(|path| self.contains_path(&path, app)) } @@ -235,7 +248,8 @@ mod tests { })); let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); - app.finish_pending_tasks().await; // Open and populate worktree. + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; // Get the first file entry. let tree = app.read(|ctx| workspace.read(ctx).worktrees.iter().next().unwrap().clone()); diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 1496ddef76..23ab0960df 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -393,7 +393,8 @@ mod tests { let settings = settings::channel(&app.font_cache()).unwrap().1; let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); - app.finish_pending_tasks().await; // Open and populate worktree. + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); let (_, workspace_view) = diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index c55dadc743..eed829fd8d 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -6,17 +6,20 @@ use crate::{ sum_tree::{self, Edit, SumTree}, }; use anyhow::{anyhow, Result}; +use futures_core::future::BoxFuture; pub use fuzzy::match_paths; use fuzzy::PathEntry; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; +use postage::{oneshot, prelude::Stream, sink::Sink}; use smol::{channel::Sender, Timer}; -use std::future::Future; use std::{ ffi::OsStr, fmt, fs, + future::Future, io::{self, Read, Write}, + mem, ops::{AddAssign, Deref}, os::unix::fs::MetadataExt, path::{Path, PathBuf}, @@ -36,6 +39,7 @@ enum ScanState { pub struct Worktree { snapshot: Snapshot, scanner: Arc, + scan_listeners: Mutex>>, scan_state: ScanState, poll_scheduled: bool, } @@ -59,7 +63,8 @@ impl Worktree { let tree = Self { snapshot, scanner, - scan_state: ScanState::Idle, + scan_listeners: Default::default(), + scan_state: ScanState::Scanning, poll_scheduled: false, }; @@ -72,6 +77,18 @@ impl Worktree { tree } + pub fn scan_complete(&self) -> BoxFuture<'static, ()> { + if self.is_scanning() { + let (tx, mut rx) = oneshot::channel::<()>(); + self.scan_listeners.lock().push(tx); + Box::pin(async move { + rx.recv().await; + }) + } else { + Box::pin(async {}) + } + } + fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { self.scan_state = scan_state; self.poll_entries(ctx); @@ -88,6 +105,18 @@ impl Worktree { }) .detach(); self.poll_scheduled = true; + } else { + let mut listeners = Vec::new(); + mem::swap(self.scan_listeners.lock().as_mut(), &mut listeners); + ctx.spawn( + async move { + for mut tx in listeners { + tx.send(()).await.ok(); + } + }, + |_, _, _| {}, + ) + .detach(); } } @@ -906,9 +935,11 @@ mod tests { unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); - assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 4)).await; + + app.read(|ctx| tree.read(ctx).scan_complete()).await; app.read(|ctx| { let tree = tree.read(ctx); + assert_eq!(tree.file_count(), 4); let results = match_paths( Some(tree.snapshot()).iter(), "bna", From a4c1fe5a0b97ace14dc7f45cf6f6a285ef25e791 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 Apr 2021 22:01:54 -0600 Subject: [PATCH 052/102] WIP: Add a condition method to model and view handles for use in tests It returns a future that resolves when the provided predicate returns true. The predicate is called any time the handle's targeted entity calls notify. Still need to add a timeout and completely remove finsih_pending_tasks. --- Cargo.lock | 6 ++- gpui/Cargo.toml | 1 + gpui/src/app.rs | 73 ++++++++++++++++++++++++++++- zed/Cargo.toml | 5 +- zed/src/test.rs | 9 +++- zed/src/workspace/workspace_view.rs | 71 +++++++++++++++------------- 6 files changed, 126 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e51e1aaaf2..4d46794322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,9 +568,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8f45d9ad417bcef4817d614a501ab55cdd96a6fdb24f49aab89a54acfd66b19" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" dependencies = [ "quote", "syn", @@ -1005,6 +1005,7 @@ dependencies = [ "pathfinder_color", "pathfinder_geometry", "png", + "postage", "rand 0.8.3", "replace_with", "resvg", @@ -2449,6 +2450,7 @@ dependencies = [ "anyhow", "arrayvec", "crossbeam-channel 0.5.0", + "ctor", "dirs", "easy-parallel", "fsevent", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 0f11bd1de2..9b7e9eef3d 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -15,6 +15,7 @@ ordered-float = "2.1.1" parking_lot = "0.11.1" pathfinder_color = "0.5" pathfinder_geometry = "0.5" +postage = {version = "0.4.1", features = ["futures-traits"]} rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 910489a4d9..25fe3cbc15 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -13,11 +13,12 @@ use keymap::MatchResult; use parking_lot::Mutex; use pathfinder_geometry::{rect::RectF, vector::vec2f}; use platform::Event; +use postage::{sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, cell::RefCell, - collections::{HashMap, HashSet, VecDeque}, + collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, @@ -384,6 +385,7 @@ pub struct MutableAppContext { next_task_id: usize, subscriptions: HashMap>, observations: HashMap>, + async_observations: HashMap>, window_invalidations: HashMap, presenters_and_platform_windows: HashMap>, Box)>, @@ -424,6 +426,7 @@ impl MutableAppContext { next_task_id: 0, subscriptions: HashMap::new(), observations: HashMap::new(), + async_observations: HashMap::new(), window_invalidations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), @@ -877,11 +880,13 @@ impl MutableAppContext { self.ctx.models.remove(&model_id); self.subscriptions.remove(&model_id); self.observations.remove(&model_id); + self.async_observations.remove(&model_id); } for (window_id, view_id) in dropped_views { self.subscriptions.remove(&view_id); self.observations.remove(&view_id); + self.async_observations.remove(&view_id); if let Some(window) = self.ctx.windows.get_mut(&window_id) { self.window_invalidations .entry(window_id) @@ -1047,6 +1052,12 @@ impl MutableAppContext { } } } + + if let Entry::Occupied(mut entry) = self.async_observations.entry(observed_id) { + if entry.get_mut().blocking_send(()).is_err() { + entry.remove_entry(); + } + } } fn notify_view_observers(&mut self, window_id: usize, view_id: usize) { @@ -2012,6 +2023,36 @@ impl ModelHandle { { app.update_model(self, update) } + + pub fn condition( + &self, + ctx: &TestAppContext, + mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool, + ) -> impl 'static + Future { + let mut ctx = ctx.0.borrow_mut(); + let tx = ctx + .async_observations + .entry(self.id()) + .or_insert_with(|| postage::broadcast::channel(128).0); + let mut rx = tx.subscribe(); + let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); + let handle = self.clone(); + + async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + if rx.recv().await.is_none() { + break; + } + } + } + } } impl Clone for ModelHandle { @@ -2145,6 +2186,36 @@ impl ViewHandle { app.focused_view_id(self.window_id) .map_or(false, |focused_id| focused_id == self.view_id) } + + pub fn condition( + &self, + ctx: &TestAppContext, + mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool, + ) -> impl 'static + Future { + let mut ctx = ctx.0.borrow_mut(); + let tx = ctx + .async_observations + .entry(self.id()) + .or_insert_with(|| postage::broadcast::channel(128).0); + let mut rx = tx.subscribe(); + let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); + let handle = self.clone(); + + async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + if rx.recv().await.is_none() { + break; + } + } + } + } } impl Clone for ViewHandle { diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 981f09c2b8..f27f79fa01 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -16,10 +16,11 @@ path = "src/main.rs" anyhow = "1.0.38" arrayvec = "0.5.2" crossbeam-channel = "0.5.0" +ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" -futures-core = "0.3" fsevent = {path = "../fsevent"} +futures-core = "0.3" gpui = {path = "../gpui"} ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"} lazy_static = "1.4.0" @@ -31,8 +32,8 @@ postage = {version = "0.4.1", features = ["futures-traits"]} rand = "0.8.3" rust-embed = "5.9.0" seahash = "4.1" +serde = {version = "1", features = ["derive"]} simplelog = "0.9" -serde = { version = "1", features = ["derive"] } smallvec = "1.6.1" smol = "1.2.5" diff --git a/zed/src/test.rs b/zed/src/test.rs index da965b257b..3850616671 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,4 +1,8 @@ +use crate::time::ReplicaId; +use ctor::ctor; +use log::LevelFilter; use rand::Rng; +use simplelog::SimpleLogger; use std::{ collections::BTreeMap, path::{Path, PathBuf}, @@ -6,7 +10,10 @@ use std::{ }; use tempdir::TempDir; -use crate::time::ReplicaId; +#[ctor] +fn init_logger() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); +} #[derive(Clone)] struct Envelope { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 23ab0960df..bf741edbab 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -402,7 +402,21 @@ mod tests { // Open the first entry workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx)); - app.finish_pending_tasks().await; + + workspace_view + .condition(&app, |workspace_view, ctx| { + workspace_view.active_pane().read(ctx).items().len() == 1 + }) + .await; + + // Open the second entry + workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx)); + + workspace_view + .condition(&app, |workspace_view, ctx| { + workspace_view.active_pane().read(ctx).items().len() == 2 + }) + .await; app.read(|ctx| { assert_eq!( @@ -410,36 +424,34 @@ mod tests { .read(ctx) .active_pane() .read(ctx) - .items() - .len(), - 1 - ) - }); - - // Open the second entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx)); - app.finish_pending_tasks().await; - - app.read(|ctx| { - let active_pane = workspace_view.read(ctx).active_pane().read(ctx); - assert_eq!(active_pane.items().len(), 2); - assert_eq!( - active_pane.active_item().unwrap().entry_id(ctx), + .active_item() + .unwrap() + .entry_id(ctx), Some(entries[1]) ); }); // Open the first entry again workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx)); - app.finish_pending_tasks().await; + + { + let entries = entries.clone(); + workspace_view + .condition(&app, move |workspace_view, ctx| { + workspace_view + .active_pane() + .read(ctx) + .active_item() + .unwrap() + .entry_id(ctx) + == Some(entries[0]) + }) + .await; + } app.read(|ctx| { let active_pane = workspace_view.read(ctx).active_pane().read(ctx); assert_eq!(active_pane.items().len(), 2); - assert_eq!( - active_pane.active_item().unwrap().entry_id(ctx), - Some(entries[0]) - ); }); // Open the third entry twice concurrently @@ -447,19 +459,12 @@ mod tests { w.open_entry(entries[2], ctx); w.open_entry(entries[2], ctx); }); - app.finish_pending_tasks().await; - app.read(|ctx| { - assert_eq!( - workspace_view - .read(ctx) - .active_pane() - .read(ctx) - .items() - .len(), - 3 - ); - }); + workspace_view + .condition(&app, |workspace_view, ctx| { + workspace_view.active_pane().read(ctx).items().len() == 3 + }) + .await; }); } From d11d5483b672903c84c20a5964ada9db633ab31a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 20 Apr 2021 11:24:46 +0200 Subject: [PATCH 053/102] Rework `Worktree::scan_complete` to use a watch --- zed/src/worktree.rs | 55 ++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index eed829fd8d..cb3f376649 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -6,20 +6,21 @@ use crate::{ sum_tree::{self, Edit, SumTree}, }; use anyhow::{anyhow, Result}; -use futures_core::future::BoxFuture; -pub use fuzzy::match_paths; use fuzzy::PathEntry; +pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::Mutex; -use postage::{oneshot, prelude::Stream, sink::Sink}; +use postage::{ + prelude::{Sink, Stream}, + watch, +}; use smol::{channel::Sender, Timer}; use std::{ ffi::OsStr, fmt, fs, future::Future, io::{self, Read, Write}, - mem, ops::{AddAssign, Deref}, os::unix::fs::MetadataExt, path::{Path, PathBuf}, @@ -27,20 +28,17 @@ use std::{ time::Duration, }; -pub use fuzzy::PathMatch; - -#[derive(Debug)] +#[derive(Clone, Debug)] enum ScanState { Idle, Scanning, - Err(io::Error), + Err(Arc), } pub struct Worktree { snapshot: Snapshot, scanner: Arc, - scan_listeners: Mutex>>, - scan_state: ScanState, + scan_state: (watch::Sender, watch::Receiver), poll_scheduled: bool, } @@ -63,8 +61,7 @@ impl Worktree { let tree = Self { snapshot, scanner, - scan_listeners: Default::default(), - scan_state: ScanState::Scanning, + scan_state: watch::channel_with(ScanState::Scanning), poll_scheduled: false, }; @@ -77,20 +74,18 @@ impl Worktree { tree } - pub fn scan_complete(&self) -> BoxFuture<'static, ()> { - if self.is_scanning() { - let (tx, mut rx) = oneshot::channel::<()>(); - self.scan_listeners.lock().push(tx); - Box::pin(async move { - rx.recv().await; - }) - } else { - Box::pin(async {}) + pub fn scan_complete(&self) -> impl Future { + let mut scan_state_rx = self.scan_state.1.clone(); + async move { + let mut next_scan_state = Some(scan_state_rx.borrow().clone()); + while let Some(ScanState::Scanning) = next_scan_state { + next_scan_state = scan_state_rx.recv().await; + } } } fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { - self.scan_state = scan_state; + let _ = self.scan_state.0.blocking_send(scan_state); self.poll_entries(ctx); } @@ -105,23 +100,11 @@ impl Worktree { }) .detach(); self.poll_scheduled = true; - } else { - let mut listeners = Vec::new(); - mem::swap(self.scan_listeners.lock().as_mut(), &mut listeners); - ctx.spawn( - async move { - for mut tx in listeners { - tx.send(()).await.ok(); - } - }, - |_, _, _| {}, - ) - .detach(); } } fn is_scanning(&self) -> bool { - if let ScanState::Scanning = self.scan_state { + if let ScanState::Scanning = *self.scan_state.1.borrow() { true } else { false @@ -554,7 +537,7 @@ impl BackgroundScanner { } if let Err(err) = self.scan_dirs() { - if smol::block_on(self.notify.send(ScanState::Err(err))).is_err() { + if smol::block_on(self.notify.send(ScanState::Err(Arc::new(err)))).is_err() { return; } } From ebb712440541473e8306414314245fba2bd8fd0a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 20 Apr 2021 11:48:12 +0200 Subject: [PATCH 054/102] Use `scan_complete` and the new `ModelHandle::condition` test method --- zed/src/worktree.rs | 51 +++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index cb3f376649..d098bb7d3a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -955,7 +955,8 @@ mod tests { })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 1)).await; + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); @@ -988,48 +989,30 @@ mod tests { fn test_rescan() { App::test_async((), |mut app| async move { let dir = temp_tree(json!({ - "dir1": { - "file1": "contents 1", + "a": { + "file1": "", }, - "dir2": { - "dir3": { - "file2": "contents 2", + "b": { + "c": { + "file2": "", } } })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - assert_condition(1, 300, || app.read(|ctx| tree.read(ctx).file_count() == 2)).await; + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 2)); - let file2_inode = app.read(|ctx| { - tree.read(ctx) - .snapshot() - .inode_for_path("dir2/dir3/file2") - .unwrap() - }); - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!( - tree.path_for_inode(file2_inode, false) - .unwrap() - .to_str() - .unwrap(), - "dir2/dir3/file2" - ); + let file2 = app.read(|ctx| { + let inode = tree.read(ctx).inode_for_path("b/c/file2").unwrap(); + let file2 = tree.file(inode, ctx).unwrap(); + assert_eq!(file2.path(ctx), Path::new("b/c/file2")); + file2 }); - std::fs::rename(dir.path().join("dir2/dir3"), dir.path().join("dir4")).unwrap(); - assert_condition(1, 300, || { - app.read(|ctx| { - let tree = tree.read(ctx); - tree.path_for_inode(file2_inode, false) - .unwrap() - .to_str() - .unwrap() - == "dir4/file2" - }) - }) - .await; + std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); + tree.condition(&app, move |_, ctx| file2.path(ctx) == Path::new("d/file2")) + .await; }); } From cd7dccd30c93935c523af47d90fd19eccdeb4a55 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 20 Apr 2021 12:28:30 +0200 Subject: [PATCH 055/102] Replace remaining usages of `finish_pending_tasks` with `condition` --- gpui/src/app.rs | 138 ++-------------------------- zed/src/test.rs | 16 ---- zed/src/workspace/pane.rs | 9 +- zed/src/workspace/workspace_view.rs | 112 ++++++++-------------- 4 files changed, 50 insertions(+), 225 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 25fe3cbc15..4b37c2dbfa 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -325,10 +325,6 @@ impl TestAppContext { result } - pub fn finish_pending_tasks(&self) -> impl Future { - self.0.borrow().finish_pending_tasks() - } - pub fn font_cache(&self) -> Arc { self.0.borrow().font_cache.clone() } @@ -1066,6 +1062,12 @@ impl MutableAppContext { .or_default() .updated .insert(view_id); + + if let Entry::Occupied(mut entry) = self.async_observations.entry(view_id) { + if entry.get_mut().blocking_send(()).is_err() { + entry.remove_entry(); + } + } } fn focus(&mut self, window_id: usize, focused_id: usize) { @@ -1207,40 +1209,6 @@ impl MutableAppContext { result } - pub fn finish_pending_tasks(&self) -> impl Future { - let mut pending_tasks = self - .future_handlers - .borrow() - .keys() - .cloned() - .collect::>(); - pending_tasks.extend(self.stream_handlers.borrow().keys()); - - let task_done = self.task_done.clone(); - let future_handlers = self.future_handlers.clone(); - let stream_handlers = self.stream_handlers.clone(); - - async move { - // A Condvar expects the condition to be protected by a Mutex, but in this case we know - // that this logic will always run on the main thread. - let mutex = async_std::sync::Mutex::new(()); - loop { - { - let future_handlers = future_handlers.borrow(); - let stream_handlers = stream_handlers.borrow(); - pending_tasks.retain(|task_id| { - future_handlers.contains_key(task_id) - || stream_handlers.contains_key(task_id) - }); - if pending_tasks.is_empty() { - break; - } - } - task_done.wait(mutex.lock().await).await; - } - } - } - pub fn write_to_clipboard(&self, item: ClipboardItem) { self.platform.write_to_clipboard(item); } @@ -3388,98 +3356,4 @@ mod tests { // assert!(invalidation.removed.is_empty()); // }); // } - - #[test] - fn test_finish_pending_tasks() { - struct View; - - impl Entity for View { - type Event = (); - } - - impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { - Empty::new().boxed() - } - - fn ui_name() -> &'static str { - "View" - } - } - - struct Model; - - impl Entity for Model { - type Event = (); - } - - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Model); - let (_, view) = app.add_window(|_| View); - - model.update(&mut app, |_, ctx| { - ctx.spawn(async {}, |_, _, _| {}).detach(); - // Cancel this task - drop(ctx.spawn(async {}, |_, _, _| {})); - }); - - view.update(&mut app, |_, ctx| { - ctx.spawn(async {}, |_, _, _| {}).detach(); - // Cancel this task - drop(ctx.spawn(async {}, |_, _, _| {})); - }); - - assert!(!app.0.borrow().future_handlers.borrow().is_empty()); - app.finish_pending_tasks().await; - assert!(app.0.borrow().future_handlers.borrow().is_empty()); - app.finish_pending_tasks().await; // Don't block if there are no tasks - - model.update(&mut app, |_, ctx| { - ctx.spawn_stream(smol::stream::iter(vec![1, 2, 3]), |_, _, _| {}, |_, _| {}) - .detach(); - // Cancel this task - drop(ctx.spawn_stream(smol::stream::iter(vec![1, 2, 3]), |_, _, _| {}, |_, _| {})); - }); - - view.update(&mut app, |_, ctx| { - ctx.spawn_stream(smol::stream::iter(vec![1, 2, 3]), |_, _, _| {}, |_, _| {}) - .detach(); - // Cancel this task - drop(ctx.spawn_stream(smol::stream::iter(vec![1, 2, 3]), |_, _, _| {}, |_, _| {})); - }); - - assert!(!app.0.borrow().stream_handlers.borrow().is_empty()); - app.finish_pending_tasks().await; - assert!(app.0.borrow().stream_handlers.borrow().is_empty()); - app.finish_pending_tasks().await; // Don't block if there are no tasks - - // Tasks are considered finished when we drop handles - let mut tasks = Vec::new(); - model.update(&mut app, |_, ctx| { - tasks.push(Box::new(ctx.spawn(async {}, |_, _, _| {}))); - tasks.push(Box::new(ctx.spawn_stream( - smol::stream::iter(vec![1, 2, 3]), - |_, _, _| {}, - |_, _| {}, - ))); - }); - - view.update(&mut app, |_, ctx| { - tasks.push(Box::new(ctx.spawn(async {}, |_, _, _| {}))); - tasks.push(Box::new(ctx.spawn_stream( - smol::stream::iter(vec![1, 2, 3]), - |_, _, _| {}, - |_, _| {}, - ))); - }); - - assert!(!app.0.borrow().stream_handlers.borrow().is_empty()); - - let finish_pending_tasks = app.finish_pending_tasks(); - drop(tasks); - finish_pending_tasks.await; - assert!(app.0.borrow().stream_handlers.borrow().is_empty()); - app.finish_pending_tasks().await; // Don't block if there are no tasks - }); - } } diff --git a/zed/src/test.rs b/zed/src/test.rs index 3850616671..61579908f7 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -6,7 +6,6 @@ use simplelog::SimpleLogger; use std::{ collections::BTreeMap, path::{Path, PathBuf}, - time::{Duration, Instant}, }; use tempdir::TempDir; @@ -144,18 +143,3 @@ fn write_tree(path: &Path, tree: serde_json::Value) { panic!("You must pass a JSON object to this helper") } } - -pub async fn assert_condition(poll_interval: u64, timeout: u64, mut f: impl FnMut() -> bool) { - let poll_interval = Duration::from_millis(poll_interval); - let timeout = Duration::from_millis(timeout); - let start = Instant::now(); - loop { - if f() { - return; - } else if Instant::now().duration_since(start) < timeout { - smol::Timer::after(poll_interval).await; - } else { - panic!("timed out waiting on condition"); - } - } -} diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index c352a522eb..ea3226715a 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -106,11 +106,10 @@ impl Pane { } pub fn activate_entry(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext) -> bool { - if let Some(index) = self - .items - .iter() - .position(|item| item.entry_id(ctx.as_ref()).map_or(false, |id| id == entry_id)) - { + if let Some(index) = self.items.iter().position(|item| { + item.entry_id(ctx.as_ref()) + .map_or(false, |id| id == entry_id) + }) { self.activate_item(index, ctx); true } else { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index bf741edbab..205806490c 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -385,9 +385,9 @@ mod tests { App::test_async((), |mut app| async move { let dir = temp_tree(json!({ "a": { - "aa": "aa contents", - "ab": "ab contents", - "ac": "ab contents", + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", }, })); @@ -396,74 +396,44 @@ mod tests { app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0]; + let file2 = entries[1]; + let file3 = entries[2]; let (_, workspace_view) = app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); + let pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); // Open the first entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx)); - - workspace_view - .condition(&app, |workspace_view, ctx| { - workspace_view.active_pane().read(ctx).items().len() == 1 - }) + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); + pane.condition(&app, |pane, _| pane.items().len() == 1) .await; // Open the second entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx)); - - workspace_view - .condition(&app, |workspace_view, ctx| { - workspace_view.active_pane().read(ctx).items().len() == 2 - }) + workspace_view.update(&mut app, |w, ctx| w.open_entry(file2, ctx)); + pane.condition(&app, |pane, _| pane.items().len() == 2) .await; - app.read(|ctx| { - assert_eq!( - workspace_view - .read(ctx) - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .entry_id(ctx), - Some(entries[1]) - ); + let pane = pane.read(ctx); + assert_eq!(pane.active_item().unwrap().entry_id(ctx), Some(file2)); }); // Open the first entry again - workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx)); - - { - let entries = entries.clone(); - workspace_view - .condition(&app, move |workspace_view, ctx| { - workspace_view - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .entry_id(ctx) - == Some(entries[0]) - }) - .await; - } - + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); + pane.condition(&app, move |pane, ctx| { + pane.active_item().unwrap().entry_id(ctx) == Some(file1) + }) + .await; app.read(|ctx| { - let active_pane = workspace_view.read(ctx).active_pane().read(ctx); - assert_eq!(active_pane.items().len(), 2); + assert_eq!(pane.read(ctx).items().len(), 2); }); // Open the third entry twice concurrently workspace_view.update(&mut app, |w, ctx| { - w.open_entry(entries[2], ctx); - w.open_entry(entries[2], ctx); + w.open_entry(file3, ctx); + w.open_entry(file3, ctx); }); - - workspace_view - .condition(&app, |workspace_view, ctx| { - workspace_view.active_pane().read(ctx).items().len() == 3 - }) + pane.condition(&app, |pane, _| pane.items().len() == 3) .await; }); } @@ -475,44 +445,42 @@ mod tests { let dir = temp_tree(json!({ "a": { - "aa": "aa contents", - "ab": "ab contents", - "ac": "ab contents", + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", }, })); let settings = settings::channel(&app.font_cache()).unwrap().1; let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx)); - app.finish_pending_tasks().await; // Open and populate worktree. + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0]; let (window_id, workspace_view) = app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); - - workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx)); - app.finish_pending_tasks().await; - let pane_1 = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); + pane_1 + .condition(&app, move |pane, ctx| { + pane.active_item().and_then(|i| i.entry_id(ctx)) == Some(file1) + }) + .await; + app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); app.update(|ctx| { let pane_2 = workspace_view.read(ctx).active_pane().clone(); assert_ne!(pane_1, pane_2); - assert_eq!( - pane_2 - .read(ctx) - .active_item() - .unwrap() - .entry_id(ctx.as_ref()), - Some(entries[0]) - ); + let pane2_item = pane_2.read(ctx).active_item().unwrap(); + assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1)); ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - - let w = workspace_view.read(ctx); - assert_eq!(w.panes.len(), 1); - assert_eq!(w.active_pane(), &pane_1); + let workspace_view = workspace_view.read(ctx); + assert_eq!(workspace_view.panes.len(), 1); + assert_eq!(workspace_view.active_pane(), &pane_1); }); }); } From 37444acc9cdc0977c1b8f3ab4e7a29a05d685958 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 20 Apr 2021 17:21:29 +0200 Subject: [PATCH 056/102] Time out `condition` after 200ms and add basic unit tests for it Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 174 ++++++++++++++++++++++++++++++++++++++------ gpui/src/util.rs | 15 ++++ zed/src/worktree.rs | 6 +- 3 files changed, 171 insertions(+), 24 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4b37c2dbfa..45fd1fb743 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -4,7 +4,7 @@ use crate::{ keymap::{self, Keystroke}, platform::{self, WindowOptions}, presenter::Presenter, - util::post_inc, + util::{post_inc, timeout}, AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; @@ -25,6 +25,7 @@ use std::{ path::PathBuf, rc::{self, Rc}, sync::{Arc, Weak}, + time::Duration, }; pub trait Entity: 'static + Send + Sync { @@ -2007,18 +2008,23 @@ impl ModelHandle { let handle = self.clone(); async move { - loop { - { - let ctx = ctx.borrow(); - let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { - break; + timeout(Duration::from_millis(200), async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + + if rx.recv().await.is_none() { + panic!("model dropped with pending condition"); } } - if rx.recv().await.is_none() { - break; - } - } + }) + .await + .expect("condition timed out"); } } } @@ -2170,18 +2176,23 @@ impl ViewHandle { let handle = self.clone(); async move { - loop { - { - let ctx = ctx.borrow(); - let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { - break; + timeout(Duration::from_millis(200), async move { + loop { + { + let ctx = ctx.borrow(); + let ctx = ctx.as_ref(); + if predicate(handle.read(ctx), ctx) { + break; + } + } + + if rx.recv().await.is_none() { + panic!("model dropped with pending condition"); } } - if rx.recv().await.is_none() { - break; - } - } + }) + .await + .expect("condition timed out"); } } } @@ -2475,6 +2486,7 @@ impl Drop for EntityTask { mod tests { use super::*; use crate::elements::*; + use smol::future::poll_once; #[test] fn test_model_handles() { @@ -3276,6 +3288,126 @@ mod tests { }); } + #[test] + fn test_model_condition() { + struct Counter(usize); + + impl super::Entity for Counter { + type Event = (); + } + + impl Counter { + fn inc(&mut self, ctx: &mut ModelContext) { + self.0 += 1; + ctx.notify(); + } + } + + App::test_async((), |mut app| async move { + let model = app.add_model(|_| Counter(0)); + + let condition1 = model.condition(&app, |model, _| model.0 == 2); + let condition2 = model.condition(&app, |model, _| model.0 == 3); + smol::pin!(condition1, condition2); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); + + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + }); + } + + #[test] + #[should_panic] + fn test_model_condition_timeout() { + struct Model; + + impl super::Entity for Model { + type Event = (); + } + + App::test_async((), |mut app| async move { + let model = app.add_model(|_| Model); + model.condition(&app, |_, _| false).await; + }); + } + + #[test] + fn test_view_condition() { + struct Counter(usize); + + impl super::Entity for Counter { + type Event = (); + } + + impl super::View for Counter { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&self, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + } + + impl Counter { + fn inc(&mut self, ctx: &mut ViewContext) { + self.0 += 1; + ctx.notify(); + } + } + + App::test_async((), |mut app| async move { + let (_, view) = app.add_window(|_| Counter(0)); + + let condition1 = view.condition(&app, |view, _| view.0 == 2); + let condition2 = view.condition(&app, |view, _| view.0 == 3); + smol::pin!(condition1, condition2); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); + + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + }); + } + + #[test] + #[should_panic] + fn test_view_condition_timeout() { + struct View; + + impl super::Entity for View { + type Event = (); + } + + impl super::View for View { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&self, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + } + + App::test_async((), |mut app| async move { + let (_, view) = app.add_window(|_| View); + view.condition(&app, |_, _| false).await; + }); + } + // #[test] // fn test_ui_and_window_updates() { // struct View { diff --git a/gpui/src/util.rs b/gpui/src/util.rs index 473c8d00f1..10731ced5c 100644 --- a/gpui/src/util.rs +++ b/gpui/src/util.rs @@ -1,5 +1,20 @@ +use smol::future::FutureExt; +use std::{future::Future, time::Duration}; + pub fn post_inc(value: &mut usize) -> usize { let prev = *value; *value += 1; prev } + +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d098bb7d3a..4b0cd1df53 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -77,9 +77,9 @@ impl Worktree { pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.scan_state.1.clone(); async move { - let mut next_scan_state = Some(scan_state_rx.borrow().clone()); - while let Some(ScanState::Scanning) = next_scan_state { - next_scan_state = scan_state_rx.recv().await; + let mut scan_state = Some(scan_state_rx.borrow().clone()); + while let Some(ScanState::Scanning) = scan_state { + scan_state = scan_state_rx.recv().await; } } } From ddd746b9f95470d21d413f08c69478558ce67afb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 20 Apr 2021 17:34:14 +0200 Subject: [PATCH 057/102] Use `env_logger` instead of `simplelog` in tests Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 ++ gpui/Cargo.toml | 1 + gpui/src/test.rs | 4 +--- zed/Cargo.toml | 1 + zed/src/test.rs | 4 +--- zed/src/worktree.rs | 4 ---- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d46794322..d0eefe9ecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,6 +993,7 @@ dependencies = [ "core-graphics", "core-text", "ctor", + "env_logger", "etagere", "font-kit", "foreign-types", @@ -2453,6 +2454,7 @@ dependencies = [ "ctor", "dirs", "easy-parallel", + "env_logger", "fsevent", "futures-core", "gpui", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 9b7e9eef3d..b0823474c8 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -34,6 +34,7 @@ bindgen = "0.57" cc = "1.0.67" [dev-dependencies] +env_logger = "0.8" png = "0.16" simplelog = "0.9" diff --git a/gpui/src/test.rs b/gpui/src/test.rs index 9048e0bfeb..7210529807 100644 --- a/gpui/src/test.rs +++ b/gpui/src/test.rs @@ -1,8 +1,6 @@ use ctor::ctor; -use simplelog::SimpleLogger; -use log::LevelFilter; #[ctor] fn init_logger() { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + env_logger::init(); } diff --git a/zed/Cargo.toml b/zed/Cargo.toml index f27f79fa01..fde1cfdbbe 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -38,6 +38,7 @@ smallvec = "1.6.1" smol = "1.2.5" [dev-dependencies] +env_logger = "0.8" serde_json = {version = "1.0.64", features = ["preserve_order"]} tempdir = "0.3.7" unindent = "0.1.7" diff --git a/zed/src/test.rs b/zed/src/test.rs index 61579908f7..1d155d4a5a 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,8 +1,6 @@ use crate::time::ReplicaId; use ctor::ctor; -use log::LevelFilter; use rand::Rng; -use simplelog::SimpleLogger; use std::{ collections::BTreeMap, path::{Path, PathBuf}, @@ -11,7 +9,7 @@ use tempdir::TempDir; #[ctor] fn init_logger() { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + env_logger::init(); } #[derive(Clone)] diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 4b0cd1df53..e11793286a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -888,10 +888,8 @@ mod tests { use crate::test::*; use anyhow::Result; use gpui::App; - use log::LevelFilter; use rand::prelude::*; use serde_json::json; - use simplelog::SimpleLogger; use std::env; use std::os::unix; use std::time::{SystemTime, UNIX_EPOCH}; @@ -1028,8 +1026,6 @@ mod tests { .map(|o| o.parse().unwrap()) .unwrap_or(20); let seeds = if let Ok(seed) = env::var("SEED").map(|s| s.parse().unwrap()) { - // Init logging so that we can debug the operations for this seed. - SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap(); seed..seed + 1 } else { 0..iterations From dcc2bdfd4cdd737d9a3032c95dbe60373de9381c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 20 Apr 2021 10:36:54 -0600 Subject: [PATCH 058/102] Panic when awaiting conditions on dropped entities Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- gpui/src/app.rs | 78 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 45fd1fb743..a91a861eef 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2005,7 +2005,7 @@ impl ModelHandle { .or_insert_with(|| postage::broadcast::channel(128).0); let mut rx = tx.subscribe(); let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); - let handle = self.clone(); + let handle = self.downgrade(); async move { timeout(Duration::from_millis(200), async move { @@ -2013,14 +2013,20 @@ impl ModelHandle { { let ctx = ctx.borrow(); let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { + if predicate( + handle + .upgrade(ctx) + .expect("model dropped with pending condition") + .read(ctx), + ctx, + ) { break; } } - if rx.recv().await.is_none() { - panic!("model dropped with pending condition"); - } + rx.recv() + .await + .expect("model dropped with pending condition"); } }) .await @@ -2173,7 +2179,7 @@ impl ViewHandle { .or_insert_with(|| postage::broadcast::channel(128).0); let mut rx = tx.subscribe(); let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap(); - let handle = self.clone(); + let handle = self.downgrade(); async move { timeout(Duration::from_millis(200), async move { @@ -2181,14 +2187,20 @@ impl ViewHandle { { let ctx = ctx.borrow(); let ctx = ctx.as_ref(); - if predicate(handle.read(ctx), ctx) { + if predicate( + handle + .upgrade(ctx) + .expect("model dropped with pending condition") + .read(ctx), + ctx, + ) { break; } } - if rx.recv().await.is_none() { - panic!("model dropped with pending condition"); - } + rx.recv() + .await + .expect("model dropped with pending condition"); } }) .await @@ -3338,6 +3350,23 @@ mod tests { }); } + #[test] + #[should_panic(expected = "model dropped with pending condition")] + fn test_model_condition_panic_on_drop() { + struct Model; + + impl super::Entity for Model { + type Event = (); + } + + App::test_async((), |mut app| async move { + let model = app.add_model(|_| Model); + let condition = model.condition(&app, |_, _| false); + app.update(|_| drop(model)); + condition.await; + }); + } + #[test] fn test_view_condition() { struct Counter(usize); @@ -3408,6 +3437,35 @@ mod tests { }); } + #[test] + #[should_panic(expected = "model dropped with pending condition")] + fn test_view_condition_panic_on_drop() { + struct View; + + impl super::Entity for View { + type Event = (); + } + + impl super::View for View { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&self, _: &AppContext) -> ElementBox { + Empty::new().boxed() + } + } + + App::test_async((), |mut app| async move { + let window_id = app.add_window(|_| View).0; + let view = app.add_view(window_id, |_| View); + + let condition = view.condition(&app, |_, _| false); + app.update(|_| drop(view)); + condition.await; + }); + } + // #[test] // fn test_ui_and_window_updates() { // struct View { From 1a8e909a3893c2f711d011ac585f6b769527d140 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 20 Apr 2021 10:43:13 -0600 Subject: [PATCH 059/102] Ensure we remove broadcast channels after all conditions resolve Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- gpui/src/app.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a91a861eef..f7dfd9cedb 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -3332,6 +3332,10 @@ mod tests { model.update(&mut app, |model, ctx| model.inc(ctx)); assert_eq!(poll_once(&mut condition2).await, Some(())); + + // Broadcast channel should be removed if no conditions remain on next notification. + model.update(&mut app, |_, ctx| ctx.notify()); + app.update(|ctx| assert!(ctx.async_observations.get(&model.id()).is_none())); }); } @@ -3409,6 +3413,10 @@ mod tests { view.update(&mut app, |view, ctx| view.inc(ctx)); assert_eq!(poll_once(&mut condition2).await, Some(())); + + // Broadcast channel should be removed if no conditions remain on next notification. + view.update(&mut app, |_, ctx| ctx.notify()); + app.update(|ctx| assert!(ctx.async_observations.get(&view.id()).is_none())); }); } From 9899614f319422950eef7de9df63a634151f08aa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 20 Apr 2021 10:45:42 -0600 Subject: [PATCH 060/102] Remove task_done condvar which supported finish_pending_tasks Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- Cargo.lock | 183 ------------------------------------------------ gpui/Cargo.toml | 1 - gpui/src/app.rs | 17 +---- 3 files changed, 1 insertion(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0eefe9ecc..4096564e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,22 +84,6 @@ dependencies = [ "futures-lite", ] -[[package]] -name = "async-global-executor" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" -dependencies = [ - "async-channel", - "async-executor", - "async-io", - "async-mutex", - "blocking", - "futures-lite", - "num_cpus", - "once_cell", -] - [[package]] name = "async-io" version = "1.3.1" @@ -129,15 +113,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-mutex" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" -dependencies = [ - "event-listener", -] - [[package]] name = "async-net" version = "1.5.0" @@ -166,34 +141,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "async-std" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" -dependencies = [ - "async-channel", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils 0.8.2", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "num_cpus", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - [[package]] name = "async-task" version = "4.0.3" @@ -306,12 +253,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "bumpalo" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" - [[package]] name = "bytemuck" version = "1.5.1" @@ -965,25 +906,11 @@ dependencies = [ "regex", ] -[[package]] -name = "gloo-timers" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "gpui" version = "0.1.0" dependencies = [ "anyhow", - "async-std", "async-task", "bindgen", "block", @@ -1092,15 +1019,6 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" -[[package]] -name = "js-sys" -version = "0.3.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "kurbo" version = "0.8.1" @@ -1110,15 +1028,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1163,7 +1072,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if 1.0.0", - "value-bag", ] [[package]] @@ -1958,12 +1866,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" - [[package]] name = "smallvec" version = "1.6.1" @@ -2236,15 +2138,6 @@ dependencies = [ "xmlwriter", ] -[[package]] -name = "value-bag" -version = "1.0.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b676010e055c99033117c2343b33a40a30b91fecd6c49055ac9cd2d6c305ab1" -dependencies = [ - "ctor", -] - [[package]] name = "variance" version = "0.1.3" @@ -2298,82 +2191,6 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" -[[package]] -name = "wasm-bindgen" -version = "0.2.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" - -[[package]] -name = "web-sys" -version = "0.3.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wepoll-sys" version = "3.0.1" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index b0823474c8..0d6a2982ad 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -5,7 +5,6 @@ name = "gpui" version = "0.1.0" [dependencies] -async-std = {version = "1.9.0", features = ["unstable"]} async-task = "4.0.3" ctor = "0.1" etagere = "0.2" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index f7dfd9cedb..d81dd8797f 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -8,7 +8,6 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; -use async_std::sync::Condvar; use keymap::MatchResult; use parking_lot::Mutex; use pathfinder_geometry::{rect::RectF, vector::vec2f}; @@ -390,7 +389,6 @@ pub struct MutableAppContext { foreground: Rc, future_handlers: Rc>>, stream_handlers: Rc>>, - task_done: Arc, pending_effects: VecDeque, pending_flushes: usize, flushing_effects: bool, @@ -430,7 +428,6 @@ impl MutableAppContext { foreground, future_handlers: Default::default(), stream_handlers: Default::default(), - task_done: Default::default(), pending_effects: VecDeque::new(), pending_flushes: 0, flushing_effects: false, @@ -1139,7 +1136,6 @@ impl MutableAppContext { task_id, task, TaskHandlerMap::Future(self.future_handlers.clone()), - self.task_done.clone(), ) } @@ -1175,7 +1171,6 @@ impl MutableAppContext { task_id, task, TaskHandlerMap::Stream(self.stream_handlers.clone()), - self.task_done.clone(), ) } @@ -1184,7 +1179,6 @@ impl MutableAppContext { let future_callback = self.future_handlers.borrow_mut().remove(&task_id).unwrap(); let result = future_callback(output, self); self.flush_effects(); - self.task_done.notify_all(); result } @@ -1206,7 +1200,6 @@ impl MutableAppContext { let result = (handler.done_callback)(self); self.flush_effects(); - self.task_done.notify_all(); result } @@ -2430,7 +2423,6 @@ pub struct EntityTask { id: usize, task: Option>, handler_map: TaskHandlerMap, - task_done: Arc, } enum TaskHandlerMap { @@ -2440,17 +2432,11 @@ enum TaskHandlerMap { } impl EntityTask { - fn new( - id: usize, - task: executor::Task, - handler_map: TaskHandlerMap, - task_done: Arc, - ) -> Self { + fn new(id: usize, task: executor::Task, handler_map: TaskHandlerMap) -> Self { Self { id, task: Some(task), handler_map, - task_done, } } @@ -2490,7 +2476,6 @@ impl Drop for EntityTask { map.borrow_mut().remove(&self.id); } } - self.task_done.notify_all(); } } From 5cbbf620edb4708d7bd0c13500a34cb8769d4e8e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 20 Apr 2021 10:59:12 -0600 Subject: [PATCH 061/102] WIP Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- zed/src/worktree.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index e11793286a..471b5d1d3a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -5,7 +5,7 @@ use crate::{ editor::{History, Snapshot as BufferSnapshot}, sum_tree::{self, Edit, SumTree}, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use fuzzy::PathEntry; pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; @@ -781,11 +781,18 @@ impl BackgroundScanner { let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); + let is_symlink = fs::symlink_metadata(&path) + .context("symlink_metadata")? + .file_type() + .is_symlink(); let parent = if path == root_path { None } else { - Some(fs::metadata(path.parent().unwrap())?.ino()) + Some( + fs::metadata(path.parent().unwrap()) + .context("parent metadata")? + .ino(), + ) }; if metadata.file_type().is_dir() { Ok(Some(( @@ -819,10 +826,11 @@ impl BackgroundScanner { } } Err(err) => { + dbg!(&err); if err.kind() == io::ErrorKind::NotFound { Ok(None) } else { - Err(anyhow::Error::new(err)) + Err(anyhow::Error::new(err)).context("fs::metadata") } } } From 85f8537017d8cce964e208efaf3cd8363c6c8f1f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 20 Apr 2021 10:12:48 -0700 Subject: [PATCH 062/102] Worktree: silence log message when dir is overwritten by file Co-Authored-By: Antonio Scandurra --- zed/src/worktree.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 471b5d1d3a..749390bf83 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -782,7 +782,7 @@ impl BackgroundScanner { let inode = metadata.ino(); let is_symlink = fs::symlink_metadata(&path) - .context("symlink_metadata")? + .context("failed to read symlink metadata")? .file_type() .is_symlink(); let parent = if path == root_path { @@ -790,7 +790,7 @@ impl BackgroundScanner { } else { Some( fs::metadata(path.parent().unwrap()) - .context("parent metadata")? + .context("failed to read parent inode")? .ino(), ) }; @@ -825,14 +825,11 @@ impl BackgroundScanner { ))) } } - Err(err) => { - dbg!(&err); - if err.kind() == io::ErrorKind::NotFound { - Ok(None) - } else { - Err(anyhow::Error::new(err)).context("fs::metadata") - } - } + Err(err) => match (err.kind(), err.raw_os_error()) { + (io::ErrorKind::NotFound, _) => Ok(None), + (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None), + _ => Err(anyhow::Error::new(err)), + }, } } From 265ad900344c2cd21cf5f2abc9a6b8efc4f1fa31 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 20 Apr 2021 10:39:14 -0700 Subject: [PATCH 063/102] Stop processing events if root path is deleted Co-Authored-By: Antonio Scandurra --- zed/src/worktree.rs | 127 +++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 749390bf83..27abd4510a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -523,7 +523,9 @@ impl BackgroundScanner { return false; } - self.process_events(events); + if !self.process_events(events) { + return false; + } if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { return false; @@ -701,12 +703,13 @@ impl BackgroundScanner { Ok(()) } - fn process_events(&self, mut events: Vec) { + fn process_events(&self, mut events: Vec) -> bool { let mut snapshot = self.snapshot(); - let root_path = snapshot - .path - .canonicalize() - .unwrap_or_else(|_| snapshot.path.to_path_buf()); + let root_path = if let Ok(path) = snapshot.path.canonicalize() { + path + } else { + return false; + }; events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut paths = events.into_iter().map(|e| e.path).peekable(); @@ -769,68 +772,68 @@ impl BackgroundScanner { }); } }); + + true } fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { - match fs::metadata(&path) { - Ok(metadata) => { - let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - if metadata.is_dir() { - ignore = ignore.add_child(&path).unwrap(); - } - let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - - let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path) - .context("failed to read symlink metadata")? - .file_type() - .is_symlink(); - let parent = if path == root_path { - None - } else { - Some( - fs::metadata(path.parent().unwrap()) - .context("failed to read parent inode")? - .ino(), - ) - }; - if metadata.file_type().is_dir() { - Ok(Some(( - Entry::Dir { - parent, - inode, - is_symlink, - is_ignored, - children: Arc::from([]), - pending: true, - }, - ignore, - ))) - } else { - Ok(Some(( - Entry::File { - parent, - path: PathEntry::new( - inode, - root_path - .parent() - .map_or(path, |parent| path.strip_prefix(parent).unwrap()), - is_ignored, - ), - inode, - is_symlink, - is_ignored, - }, - ignore, - ))) + let metadata = match fs::metadata(&path) { + Err(err) => { + return match (err.kind(), err.raw_os_error()) { + (io::ErrorKind::NotFound, _) => Ok(None), + (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None), + _ => Err(anyhow::Error::new(err)), } } - Err(err) => match (err.kind(), err.raw_os_error()) { - (io::ErrorKind::NotFound, _) => Ok(None), - (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None), - _ => Err(anyhow::Error::new(err)), - }, + Ok(metadata) => metadata, + }; + + let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); + if metadata.is_dir() { + ignore = ignore.add_child(&path).unwrap(); } + let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); + let inode = metadata.ino(); + let is_symlink = fs::symlink_metadata(&path) + .context("failed to read symlink metadata")? + .file_type() + .is_symlink(); + let parent = if path == root_path { + None + } else { + Some( + fs::metadata(path.parent().unwrap()) + .context("failed to read parent inode")? + .ino(), + ) + }; + + let entry = if metadata.file_type().is_dir() { + Entry::Dir { + inode, + parent, + is_symlink, + is_ignored, + pending: true, + children: Arc::from([]), + } + } else { + Entry::File { + inode, + parent, + is_symlink, + is_ignored, + path: PathEntry::new( + inode, + root_path + .parent() + .map_or(path, |parent| path.strip_prefix(parent).unwrap()), + is_ignored, + ), + } + }; + + Ok(Some((entry, ignore))) } fn insert_entries(&self, entries: impl IntoIterator) { From 9fd8acdce7fd8a9199523238a6035917ef990379 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 20 Apr 2021 13:51:24 -0700 Subject: [PATCH 064/102] Use our own scoped_pool implementation --- Cargo.lock | 28 +----- Cargo.toml | 2 +- gpui/Cargo.toml | 2 +- gpui/src/app.rs | 2 +- scoped_pool/Cargo.toml | 8 ++ scoped_pool/src/lib.rs | 188 +++++++++++++++++++++++++++++++++++++++++ zed/src/worktree.rs | 12 +-- 7 files changed, 205 insertions(+), 37 deletions(-) create mode 100644 scoped_pool/Cargo.toml create mode 100644 scoped_pool/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4096564e92..06a6e41f5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,12 +448,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" - [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -1062,7 +1056,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" dependencies = [ - "scopeguard 1.1.0", + "scopeguard", ] [[package]] @@ -1714,13 +1708,9 @@ dependencies = [ [[package]] name = "scoped-pool" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817a3a15e704545ce59ed2b5c60a5d32bda4d7869befb8b36667b658a6c00b43" +version = "0.0.1" dependencies = [ - "crossbeam", - "scopeguard 0.1.2", - "variance", + "crossbeam-channel 0.5.0", ] [[package]] @@ -1729,12 +1719,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" -[[package]] -name = "scopeguard" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a076157c1e2dc561d8de585151ee6965d910dd4dcb5dabb7ae3e83981a6c57" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2138,12 +2122,6 @@ dependencies = [ "xmlwriter", ] -[[package]] -name = "variance" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3abfc2be1fb59663871379ea884fd81de80c496f2274e021c01d6fe56cd77b05" - [[package]] name = "vec-arena" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 1ca7597d18..c58e56b67a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["zed", "gpui", "fsevent"] +members = ["zed", "gpui", "fsevent", "scoped_pool"] [patch.crates-io] async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"} diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 0d6a2982ad..8c7c3bf4cb 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,7 +18,7 @@ postage = {version = "0.4.1", features = ["futures-traits"]} rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" -scoped-pool = "1.0.0" +scoped-pool = {path = "../scoped_pool"} seahash = "4.1" serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.64" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index d81dd8797f..7c396a51e7 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -411,7 +411,7 @@ impl MutableAppContext { windows: HashMap::new(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background: Arc::new(executor::Background::new()), - thread_pool: scoped_pool::Pool::new(num_cpus::get()), + thread_pool: scoped_pool::Pool::new(num_cpus::get(), "app"), }, actions: HashMap::new(), global_actions: HashMap::new(), diff --git a/scoped_pool/Cargo.toml b/scoped_pool/Cargo.toml new file mode 100644 index 0000000000..a2e5a1206f --- /dev/null +++ b/scoped_pool/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "scoped-pool" +version = "0.0.1" +license = "MIT" +edition = "2018" + +[dependencies] +crossbeam-channel = "0.5" diff --git a/scoped_pool/src/lib.rs b/scoped_pool/src/lib.rs new file mode 100644 index 0000000000..2ad56f2b65 --- /dev/null +++ b/scoped_pool/src/lib.rs @@ -0,0 +1,188 @@ +use crossbeam_channel as chan; +use std::{marker::PhantomData, mem::transmute, thread}; + +#[derive(Clone)] +pub struct Pool { + req_tx: chan::Sender, + thread_count: usize, +} + +pub struct Scope<'a> { + req_count: usize, + req_tx: chan::Sender, + resp_tx: chan::Sender<()>, + resp_rx: chan::Receiver<()>, + phantom: PhantomData<&'a ()>, +} + +struct Request { + callback: Box, + resp_tx: chan::Sender<()>, +} + +impl Pool { + pub fn new(thread_count: usize, name: &str) -> Self { + let (req_tx, req_rx) = chan::unbounded(); + for i in 0..thread_count { + thread::Builder::new() + .name(format!("scoped_pool {} {}", name, i)) + .spawn({ + let req_rx = req_rx.clone(); + move || loop { + match req_rx.recv() { + Err(_) => break, + Ok(Request { callback, resp_tx }) => { + callback(); + resp_tx.send(()).ok(); + } + } + } + }) + .expect("scoped_pool: failed to spawn thread"); + } + Self { + req_tx, + thread_count, + } + } + + pub fn thread_count(&self) -> usize { + self.thread_count + } + + pub fn scoped<'scope, F, R>(&self, scheduler: F) -> R + where + F: FnOnce(&mut Scope<'scope>) -> R, + { + let (resp_tx, resp_rx) = chan::bounded(1); + let mut scope = Scope { + resp_tx, + resp_rx, + req_count: 0, + phantom: PhantomData, + req_tx: self.req_tx.clone(), + }; + let result = scheduler(&mut scope); + scope.wait(); + result + } +} + +impl<'scope> Scope<'scope> { + pub fn execute(&mut self, callback: F) + where + F: FnOnce() + Send + 'scope, + { + // Transmute the callback's lifetime to be 'static. This is safe because in ::wait, + // we block until all the callbacks have been called and dropped. + let callback = unsafe { + transmute::, Box>( + Box::new(callback), + ) + }; + + self.req_count += 1; + self.req_tx + .send(Request { + callback, + resp_tx: self.resp_tx.clone(), + }) + .unwrap(); + } + + fn wait(&self) { + for _ in 0..self.req_count { + self.resp_rx.recv().unwrap(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + #[test] + fn test_execute() { + let pool = Pool::new(3, "test"); + + { + let vec = Mutex::new(Vec::new()); + pool.scoped(|scope| { + for _ in 0..3 { + scope.execute(|| { + for i in 0..5 { + vec.lock().unwrap().push(i); + } + }); + } + }); + + let mut vec = vec.into_inner().unwrap(); + vec.sort_unstable(); + assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) + } + } + + #[test] + fn test_clone_send_and_execute() { + let pool = Pool::new(3, "test"); + + let mut threads = Vec::new(); + for _ in 0..3 { + threads.push(thread::spawn({ + let pool = pool.clone(); + move || { + let vec = Mutex::new(Vec::new()); + pool.scoped(|scope| { + for _ in 0..3 { + scope.execute(|| { + for i in 0..5 { + vec.lock().unwrap().push(i); + } + }); + } + }); + let mut vec = vec.into_inner().unwrap(); + vec.sort_unstable(); + assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) + } + })); + } + + for thread in threads { + thread.join().unwrap(); + } + } + + #[test] + fn test_share_and_execute() { + let pool = Arc::new(Pool::new(3, "test")); + + let mut threads = Vec::new(); + for _ in 0..3 { + threads.push(thread::spawn({ + let pool = pool.clone(); + move || { + let vec = Mutex::new(Vec::new()); + pool.scoped(|scope| { + for _ in 0..3 { + scope.execute(|| { + for i in 0..5 { + vec.lock().unwrap().push(i); + } + }); + } + }); + let mut vec = vec.into_inner().unwrap(); + vec.sort_unstable(); + assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) + } + })); + } + + for thread in threads { + thread.join().unwrap(); + } + } +} diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 27abd4510a..a54b11ee8e 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -500,7 +500,7 @@ impl BackgroundScanner { Self { snapshot: Mutex::new(snapshot), notify, - thread_pool: scoped_pool::Pool::new(16), + thread_pool: scoped_pool::Pool::new(16, "background-scanner"), } } @@ -592,7 +592,7 @@ impl BackgroundScanner { drop(tx); let mut results = Vec::new(); - results.resize_with(self.thread_pool.workers(), || Ok(())); + results.resize_with(self.thread_pool.thread_count(), || Ok(())); self.thread_pool.scoped(|pool| { for result in &mut results { pool.execute(|| { @@ -762,7 +762,7 @@ impl BackgroundScanner { // Scan any directories that were created as part of this event batch. drop(scan_queue_tx); self.thread_pool.scoped(|pool| { - for _ in 0..self.thread_pool.workers() { + for _ in 0..self.thread_pool.thread_count() { pool.execute(|| { while let Ok(job) = scan_queue_rx.recv() { if let Err(err) = job.and_then(|job| self.scan_dir(job)) { @@ -844,12 +844,6 @@ impl BackgroundScanner { } } -impl Drop for BackgroundScanner { - fn drop(&mut self) { - self.thread_pool.shutdown(); - } -} - struct ScanJob { inode: u64, path: Arc, From b20f5e91392e655f291bd93270fbd00564f4a7fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 20 Apr 2021 15:55:29 -0700 Subject: [PATCH 065/102] Fully halt background scanner threads when dropping Worktree * Rework fsevent API to expose a handle for halting the event stream Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + fsevent/Cargo.toml | 1 + fsevent/examples/events.rs | 4 +- fsevent/src/lib.rs | 130 ++++++++++++++++++++++++++++++------- scoped_pool/src/lib.rs | 4 +- zed/src/worktree.rs | 86 ++++++++++++------------ 6 files changed, 158 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06a6e41f5a..8d6115fd1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,7 @@ version = "2.0.2" dependencies = [ "bitflags", "fsevent-sys", + "parking_lot", "tempdir", ] diff --git a/fsevent/Cargo.toml b/fsevent/Cargo.toml index cd7b2f0e7f..03b565cb9f 100644 --- a/fsevent/Cargo.toml +++ b/fsevent/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] bitflags = "1" fsevent-sys = "3.0.2" +parking_lot = "0.11.1" [dev-dependencies] tempdir = "0.3.7" diff --git a/fsevent/examples/events.rs b/fsevent/examples/events.rs index 7f064d9050..73ec6405d4 100644 --- a/fsevent/examples/events.rs +++ b/fsevent/examples/events.rs @@ -5,12 +5,12 @@ fn main() { let paths = args().skip(1).collect::>(); let paths = paths.iter().map(Path::new).collect::>(); assert!(paths.len() > 0, "Must pass 1 or more paths as arguments"); - let stream = EventStream::new(&paths, Duration::from_millis(100), |events| { + let (stream, _handle) = EventStream::new(&paths, Duration::from_millis(100)); + stream.run(|events| { eprintln!("event batch"); for event in events { eprintln!(" {:?}", event); } true }); - stream.run(); } diff --git a/fsevent/src/lib.rs b/fsevent/src/lib.rs index 38baaf2c84..c53be01686 100644 --- a/fsevent/src/lib.rs +++ b/fsevent/src/lib.rs @@ -2,12 +2,14 @@ use bitflags::bitflags; use fsevent_sys::{self as fs, core_foundation as cf}; +use parking_lot::Mutex; use std::{ convert::AsRef, ffi::{c_void, CStr, OsStr}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, slice, + sync::Arc, time::Duration, }; @@ -18,20 +20,29 @@ pub struct Event { pub path: PathBuf, } -pub struct EventStream { +pub struct EventStream { stream: fs::FSEventStreamRef, - _callback: Box, + state: Arc>, + callback: Box>, } -unsafe impl Send for EventStream {} +type RunCallback = Box) -> bool>; -impl EventStream -where - F: FnMut(Vec) -> bool, -{ - pub fn new(paths: &[&Path], latency: Duration, callback: F) -> Self { +enum Lifecycle { + New, + Running(cf::CFRunLoopRef), + Stopped, +} + +pub struct Handle(Arc>); + +unsafe impl Send for EventStream {} +unsafe impl Send for Lifecycle {} + +impl EventStream { + pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) { unsafe { - let callback = Box::new(callback); + let callback = Box::new(None); let stream_context = fs::FSEventStreamContext { version: 0, info: callback.as_ref() as *const _ as *mut c_void, @@ -71,20 +82,35 @@ where ); cf::CFRelease(cf_paths); - EventStream { - stream, - _callback: callback, - } + let state = Arc::new(Mutex::new(Lifecycle::New)); + + ( + EventStream { + stream, + state: state.clone(), + callback, + }, + Handle(state), + ) } } - pub fn run(self) { + pub fn run(mut self, f: F) + where + F: FnMut(Vec) -> bool + 'static, + { + *self.callback = Some(Box::new(f)); unsafe { - fs::FSEventStreamScheduleWithRunLoop( - self.stream, - cf::CFRunLoopGetCurrent(), - cf::kCFRunLoopDefaultMode, - ); + let run_loop = cf::CFRunLoopGetCurrent(); + { + let mut state = self.state.lock(); + match *state { + Lifecycle::New => *state = Lifecycle::Running(run_loop), + Lifecycle::Running(_) => unreachable!(), + Lifecycle::Stopped => return, + } + } + fs::FSEventStreamScheduleWithRunLoop(self.stream, run_loop, cf::kCFRunLoopDefaultMode); fs::FSEventStreamStart(self.stream); cf::CFRunLoopRun(); @@ -107,7 +133,11 @@ where let event_paths = event_paths as *const *const ::std::os::raw::c_char; let e_ptr = event_flags as *mut u32; let i_ptr = event_ids as *mut u64; - let callback = (info as *mut F).as_mut().unwrap(); + let callback = (info as *mut Option) + .as_mut() + .unwrap() + .as_mut() + .unwrap(); let paths = slice::from_raw_parts(event_paths, num); let flags = slice::from_raw_parts_mut(e_ptr, num); @@ -136,6 +166,18 @@ where } } +impl Drop for Handle { + fn drop(&mut self) { + let mut state = self.0.lock(); + if let Lifecycle::Running(run_loop) = *state { + unsafe { + cf::CFRunLoopStop(run_loop); + } + } + *state = Lifecycle::Stopped; + } +} + // Synchronize with // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h bitflags! { @@ -253,10 +295,8 @@ fn test_event_stream() { fs::write(path.join("a"), "a contents").unwrap(); let (tx, rx) = mpsc::channel(); - let stream = EventStream::new(&[&path], Duration::from_millis(50), move |events| { - tx.send(events.to_vec()).is_ok() - }); - std::thread::spawn(move || stream.run()); + let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50)); + std::thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok())); fs::write(path.join("b"), "b contents").unwrap(); let events = rx.recv_timeout(Duration::from_millis(500)).unwrap(); @@ -269,4 +309,46 @@ fn test_event_stream() { let event = events.last().unwrap(); assert_eq!(event.path, path.join("a")); assert!(event.flags.contains(StreamFlags::ITEM_REMOVED)); + drop(handle); +} + +#[test] +fn test_event_stream_shutdown() { + use std::{fs, sync::mpsc, time::Duration}; + use tempdir::TempDir; + + let dir = TempDir::new("test_observe").unwrap(); + let path = dir.path().canonicalize().unwrap(); + + let (tx, rx) = mpsc::channel(); + let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50)); + std::thread::spawn(move || { + stream.run({ + let tx = tx.clone(); + move |_| { + tx.send(()).unwrap(); + true + } + }); + tx.send(()).unwrap(); + }); + + fs::write(path.join("b"), "b contents").unwrap(); + rx.recv_timeout(Duration::from_millis(500)).unwrap(); + + drop(handle); + rx.recv_timeout(Duration::from_millis(500)).unwrap(); +} + +#[test] +fn test_event_stream_shutdown_before_run() { + use std::time::Duration; + use tempdir::TempDir; + + let dir = TempDir::new("test_observe").unwrap(); + let path = dir.path().canonicalize().unwrap(); + + let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50)); + drop(handle); + stream.run(|_| true); } diff --git a/scoped_pool/src/lib.rs b/scoped_pool/src/lib.rs index 2ad56f2b65..da4d193e07 100644 --- a/scoped_pool/src/lib.rs +++ b/scoped_pool/src/lib.rs @@ -21,11 +21,11 @@ struct Request { } impl Pool { - pub fn new(thread_count: usize, name: &str) -> Self { + pub fn new(thread_count: usize, name: impl AsRef) -> Self { let (req_tx, req_rx) = chan::unbounded(); for i in 0..thread_count { thread::Builder::new() - .name(format!("scoped_pool {} {}", name, i)) + .name(format!("scoped_pool {} {}", name.as_ref(), i)) .spawn({ let req_rx = req_rx.clone(); move || loop { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index a54b11ee8e..5d5e7b5742 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -37,8 +37,9 @@ enum ScanState { pub struct Worktree { snapshot: Snapshot, - scanner: Arc, + background_snapshot: Arc>, scan_state: (watch::Sender, watch::Receiver), + _event_stream_handle: fsevent::Handle, poll_scheduled: bool, } @@ -50,25 +51,33 @@ pub struct FileHandle { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { - let scan_state = smol::channel::unbounded(); + let (scan_state_tx, scan_state_rx) = smol::channel::unbounded(); + let id = ctx.model_id(); let snapshot = Snapshot { - id: ctx.model_id(), + id, path: path.into(), root_inode: None, entries: Default::default(), }; - let scanner = Arc::new(BackgroundScanner::new(snapshot.clone(), scan_state.0)); + let (event_stream, event_stream_handle) = + fsevent::EventStream::new(&[snapshot.path.as_ref()], Duration::from_millis(100)); + + let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); + let tree = Self { snapshot, - scanner, + background_snapshot: background_snapshot.clone(), scan_state: watch::channel_with(ScanState::Scanning), + _event_stream_handle: event_stream_handle, poll_scheduled: false, }; - let scanner = tree.scanner.clone(); - std::thread::spawn(move || scanner.run()); + std::thread::spawn(move || { + let scanner = BackgroundScanner::new(background_snapshot, scan_state_tx, id); + scanner.run(event_stream) + }); - ctx.spawn_stream(scan_state.1, Self::observe_scan_state, |_, _| {}) + ctx.spawn_stream(scan_state_rx, Self::observe_scan_state, |_, _| {}) .detach(); tree @@ -90,7 +99,7 @@ impl Worktree { } fn poll_entries(&mut self, ctx: &mut ModelContext) { - self.snapshot = self.scanner.snapshot(); + self.snapshot = self.background_snapshot.lock().clone(); ctx.notify(); if self.is_scanning() && !self.poll_scheduled { @@ -490,17 +499,17 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { } struct BackgroundScanner { - snapshot: Mutex, + snapshot: Arc>, notify: Sender, thread_pool: scoped_pool::Pool, } impl BackgroundScanner { - fn new(snapshot: Snapshot, notify: Sender) -> Self { + fn new(snapshot: Arc>, notify: Sender, worktree_id: usize) -> Self { Self { - snapshot: Mutex::new(snapshot), + snapshot, notify, - thread_pool: scoped_pool::Pool::new(16, "background-scanner"), + thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)), } } @@ -512,28 +521,7 @@ impl BackgroundScanner { self.snapshot.lock().clone() } - fn run(&self) { - let path = self.snapshot.lock().path.clone(); - - // Create the event stream before we start scanning to ensure we receive events for changes - // that occur in the middle of the scan. - let event_stream = - fsevent::EventStream::new(&[path.as_ref()], Duration::from_millis(100), |events| { - if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { - return false; - } - - if !self.process_events(events) { - return false; - } - - if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { - return false; - } - - true - }); - + fn run(self, event_stream: fsevent::EventStream) { if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return; } @@ -548,7 +536,21 @@ impl BackgroundScanner { return; } - event_stream.run(); + event_stream.run(move |events| { + if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { + return false; + } + + if !self.process_events(events) { + return false; + } + + if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { + return false; + } + + true + }); } fn scan_dirs(&self) -> io::Result<()> { @@ -945,6 +947,8 @@ mod tests { ); }) }); + + eprintln!("HI"); } #[test] @@ -1045,13 +1049,14 @@ mod tests { let (notify_tx, _notify_rx) = smol::channel::unbounded(); let scanner = BackgroundScanner::new( - Snapshot { + Arc::new(Mutex::new(Snapshot { id: 0, path: root_dir.path().into(), root_inode: None, entries: Default::default(), - }, + })), notify_tx, + 0, ); scanner.scan_dirs().unwrap(); @@ -1073,13 +1078,14 @@ mod tests { let (notify_tx, _notify_rx) = smol::channel::unbounded(); let new_scanner = BackgroundScanner::new( - Snapshot { + Arc::new(Mutex::new(Snapshot { id: 0, path: root_dir.path().into(), root_inode: None, entries: Default::default(), - }, + })), notify_tx, + 1, ); new_scanner.scan_dirs().unwrap(); assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); From fd8125b22c4509e468a1acc234a8401ee92fedd6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 21 Apr 2021 10:07:32 +0200 Subject: [PATCH 066/102] Re-enable file finder test and bind `menu:select-{next-prev}` --- zed/src/file_finder.rs | 52 ++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 065b32ebac..eac0b3d5fb 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -32,8 +32,8 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("file_finder:toggle", FileFinder::toggle); app.add_action("file_finder:confirm", FileFinder::confirm); app.add_action("file_finder:select", FileFinder::select); - app.add_action("buffer:move_up", FileFinder::select_prev); - app.add_action("buffer:move_down", FileFinder::select_next); + app.add_action("menu:select_prev", FileFinder::select_prev); + app.add_action("menu:select_next", FileFinder::select_next); app.add_action("uniform_list:scroll", FileFinder::scroll); app.add_bindings(vec![ @@ -445,32 +445,30 @@ mod tests { app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); + finder + .condition(&app, |finder, _| finder.matches.len() == 2) + .await; - // let view_state = finder.state(&app); - // assert!(view_state.matches.len() > 1); - // app.dispatch_action( - // window_id, - // vec![workspace_view.id(), finder.id()], - // "menu:select_next", - // (), - // ); - // app.dispatch_action( - // window_id, - // vec![workspace_view.id(), finder.id()], - // "file_finder:confirm", - // (), - // ); - // app.finish_pending_tasks().await; // Load Buffer and open BufferView. - // let active_pane = workspace_view.as_ref(app).active_pane().clone(); - // assert_eq!( - // active_pane.state(&app), - // pane::State { - // tabs: vec![pane::TabState { - // title: "bandana".into(), - // active: true, - // }] - // } - // ); + let active_pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); + app.dispatch_action( + window_id, + vec![workspace_view.id(), finder.id()], + "menu:select_next", + (), + ); + app.dispatch_action( + window_id, + vec![workspace_view.id(), finder.id()], + "file_finder:confirm", + (), + ); + active_pane + .condition(&app, |pane, _| pane.active_item().is_some()) + .await; + app.read(|ctx| { + let active_item = active_pane.read(ctx).active_item().unwrap(); + assert_eq!(active_item.title(ctx), "bandana"); + }); }); } } From 031d5ac7d4e8f538a0519805cfe2047339918e69 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 21 Apr 2021 16:58:17 +0200 Subject: [PATCH 067/102] WIP Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 56 +++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 5d5e7b5742..6e8f5c6dc7 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -299,45 +299,33 @@ impl Snapshot { self.entries.edit(edits); } - fn remove_entry(&mut self, path: &Path) { - let mut parent_entry = match path.parent().and_then(|p| self.entry_for_path(p).cloned()) { - Some(e) => e, - None => return, - }; + fn remove_subtree(&mut self, subtree_inode: u64) { + let entry = self.entries.get(&subtree_inode).unwrap(); let mut edits = Vec::new(); - let parent_inode = parent_entry.inode(); - let mut entry_inode = None; - if let Entry::Dir { children, .. } = &mut parent_entry { - let mut new_children = Vec::new(); - for (child_inode, child_name) in children.as_ref() { - if Some(child_name.as_ref()) == path.file_name() { - entry_inode = Some(*child_inode); - } else { - new_children.push((*child_inode, child_name.clone())); - } - } - if new_children.iter().any(|c| Some(c.0) == entry_inode) { - entry_inode = None; + // Update the parent entry to not include this subtree as one of its children. + if let Some(parent_inode) = entry.parent() { + let mut parent_entry = self.entries.get(&parent_inode).unwrap().clone(); + if let Entry::Dir { children, .. } = &mut parent_entry { + *children = children + .into_iter() + .filter(|(child_inode, _)| *child_inode != subtree_inode) + .cloned() + .collect::>() + .into(); + } else { + unreachable!("parent was not a directory"); } - - *children = new_children.into(); edits.push(Edit::Insert(parent_entry)); } - if let Some(entry_inode) = entry_inode { - let mut descendant_stack = Vec::new(); - descendant_stack.push((entry_inode, parent_inode)); - while let Some((inode, parent_inode)) = descendant_stack.pop() { - if let Some(entry) = self.entries.get(&inode) { - if entry.parent() == Some(parent_inode) { - edits.push(Edit::Remove(inode)); - if let Entry::Dir { children, .. } = entry { - descendant_stack.extend(children.iter().map(|c| (c.0, entry.inode()))); - } - } - } + // Remove all descendant entries for this subtree. + let mut stack = vec![subtree_inode]; + while let Some(inode) = stack.pop() { + edits.push(Edit::Remove(inode)); + if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { + stack.extend(children.iter().map(|(child_inode, _)| *child_inode)); } } @@ -729,7 +717,9 @@ impl BackgroundScanner { paths.next(); } - snapshot.remove_entry(&relative_path); + if let Some(inode) = snapshot.inode_for_path(&relative_path) { + snapshot.remove_subtree(inode); + } match self.fs_entry_for_path(&root_path, &path) { Ok(Some((fs_entry, ignore))) => { From 56b48c822997078732f57f5d666bae0ae0878bba Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Apr 2021 12:08:09 -0600 Subject: [PATCH 068/102] WIP: Establish new invariant-maintaining API for mutating worktree snapshots Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 141 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6e8f5c6dc7..781770c22b 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -17,6 +17,7 @@ use postage::{ }; use smol::{channel::Sender, Timer}; use std::{ + collections::{HashMap, HashSet}, ffi::OsStr, fmt, fs, future::Future, @@ -277,7 +278,143 @@ impl Snapshot { Ok(components.into_iter().rev().collect()) } - fn insert_entry(&mut self, path: &Path, entry: Entry) { + fn insert_entry(&mut self, name: Option<&OsStr>, entry: Entry) { + let mut edits = Vec::new(); + + if let Some(old_entry) = self.entries.get(&entry.inode()) { + // If the entry's parent changed, remove the entry from the old parent's children. + if old_entry.parent() != entry.parent() { + if let Some(old_parent_inode) = old_entry.parent() { + let old_parent = self.entries.get(&old_parent_inode).unwrap().clone(); + self.remove_children(old_parent, &mut edits, |inode| inode == entry.inode()); + } + } + + // Remove all descendants of the old version of the entry being inserted. + self.clear_descendants(entry.inode(), &mut edits); + } + + // Insert the entry in its new parent with the correct name. + if let Some(new_parent_inode) = entry.parent() { + let mut new_parent = self.entries.get(&new_parent_inode).unwrap().clone(); + if let Entry::Dir { children, .. } = &mut new_parent { + *children = children + .iter() + .filter(|(inode, _)| *inode != entry.inode()) + .cloned() + .chain(Some((entry.inode(), name.unwrap().into()))) + .collect::>() + .into(); + } else { + unreachable!("non-directory parent"); + } + edits.push(Edit::Insert(new_parent)); + } + + // Insert the entry itself. + edits.push(Edit::Insert(entry)); + + self.entries.edit(edits); + } + + fn populate_dir<'a>( + &mut self, + parent_inode: u64, + children: impl IntoIterator, + ) { + let mut edits = Vec::new(); + + self.clear_descendants(parent_inode, &mut edits); + + // Determine which children are being re-parented and populate array of new children to + // assign to the parent. + let mut new_children = Vec::new(); + let mut old_children = HashMap::>::new(); + for (name, child) in children.into_iter() { + new_children.push((child.inode(), name.into())); + if let Some(old_child) = self.entries.get(&child.inode()) { + if let Some(old_parent_inode) = old_child.parent() { + if old_parent_inode != parent_inode { + old_children + .entry(old_parent_inode) + .or_default() + .insert(child.inode()); + } + } + } + } + + // Replace the parent with a clone that includes the children and isn't pending + let mut parent = self.entries.get(&parent_inode).unwrap().clone(); + if let Entry::Dir { + children, pending, .. + } = &mut parent + { + *children = new_children.into(); + *pending = false; + } else { + unreachable!("non-directory parent"); + } + edits.push(Edit::Insert(parent)); + + // For any children that were re-parented, remove them from their old parents + for (parent_inode, to_remove) in old_children { + let mut parent = self.entries.get(&parent_inode).unwrap().clone(); + self.remove_children(parent, &mut edits, |inode| to_remove.contains(&inode)); + } + + self.entries.edit(edits); + } + + fn remove_path(&mut self, path: &Path) { + if let Some(entry) = self.entry_for_path(path).cloned() { + let mut edits = Vec::new(); + + self.clear_descendants(entry.inode(), &mut edits); + + if let Some(parent_inode) = entry.parent() { + let parent = self.entries.get(&parent_inode).unwrap().clone(); + self.remove_children(parent, &mut edits, |inode| inode == entry.inode()); + } + + edits.push(Edit::Remove(entry.inode())); + + self.entries.edit(edits); + } + } + + fn clear_descendants(&mut self, mut inode: u64, edits: &mut Vec>) { + let mut stack = vec![inode]; + while let Some(inode) = stack.pop() { + if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { + for (child_inode, _) in children.iter() { + edits.push(Edit::Remove(*child_inode)); + stack.push(*child_inode); + } + } + } + } + + fn remove_children( + &mut self, + mut parent: Entry, + edits: &mut Vec>, + predicate: impl Fn(u64) -> bool, + ) { + if let Entry::Dir { children, .. } = &mut parent { + *children = children + .iter() + .filter(|(inode, _)| !predicate(*inode)) + .cloned() + .collect::>() + .into(); + } else { + unreachable!("non-directory parent"); + } + edits.push(Edit::Insert(parent)); + } + + fn insert_entry_old(&mut self, path: &Path, entry: Entry) { let mut edits = Vec::new(); edits.push(Edit::Insert(entry.clone())); if let Some(parent) = entry.parent() { @@ -723,7 +860,7 @@ impl BackgroundScanner { match self.fs_entry_for_path(&root_path, &path) { Ok(Some((fs_entry, ignore))) => { - snapshot.insert_entry(&path, fs_entry.clone()); + snapshot.insert_entry_old(&path, fs_entry.clone()); if fs_entry.is_dir() { scan_queue_tx From a898acf6b591a3ba1444279440a2b1292116120c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Apr 2021 12:29:16 -0600 Subject: [PATCH 069/102] WIP Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 195 +++++++++++++++----------------------------- 1 file changed, 64 insertions(+), 131 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 781770c22b..63a84f8367 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -317,10 +317,10 @@ impl Snapshot { self.entries.edit(edits); } - fn populate_dir<'a>( + fn populate_dir( &mut self, parent_inode: u64, - children: impl IntoIterator, + children: impl IntoIterator, Entry)>, ) { let mut edits = Vec::new(); @@ -359,7 +359,7 @@ impl Snapshot { // For any children that were re-parented, remove them from their old parents for (parent_inode, to_remove) in old_children { - let mut parent = self.entries.get(&parent_inode).unwrap().clone(); + let parent = self.entries.get(&parent_inode).unwrap().clone(); self.remove_children(parent, &mut edits, |inode| to_remove.contains(&inode)); } @@ -383,7 +383,7 @@ impl Snapshot { } } - fn clear_descendants(&mut self, mut inode: u64, edits: &mut Vec>) { + fn clear_descendants(&mut self, inode: u64, edits: &mut Vec>) { let mut stack = vec![inode]; while let Some(inode) = stack.pop() { if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { @@ -414,61 +414,6 @@ impl Snapshot { edits.push(Edit::Insert(parent)); } - fn insert_entry_old(&mut self, path: &Path, entry: Entry) { - let mut edits = Vec::new(); - edits.push(Edit::Insert(entry.clone())); - if let Some(parent) = entry.parent() { - if let Some(mut parent_entry) = self.entries.get(&parent).cloned() { - if let Entry::Dir { children, .. } = &mut parent_entry { - let name = Arc::from(path.file_name().unwrap()); - *children = children - .into_iter() - .cloned() - .chain(Some((entry.inode(), name))) - .collect::>() - .into(); - edits.push(Edit::Insert(parent_entry)); - } else { - unreachable!(); - } - } - } - self.entries.edit(edits); - } - - fn remove_subtree(&mut self, subtree_inode: u64) { - let entry = self.entries.get(&subtree_inode).unwrap(); - - let mut edits = Vec::new(); - - // Update the parent entry to not include this subtree as one of its children. - if let Some(parent_inode) = entry.parent() { - let mut parent_entry = self.entries.get(&parent_inode).unwrap().clone(); - if let Entry::Dir { children, .. } = &mut parent_entry { - *children = children - .into_iter() - .filter(|(child_inode, _)| *child_inode != subtree_inode) - .cloned() - .collect::>() - .into(); - } else { - unreachable!("parent was not a directory"); - } - edits.push(Edit::Insert(parent_entry)); - } - - // Remove all descendant entries for this subtree. - let mut stack = vec![subtree_inode]; - while let Some(inode) = stack.pop() { - edits.push(Edit::Remove(inode)); - if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { - stack.extend(children.iter().map(|(child_inode, _)| *child_inode)); - } - } - - self.entries.edit(edits); - } - fn fmt_entry( &self, f: &mut fmt::Formatter<'_>, @@ -702,8 +647,12 @@ impl BackgroundScanner { children: Arc::from([]), pending: true, }; - self.insert_entries(Some(dir_entry.clone())); - self.snapshot.lock().root_inode = Some(inode); + + { + let mut snapshot = self.snapshot.lock(); + snapshot.insert_entry(None, dir_entry); + snapshot.root_inode = Some(inode); + } let (tx, rx) = crossbeam_channel::unbounded(); @@ -711,7 +660,6 @@ impl BackgroundScanner { inode, path: path.clone(), relative_path, - dir_entry, ignore: Some(ignore), scan_queue: tx.clone(), })) @@ -735,14 +683,18 @@ impl BackgroundScanner { }); results.into_iter().collect::>()?; } else { - self.insert_entries(Some(Entry::File { - parent: None, - path: PathEntry::new(inode, &relative_path, is_ignored), - inode, - is_symlink, - is_ignored, - })); - self.snapshot.lock().root_inode = Some(inode); + let mut snapshot = self.snapshot.lock(); + snapshot.insert_entry( + None, + Entry::File { + parent: None, + path: PathEntry::new(inode, &relative_path, is_ignored), + inode, + is_symlink, + is_ignored, + }, + ); + snapshot.root_inode = Some(inode); } Ok(()) @@ -750,49 +702,47 @@ impl BackgroundScanner { fn scan_dir(&self, job: ScanJob) -> io::Result<()> { let scan_queue = job.scan_queue; - let mut dir_entry = job.dir_entry; - let mut new_children = Vec::new(); let mut new_entries = Vec::new(); let mut new_jobs = Vec::new(); for child_entry in fs::read_dir(&job.path)? { let child_entry = child_entry?; - let name: Arc = child_entry.file_name().into(); - let relative_path = job.relative_path.join(name.as_ref()); - let metadata = child_entry.metadata()?; - let ino = metadata.ino(); - let is_symlink = metadata.file_type().is_symlink(); - let path = job.path.join(name.as_ref()); + let child_name: Arc = child_entry.file_name().into(); + let child_relative_path = job.relative_path.join(child_name.as_ref()); + let child_metadata = child_entry.metadata()?; + let child_inode = child_metadata.ino(); + let child_is_symlink = child_metadata.file_type().is_symlink(); + let child_path = job.path.join(child_name.as_ref()); - new_children.push((ino, name.clone())); - if metadata.is_dir() { + if child_metadata.is_dir() { let mut is_ignored = true; let mut ignore = None; if let Some(parent_ignore) = job.ignore.as_ref() { - let child_ignore = parent_ignore.add_child(&path).unwrap(); - is_ignored = - child_ignore.matched(&path, true).is_ignore() || name.as_ref() == ".git"; + let child_ignore = parent_ignore.add_child(&child_path).unwrap(); + is_ignored = child_ignore.matched(&child_path, true).is_ignore() + || child_name.as_ref() == ".git"; if !is_ignored { ignore = Some(child_ignore); } } - let dir_entry = Entry::Dir { - parent: Some(job.inode), - inode: ino, - is_symlink, - is_ignored, - children: Arc::from([]), - pending: true, - }; - new_entries.push(dir_entry.clone()); + new_entries.push(( + child_name, + Entry::Dir { + parent: Some(job.inode), + inode: child_inode, + is_symlink: child_is_symlink, + is_ignored, + children: Arc::from([]), + pending: true, + }, + )); new_jobs.push(ScanJob { - inode: ino, - path: Arc::from(path), - relative_path, - dir_entry, + inode: child_inode, + path: Arc::from(child_path), + relative_path: child_relative_path, ignore, scan_queue: scan_queue.clone(), }); @@ -800,29 +750,21 @@ impl BackgroundScanner { let is_ignored = job .ignore .as_ref() - .map_or(true, |i| i.matched(&path, false).is_ignore()); - new_entries.push(Entry::File { - parent: Some(job.inode), - path: PathEntry::new(ino, &relative_path, is_ignored), - inode: ino, - is_symlink, - is_ignored, - }); + .map_or(true, |i| i.matched(&child_path, false).is_ignore()); + new_entries.push(( + child_name, + Entry::File { + parent: Some(job.inode), + path: PathEntry::new(child_inode, &child_relative_path, is_ignored), + inode: child_inode, + is_symlink: child_is_symlink, + is_ignored, + }, + )); }; } - if let Entry::Dir { - children, pending, .. - } = &mut dir_entry - { - *children = Arc::from(new_children); - *pending = false; - } else { - unreachable!() - } - new_entries.push(dir_entry); - - self.insert_entries(new_entries); + self.snapshot.lock().populate_dir(job.inode, new_entries); for new_job in new_jobs { scan_queue.send(Ok(new_job)).unwrap(); } @@ -854,24 +796,23 @@ impl BackgroundScanner { paths.next(); } - if let Some(inode) = snapshot.inode_for_path(&relative_path) { - snapshot.remove_subtree(inode); - } + snapshot.remove_path(&relative_path); match self.fs_entry_for_path(&root_path, &path) { Ok(Some((fs_entry, ignore))) => { - snapshot.insert_entry_old(&path, fs_entry.clone()); + let is_dir = fs_entry.is_dir(); + let inode = fs_entry.inode(); - if fs_entry.is_dir() { + snapshot.insert_entry(path.file_name(), fs_entry); + if is_dir { scan_queue_tx .send(Ok(ScanJob { - inode: fs_entry.inode(), + inode, path: Arc::from(path), relative_path: snapshot .root_name() .map_or(PathBuf::new(), PathBuf::from) .join(relative_path), - dir_entry: fs_entry, ignore: Some(ignore), scan_queue: scan_queue_tx.clone(), })) @@ -964,20 +905,12 @@ impl BackgroundScanner { Ok(Some((entry, ignore))) } - - fn insert_entries(&self, entries: impl IntoIterator) { - self.snapshot - .lock() - .entries - .edit(entries.into_iter().map(Edit::Insert).collect::>()); - } } struct ScanJob { inode: u64, path: Arc, relative_path: PathBuf, - dir_entry: Entry, ignore: Option, scan_queue: crossbeam_channel::Sender>, } From ce28dc39ffc903843d163cefba13acbfd3f579c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Apr 2021 11:39:50 -0700 Subject: [PATCH 070/102] Fix missing insert in populate_dir Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 63a84f8367..26e9d5788e 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -342,6 +342,7 @@ impl Snapshot { } } } + edits.push(Edit::Insert(child)); } // Replace the parent with a clone that includes the children and isn't pending From 955268e2a6163d2cd1432993eeb837deb894c1de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Apr 2021 11:46:49 -0700 Subject: [PATCH 071/102] Check worktree invariants in randomized test Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 26e9d5788e..8680a9f867 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1149,6 +1149,7 @@ mod tests { 1, ); new_scanner.scan_dirs().unwrap(); + scanner.snapshot().check_invariants(); assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); } } @@ -1268,6 +1269,13 @@ mod tests { } impl Snapshot { + fn check_invariants(&self) { + for entry in self.entries.items() { + let path = self.path_for_inode(entry.inode(), false).unwrap(); + assert_eq!(self.inode_for_path(path).unwrap(), entry.inode()); + } + } + fn to_vec(&self) -> Vec<(PathBuf, u64)> { use std::iter::FromIterator; @@ -1291,6 +1299,7 @@ mod tests { paths.push((computed_path, inode)); } + assert_eq!(paths.len(), self.entries.items().len()); paths.sort_by(|a, b| a.0.cmp(&b.0)); paths } From 8e0ca2056e97045af3693b649fde41c26b07ee4d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Apr 2021 12:05:34 -0700 Subject: [PATCH 072/102] Store paths as strings on PathMatch structs Co-Authored-By: Nathan Sobo --- zed/src/file_finder.rs | 11 ++++------- zed/src/worktree/fuzzy.rs | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index eac0b3d5fb..d959cae312 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -14,7 +14,7 @@ use gpui::{ AppContext, Axis, Border, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use std::cmp; +use std::{cmp, path::Path}; pub struct FileFinder { handle: WeakViewHandle, @@ -139,17 +139,14 @@ impl FileFinder { let tree_id = path_match.tree_id; let entry_id = path_match.entry_id; - self.worktree(tree_id, app).map(|tree| { - let path = tree - .path_for_inode(entry_id, path_match.include_root) - .unwrap(); - let file_name = path + self.worktree(tree_id, app).map(|_| { + let path = &path_match.path; + let file_name = Path::new(path) .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); - let path = path.to_string_lossy().to_string(); let path_positions = path_match.positions.clone(); let file_name_start = path.chars().count() - file_name.chars().count(); let mut file_name_positions = Vec::new(); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 55efdfe737..dbb2985fac 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -45,9 +45,9 @@ impl PathEntry { pub struct PathMatch { pub score: f64, pub positions: Vec, + pub path: String, pub tree_id: usize, pub entry_id: u64, - pub include_root: bool, } impl PartialEq for PathMatch { @@ -234,9 +234,9 @@ fn match_single_tree_paths<'a>( results.push(Reverse(PathMatch { tree_id, entry_id: path_entry.ino, + path: path_entry.path.iter().skip(skipped_prefix_len).collect(), score, positions: match_positions.clone(), - include_root: skipped_prefix_len == 0, })); if results.len() == max_results { *min_score = results.peek().unwrap().0.score; From 427930cd8a70db8dea23be505657f3d35a133596 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Apr 2021 12:30:30 -0700 Subject: [PATCH 073/102] wip - gitignore handling on rescan --- zed/src/worktree.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 8680a9f867..b4b798e7e8 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -94,6 +94,21 @@ impl Worktree { } } + pub fn next_scan_complete(&self) -> impl Future { + let mut scan_state_rx = self.scan_state.1.clone(); + let mut did_scan = matches!(*scan_state_rx.borrow(), ScanState::Scanning); + async move { + loop { + if let ScanState::Scanning = *scan_state_rx.borrow() { + did_scan = true; + } else if did_scan { + break; + } + scan_state_rx.recv().await; + } + } + } + fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { let _ = self.scan_state.0.blocking_send(scan_state); self.poll_entries(ctx); @@ -1081,6 +1096,53 @@ mod tests { }); } + #[test] + fn test_rescan_with_gitignore() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + ".git": {}, + ".gitignore": "ignored-dir\n", + "tracked-dir": { + "tracked-file1": "tracked contents", + }, + "ignored-dir": { + "ignored-file1": "ignored contents", + } + })); + + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| { + let tree = tree.read(ctx); + assert!(!tree + .entry_for_path("tracked-dir/tracked-file1") + .unwrap() + .is_ignored()); + assert!(tree + .entry_for_path("ignored-dir/ignored-file1") + .unwrap() + .is_ignored()); + }); + + fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); + fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); + // tree.condition(&app, move |_, ctx| file2.path(ctx) == Path::new("d/file2")) + // .await; + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| { + let tree = tree.read(ctx); + assert!(!tree + .entry_for_path("tracked-dir/tracked-file2") + .unwrap() + .is_ignored()); + assert!(tree + .entry_for_path("ignored-dir/ignored-file2") + .unwrap() + .is_ignored()); + }); + }); + } + #[test] fn test_random() { let iterations = env::var("ITERATIONS") @@ -1268,6 +1330,14 @@ mod tests { .collect() } + impl Entry { + fn is_ignored(&self) -> bool { + match self { + Entry::Dir { is_ignored, .. } | Entry::File { is_ignored, .. } => *is_ignored, + } + } + } + impl Snapshot { fn check_invariants(&self) { for entry in self.entries.items() { From 499e55e9501d354221755af8fc2d5e21fa44fb37 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Apr 2021 18:11:52 -0700 Subject: [PATCH 074/102] Start work on handling changes to gitignore files * Use the published ignore crate * Store ignore objects on snapshot, and use them to compute files' ignored status dynamically, instead of storing the ignored status on the file. --- Cargo.lock | 50 +++------- zed/Cargo.toml | 2 +- zed/src/worktree.rs | 187 ++++++++++++++++++++------------------ zed/src/worktree/fuzzy.rs | 25 +++-- 4 files changed, 126 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d6115fd1e..b8b7d9ad22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,16 +448,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam-channel" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" -dependencies = [ - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - [[package]] name = "crossbeam-channel" version = "0.5.0" @@ -465,7 +455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.2", + "crossbeam-utils", ] [[package]] @@ -475,18 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.2", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "crossbeam-utils", ] [[package]] @@ -891,8 +870,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "globset" -version = "0.4.4" -source = "git+https://github.com/zed-industries/ripgrep?rev=1d152118f35b3e3590216709b86277062d79b8a0#1d152118f35b3e3590216709b86277062d79b8a0" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" dependencies = [ "aho-corasick", "bstr", @@ -967,11 +947,11 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "ignore" -version = "0.4.11" -source = "git+https://github.com/zed-industries/ripgrep?rev=1d152118f35b3e3590216709b86277062d79b8a0#1d152118f35b3e3590216709b86277062d79b8a0" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" dependencies = [ - "crossbeam-channel 0.4.4", - "crossbeam-utils 0.7.2", + "crossbeam-utils", "globset", "lazy_static", "log", @@ -1095,12 +1075,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.3.4" @@ -1616,7 +1590,7 @@ dependencies = [ "base64", "blake2b_simd", "constant_time_eq", - "crossbeam-utils 0.8.2", + "crossbeam-utils", ] [[package]] @@ -1711,7 +1685,7 @@ dependencies = [ name = "scoped-pool" version = "0.0.1" dependencies = [ - "crossbeam-channel 0.5.0", + "crossbeam-channel", ] [[package]] @@ -2246,7 +2220,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec", - "crossbeam-channel 0.5.0", + "crossbeam-channel", "ctor", "dirs", "easy-parallel", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index fde1cfdbbe..c38db66d0f 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -22,7 +22,7 @@ easy-parallel = "3.1.0" fsevent = {path = "../fsevent"} futures-core = "0.3" gpui = {path = "../gpui"} -ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"} +ignore = "0.4" lazy_static = "1.4.0" libc = "0.2" log = "0.4" diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index b4b798e7e8..2ce2af9305 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -9,7 +9,7 @@ use anyhow::{anyhow, Context, Result}; use fuzzy::PathEntry; pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; -use ignore::dir::{Ignore, IgnoreBuilder}; +use ignore::gitignore::Gitignore; use parking_lot::Mutex; use postage::{ prelude::{Sink, Stream}, @@ -29,6 +29,8 @@ use std::{ time::Duration, }; +const GITIGNORE: &'static str = ".gitignore"; + #[derive(Clone, Debug)] enum ScanState { Idle, @@ -58,6 +60,7 @@ impl Worktree { id, path: path.into(), root_inode: None, + ignores: Default::default(), entries: Default::default(), }; let (event_stream, event_stream_handle) = @@ -229,6 +232,7 @@ pub struct Snapshot { id: usize, path: Arc, root_inode: Option, + ignores: HashMap, entries: SumTree, } @@ -268,6 +272,48 @@ impl Snapshot { }) } + pub fn is_path_ignored(&self, path: impl AsRef) -> Result { + 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 { + let mut components = Vec::new(); + let mut relative_path = PathBuf::new(); + let mut entry = self + .entries + .get(&inode) + .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; + while let Some(parent) = entry.parent() { + let parent_entry = self.entries.get(&parent).unwrap(); + if let Entry::Dir { children, .. } = parent_entry { + let (_, child_name) = children + .iter() + .find(|(child_inode, _)| *child_inode == inode) + .unwrap(); + components.push(child_name.as_ref()); + inode = parent; + + 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()) { + ignore::Match::Whitelist(_) => return Ok(false), + ignore::Match::Ignore(_) => return Ok(true), + ignore::Match::None => {} + } + } + } else { + unreachable!(); + } + entry = parent_entry; + } + Ok(false) + } + pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result { let mut components = Vec::new(); let mut entry = self @@ -311,13 +357,18 @@ impl Snapshot { // Insert the entry in its new parent with the correct name. if let Some(new_parent_inode) = entry.parent() { + let name = name.unwrap(); + if name == GITIGNORE { + self.insert_ignore_file(new_parent_inode); + } + let mut new_parent = self.entries.get(&new_parent_inode).unwrap().clone(); if let Entry::Dir { children, .. } = &mut new_parent { *children = children .iter() .filter(|(inode, _)| *inode != entry.inode()) .cloned() - .chain(Some((entry.inode(), name.unwrap().into()))) + .chain(Some((entry.inode(), name.into()))) .collect::>() .into(); } else { @@ -346,6 +397,10 @@ impl Snapshot { let mut new_children = Vec::new(); let mut old_children = HashMap::>::new(); for (name, child) in children.into_iter() { + if *name == *GITIGNORE { + self.insert_ignore_file(parent_inode); + } + new_children.push((child.inode(), name.into())); if let Some(old_child) = self.entries.get(&child.inode()) { if let Some(old_parent_inode) = old_child.parent() { @@ -389,6 +444,11 @@ impl Snapshot { self.clear_descendants(entry.inode(), &mut edits); if let Some(parent_inode) = entry.parent() { + if let Some(file_name) = path.file_name() { + if file_name == GITIGNORE { + self.remove_ignore_file(parent_inode); + } + } let parent = self.entries.get(&parent_inode).unwrap().clone(); self.remove_children(parent, &mut edits, |inode| inode == entry.inode()); } @@ -402,12 +462,19 @@ impl Snapshot { fn clear_descendants(&mut self, inode: u64, edits: &mut Vec>) { let mut stack = vec![inode]; while let Some(inode) = stack.pop() { + let mut has_gitignore = false; if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { - for (child_inode, _) in children.iter() { + for (child_inode, child_name) in children.iter() { + if **child_name == *GITIGNORE { + has_gitignore = true; + } edits.push(Edit::Remove(*child_inode)); stack.push(*child_inode); } } + if has_gitignore { + self.remove_ignore_file(inode); + } } } @@ -430,6 +497,22 @@ impl Snapshot { edits.push(Edit::Insert(parent)); } + fn insert_ignore_file(&mut self, dir_inode: u64) { + let mut path = self.path.to_path_buf(); + path.push(self.path_for_inode(dir_inode, false).unwrap()); + path.push(GITIGNORE); + let (ignore, err) = Gitignore::new(&path); + if let Some(err) = err { + log::info!("error in ignore file {:?} - {:?}", path, err); + } + + self.ignores.insert(dir_inode, ignore); + } + + fn remove_ignore_file(&mut self, dir_inode: u64) { + self.ignores.remove(&dir_inode); + } + fn fmt_entry( &self, f: &mut fmt::Formatter<'_>, @@ -500,7 +583,6 @@ pub enum Entry { inode: u64, parent: Option, is_symlink: bool, - is_ignored: bool, children: Arc<[(u64, Arc)]>, pending: bool, }, @@ -508,7 +590,6 @@ pub enum Entry { inode: u64, parent: Option, is_symlink: bool, - is_ignored: bool, path: PathEntry, }, } @@ -647,19 +728,11 @@ impl BackgroundScanner { let name = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); let relative_path = PathBuf::from(&name); - let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - if metadata.is_dir() { - ignore = ignore.add_child(&path).unwrap(); - } - let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); - if metadata.file_type().is_dir() { - let is_ignored = is_ignored || name.as_ref() == ".git"; let dir_entry = Entry::Dir { parent: None, inode, is_symlink, - is_ignored, children: Arc::from([]), pending: true, }; @@ -676,7 +749,6 @@ impl BackgroundScanner { inode, path: path.clone(), relative_path, - ignore: Some(ignore), scan_queue: tx.clone(), })) .unwrap(); @@ -704,10 +776,9 @@ impl BackgroundScanner { None, Entry::File { parent: None, - path: PathEntry::new(inode, &relative_path, is_ignored), + path: PathEntry::new(inode, &relative_path), inode, is_symlink, - is_ignored, }, ); snapshot.root_inode = Some(inode); @@ -732,25 +803,12 @@ impl BackgroundScanner { let child_path = job.path.join(child_name.as_ref()); if child_metadata.is_dir() { - let mut is_ignored = true; - let mut ignore = None; - - if let Some(parent_ignore) = job.ignore.as_ref() { - let child_ignore = parent_ignore.add_child(&child_path).unwrap(); - is_ignored = child_ignore.matched(&child_path, true).is_ignore() - || child_name.as_ref() == ".git"; - if !is_ignored { - ignore = Some(child_ignore); - } - } - new_entries.push(( child_name, Entry::Dir { parent: Some(job.inode), inode: child_inode, is_symlink: child_is_symlink, - is_ignored, children: Arc::from([]), pending: true, }, @@ -759,22 +817,16 @@ impl BackgroundScanner { inode: child_inode, path: Arc::from(child_path), relative_path: child_relative_path, - ignore, scan_queue: scan_queue.clone(), }); } else { - let is_ignored = job - .ignore - .as_ref() - .map_or(true, |i| i.matched(&child_path, false).is_ignore()); new_entries.push(( child_name, Entry::File { parent: Some(job.inode), - path: PathEntry::new(child_inode, &child_relative_path, is_ignored), + path: PathEntry::new(child_inode, &child_relative_path), inode: child_inode, is_symlink: child_is_symlink, - is_ignored, }, )); }; @@ -815,7 +867,7 @@ impl BackgroundScanner { snapshot.remove_path(&relative_path); match self.fs_entry_for_path(&root_path, &path) { - Ok(Some((fs_entry, ignore))) => { + Ok(Some(fs_entry)) => { let is_dir = fs_entry.is_dir(); let inode = fs_entry.inode(); @@ -829,7 +881,6 @@ impl BackgroundScanner { .root_name() .map_or(PathBuf::new(), PathBuf::from) .join(relative_path), - ignore: Some(ignore), scan_queue: scan_queue_tx.clone(), })) .unwrap(); @@ -862,7 +913,7 @@ impl BackgroundScanner { true } - fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { + fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { let metadata = match fs::metadata(&path) { Err(err) => { return match (err.kind(), err.raw_os_error()) { @@ -874,11 +925,6 @@ impl BackgroundScanner { Ok(metadata) => metadata, }; - let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap(); - if metadata.is_dir() { - ignore = ignore.add_child(&path).unwrap(); - } - let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore(); let inode = metadata.ino(); let is_symlink = fs::symlink_metadata(&path) .context("failed to read symlink metadata")? @@ -899,7 +945,6 @@ impl BackgroundScanner { inode, parent, is_symlink, - is_ignored, pending: true, children: Arc::from([]), } @@ -908,18 +953,16 @@ impl BackgroundScanner { inode, parent, is_symlink, - is_ignored, path: PathEntry::new( inode, root_path .parent() .map_or(path, |parent| path.strip_prefix(parent).unwrap()), - is_ignored, ), } }; - Ok(Some((entry, ignore))) + Ok(Some(entry)) } } @@ -927,7 +970,6 @@ struct ScanJob { inode: u64, path: Arc, relative_path: PathBuf, - ignore: Option, scan_queue: crossbeam_channel::Sender>, } @@ -948,19 +990,6 @@ impl WorktreeHandle for ModelHandle { } } -trait UnwrapIgnoreTuple { - fn unwrap(self) -> Ignore; -} - -impl UnwrapIgnoreTuple for (Ignore, Option) { - fn unwrap(self) -> Ignore { - if let Some(error) = self.1 { - log::error!("error loading gitignore data: {}", error); - } - self.0 - } -} - #[cfg(test)] mod tests { use super::*; @@ -1114,31 +1143,17 @@ mod tests { app.read(|ctx| tree.read(ctx).scan_complete()).await; app.read(|ctx| { let tree = tree.read(ctx); - assert!(!tree - .entry_for_path("tracked-dir/tracked-file1") - .unwrap() - .is_ignored()); - assert!(tree - .entry_for_path("ignored-dir/ignored-file1") - .unwrap() - .is_ignored()); + assert!(!tree.is_path_ignored("tracked-dir/tracked-file1").unwrap()); + assert!(tree.is_path_ignored("ignored-dir/ignored-file1").unwrap()); }); fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); - // tree.condition(&app, move |_, ctx| file2.path(ctx) == Path::new("d/file2")) - // .await; - app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| tree.read(ctx).next_scan_complete()).await; app.read(|ctx| { let tree = tree.read(ctx); - assert!(!tree - .entry_for_path("tracked-dir/tracked-file2") - .unwrap() - .is_ignored()); - assert!(tree - .entry_for_path("ignored-dir/ignored-file2") - .unwrap() - .is_ignored()); + assert!(!tree.is_path_ignored("tracked-dir/tracked-file2").unwrap()); + assert!(tree.is_path_ignored("ignored-dir/ignored-file2").unwrap()); }); }); } @@ -1177,6 +1192,7 @@ mod tests { path: root_dir.path().into(), root_inode: None, entries: Default::default(), + ignores: Default::default(), })), notify_tx, 0, @@ -1206,6 +1222,7 @@ mod tests { path: root_dir.path().into(), root_inode: None, entries: Default::default(), + ignores: Default::default(), })), notify_tx, 1, @@ -1330,14 +1347,6 @@ mod tests { .collect() } - impl Entry { - fn is_ignored(&self) -> bool { - match self { - Entry::Dir { is_ignored, .. } | Entry::File { is_ignored, .. } => *is_ignored, - } - } - } - impl Snapshot { fn check_invariants(&self) { for entry in self.entries.items() { diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index dbb2985fac..c647164a9b 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -21,11 +21,10 @@ pub struct PathEntry { pub path_chars: CharBag, pub path: Arc<[char]>, pub lowercase_path: Arc<[char]>, - pub is_ignored: bool, } impl PathEntry { - pub fn new(ino: u64, path: &Path, is_ignored: bool) -> Self { + pub fn new(ino: u64, path: &Path) -> Self { let path = path.to_string_lossy(); let lowercase_path = path.to_lowercase().chars().collect::>().into(); let path: Arc<[char]> = path.chars().collect::>().into(); @@ -36,7 +35,6 @@ impl PathEntry { path_chars, path, lowercase_path, - is_ignored, } } } @@ -84,6 +82,7 @@ where { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); + let lowercase_query = &lowercase_query; let query = &query; let query_chars = CharBag::from(&lowercase_query[..]); @@ -139,7 +138,7 @@ where }; match_single_tree_paths( - snapshot.id, + snapshot, skipped_prefix_len, path_entries, query, @@ -176,7 +175,7 @@ where } fn match_single_tree_paths<'a>( - tree_id: usize, + snapshot: &Snapshot, skipped_prefix_len: usize, path_entries: impl Iterator, query: &[char], @@ -193,11 +192,11 @@ fn match_single_tree_paths<'a>( best_position_matrix: &mut Vec, ) { for path_entry in path_entries { - if !include_ignored && path_entry.is_ignored { + if !path_entry.path_chars.is_superset(query_chars.clone()) { continue; } - if !path_entry.path_chars.is_superset(query_chars.clone()) { + if !include_ignored && snapshot.is_inode_ignored(path_entry.ino).unwrap_or(true) { continue; } @@ -232,7 +231,7 @@ fn match_single_tree_paths<'a>( if score > 0.0 { results.push(Reverse(PathMatch { - tree_id, + tree_id: snapshot.id, entry_id: path_entry.ino, path: path_entry.path.iter().skip(skipped_prefix_len).collect(), score, @@ -438,6 +437,7 @@ fn recursive_score_match( #[cfg(test)] mod tests { use super::*; + use std::path::PathBuf; #[test] fn test_match_path_entries() { @@ -500,7 +500,6 @@ mod tests { path_chars, path, lowercase_path, - is_ignored: false, }); } @@ -511,7 +510,13 @@ mod tests { let mut results = BinaryHeap::new(); match_single_tree_paths( - 0, + &Snapshot { + id: 0, + path: PathBuf::new().into(), + root_inode: None, + ignores: Default::default(), + entries: Default::default(), + }, 0, path_entries.iter(), &query[..], From af3bc236b78c96baef1962fb82abd3006c44918c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 15:14:23 +0200 Subject: [PATCH 075/102] Recompute ignore status when .gitignore changes or for new entries --- zed/src/worktree.rs | 202 ++++++++++++++++++++++++++++++++++---- zed/src/worktree/fuzzy.rs | 8 +- 2 files changed, 189 insertions(+), 21 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 2ce2af9305..64e32fd767 100644 --- a/zed/src/worktree.rs +++ b/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, root_inode: Option, - ignores: HashMap, + ignores: HashMap, usize)>, entries: SumTree, } @@ -272,15 +276,7 @@ impl Snapshot { }) } - pub fn is_path_ignored(&self, path: impl AsRef) -> Result { - 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 { + fn is_inode_ignored(&self, mut inode: u64) -> Result { 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)]>, pending: bool, + is_ignored: Option, }, File { inode: u64, parent: Option, is_symlink: bool, path: PathEntry, + is_ignored: Option, }, } impl Entry { + fn is_ignored(&self) -> Option { + 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) -> 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, + } + + 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> { 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(), diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index c647164a9b..6be7ea98be 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/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, } impl PathEntry { - pub fn new(ino: u64, path: &Path) -> Self { + pub fn new(ino: u64, path: &Path, is_ignored: Option) -> Self { let path = path.to_string_lossy(); let lowercase_path = path.to_lowercase().chars().collect::>().into(); let path: Arc<[char]> = path.chars().collect::>().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(), From 19a28567800c6d92670221a71bbfac0a540ccac9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 16:47:41 +0200 Subject: [PATCH 076/102] Let ignore edits batch naturally without imposing a max batch len --- zed/src/worktree.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 64e32fd767..8ac584844b 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -31,7 +31,6 @@ use std::{ }; const GITIGNORE: &'static str = ".gitignore"; -const EDIT_BATCH_LEN: usize = 2000; #[derive(Clone, Debug)] enum ScanState { @@ -995,9 +994,6 @@ impl BackgroundScanner { 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)); } @@ -1040,9 +1036,6 @@ impl BackgroundScanner { 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)); } From f3b663e3750ad61772b126833b2182749dd7b30d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 16:48:13 +0200 Subject: [PATCH 077/102] Ignore `.git` folder --- zed/src/worktree.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 8ac584844b..db244e0bf2 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -306,7 +306,10 @@ impl Snapshot { } entry = parent_entry; } - Ok(false) + + relative_path.clear(); + relative_path.extend(components.iter().rev()); + Ok(relative_path.starts_with(".git")) } pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result { From c429a937bec9731fbeaf0c6b2cb7439998d29903 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 16:49:11 +0200 Subject: [PATCH 078/102] Remove `is_ignored` from `PathEntry` and lean more on the tree instead --- zed/src/worktree.rs | 59 +++++++++++++++++++--------- zed/src/worktree/fuzzy.rs | 82 ++++++++++++++++++++++++++------------- 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index db244e0bf2..52071506fc 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -244,6 +244,10 @@ impl Snapshot { self.entries.summary().file_count } + pub fn visible_file_count(&self) -> usize { + self.entries.summary().visible_file_count + } + pub fn root_entry(&self) -> Option<&Entry> { self.root_inode.and_then(|inode| self.entries.get(&inode)) } @@ -606,12 +610,7 @@ impl Entry { 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); - } + Entry::File { is_ignored, .. } => *is_ignored = Some(ignored), } } @@ -638,14 +637,25 @@ impl sum_tree::Item for Entry { type Summary = EntrySummary; fn summary(&self) -> Self::Summary { + let file_count; + let visible_file_count; + if let Entry::File { is_ignored, .. } = self { + file_count = 1; + if is_ignored.unwrap_or(false) { + visible_file_count = 0; + } else { + visible_file_count = 1; + } + } else { + file_count = 0; + visible_file_count = 0; + } + EntrySummary { max_ino: self.inode(), - file_count: if matches!(self, Self::File { .. }) { - 1 - } else { - 0 - }, - recompute_is_ignored: self.is_ignored().is_none(), + file_count, + visible_file_count, + recompute_ignore_status: self.is_ignored().is_none(), } } } @@ -662,14 +672,16 @@ impl sum_tree::KeyedItem for Entry { pub struct EntrySummary { max_ino: u64, file_count: usize, - recompute_is_ignored: bool, + visible_file_count: usize, + recompute_ignore_status: 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; + self.visible_file_count += rhs.visible_file_count; + self.recompute_ignore_status |= rhs.recompute_ignore_status; } } @@ -688,6 +700,15 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { } } +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +struct VisibleFileCount(usize); + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { + fn add_summary(&mut self, summary: &'a EntrySummary) { + self.0 += summary.visible_file_count; + } +} + struct BackgroundScanner { snapshot: Arc>, notify: Sender, @@ -802,7 +823,7 @@ impl BackgroundScanner { None, Entry::File { parent: None, - path: PathEntry::new(inode, &relative_path, None), + path: PathEntry::new(inode, &relative_path), inode, is_symlink, is_ignored: None, @@ -854,7 +875,7 @@ impl BackgroundScanner { child_name, Entry::File { parent: Some(job.inode), - path: PathEntry::new(child_inode, &child_relative_path, None), + path: PathEntry::new(child_inode, &child_relative_path), inode: child_inode, is_symlink: child_is_symlink, is_ignored: None, @@ -1046,7 +1067,10 @@ impl BackgroundScanner { scope.execute(|| { let entries_tx = entries_tx; - for entry in snapshot.entries.filter::<_, ()>(|e| e.recompute_is_ignored) { + for entry in snapshot + .entries + .filter::<_, ()>(|e| e.recompute_ignore_status) + { entries_tx.send(entry.clone()).unwrap(); } }); @@ -1110,7 +1134,6 @@ impl BackgroundScanner { root_path .parent() .map_or(path, |parent| path.strip_prefix(parent).unwrap()), - None, ), is_ignored: None, } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 6be7ea98be..41c0751561 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -2,7 +2,7 @@ use gpui::scoped_pool; use crate::sum_tree::SeekBias; -use super::{char_bag::CharBag, Entry, FileCount, Snapshot}; +use super::{char_bag::CharBag, Entry, FileCount, Snapshot, VisibleFileCount}; use std::{ cmp::{max, min, Ordering, Reverse}, @@ -21,11 +21,10 @@ pub struct PathEntry { pub path_chars: CharBag, pub path: Arc<[char]>, pub lowercase_path: Arc<[char]>, - pub is_ignored: Option, } impl PathEntry { - pub fn new(ino: u64, path: &Path, is_ignored: Option) -> Self { + pub fn new(ino: u64, path: &Path) -> Self { let path = path.to_string_lossy(); let lowercase_path = path.to_lowercase().chars().collect::>().into(); let path: Arc<[char]> = path.chars().collect::>().into(); @@ -36,7 +35,6 @@ impl PathEntry { path_chars, path, lowercase_path, - is_ignored, } } } @@ -90,7 +88,12 @@ where let query_chars = CharBag::from(&lowercase_query[..]); let cpus = num_cpus::get(); - let path_count: usize = snapshots.clone().map(Snapshot::file_count).sum(); + let path_count: usize = if include_ignored { + snapshots.clone().map(Snapshot::file_count).sum() + } else { + snapshots.clone().map(Snapshot::visible_file_count).sum() + }; + let segment_size = (path_count + cpus - 1) / cpus; let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); @@ -111,22 +114,15 @@ where let mut tree_start = 0; for snapshot in trees { - let tree_end = tree_start + snapshot.file_count(); + let tree_end = if include_ignored { + tree_start + snapshot.file_count() + } else { + tree_start + snapshot.visible_file_count() + }; if tree_start < segment_end && segment_start < tree_end { let start = max(tree_start, segment_start) - tree_start; let end = min(tree_end, segment_end) - tree_start; - let mut cursor = snapshot.entries.cursor::<_, ()>(); - cursor.seek(&FileCount(start), SeekBias::Right); - let path_entries = cursor - .filter_map(|e| { - if let Entry::File { path, .. } = e { - Some(path) - } else { - None - } - }) - .take(end - start); - + let path_entries = path_entries_iter(snapshot, start, end, include_ignored); let skipped_prefix_len = if include_root_name { 0 } else if let Some(Entry::Dir { .. }) = snapshot.root_entry() { @@ -145,8 +141,7 @@ where path_entries, query, lowercase_query, - query_chars.clone(), - include_ignored, + query_chars, smart_case, results, max_results, @@ -176,6 +171,44 @@ where results } +fn path_entries_iter<'a>( + snapshot: &'a Snapshot, + start: usize, + end: usize, + include_ignored: bool, +) -> impl Iterator { + let mut files_cursor = None; + let mut visible_files_cursor = None; + if include_ignored { + let mut cursor = snapshot.entries.cursor::<_, ()>(); + cursor.seek(&FileCount(start), SeekBias::Right); + files_cursor = Some(cursor); + } else { + let mut cursor = snapshot.entries.cursor::<_, ()>(); + cursor.seek(&VisibleFileCount(start), SeekBias::Right); + visible_files_cursor = Some(cursor); + } + files_cursor + .into_iter() + .flatten() + .chain(visible_files_cursor.into_iter().flatten()) + .filter_map(move |e| { + if let Entry::File { + path, is_ignored, .. + } = e + { + if is_ignored.unwrap_or(false) && !include_ignored { + None + } else { + Some(path) + } + } else { + None + } + }) + .take(end - start) +} + fn match_single_tree_paths<'a>( snapshot: &Snapshot, skipped_prefix_len: usize, @@ -183,7 +216,6 @@ fn match_single_tree_paths<'a>( query: &[char], lowercase_query: &[char], query_chars: CharBag, - include_ignored: bool, smart_case: bool, results: &mut BinaryHeap>, max_results: usize, @@ -194,11 +226,7 @@ fn match_single_tree_paths<'a>( best_position_matrix: &mut Vec, ) { for path_entry in path_entries { - if !path_entry.path_chars.is_superset(query_chars.clone()) { - continue; - } - - if !include_ignored && path_entry.is_ignored.unwrap_or(false) { + if !path_entry.path_chars.is_superset(query_chars) { continue; } @@ -502,7 +530,6 @@ mod tests { path_chars, path, lowercase_path, - is_ignored: Some(false), }); } @@ -526,7 +553,6 @@ mod tests { &query[..], &lowercase_query[..], query_chars, - true, smart_case, &mut results, 100, From 9723e46af43252558e4b6b866fadf148287e66f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 17:29:36 +0200 Subject: [PATCH 079/102] Replace linear scan of entries with a custom `FileIter` --- zed/src/workspace/workspace.rs | 4 +- zed/src/worktree.rs | 94 +++++++++++++++++++++++++--------- zed/src/worktree/fuzzy.rs | 58 +++++---------------- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index e8fc21c819..50bc959404 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -224,7 +224,7 @@ impl WorkspaceHandle for ModelHandle { .iter() .flat_map(|tree| { let tree_id = tree.id(); - tree.read(app).files().map(move |inode| (tree_id, inode)) + tree.read(app).files(0).map(move |f| (tree_id, f.inode())) }) .collect::>() } @@ -253,7 +253,7 @@ mod tests { // Get the first file entry. let tree = app.read(|ctx| workspace.read(ctx).worktrees.iter().next().unwrap().clone()); - let file_inode = app.read(|ctx| tree.read(ctx).files().next().unwrap()); + let file_inode = app.read(|ctx| tree.read(ctx).files(0).next().unwrap().inode()); let entry = (tree.id(), file_inode); // Open the same entry twice before it finishes loading. diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 52071506fc..d40669a845 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -3,7 +3,7 @@ mod fuzzy; use crate::{ editor::{History, Snapshot as BufferSnapshot}, - sum_tree::{self, Edit, SumTree}, + sum_tree::{self, Cursor, Edit, SeekBias, SumTree}, }; use anyhow::{anyhow, Context, Result}; use fuzzy::PathEntry; @@ -153,10 +153,6 @@ impl Worktree { self.snapshot.entries.get(&inode).is_some() } - pub fn file_count(&self) -> usize { - self.snapshot.entries.summary().file_count - } - pub fn abs_path_for_inode(&self, ino: u64) -> Result { let mut result = self.snapshot.path.to_path_buf(); result.push(self.path_for_inode(ino, false)?); @@ -195,20 +191,6 @@ impl Worktree { Ok(()) }) } - - #[cfg(test)] - pub fn files<'a>(&'a self) -> impl Iterator + 'a { - self.snapshot - .entries - .cursor::<(), ()>() - .filter_map(|entry| { - if let Entry::File { inode, .. } = entry { - Some(*inode) - } else { - None - } - }) - } } impl Entity for Worktree { @@ -248,6 +230,14 @@ impl Snapshot { self.entries.summary().visible_file_count } + pub fn files(&self, start: usize) -> FileIter { + FileIter::all(self, start) + } + + pub fn visible_files(&self, start: usize) -> FileIter { + FileIter::visible(self, start) + } + pub fn root_entry(&self) -> Option<&Entry> { self.root_inode.and_then(|inode| self.entries.get(&inode)) } @@ -614,7 +604,7 @@ impl Entry { } } - fn inode(&self) -> u64 { + pub fn inode(&self) -> u64 { match self { Entry::Dir { inode, .. } => *inode, Entry::File { inode, .. } => *inode, @@ -692,7 +682,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for u64 { } #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] -struct FileCount(usize); +pub struct FileCount(usize); impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { fn add_summary(&mut self, summary: &'a EntrySummary) { @@ -701,7 +691,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { } #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] -struct VisibleFileCount(usize); +pub struct VisibleFileCount(usize); impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { fn add_summary(&mut self, summary: &'a EntrySummary) { @@ -1167,6 +1157,58 @@ impl WorktreeHandle for ModelHandle { } } +pub enum FileIter<'a> { + All(Cursor<'a, Entry, FileCount, FileCount>), + Visible(Cursor<'a, Entry, VisibleFileCount, VisibleFileCount>), +} + +impl<'a> FileIter<'a> { + fn all(snapshot: &'a Snapshot, start: usize) -> Self { + let mut cursor = snapshot.entries.cursor(); + cursor.seek(&FileCount(start), SeekBias::Right); + Self::All(cursor) + } + + fn visible(snapshot: &'a Snapshot, start: usize) -> Self { + let mut cursor = snapshot.entries.cursor(); + cursor.seek(&VisibleFileCount(start), SeekBias::Right); + Self::Visible(cursor) + } + + fn next_internal(&mut self) { + match self { + Self::All(cursor) => { + let ix = *cursor.start(); + cursor.seek_forward(&FileCount(ix.0 + 1), SeekBias::Right); + } + Self::Visible(cursor) => { + let ix = *cursor.start(); + cursor.seek_forward(&VisibleFileCount(ix.0 + 1), SeekBias::Right); + } + } + } + + fn item(&self) -> Option<&'a Entry> { + match self { + Self::All(cursor) => cursor.item(), + Self::Visible(cursor) => cursor.item(), + } + } +} + +impl<'a> Iterator for FileIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(entry) = self.item() { + self.next_internal(); + Some(entry) + } else { + None + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1248,7 +1290,7 @@ mod tests { let file_inode = app.read(|ctx| { let tree = tree.read(ctx); - let inode = tree.files().next().unwrap(); + let inode = tree.files(0).next().unwrap().inode(); assert_eq!( tree.path_for_inode(inode, false) .unwrap() @@ -1532,10 +1574,16 @@ mod tests { impl Snapshot { fn check_invariants(&self) { + let mut path_entries = self.files(0); for entry in self.entries.items() { let path = self.path_for_inode(entry.inode(), false).unwrap(); assert_eq!(self.inode_for_path(path).unwrap(), entry.inode()); + + if let Entry::File { inode, .. } = entry { + assert_eq!(path_entries.next().unwrap().inode(), inode); + } } + assert!(path_entries.next().is_none()); } fn to_vec(&self) -> Vec<(PathBuf, u64)> { diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 41c0751561..85cafd8441 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -1,9 +1,5 @@ +use super::{char_bag::CharBag, Entry, Snapshot}; use gpui::scoped_pool; - -use crate::sum_tree::SeekBias; - -use super::{char_bag::CharBag, Entry, FileCount, Snapshot, VisibleFileCount}; - use std::{ cmp::{max, min, Ordering, Reverse}, collections::BinaryHeap, @@ -122,7 +118,19 @@ where if tree_start < segment_end && segment_start < tree_end { let start = max(tree_start, segment_start) - tree_start; let end = min(tree_end, segment_end) - tree_start; - let path_entries = path_entries_iter(snapshot, start, end, include_ignored); + let entries = if include_ignored { + snapshot.files(start).take(end - start) + } else { + snapshot.visible_files(start).take(end - start) + }; + let path_entries = entries.map(|entry| { + if let Entry::File { path, .. } = entry { + path + } else { + unreachable!() + } + }); + let skipped_prefix_len = if include_root_name { 0 } else if let Some(Entry::Dir { .. }) = snapshot.root_entry() { @@ -171,44 +179,6 @@ where results } -fn path_entries_iter<'a>( - snapshot: &'a Snapshot, - start: usize, - end: usize, - include_ignored: bool, -) -> impl Iterator { - let mut files_cursor = None; - let mut visible_files_cursor = None; - if include_ignored { - let mut cursor = snapshot.entries.cursor::<_, ()>(); - cursor.seek(&FileCount(start), SeekBias::Right); - files_cursor = Some(cursor); - } else { - let mut cursor = snapshot.entries.cursor::<_, ()>(); - cursor.seek(&VisibleFileCount(start), SeekBias::Right); - visible_files_cursor = Some(cursor); - } - files_cursor - .into_iter() - .flatten() - .chain(visible_files_cursor.into_iter().flatten()) - .filter_map(move |e| { - if let Entry::File { - path, is_ignored, .. - } = e - { - if is_ignored.unwrap_or(false) && !include_ignored { - None - } else { - Some(path) - } - } else { - None - } - }) - .take(end - start) -} - fn match_single_tree_paths<'a>( snapshot: &Snapshot, skipped_prefix_len: usize, From 928ce0eec8f926aaa21700505b4634e6907ef544 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 18:42:11 +0200 Subject: [PATCH 080/102] Clear descendants of existing children when scanning a directory Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d40669a845..e9a5509462 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -403,6 +403,8 @@ impl Snapshot { .insert(child.inode()); } } + + self.clear_descendants(child.inode(), &mut edits); } edits.push(Edit::Insert(child)); } From 99f51c0d853091adb97005e262e289f226a6c3f7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 18:57:46 +0200 Subject: [PATCH 081/102] Simulate `.gitignore`s in the randomized test Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 54 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index e9a5509462..8d14ee9a46 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1221,6 +1221,7 @@ mod tests { use rand::prelude::*; use serde_json::json; use std::env; + use std::fmt::Write; use std::os::unix; use std::time::{SystemTime, UNIX_EPOCH}; @@ -1424,6 +1425,7 @@ mod tests { 0, ); scanner.scan_dirs().unwrap(); + scanner.snapshot().check_invariants(); let mut events = Vec::new(); let mut mutations_len = operations; @@ -1433,6 +1435,7 @@ mod tests { let to_deliver = events.drain(0..len).collect::>(); log::info!("Delivering events: {:#?}", to_deliver); scanner.process_events(to_deliver); + scanner.snapshot().check_invariants(); } else { events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap()); mutations_len -= 1; @@ -1440,6 +1443,7 @@ mod tests { } log::info!("Quiescing: {:#?}", events); scanner.process_events(events); + scanner.snapshot().check_invariants(); let (notify_tx, _notify_rx) = smol::channel::unbounded(); let new_scanner = BackgroundScanner::new( @@ -1455,7 +1459,6 @@ mod tests { 1, ); new_scanner.scan_dirs().unwrap(); - scanner.snapshot().check_invariants(); assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec()); } } @@ -1492,6 +1495,39 @@ mod tests { fs::write(&new_path, "")?; } record_event(new_path); + } else if rng.gen_bool(0.05) { + let ignore_dir_path = dirs.choose(rng).unwrap(); + let ignore_path = ignore_dir_path.join(GITIGNORE); + + let (subdirs, subfiles) = read_dir_recursive(ignore_dir_path.clone()); + let files_to_ignore = { + let len = rng.gen_range(0..=subfiles.len()); + subfiles.choose_multiple(rng, len) + }; + let dirs_to_ignore = { + let len = rng.gen_range(0..subdirs.len()); + subdirs.choose_multiple(rng, len) + }; + + let mut ignore_contents = String::new(); + for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) { + write!( + ignore_contents, + "{}\n", + path_to_ignore + .strip_prefix(&ignore_dir_path)? + .to_str() + .unwrap() + ) + .unwrap(); + } + log::info!( + "Creating {:?} with contents:\n{}", + ignore_path.strip_prefix(&root_path)?, + ignore_contents + ); + fs::write(&ignore_path, ignore_contents).unwrap(); + record_event(ignore_path); } else { let old_path = { let file_path = files.choose(rng); @@ -1576,16 +1612,24 @@ mod tests { impl Snapshot { fn check_invariants(&self) { - let mut path_entries = self.files(0); + let mut files = self.files(0); + let mut visible_files = self.visible_files(0); for entry in self.entries.items() { let path = self.path_for_inode(entry.inode(), false).unwrap(); assert_eq!(self.inode_for_path(path).unwrap(), entry.inode()); - if let Entry::File { inode, .. } = entry { - assert_eq!(path_entries.next().unwrap().inode(), inode); + if let Entry::File { + inode, is_ignored, .. + } = entry + { + assert_eq!(files.next().unwrap().inode(), inode); + if !is_ignored.unwrap() { + assert_eq!(visible_files.next().unwrap().inode(), inode); + } } } - assert!(path_entries.next().is_none()); + assert!(files.next().is_none()); + assert!(visible_files.next().is_none()); } fn to_vec(&self) -> Vec<(PathBuf, u64)> { From b2e7a2c42abbb77e0686b384bc153382d7eaf1eb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Apr 2021 19:15:37 +0200 Subject: [PATCH 082/102] Log when there's an error while scanning a directory Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 8d14ee9a46..cca39113c7 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -784,12 +784,12 @@ impl BackgroundScanner { let (tx, rx) = crossbeam_channel::unbounded(); - tx.send(Ok(ScanJob { + tx.send(ScanJob { inode, path: path.clone(), relative_path, scan_queue: tx.clone(), - })) + }) .unwrap(); drop(tx); @@ -800,7 +800,8 @@ impl BackgroundScanner { pool.execute(|| { let result = result; while let Ok(job) = rx.recv() { - if let Err(err) = job.and_then(|job| self.scan_dir(job)) { + if let Err(err) = self.scan_dir(&job) { + log::error!("error scanning {:?}: {}", job.path, err); *result = Err(err); break; } @@ -829,9 +830,7 @@ impl BackgroundScanner { Ok(()) } - fn scan_dir(&self, job: ScanJob) -> io::Result<()> { - let scan_queue = job.scan_queue; - + fn scan_dir(&self, job: &ScanJob) -> io::Result<()> { let mut new_entries = Vec::new(); let mut new_jobs = Vec::new(); @@ -860,7 +859,7 @@ impl BackgroundScanner { inode: child_inode, path: Arc::from(child_path), relative_path: child_relative_path, - scan_queue: scan_queue.clone(), + scan_queue: job.scan_queue.clone(), }); } else { new_entries.push(( @@ -878,7 +877,7 @@ impl BackgroundScanner { self.snapshot.lock().populate_dir(job.inode, new_entries); for new_job in new_jobs { - scan_queue.send(Ok(new_job)).unwrap(); + job.scan_queue.send(new_job).unwrap(); } Ok(()) @@ -920,7 +919,7 @@ impl BackgroundScanner { snapshot.insert_entry(path.file_name(), fs_entry); if is_dir { scan_queue_tx - .send(Ok(ScanJob { + .send(ScanJob { inode, path: Arc::from(path), relative_path: snapshot @@ -928,7 +927,7 @@ impl BackgroundScanner { .map_or(PathBuf::new(), PathBuf::from) .join(relative_path), scan_queue: scan_queue_tx.clone(), - })) + }) .unwrap(); } } @@ -948,8 +947,8 @@ impl BackgroundScanner { for _ in 0..self.thread_pool.thread_count() { pool.execute(|| { while let Ok(job) = scan_queue_rx.recv() { - if let Err(err) = job.and_then(|job| self.scan_dir(job)) { - log::error!("Error scanning {:?}", err); + if let Err(err) = self.scan_dir(&job) { + log::error!("error scanning {:?}: {}", job.path, err); } } }); @@ -1139,7 +1138,7 @@ struct ScanJob { inode: u64, path: Arc, relative_path: PathBuf, - scan_queue: crossbeam_channel::Sender>, + scan_queue: crossbeam_channel::Sender, } pub trait WorktreeHandle { From 3e44a0adae9cf31cdb260c670683fb9591ba33d2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 22 Apr 2021 10:30:47 -0700 Subject: [PATCH 083/102] Avoid redundant clearing of descendants when scanning dirs Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index cca39113c7..4a96729ef4 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -401,10 +401,9 @@ impl Snapshot { .entry(old_parent_inode) .or_default() .insert(child.inode()); + self.clear_descendants(child.inode(), &mut edits); } } - - self.clear_descendants(child.inode(), &mut edits); } edits.push(Edit::Insert(child)); } @@ -875,6 +874,7 @@ impl BackgroundScanner { }; } + dbg!(&job.path); self.snapshot.lock().populate_dir(job.inode, new_entries); for new_job in new_jobs { job.scan_queue.send(new_job).unwrap(); From b37b04740082e7a69a31c08b5e2a02385d566561 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 22 Apr 2021 12:10:46 -0700 Subject: [PATCH 084/102] Start work on excluding separate mount dirs Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 ++ zed/Cargo.toml | 4 +++ zed/src/worktree.rs | 84 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8b7d9ad22..c6a91422ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2220,6 +2220,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec", + "cocoa", "crossbeam-channel", "ctor", "dirs", @@ -2233,6 +2234,7 @@ dependencies = [ "libc", "log", "num_cpus", + "objc", "parking_lot", "postage", "rand 0.8.3", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index c38db66d0f..0ab9b2495c 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -42,3 +42,7 @@ env_logger = "0.8" serde_json = {version = "1.0.64", features = ["preserve_order"]} tempdir = "0.3.7" unindent = "0.1.7" + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.24" +objc = "0.2" diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 4a96729ef4..bd2de1cfdd 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -354,7 +354,11 @@ impl Snapshot { self.insert_ignore_file(new_parent_inode); } - let mut new_parent = self.entries.get(&new_parent_inode).unwrap().clone(); + let mut new_parent = self + .entries + .get(&new_parent_inode) + .expect(&format!("no entry for inode {}", new_parent_inode)) + .clone(); if let Entry::Dir { children, .. } = &mut new_parent { *children = children .iter() @@ -417,7 +421,7 @@ impl Snapshot { *children = new_children.into(); *pending = false; } else { - unreachable!("non-directory parent"); + unreachable!("non-directory parent {}", parent_inode); } edits.push(Edit::Insert(parent)); @@ -703,16 +707,30 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { struct BackgroundScanner { snapshot: Arc>, notify: Sender, + other_mount_paths: HashSet, thread_pool: scoped_pool::Pool, } impl BackgroundScanner { fn new(snapshot: Arc>, notify: Sender, worktree_id: usize) -> Self { - Self { + let mut scanner = Self { snapshot, notify, + other_mount_paths: Default::default(), thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)), - } + }; + scanner.update_other_mount_paths(); + scanner + } + + fn update_other_mount_paths(&mut self) { + let path = self.snapshot.lock().path.clone(); + self.other_mount_paths.clear(); + self.other_mount_paths.extend( + mounted_volume_paths() + .into_iter() + .filter(|mount_path| !path.starts_with(mount_path)), + ); } fn path(&self) -> Arc { @@ -723,7 +741,7 @@ impl BackgroundScanner { self.snapshot.lock().clone() } - fn run(self, event_stream: fsevent::EventStream) { + fn run(mut self, event_stream: fsevent::EventStream) { if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return; } @@ -792,23 +810,17 @@ impl BackgroundScanner { .unwrap(); drop(tx); - let mut results = Vec::new(); - results.resize_with(self.thread_pool.thread_count(), || Ok(())); self.thread_pool.scoped(|pool| { - for result in &mut results { + for _ in 0..self.thread_pool.thread_count() { pool.execute(|| { - let result = result; while let Ok(job) = rx.recv() { if let Err(err) = self.scan_dir(&job) { log::error!("error scanning {:?}: {}", job.path, err); - *result = Err(err); - break; } } }); } }); - results.into_iter().collect::>()?; } else { let mut snapshot = self.snapshot.lock(); snapshot.insert_entry( @@ -842,6 +854,11 @@ impl BackgroundScanner { let child_is_symlink = child_metadata.file_type().is_symlink(); let child_path = job.path.join(child_name.as_ref()); + // Disallow mount points outside the file system containing the root of this worktree + if self.other_mount_paths.contains(&child_path) { + continue; + } + if child_metadata.is_dir() { new_entries.push(( child_name, @@ -874,7 +891,6 @@ impl BackgroundScanner { }; } - dbg!(&job.path); self.snapshot.lock().populate_dir(job.inode, new_entries); for new_job in new_jobs { job.scan_queue.send(new_job).unwrap(); @@ -883,7 +899,9 @@ impl BackgroundScanner { Ok(()) } - fn process_events(&self, mut events: Vec) -> bool { + fn process_events(&mut self, mut events: Vec) -> bool { + self.update_other_mount_paths(); + let mut snapshot = self.snapshot(); snapshot.scan_id += 1; @@ -1210,6 +1228,34 @@ impl<'a> Iterator for FileIter<'a> { } } +fn mounted_volume_paths() -> Vec { + use cocoa::{ + base::{id, nil}, + foundation::{NSArray, NSString, NSURL}, + }; + use objc::{class, msg_send, sel, sel_impl}; + + unsafe { + let manager: id = msg_send![class!(NSFileManager), defaultManager]; + let array = NSArray::array(nil); + let urls: id = + msg_send![manager, mountedVolumeURLsIncludingResourceValuesForKeys:array options:0]; + let len = urls.count() as usize; + let mut result = Vec::with_capacity(len); + for i in 0..len { + let url = urls.objectAtIndex(i as u64); + let string = url.absoluteString(); + let string = std::ffi::CStr::from_ptr(string.UTF8String()) + .to_string_lossy() + .to_string(); + if let Some(path) = string.strip_prefix("file://") { + result.push(PathBuf::from(path)); + } + } + result + } +} + #[cfg(test)] mod tests { use super::*; @@ -1273,8 +1319,6 @@ mod tests { ); }) }); - - eprintln!("HI"); } #[test] @@ -1383,6 +1427,12 @@ mod tests { }); } + #[test] + fn test_mounted_volume_paths() { + let paths = mounted_volume_paths(); + assert!(paths.contains(&"/".into())); + } + #[test] fn test_random() { let iterations = env::var("ITERATIONS") @@ -1411,7 +1461,7 @@ mod tests { log::info!("Generated initial tree"); let (notify_tx, _notify_rx) = smol::channel::unbounded(); - let scanner = BackgroundScanner::new( + let mut scanner = BackgroundScanner::new( Arc::new(Mutex::new(Snapshot { id: 0, scan_id: 0, From ae5f02d2e90a69686ba8f0e77f7938b3a229c4cf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 22 Apr 2021 15:40:39 -0700 Subject: [PATCH 085/102] Use getmntinfo(3) to list mounted volumes Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 -- zed/Cargo.toml | 4 ---- zed/src/worktree.rs | 39 +++++++++++++++------------------------ 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6a91422ad..b8b7d9ad22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2220,7 +2220,6 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec", - "cocoa", "crossbeam-channel", "ctor", "dirs", @@ -2234,7 +2233,6 @@ dependencies = [ "libc", "log", "num_cpus", - "objc", "parking_lot", "postage", "rand 0.8.3", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 0ab9b2495c..c38db66d0f 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -42,7 +42,3 @@ env_logger = "0.8" serde_json = {version = "1.0.64", features = ["preserve_order"]} tempdir = "0.3.7" unindent = "0.1.7" - -[target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.24" -objc = "0.2" diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index bd2de1cfdd..395d868383 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -18,13 +18,13 @@ use postage::{ use smol::{channel::Sender, Timer}; use std::{ collections::{BTreeMap, HashMap, HashSet}, - ffi::OsStr, + ffi::{CStr, OsStr}, fmt, fs, future::Future, io::{self, Read, Write}, mem, ops::{AddAssign, Deref}, - os::unix::fs::MetadataExt, + os::unix::{ffi::OsStrExt, fs::MetadataExt}, path::{Path, PathBuf}, sync::Arc, time::Duration, @@ -1229,30 +1229,21 @@ impl<'a> Iterator for FileIter<'a> { } fn mounted_volume_paths() -> Vec { - use cocoa::{ - base::{id, nil}, - foundation::{NSArray, NSString, NSURL}, - }; - use objc::{class, msg_send, sel, sel_impl}; - unsafe { - let manager: id = msg_send![class!(NSFileManager), defaultManager]; - let array = NSArray::array(nil); - let urls: id = - msg_send![manager, mountedVolumeURLsIncludingResourceValuesForKeys:array options:0]; - let len = urls.count() as usize; - let mut result = Vec::with_capacity(len); - for i in 0..len { - let url = urls.objectAtIndex(i as u64); - let string = url.absoluteString(); - let string = std::ffi::CStr::from_ptr(string.UTF8String()) - .to_string_lossy() - .to_string(); - if let Some(path) = string.strip_prefix("file://") { - result.push(PathBuf::from(path)); + let mut stat_ptr: *mut libc::statfs = std::ptr::null_mut(); + let count = libc::getmntinfo(&mut stat_ptr as *mut _, libc::MNT_WAIT); + if count >= 0 { + std::slice::from_raw_parts(stat_ptr, count as usize) + .iter() + .map(|stat| { + PathBuf::from(OsStr::from_bytes( + CStr::from_ptr(&stat.f_mntonname[0]).to_bytes(), + )) + }) + .collect() + } else { + panic!("failed to run getmntinfo"); } - } - result } } From 8f2fc079fdcc6f35827483617136ebe3b7f34598 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 23 Apr 2021 11:22:25 +0200 Subject: [PATCH 086/102] Restructure Worktree to index entries by path --- zed/src/worktree.rs | 706 ++++++++++++++------------------------ zed/src/worktree/fuzzy.rs | 8 +- 2 files changed, 269 insertions(+), 445 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 395d868383..7354c06d5b 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -10,6 +10,7 @@ use fuzzy::PathEntry; pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::gitignore::Gitignore; +use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ prelude::{Sink, Stream}, @@ -17,7 +18,8 @@ use postage::{ }; use smol::{channel::Sender, Timer}; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + cmp, + collections::{BTreeMap, HashSet}, ffi::{CStr, OsStr}, fmt, fs, future::Future, @@ -30,7 +32,9 @@ use std::{ time::Duration, }; -const GITIGNORE: &'static str = ".gitignore"; +lazy_static! { + static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} #[derive(Clone, Debug)] enum ScanState { @@ -61,7 +65,6 @@ impl Worktree { id, scan_id: 0, path: path.into(), - root_inode: None, ignores: Default::default(), entries: Default::default(), }; @@ -150,7 +153,8 @@ impl Worktree { } pub fn has_inode(&self, inode: u64) -> bool { - self.snapshot.entries.get(&inode).is_some() + todo!() + // self.snapshot.entries.get(&inode).is_some() } pub fn abs_path_for_inode(&self, ino: u64) -> Result { @@ -216,8 +220,7 @@ pub struct Snapshot { id: usize, scan_id: usize, path: Arc, - root_inode: Option, - ignores: HashMap, usize)>, + ignores: BTreeMap, (Arc, usize)>, entries: SumTree, } @@ -238,318 +241,123 @@ impl Snapshot { FileIter::visible(self, start) } - pub fn root_entry(&self) -> Option<&Entry> { - self.root_inode.and_then(|inode| self.entries.get(&inode)) + pub fn root_entry(&self) -> Entry { + self.entry_for_path(&self.path).unwrap() } pub fn root_name(&self) -> Option<&OsStr> { self.path.file_name() } - fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { - self.inode_for_path(path) - .and_then(|inode| self.entries.get(&inode)) + fn entry_for_path(&self, path: impl AsRef) -> Option { + let mut cursor = self.entries.cursor::<_, ()>(); + if cursor.seek(&PathSearch::Exact(path.as_ref()), SeekBias::Left) { + cursor.item().cloned() + } else { + None + } } fn inode_for_path(&self, path: impl AsRef) -> Option { - let path = path.as_ref(); - self.root_inode.and_then(|mut inode| { - 'components: for path_component in path { - if let Some(Entry::Dir { children, .. }) = &self.entries.get(&inode) { - for (child_inode, name) in children.as_ref() { - if name.as_ref() == path_component { - inode = *child_inode; - continue 'components; - } - } - } - return None; - } - Some(inode) - }) + self.entry_for_path(path.as_ref()).map(|e| e.inode()) } - fn is_inode_ignored(&self, mut inode: u64) -> Result { - let mut components = Vec::new(); - let mut relative_path = PathBuf::new(); + fn is_path_ignored(&self, path: &Path) -> Result { let mut entry = self - .entries - .get(&inode) + .entry_for_path(path) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - while let Some(parent) = entry.parent() { - let parent_entry = self.entries.get(&parent).unwrap(); - if let Entry::Dir { children, .. } = parent_entry { - let (_, child_name) = children - .iter() - .find(|(child_inode, _)| *child_inode == inode) - .unwrap(); - components.push(child_name.as_ref()); - inode = parent; - 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()) { + if path.starts_with(".git") { + Ok(true) + } else { + while let Some(parent_entry) = + entry.path().parent().and_then(|p| self.entry_for_path(p)) + { + let parent_path = parent_entry.path(); + if let Some((ignore, _)) = self.ignores.get(parent_path) { + let relative_path = path.strip_prefix(parent_path).unwrap(); + match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { ignore::Match::Whitelist(_) => return Ok(false), ignore::Match::Ignore(_) => return Ok(true), ignore::Match::None => {} } } - } else { - unreachable!(); + entry = parent_entry; } - entry = parent_entry; + Ok(false) } - - relative_path.clear(); - relative_path.extend(components.iter().rev()); - Ok(relative_path.starts_with(".git")) } pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result { - let mut components = Vec::new(); - let mut entry = self - .entries - .get(&inode) - .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - while let Some(parent) = entry.parent() { - entry = self.entries.get(&parent).unwrap(); - if let Entry::Dir { children, .. } = entry { - let (_, child_name) = children - .iter() - .find(|(child_inode, _)| *child_inode == inode) - .unwrap(); - components.push(child_name.as_ref()); - inode = parent; - } else { - unreachable!(); - } - } - if include_root { - components.push(self.root_name().unwrap()); - } - Ok(components.into_iter().rev().collect()) + todo!("this method should go away") } - fn insert_entry(&mut self, name: Option<&OsStr>, entry: Entry) { - let mut edits = Vec::new(); - - if let Some(old_entry) = self.entries.get(&entry.inode()) { - // If the entry's parent changed, remove the entry from the old parent's children. - if old_entry.parent() != entry.parent() { - if let Some(old_parent_inode) = old_entry.parent() { - let old_parent = self.entries.get(&old_parent_inode).unwrap().clone(); - self.remove_children(old_parent, &mut edits, |inode| inode == entry.inode()); - } - } - - // Remove all descendants of the old version of the entry being inserted. - self.clear_descendants(entry.inode(), &mut edits); + fn insert_entry(&mut self, entry: Entry) { + if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { + self.insert_ignore_file(entry.path()); } - - // Insert the entry in its new parent with the correct name. - if let Some(new_parent_inode) = entry.parent() { - let name = name.unwrap(); - if name == GITIGNORE { - self.insert_ignore_file(new_parent_inode); - } - - let mut new_parent = self - .entries - .get(&new_parent_inode) - .expect(&format!("no entry for inode {}", new_parent_inode)) - .clone(); - if let Entry::Dir { children, .. } = &mut new_parent { - *children = children - .iter() - .filter(|(inode, _)| *inode != entry.inode()) - .cloned() - .chain(Some((entry.inode(), name.into()))) - .collect::>() - .into(); - } else { - unreachable!("non-directory parent"); - } - edits.push(Edit::Insert(new_parent)); - } - - // Insert the entry itself. - edits.push(Edit::Insert(entry)); - - self.entries.edit(edits); + self.entries.insert(entry); } - fn populate_dir( - &mut self, - parent_inode: u64, - children: impl IntoIterator, Entry)>, - ) { + fn populate_dir(&mut self, parent_path: Arc, entries: impl IntoIterator) { let mut edits = Vec::new(); - self.clear_descendants(parent_inode, &mut edits); - - // Determine which children are being re-parented and populate array of new children to - // assign to the parent. - let mut new_children = Vec::new(); - let mut old_children = HashMap::>::new(); - for (name, child) in children.into_iter() { - if *name == *GITIGNORE { - self.insert_ignore_file(parent_inode); - } - - new_children.push((child.inode(), name.into())); - if let Some(old_child) = self.entries.get(&child.inode()) { - if let Some(old_parent_inode) = old_child.parent() { - if old_parent_inode != parent_inode { - old_children - .entry(old_parent_inode) - .or_default() - .insert(child.inode()); - self.clear_descendants(child.inode(), &mut edits); - } - } - } - edits.push(Edit::Insert(child)); - } - - // Replace the parent with a clone that includes the children and isn't pending - let mut parent = self.entries.get(&parent_inode).unwrap().clone(); - if let Entry::Dir { - children, pending, .. - } = &mut parent - { - *children = new_children.into(); + let mut parent_entry = self.entries.get(&PathKey(parent_path)).unwrap().clone(); + if let Entry::Dir { pending, .. } = &mut parent_entry { *pending = false; } else { - unreachable!("non-directory parent {}", parent_inode); + unreachable!(); } - edits.push(Edit::Insert(parent)); + edits.push(Edit::Insert(parent_entry)); - // For any children that were re-parented, remove them from their old parents - for (parent_inode, to_remove) in old_children { - let parent = self.entries.get(&parent_inode).unwrap().clone(); - self.remove_children(parent, &mut edits, |inode| to_remove.contains(&inode)); + for entry in entries { + if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { + self.insert_ignore_file(entry.path()); + } + edits.push(Edit::Insert(entry)); } - self.entries.edit(edits); } fn remove_path(&mut self, path: &Path) { - if let Some(entry) = self.entry_for_path(path).cloned() { - let mut edits = Vec::new(); + let new_entries = { + let mut cursor = self.entries.cursor::<_, ()>(); + let mut new_entries = cursor.slice(&PathSearch::Exact(path), SeekBias::Left); + cursor.seek_forward(&PathSearch::Sibling(path), SeekBias::Left); + new_entries.push_tree(cursor.suffix()); + new_entries + }; + self.entries = new_entries; - self.clear_descendants(entry.inode(), &mut edits); - - if let Some(parent_inode) = entry.parent() { - if let Some(file_name) = path.file_name() { - if file_name == GITIGNORE { - self.remove_ignore_file(parent_inode); - } - } - let parent = self.entries.get(&parent_inode).unwrap().clone(); - self.remove_children(parent, &mut edits, |inode| inode == entry.inode()); - } - - edits.push(Edit::Remove(entry.inode())); - - self.entries.edit(edits); - } - } - - fn clear_descendants(&mut self, inode: u64, edits: &mut Vec>) { - let mut stack = vec![inode]; - while let Some(inode) = stack.pop() { - let mut has_gitignore = false; - if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() { - for (child_inode, child_name) in children.iter() { - if **child_name == *GITIGNORE { - has_gitignore = true; - } - edits.push(Edit::Remove(*child_inode)); - stack.push(*child_inode); - } - } - if has_gitignore { - self.remove_ignore_file(inode); + if path.file_name() == Some(&GITIGNORE) { + if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { + *scan_id = self.scan_id; } } } - fn remove_children( - &mut self, - mut parent: Entry, - edits: &mut Vec>, - predicate: impl Fn(u64) -> bool, - ) { - if let Entry::Dir { children, .. } = &mut parent { - *children = children - .iter() - .filter(|(inode, _)| !predicate(*inode)) - .cloned() - .collect::>() - .into(); - } else { - unreachable!("non-directory parent"); - } - edits.push(Edit::Insert(parent)); - } - - fn insert_ignore_file(&mut self, dir_inode: u64) { - let mut path = self.path.to_path_buf(); - path.push(self.path_for_inode(dir_inode, false).unwrap()); - path.push(GITIGNORE); - let (ignore, err) = Gitignore::new(&path); + fn insert_ignore_file(&mut self, path: &Path) { + let root_path = self.path.parent().unwrap_or(Path::new("")); + let (ignore, err) = Gitignore::new(root_path.join(path)); if let Some(err) = err { - log::info!("error in ignore file {:?} - {:?}", path, err); + log::error!("error in ignore file {:?} - {:?}", path, err); } + let ignore_parent_path = path.parent().unwrap().into(); self.ignores - .insert(dir_inode, (Arc::new(ignore), self.scan_id)); - } - - fn remove_ignore_file(&mut self, dir_inode: u64) { - self.ignores.remove(&dir_inode); - } - - fn fmt_entry( - &self, - f: &mut fmt::Formatter<'_>, - ino: u64, - name: &OsStr, - indent: usize, - ) -> fmt::Result { - match self.entries.get(&ino).unwrap() { - Entry::Dir { children, .. } => { - write!( - f, - "{}{}/ ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - ino - )?; - for (child_inode, child_name) in children.iter() { - self.fmt_entry(f, *child_inode, child_name, indent + 2)?; - } - Ok(()) - } - Entry::File { .. } => write!( - f, - "{}{} ({})\n", - " ".repeat(indent), - name.to_string_lossy(), - ino - ), - } + .insert(ignore_parent_path, (Arc::new(ignore), self.scan_id)); } } impl fmt::Debug for Snapshot { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(root_ino) = self.root_inode { - self.fmt_entry(f, root_ino, self.path.file_name().unwrap(), 0) - } else { - write!(f, "Empty tree\n") + for entry in self.entries.cursor::<(), ()>() { + for _ in entry.path().ancestors().skip(1) { + write!(f, " ")?; + } + writeln!(f, "{:?} (inode: {})", entry.path(), entry.inode())?; } + Ok(()) } } @@ -578,23 +386,29 @@ impl FileHandle { #[derive(Clone, Debug)] pub enum Entry { Dir { + path: Arc, inode: u64, - parent: Option, is_symlink: bool, - children: Arc<[(u64, Arc)]>, pending: bool, is_ignored: Option, }, File { + path: Arc, inode: u64, - parent: Option, is_symlink: bool, - path: PathEntry, + path_entry: PathEntry, is_ignored: Option, }, } impl Entry { + fn path(&self) -> &Arc { + match self { + Entry::Dir { path, .. } => path, + Entry::File { path, .. } => path, + } + } + fn is_ignored(&self) -> Option { match self { Entry::Dir { is_ignored, .. } => *is_ignored, @@ -619,13 +433,6 @@ impl Entry { fn is_dir(&self) -> bool { matches!(self, Entry::Dir { .. }) } - - fn parent(&self) -> Option { - match self { - Entry::Dir { parent, .. } => *parent, - Entry::File { parent, .. } => *parent, - } - } } impl sum_tree::Item for Entry { @@ -647,7 +454,7 @@ impl sum_tree::Item for Entry { } EntrySummary { - max_ino: self.inode(), + max_path: self.path().clone(), file_count, visible_file_count, recompute_ignore_status: self.is_ignored().is_none(), @@ -656,33 +463,93 @@ impl sum_tree::Item for Entry { } impl sum_tree::KeyedItem for Entry { - type Key = u64; + type Key = PathKey; fn key(&self) -> Self::Key { - self.inode() + PathKey(self.path().clone()) } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct EntrySummary { - max_ino: u64, + max_path: Arc, file_count: usize, visible_file_count: usize, recompute_ignore_status: bool, } +impl Default for EntrySummary { + fn default() -> Self { + Self { + max_path: Arc::from(Path::new("")), + file_count: 0, + visible_file_count: 0, + recompute_ignore_status: false, + } + } +} + impl<'a> AddAssign<&'a EntrySummary> for EntrySummary { fn add_assign(&mut self, rhs: &'a EntrySummary) { - self.max_ino = rhs.max_ino; + self.max_path = rhs.max_path.clone(); self.file_count += rhs.file_count; self.visible_file_count += rhs.visible_file_count; self.recompute_ignore_status |= rhs.recompute_ignore_status; } } -impl<'a> sum_tree::Dimension<'a, EntrySummary> for u64 { +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct PathKey(Arc); + +impl Default for PathKey { + fn default() -> Self { + Self(Path::new("").into()) + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { fn add_summary(&mut self, summary: &'a EntrySummary) { - *self = summary.max_ino; + self.0 = summary.max_path.clone(); + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum PathSearch<'a> { + Exact(&'a Path), + Sibling(&'a Path), +} + +impl<'a> Ord for PathSearch<'a> { + fn cmp(&self, other: &Self) -> cmp::Ordering { + match (self, other) { + (Self::Exact(a), Self::Exact(b)) => a.cmp(b), + (Self::Sibling(a), Self::Exact(b)) => { + if b.starts_with(a) { + cmp::Ordering::Greater + } else { + a.cmp(b) + } + } + _ => todo!("not sure we need the other two cases"), + } + } +} + +impl<'a> PartialOrd for PathSearch<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl<'a> Default for PathSearch<'a> { + fn default() -> Self { + Self::Exact(Path::new("").into()) + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathSearch<'a> { + fn add_summary(&mut self, summary: &'a EntrySummary) { + *self = Self::Exact(summary.max_path.as_ref()); } } @@ -780,30 +647,23 @@ impl BackgroundScanner { let metadata = fs::metadata(&path)?; let inode = metadata.ino(); let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); - let name = Arc::from(path.file_name().unwrap_or(OsStr::new("/"))); - let relative_path = PathBuf::from(&name); + let name: Arc = path.file_name().unwrap_or(OsStr::new("/")).into(); + let relative_path: Arc = Arc::from((*name).as_ref()); if metadata.file_type().is_dir() { let dir_entry = Entry::Dir { - parent: None, + path: relative_path.clone(), inode, is_symlink, - children: Arc::from([]), pending: true, is_ignored: None, }; - - { - let mut snapshot = self.snapshot.lock(); - snapshot.insert_entry(None, dir_entry); - snapshot.root_inode = Some(inode); - } + self.snapshot.lock().insert_entry(dir_entry); let (tx, rx) = crossbeam_channel::unbounded(); tx.send(ScanJob { - inode, - path: path.clone(), + path: path.to_path_buf(), relative_path, scan_queue: tx.clone(), }) @@ -822,18 +682,13 @@ impl BackgroundScanner { } }); } else { - let mut snapshot = self.snapshot.lock(); - snapshot.insert_entry( - None, - Entry::File { - parent: None, - path: PathEntry::new(inode, &relative_path), - inode, - is_symlink, - is_ignored: None, - }, - ); - snapshot.root_inode = Some(inode); + self.snapshot.lock().insert_entry(Entry::File { + path_entry: PathEntry::new(inode, &relative_path), + path: relative_path, + inode, + is_symlink, + is_ignored: None, + }); } self.recompute_ignore_statuses(); @@ -848,7 +703,7 @@ impl BackgroundScanner { for child_entry in fs::read_dir(&job.path)? { let child_entry = child_entry?; let child_name: Arc = child_entry.file_name().into(); - let child_relative_path = job.relative_path.join(child_name.as_ref()); + let child_relative_path: Arc = job.relative_path.join(child_name.as_ref()).into(); let child_metadata = child_entry.metadata()?; let child_inode = child_metadata.ino(); let child_is_symlink = child_metadata.file_type().is_symlink(); @@ -860,38 +715,32 @@ impl BackgroundScanner { } if child_metadata.is_dir() { - new_entries.push(( - child_name, - Entry::Dir { - parent: Some(job.inode), - inode: child_inode, - is_symlink: child_is_symlink, - children: Arc::from([]), - pending: true, - is_ignored: None, - }, - )); - new_jobs.push(ScanJob { + new_entries.push(Entry::Dir { + path: child_relative_path.clone(), inode: child_inode, - path: Arc::from(child_path), + is_symlink: child_is_symlink, + pending: true, + is_ignored: None, + }); + new_jobs.push(ScanJob { + path: child_path, relative_path: child_relative_path, scan_queue: job.scan_queue.clone(), }); } else { - new_entries.push(( - child_name, - Entry::File { - parent: Some(job.inode), - path: PathEntry::new(child_inode, &child_relative_path), - inode: child_inode, - is_symlink: child_is_symlink, - is_ignored: None, - }, - )); + new_entries.push(Entry::File { + path_entry: PathEntry::new(child_inode, &child_relative_path), + path: child_relative_path, + inode: child_inode, + is_symlink: child_is_symlink, + is_ignored: None, + }); }; } - self.snapshot.lock().populate_dir(job.inode, new_entries); + self.snapshot + .lock() + .populate_dir(job.relative_path.clone(), new_entries); for new_job in new_jobs { job.scan_queue.send(new_job).unwrap(); } @@ -915,13 +764,14 @@ impl BackgroundScanner { let mut paths = events.into_iter().map(|e| e.path).peekable(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); while let Some(path) = paths.next() { - let relative_path = match path.strip_prefix(&root_path) { - Ok(relative_path) => relative_path.to_path_buf(), - Err(_) => { - log::error!("unexpected event {:?} for root path {:?}", path, root_path); - continue; - } - }; + let relative_path = + match path.strip_prefix(&root_path.parent().unwrap_or(Path::new(""))) { + Ok(relative_path) => relative_path.to_path_buf(), + Err(_) => { + log::error!("unexpected event {:?} for root path {:?}", path, root_path); + continue; + } + }; while paths.peek().map_or(false, |p| p.starts_with(&path)) { paths.next(); @@ -932,18 +782,12 @@ impl BackgroundScanner { match self.fs_entry_for_path(&root_path, &path) { Ok(Some(fs_entry)) => { let is_dir = fs_entry.is_dir(); - let inode = fs_entry.inode(); - - snapshot.insert_entry(path.file_name(), fs_entry); + snapshot.insert_entry(fs_entry); if is_dir { scan_queue_tx .send(ScanJob { - inode, - path: Arc::from(path), - relative_path: snapshot - .root_name() - .map_or(PathBuf::new(), PathBuf::from) - .join(relative_path), + path, + relative_path: relative_path.into(), scan_queue: scan_queue_tx.clone(), }) .unwrap(); @@ -984,43 +828,35 @@ impl BackgroundScanner { } fn compute_ignore_status_for_new_ignores(&self) { - struct IgnoreJob { - inode: u64, - ignore_queue_tx: crossbeam_channel::Sender, - } + let mut snapshot = self.snapshot(); - 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)) + let mut ignores_to_delete = Vec::new(); + let mut changed_ignore_parents = Vec::new(); + for (parent_path, (_, scan_id)) in &snapshot.ignores { + let prev_ignore_parent = changed_ignore_parents.last(); + if *scan_id == snapshot.scan_id + && prev_ignore_parent.map_or(true, |l| !parent_path.starts_with(l)) { - new_ignores.next().unwrap(); + changed_ignore_parents.push(parent_path.clone()); } - ignore_queue_tx - .send(IgnoreJob { - inode: parent_inode, - ignore_queue_tx: ignore_queue_tx.clone(), - }) - .unwrap(); + let ignore_parent_exists = snapshot.entry_for_path(parent_path).is_some(); + let ignore_exists = snapshot + .entry_for_path(parent_path.join(&*GITIGNORE)) + .is_some(); + if !ignore_parent_exists || !ignore_exists { + ignores_to_delete.push(parent_path.clone()); + } } - drop(ignore_queue_tx); + for parent_path in ignores_to_delete { + snapshot.ignores.remove(&parent_path); + self.snapshot.lock().ignores.remove(&parent_path); + } + + 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() { @@ -1032,24 +868,28 @@ impl BackgroundScanner { } }); - for _ in 0..self.thread_pool.thread_count() - 1 { + scope.execute(|| { + let entries_tx = entries_tx; + let mut cursor = snapshot.entries.cursor::<_, ()>(); + for ignore_parent_path in &changed_ignore_parents { + cursor.seek(&PathSearch::Exact(ignore_parent_path), SeekBias::Right); + while let Some(entry) = cursor.item() { + if entry.path().starts_with(ignore_parent_path) { + entries_tx.send(entry.clone()).unwrap(); + cursor.next(); + } else { + break; + } + } + } + }); + + 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(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(); - } - } - + while let Ok(mut entry) = entries_rx.recv() { + entry.set_ignored(snapshot.is_path_ignored(entry.path()).unwrap()); edits_tx.send(Edit::Insert(entry)).unwrap(); } }); @@ -1089,7 +929,7 @@ impl BackgroundScanner { 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()); + entry.set_ignored(snapshot.is_path_ignored(entry.path()).unwrap()); edits_tx.send(Edit::Insert(entry)).unwrap(); } }); @@ -1114,36 +954,24 @@ impl BackgroundScanner { .context("failed to read symlink metadata")? .file_type() .is_symlink(); - let parent = if path == root_path { - None - } else { - Some( - fs::metadata(path.parent().unwrap()) - .context("failed to read parent inode")? - .ino(), - ) - }; + let relative_path_with_root = root_path + .parent() + .map_or(path, |parent| path.strip_prefix(parent).unwrap()); let entry = if metadata.file_type().is_dir() { Entry::Dir { + path: Arc::from(relative_path_with_root), inode, - parent, is_symlink, pending: true, - children: Arc::from([]), is_ignored: None, } } else { Entry::File { + path_entry: PathEntry::new(inode, relative_path_with_root), + path: Arc::from(relative_path_with_root), inode, - parent, is_symlink, - path: PathEntry::new( - inode, - root_path - .parent() - .map_or(path, |parent| path.strip_prefix(parent).unwrap()), - ), is_ignored: None, } }; @@ -1153,9 +981,8 @@ impl BackgroundScanner { } struct ScanJob { - inode: u64, - path: Arc, - relative_path: PathBuf, + path: PathBuf, + relative_path: Arc, scan_queue: crossbeam_channel::Sender, } @@ -1243,7 +1070,7 @@ fn mounted_volume_paths() -> Vec { .collect() } else { panic!("failed to run getmntinfo"); - } + } } } @@ -1457,7 +1284,6 @@ mod tests { id: 0, scan_id: 0, path: root_dir.path().into(), - root_inode: None, entries: Default::default(), ignores: Default::default(), })), @@ -1491,7 +1317,6 @@ mod tests { id: 0, scan_id: 0, path: root_dir.path().into(), - root_inode: None, entries: Default::default(), ignores: Default::default(), })), @@ -1537,7 +1362,7 @@ mod tests { record_event(new_path); } else if rng.gen_bool(0.05) { let ignore_dir_path = dirs.choose(rng).unwrap(); - let ignore_path = ignore_dir_path.join(GITIGNORE); + let ignore_path = ignore_dir_path.join(&*GITIGNORE); let (subdirs, subfiles) = read_dir_recursive(ignore_dir_path.clone()); let files_to_ignore = { @@ -1654,48 +1479,45 @@ mod tests { fn check_invariants(&self) { let mut files = self.files(0); let mut visible_files = self.visible_files(0); - for entry in self.entries.items() { - let path = self.path_for_inode(entry.inode(), false).unwrap(); - assert_eq!(self.inode_for_path(path).unwrap(), entry.inode()); - + for entry in self.entries.cursor::<(), ()>() { if let Entry::File { inode, is_ignored, .. } = entry { - assert_eq!(files.next().unwrap().inode(), inode); + assert_eq!(files.next().unwrap().inode(), *inode); if !is_ignored.unwrap() { - assert_eq!(visible_files.next().unwrap().inode(), inode); + assert_eq!(visible_files.next().unwrap().inode(), *inode); } } } assert!(files.next().is_none()); assert!(visible_files.next().is_none()); + + for (ignore_parent_path, _) in &self.ignores { + assert!(self.entry_for_path(ignore_parent_path).is_some()); + assert!(self + .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) + .is_some()); + } } - fn to_vec(&self) -> Vec<(PathBuf, u64)> { + fn to_vec(&self) -> Vec<(&Path, u64, bool)> { use std::iter::FromIterator; let mut paths = Vec::new(); - - let mut stack = Vec::new(); - stack.extend(self.root_inode); - while let Some(inode) = stack.pop() { - let computed_path = self.path_for_inode(inode, true).unwrap(); - match self.entries.get(&inode).unwrap() { - Entry::Dir { children, .. } => { - stack.extend(children.iter().map(|c| c.0)); - } - Entry::File { path, .. } => { - assert_eq!( - String::from_iter(path.path.iter()), - computed_path.to_str().unwrap() - ); - } + for entry in self.entries.cursor::<(), ()>() { + paths.push(( + entry.path().as_ref(), + entry.inode(), + entry.is_ignored().unwrap(), + )); + if let Entry::File { path_entry, .. } = entry { + assert_eq!( + String::from_iter(path_entry.path.iter()), + entry.path().to_str().unwrap() + ); } - paths.push((computed_path, inode)); } - - assert_eq!(paths.len(), self.entries.items().len()); paths.sort_by(|a, b| a.0.cmp(&b.0)); paths } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 85cafd8441..d34d963fc7 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -124,7 +124,10 @@ where snapshot.visible_files(start).take(end - start) }; let path_entries = entries.map(|entry| { - if let Entry::File { path, .. } = entry { + if let Entry::File { + path_entry: path, .. + } = entry + { path } else { unreachable!() @@ -133,7 +136,7 @@ where let skipped_prefix_len = if include_root_name { 0 - } else if let Some(Entry::Dir { .. }) = snapshot.root_entry() { + } else if let Entry::Dir { .. } = snapshot.root_entry() { if let Some(name) = snapshot.root_name() { name.to_string_lossy().chars().count() + 1 } else { @@ -514,7 +517,6 @@ mod tests { id: 0, scan_id: 0, path: PathBuf::new().into(), - root_inode: None, ignores: Default::default(), entries: Default::default(), }, From ec2e1c3045983ce92e0c1e06a929f55798d448a6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 23 Apr 2021 17:13:31 +0200 Subject: [PATCH 087/102] Rename `PathSearch::Sibling` to `PathSearch::Successor` Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 7354c06d5b..a402d6f41c 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -323,7 +323,7 @@ impl Snapshot { let new_entries = { let mut cursor = self.entries.cursor::<_, ()>(); let mut new_entries = cursor.slice(&PathSearch::Exact(path), SeekBias::Left); - cursor.seek_forward(&PathSearch::Sibling(path), SeekBias::Left); + cursor.seek_forward(&PathSearch::Successor(path), SeekBias::Left); new_entries.push_tree(cursor.suffix()); new_entries }; @@ -516,14 +516,14 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum PathSearch<'a> { Exact(&'a Path), - Sibling(&'a Path), + Successor(&'a Path), } impl<'a> Ord for PathSearch<'a> { fn cmp(&self, other: &Self) -> cmp::Ordering { match (self, other) { (Self::Exact(a), Self::Exact(b)) => a.cmp(b), - (Self::Sibling(a), Self::Exact(b)) => { + (Self::Successor(a), Self::Exact(b)) => { if b.starts_with(a) { cmp::Ordering::Greater } else { From c9d7249305f36642ef41d58eb61d7136ada410e2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 23 Apr 2021 11:37:23 -0600 Subject: [PATCH 088/102] WIP Co-Authored-By: Max Brunsfeld --- zed/src/editor/buffer/mod.rs | 10 +-- zed/src/editor/buffer_view.rs | 5 +- zed/src/file_finder.rs | 27 +++--- zed/src/workspace/pane.rs | 8 +- zed/src/workspace/workspace.rs | 47 ++++++---- zed/src/workspace/workspace_view.rs | 69 +++++++------- zed/src/worktree.rs | 134 +++++++++++----------------- zed/src/worktree/fuzzy.rs | 47 ++++++---- 8 files changed, 176 insertions(+), 171 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 14553c9119..19d2af917b 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -18,7 +18,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext}; +use gpui::{Entity, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -26,7 +26,7 @@ use std::{ hash::BuildHasher, iter::{self, Iterator}, ops::{AddAssign, Range}, - path::PathBuf, + path::Path, str, sync::Arc, time::{Duration, Instant}, @@ -429,11 +429,11 @@ impl Buffer { } } - pub fn path(&self, app: &AppContext) -> Option { - self.file.as_ref().map(|file| file.path(app)) + pub fn path(&self) -> Option<&Arc> { + self.file.as_ref().map(|file| file.path()) } - pub fn entry_id(&self) -> Option<(usize, u64)> { + pub fn entry_id(&self) -> Option<(usize, Arc)> { self.file.as_ref().map(|file| file.entry_id()) } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 63a2f278fa..b3c2eb0496 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -20,6 +20,7 @@ use std::{ fmt::Write, iter::FromIterator, ops::Range, + path::Path, sync::Arc, time::Duration, }; @@ -1375,7 +1376,7 @@ impl workspace::ItemView for BufferView { } fn title(&self, app: &AppContext) -> std::string::String { - if let Some(path) = self.buffer.read(app).path(app) { + if let Some(path) = self.buffer.read(app).path() { path.file_name() .expect("buffer's path is always to a file") .to_string_lossy() @@ -1385,7 +1386,7 @@ impl workspace::ItemView for BufferView { } } - fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> { + fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)> { self.buffer.read(app).entry_id() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index d959cae312..cfddd00a84 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -14,7 +14,7 @@ use gpui::{ AppContext, Axis, Border, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use std::{cmp, path::Path}; +use std::{cmp, path::Path, sync::Arc}; pub struct FileFinder { handle: WeakViewHandle, @@ -44,7 +44,7 @@ pub fn init(app: &mut MutableAppContext) { } pub enum Event { - Selected(usize, u64), + Selected(usize, Arc), Dismissed, } @@ -137,18 +137,18 @@ impl FileFinder { app: &AppContext, ) -> Option { let tree_id = path_match.tree_id; - let entry_id = path_match.entry_id; self.worktree(tree_id, app).map(|_| { - let path = &path_match.path; - let file_name = Path::new(path) + let path = path_match.path.clone(); + let path_string = &path_match.path_string; + let file_name = Path::new(&path_string) .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); let path_positions = path_match.positions.clone(); - let file_name_start = path.chars().count() - file_name.chars().count(); + let file_name_start = path_string.chars().count() - file_name.chars().count(); let mut file_name_positions = Vec::new(); file_name_positions.extend(path_positions.iter().filter_map(|pos| { if pos >= &file_name_start { @@ -191,7 +191,7 @@ impl FileFinder { ) .with_child( Label::new( - path.into(), + path_string.into(), settings.ui_font_family, settings.ui_font_size, ) @@ -217,7 +217,7 @@ impl FileFinder { EventHandler::new(container.boxed()) .on_mouse_down(move |ctx| { - ctx.dispatch_action("file_finder:select", (tree_id, entry_id)); + ctx.dispatch_action("file_finder:select", (tree_id, path.clone())); true }) .named("match") @@ -245,8 +245,8 @@ impl FileFinder { ctx: &mut ViewContext, ) { match event { - Event::Selected(tree_id, entry_id) => { - workspace_view.open_entry((*tree_id, *entry_id), ctx); + Event::Selected(tree_id, path) => { + workspace_view.open_entry((*tree_id, path.clone()), ctx); workspace_view.dismiss_modal(ctx); } Event::Dismissed => { @@ -329,13 +329,12 @@ impl FileFinder { fn confirm(&mut self, _: &(), ctx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected) { - ctx.emit(Event::Selected(m.tree_id, m.entry_id)); + ctx.emit(Event::Selected(m.tree_id, m.path.clone())); } } - fn select(&mut self, entry: &(usize, u64), ctx: &mut ViewContext) { - let (tree_id, entry_id) = *entry; - ctx.emit(Event::Selected(tree_id, entry_id)); + fn select(&mut self, (tree_id, path): &(usize, Arc), ctx: &mut ViewContext) { + ctx.emit(Event::Selected(*tree_id, path.clone())); } fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index ea3226715a..076373cf88 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -7,7 +7,7 @@ use gpui::{ keymap::Binding, AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext, }; -use std::cmp; +use std::{cmp, path::Path, sync::Arc}; pub fn init(app: &mut MutableAppContext) { app.add_action( @@ -105,7 +105,11 @@ impl Pane { self.items.get(self.active_item).cloned() } - pub fn activate_entry(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext) -> bool { + pub fn activate_entry( + &mut self, + entry_id: (usize, Arc), + ctx: &mut ViewContext, + ) -> bool { if let Some(index) = self.items.iter().position(|item| { item.entry_id(ctx.as_ref()) .map_or(false, |id| id == entry_id) diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index 50bc959404..eceaf45578 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -138,10 +138,22 @@ impl Workspace { pub fn open_entry( &mut self, - entry: (usize, u64), + (worktree_id, path): (usize, Arc), ctx: &mut ModelContext<'_, Self>, ) -> anyhow::Result + Send>>> { - if let Some(item) = self.items.get(&entry).cloned() { + let worktree = self + .worktrees + .get(&worktree_id) + .cloned() + .ok_or_else(|| anyhow!("worktree {} does not exist", worktree_id,))?; + + let inode = worktree + .read(ctx) + .inode_for_path(&path) + .ok_or_else(|| anyhow!("path {:?} does not exist", path))?; + + let item_key = (worktree_id, inode); + if let Some(item) = self.items.get(&item_key).cloned() { return Ok(async move { match item { OpenedItem::Loaded(handle) => { @@ -159,25 +171,20 @@ impl Workspace { .boxed()); } - let worktree = self - .worktrees - .get(&entry.0) - .cloned() - .ok_or(anyhow!("worktree {} does not exist", entry.0,))?; - let replica_id = self.replica_id; - let file = worktree.file(entry.1, ctx.as_ref())?; + let file = worktree.file(path.clone(), ctx.as_ref())?; let history = file.load_history(ctx.as_ref()); let buffer = async move { Ok(Buffer::from_history(replica_id, file, history.await?)) }; let (mut tx, rx) = watch::channel(None); - self.items.insert(entry, OpenedItem::Loading(rx)); + self.items.insert(item_key, OpenedItem::Loading(rx)); ctx.spawn( buffer, move |me, buffer: anyhow::Result, ctx| match buffer { Ok(buffer) => { let handle = Box::new(ctx.add_model(|_| buffer)) as Box; - me.items.insert(entry, OpenedItem::Loaded(handle.clone())); + me.items + .insert(item_key, OpenedItem::Loaded(handle.clone())); ctx.spawn( async move { tx.update(|value| *value = Some(Ok(handle))).await; @@ -199,7 +206,7 @@ impl Workspace { ) .detach(); - self.open_entry(entry, ctx) + self.open_entry((worktree_id, path), ctx) } fn on_worktree_updated(&mut self, _: ModelHandle, ctx: &mut ModelContext) { @@ -213,18 +220,20 @@ impl Entity for Workspace { #[cfg(test)] pub trait WorkspaceHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)>; + fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)>; } #[cfg(test)] impl WorkspaceHandle for ModelHandle { - fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)> { + fn file_entries(&self, app: &AppContext) -> Vec<(usize, Arc)> { self.read(app) .worktrees() .iter() .flat_map(|tree| { let tree_id = tree.id(); - tree.read(app).files(0).map(move |f| (tree_id, f.inode())) + tree.read(app) + .files(0) + .map(move |f| (tree_id, f.path().clone())) }) .collect::>() } @@ -253,14 +262,14 @@ mod tests { // Get the first file entry. let tree = app.read(|ctx| workspace.read(ctx).worktrees.iter().next().unwrap().clone()); - let file_inode = app.read(|ctx| tree.read(ctx).files(0).next().unwrap().inode()); - let entry = (tree.id(), file_inode); + let path = app.read(|ctx| tree.read(ctx).files(0).next().unwrap().path().clone()); + let entry = (tree.id(), path); // Open the same entry twice before it finishes loading. let (future_1, future_2) = workspace.update(&mut app, |w, app| { ( - w.open_entry(entry, app).unwrap(), - w.open_entry(entry, app).unwrap(), + w.open_entry(entry.clone(), app).unwrap(), + w.open_entry(entry.clone(), app).unwrap(), ) }); diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 205806490c..f8ca8822b2 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -6,7 +6,11 @@ use gpui::{ ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use log::error; -use std::{collections::HashSet, path::PathBuf}; +use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::Arc, +}; pub fn init(app: &mut MutableAppContext) { app.add_action("workspace:save", WorkspaceView::save_active_item); @@ -19,7 +23,7 @@ pub fn init(app: &mut MutableAppContext) { pub trait ItemView: View { fn title(&self, app: &AppContext) -> String; - fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>; + fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)>; fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, @@ -42,7 +46,7 @@ pub trait ItemView: View { pub trait ItemViewHandle: Send + Sync { fn title(&self, app: &AppContext) -> String; - fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>; + fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)>; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, app: &mut MutableAppContext) -> Option>; fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext); @@ -57,7 +61,7 @@ impl ItemViewHandle for ViewHandle { self.read(app).title(app) } - fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> { + fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc)> { self.read(app).entry_id(app) } @@ -124,7 +128,7 @@ pub struct WorkspaceView { center: PaneGroup, panes: Vec>, active_pane: ViewHandle, - loading_entries: HashSet<(usize, u64)>, + loading_entries: HashSet<(usize, Arc)>, } impl WorkspaceView { @@ -189,24 +193,23 @@ impl WorkspaceView { } } - pub fn open_entry(&mut self, entry: (usize, u64), ctx: &mut ViewContext) { + pub fn open_entry(&mut self, entry: (usize, Arc), ctx: &mut ViewContext) { if self.loading_entries.contains(&entry) { return; } if self .active_pane() - .update(ctx, |pane, ctx| pane.activate_entry(entry, ctx)) + .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx)) { return; } - self.loading_entries.insert(entry); + self.loading_entries.insert(entry.clone()); - match self - .workspace - .update(ctx, |workspace, ctx| workspace.open_entry(entry, ctx)) - { + match self.workspace.update(ctx, |workspace, ctx| { + workspace.open_entry(entry.clone(), ctx) + }) { Err(error) => error!("{}", error), Ok(item) => { let settings = self.settings.clone(); @@ -396,32 +399,35 @@ mod tests { app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0]; - let file2 = entries[1]; - let file3 = entries[2]; + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); let (_, workspace_view) = app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); let pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); // Open the first entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); pane.condition(&app, |pane, _| pane.items().len() == 1) .await; // Open the second entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(file2, ctx)); + workspace_view.update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)); pane.condition(&app, |pane, _| pane.items().len() == 2) .await; app.read(|ctx| { let pane = pane.read(ctx); - assert_eq!(pane.active_item().unwrap().entry_id(ctx), Some(file2)); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file2.clone()) + ); }); // Open the first entry again - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); pane.condition(&app, move |pane, ctx| { - pane.active_item().unwrap().entry_id(ctx) == Some(file1) + pane.active_item().unwrap().entry_id(ctx) == Some(file1.clone()) }) .await; app.read(|ctx| { @@ -430,8 +436,8 @@ mod tests { // Open the third entry twice concurrently workspace_view.update(&mut app, |w, ctx| { - w.open_entry(file3, ctx); - w.open_entry(file3, ctx); + w.open_entry(file3.clone(), ctx); + w.open_entry(file3.clone(), ctx); }); pane.condition(&app, |pane, _| pane.items().len() == 3) .await; @@ -456,18 +462,21 @@ mod tests { app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0]; + let file1 = entries[0].clone(); let (window_id, workspace_view) = app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); let pane_1 = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1, ctx)); - pane_1 - .condition(&app, move |pane, ctx| { - pane.active_item().and_then(|i| i.entry_id(ctx)) == Some(file1) - }) - .await; + workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); + { + let file1 = file1.clone(); + pane_1 + .condition(&app, move |pane, ctx| { + pane.active_item().and_then(|i| i.entry_id(ctx)) == Some(file1.clone()) + }) + .await; + } app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); app.update(|ctx| { @@ -475,7 +484,7 @@ mod tests { assert_ne!(pane_1, pane_2); let pane2_item = pane_2.read(ctx).active_item().unwrap(); - assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1)); + assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); let workspace_view = workspace_view.read(ctx); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index a402d6f41c..0f5b8fd0ef 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -54,7 +54,7 @@ pub struct Worktree { #[derive(Clone)] pub struct FileHandle { worktree: ModelHandle, - inode: u64, + path: Arc, } impl Worktree { @@ -152,25 +152,14 @@ impl Worktree { path.starts_with(&self.snapshot.path) } - pub fn has_inode(&self, inode: u64) -> bool { - todo!() - // self.snapshot.entries.get(&inode).is_some() - } - - pub fn abs_path_for_inode(&self, ino: u64) -> Result { - let mut result = self.snapshot.path.to_path_buf(); - result.push(self.path_for_inode(ino, false)?); - Ok(result) - } - pub fn load_history( &self, - ino: u64, + relative_path: &Path, ctx: &AppContext, ) -> impl Future> { - let path = self.abs_path_for_inode(ino); + let path = self.snapshot.path.join(relative_path); ctx.background_executor().spawn(async move { - let mut file = std::fs::File::open(&path?)?; + let mut file = std::fs::File::open(&path)?; let mut base_text = String::new(); file.read_to_string(&mut base_text)?; Ok(History::new(Arc::from(base_text))) @@ -179,14 +168,14 @@ impl Worktree { pub fn save<'a>( &self, - ino: u64, + relative_path: &Path, content: BufferSnapshot, ctx: &AppContext, ) -> Task> { - let path = self.abs_path_for_inode(ino); + let path = self.snapshot.path.join(relative_path); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); - let file = std::fs::File::create(&path?)?; + let file = std::fs::File::create(&path)?; let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); for chunk in content.fragments() { writer.write(chunk.as_bytes())?; @@ -258,7 +247,7 @@ impl Snapshot { } } - fn inode_for_path(&self, path: impl AsRef) -> Option { + pub fn inode_for_path(&self, path: impl AsRef) -> Option { self.entry_for_path(path.as_ref()).map(|e| e.inode()) } @@ -288,10 +277,6 @@ impl Snapshot { } } - pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result { - todo!("this method should go away") - } - fn insert_entry(&mut self, entry: Entry) { if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { self.insert_ignore_file(entry.path()); @@ -362,24 +347,21 @@ impl fmt::Debug for Snapshot { } impl FileHandle { - pub fn path(&self, ctx: &AppContext) -> PathBuf { - self.worktree - .read(ctx) - .path_for_inode(self.inode, false) - .unwrap() + pub fn path(&self) -> &Arc { + &self.path } pub fn load_history(&self, ctx: &AppContext) -> impl Future> { - self.worktree.read(ctx).load_history(self.inode, ctx) + self.worktree.read(ctx).load_history(&self.path, ctx) } pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task> { let worktree = self.worktree.read(ctx); - worktree.save(self.inode, content, ctx) + worktree.save(&self.path, content, ctx) } - pub fn entry_id(&self) -> (usize, u64) { - (self.worktree.id(), self.inode) + pub fn entry_id(&self) -> (usize, Arc) { + (self.worktree.id(), self.path.clone()) } } @@ -402,13 +384,20 @@ pub enum Entry { } impl Entry { - fn path(&self) -> &Arc { + pub fn path(&self) -> &Arc { match self { Entry::Dir { path, .. } => path, Entry::File { path, .. } => path, } } + pub fn inode(&self) -> u64 { + match self { + Entry::Dir { inode, .. } => *inode, + Entry::File { inode, .. } => *inode, + } + } + fn is_ignored(&self) -> Option { match self { Entry::Dir { is_ignored, .. } => *is_ignored, @@ -423,13 +412,6 @@ impl Entry { } } - pub fn inode(&self) -> u64 { - match self { - Entry::Dir { inode, .. } => *inode, - Entry::File { inode, .. } => *inode, - } - } - fn is_dir(&self) -> bool { matches!(self, Entry::Dir { .. }) } @@ -683,7 +665,7 @@ impl BackgroundScanner { }); } else { self.snapshot.lock().insert_entry(Entry::File { - path_entry: PathEntry::new(inode, &relative_path), + path_entry: PathEntry::new(inode, relative_path.clone()), path: relative_path, inode, is_symlink, @@ -729,7 +711,7 @@ impl BackgroundScanner { }); } else { new_entries.push(Entry::File { - path_entry: PathEntry::new(child_inode, &child_relative_path), + path_entry: PathEntry::new(child_inode, child_relative_path.clone()), path: child_relative_path, inode: child_inode, is_symlink: child_is_symlink, @@ -956,11 +938,12 @@ impl BackgroundScanner { .is_symlink(); let relative_path_with_root = root_path .parent() - .map_or(path, |parent| path.strip_prefix(parent).unwrap()); + .map_or(path, |parent| path.strip_prefix(parent).unwrap()) + .into(); let entry = if metadata.file_type().is_dir() { Entry::Dir { - path: Arc::from(relative_path_with_root), + path: relative_path_with_root, inode, is_symlink, pending: true, @@ -968,8 +951,8 @@ impl BackgroundScanner { } } else { Entry::File { - path_entry: PathEntry::new(inode, relative_path_with_root), - path: Arc::from(relative_path_with_root), + path_entry: PathEntry::new(inode, relative_path_with_root.clone()), + path: relative_path_with_root, inode, is_symlink, is_ignored: None, @@ -987,19 +970,18 @@ struct ScanJob { } pub trait WorktreeHandle { - fn file(&self, entry_id: u64, app: &AppContext) -> Result; + fn file(&self, path: impl AsRef, app: &AppContext) -> Result; } impl WorktreeHandle for ModelHandle { - fn file(&self, inode: u64, app: &AppContext) -> Result { - if self.read(app).has_inode(inode) { - Ok(FileHandle { + fn file(&self, path: impl AsRef, app: &AppContext) -> Result { + self.read(app) + .entry_for_path(&path) + .map(|entry| FileHandle { worktree: self.clone(), - inode, + path: entry.path().clone(), }) - } else { - Err(anyhow!("entry does not exist in tree")) - } + .ok_or_else(|| anyhow!("path does not exist in tree")) } } @@ -1125,14 +1107,13 @@ mod tests { ctx.thread_pool().clone(), ) .iter() - .map(|result| tree.path_for_inode(result.entry_id, true)) - .collect::, _>>() - .unwrap(); + .map(|result| result.path.clone()) + .collect::>>(); assert_eq!( results, vec![ - PathBuf::from("root_link/banana/carrot/date"), - PathBuf::from("root_link/banana/carrot/endive"), + PathBuf::from("root_link/banana/carrot/date").into(), + PathBuf::from("root_link/banana/carrot/endive").into(), ] ); }) @@ -1152,25 +1133,15 @@ mod tests { let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); - let file_inode = app.read(|ctx| { - let tree = tree.read(ctx); - let inode = tree.files(0).next().unwrap().inode(); - assert_eq!( - tree.path_for_inode(inode, false) - .unwrap() - .file_name() - .unwrap(), - "file1" - ); - inode - }); - - tree.update(&mut app, |tree, ctx| { - smol::block_on(tree.save(file_inode, buffer.snapshot(), ctx.as_ref())).unwrap() + let path = tree.update(&mut app, |tree, ctx| { + let path = tree.files(0).next().unwrap().path().clone(); + assert_eq!(path.file_name().unwrap(), "file1"); + smol::block_on(tree.save(&path, buffer.snapshot(), ctx.as_ref())).unwrap(); + path }); let loaded_history = app - .read(|ctx| tree.read(ctx).load_history(file_inode, ctx)) + .read(|ctx| tree.read(ctx).load_history(&path, ctx)) .await .unwrap(); assert_eq!(loaded_history.base_text.as_ref(), buffer.text()); @@ -1196,15 +1167,16 @@ mod tests { app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 2)); let file2 = app.read(|ctx| { - let inode = tree.read(ctx).inode_for_path("b/c/file2").unwrap(); - let file2 = tree.file(inode, ctx).unwrap(); - assert_eq!(file2.path(ctx), Path::new("b/c/file2")); + let file2 = tree.file("b/c/file2", ctx).unwrap(); + assert_eq!(file2.path().as_ref(), Path::new("b/c/file2")); file2 }); std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - tree.condition(&app, move |_, ctx| file2.path(ctx) == Path::new("d/file2")) - .await; + tree.condition(&app, move |_, _| { + file2.path().as_ref() == Path::new("d/file2") + }) + .await; }); } @@ -1513,7 +1485,7 @@ mod tests { )); if let Entry::File { path_entry, .. } = entry { assert_eq!( - String::from_iter(path_entry.path.iter()), + String::from_iter(path_entry.path_chars.iter()), entry.path().to_str().unwrap() ); } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index d34d963fc7..5b1a61e8c0 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -14,20 +14,22 @@ const MIN_DISTANCE_PENALTY: f64 = 0.2; #[derive(Clone, Debug)] pub struct PathEntry { pub ino: u64, - pub path_chars: CharBag, - pub path: Arc<[char]>, + pub char_bag: CharBag, + pub path_chars: Arc<[char]>, + pub path: Arc, pub lowercase_path: Arc<[char]>, } impl PathEntry { - pub fn new(ino: u64, path: &Path) -> Self { - let path = path.to_string_lossy(); - let lowercase_path = path.to_lowercase().chars().collect::>().into(); - let path: Arc<[char]> = path.chars().collect::>().into(); - let path_chars = CharBag::from(path.as_ref()); + pub fn new(ino: u64, path: Arc) -> Self { + let path_str = path.to_string_lossy(); + let lowercase_path = path_str.to_lowercase().chars().collect::>().into(); + let path_chars: Arc<[char]> = path_str.chars().collect::>().into(); + let char_bag = CharBag::from(path_chars.as_ref()); Self { ino, + char_bag, path_chars, path, lowercase_path, @@ -39,9 +41,9 @@ impl PathEntry { pub struct PathMatch { pub score: f64, pub positions: Vec, - pub path: String, + pub path_string: String, pub tree_id: usize, - pub entry_id: u64, + pub path: Arc, } impl PartialEq for PathMatch { @@ -199,7 +201,7 @@ fn match_single_tree_paths<'a>( best_position_matrix: &mut Vec, ) { for path_entry in path_entries { - if !path_entry.path_chars.is_superset(query_chars) { + if !path_entry.char_bag.is_superset(query_chars) { continue; } @@ -212,7 +214,7 @@ fn match_single_tree_paths<'a>( continue; } - let matrix_len = query.len() * (path_entry.path.len() - skipped_prefix_len); + let matrix_len = query.len() * (path_entry.path_chars.len() - skipped_prefix_len); score_matrix.clear(); score_matrix.resize(matrix_len, None); best_position_matrix.clear(); @@ -221,7 +223,7 @@ fn match_single_tree_paths<'a>( let score = score_match( &query[..], &lowercase_query[..], - &path_entry.path, + &path_entry.path_chars, &path_entry.lowercase_path, skipped_prefix_len, smart_case, @@ -235,8 +237,12 @@ fn match_single_tree_paths<'a>( if score > 0.0 { results.push(Reverse(PathMatch { tree_id: snapshot.id, - entry_id: path_entry.ino, - path: path_entry.path.iter().skip(skipped_prefix_len).collect(), + path_string: path_entry + .path_chars + .iter() + .skip(skipped_prefix_len) + .collect(), + path: path_entry.path.clone(), score, positions: match_positions.clone(), })); @@ -496,12 +502,13 @@ mod tests { for (i, path) in paths.iter().enumerate() { let lowercase_path: Arc<[char]> = path.to_lowercase().chars().collect::>().into(); - let path_chars = CharBag::from(lowercase_path.as_ref()); - let path = path.chars().collect(); + let char_bag = CharBag::from(lowercase_path.as_ref()); + let path_chars = path.chars().collect(); path_entries.push(PathEntry { ino: i as u64, + char_bag, path_chars, - path, + path: Arc::from(PathBuf::from(path)), lowercase_path, }); } @@ -540,7 +547,11 @@ mod tests { .rev() .map(|result| { ( - paths[result.0.entry_id as usize].clone(), + paths + .iter() + .copied() + .find(|p| result.0.path.as_ref() == Path::new(p)) + .unwrap(), result.0.positions, ) }) From dced9469f5742fac6d9acf1976a610372d91f1a4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 23 Apr 2021 12:47:23 -0600 Subject: [PATCH 089/102] WIP Co-Authored-By: Max Brunsfeld --- zed/src/sum_tree/cursor.rs | 3 + zed/src/workspace/workspace.rs | 4 +- zed/src/worktree.rs | 184 +++++++++++++++++++-------------- zed/src/worktree/fuzzy.rs | 2 +- 4 files changed, 113 insertions(+), 80 deletions(-) diff --git a/zed/src/sum_tree/cursor.rs b/zed/src/sum_tree/cursor.rs index 4f6ec81055..8b2e9e78b7 100644 --- a/zed/src/sum_tree/cursor.rs +++ b/zed/src/sum_tree/cursor.rs @@ -199,6 +199,9 @@ where } pub fn next(&mut self) { + if !self.did_seek { + self.descend_to_first_item(self.tree, |_| true) + } self.next_internal(|_| true) } diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index eceaf45578..d98bd9c798 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -114,7 +114,7 @@ impl Workspace { pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool { self.worktrees .iter() - .any(|worktree| worktree.read(app).contains_path(path)) + .any(|worktree| worktree.read(app).contains_abs_path(path)) } pub fn open_paths(&mut self, paths: &[PathBuf], ctx: &mut ModelContext) { @@ -125,7 +125,7 @@ impl Workspace { pub fn open_path<'a>(&'a mut self, path: PathBuf, ctx: &mut ModelContext) { for tree in self.worktrees.iter() { - if tree.read(ctx).contains_path(&path) { + if tree.read(ctx).contains_abs_path(&path) { return; } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 0f5b8fd0ef..7d42d5ca4f 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -64,12 +64,12 @@ impl Worktree { let snapshot = Snapshot { id, scan_id: 0, - path: path.into(), + abs_path: path.into(), ignores: Default::default(), entries: Default::default(), }; let (event_stream, event_stream_handle) = - fsevent::EventStream::new(&[snapshot.path.as_ref()], Duration::from_millis(100)); + fsevent::EventStream::new(&[snapshot.abs_path.as_ref()], Duration::from_millis(100)); let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); @@ -148,18 +148,18 @@ impl Worktree { self.snapshot.clone() } - pub fn contains_path(&self, path: &Path) -> bool { - path.starts_with(&self.snapshot.path) + pub fn contains_abs_path(&self, path: &Path) -> bool { + path.starts_with(&self.snapshot.abs_path) } pub fn load_history( &self, - relative_path: &Path, + path: &Path, ctx: &AppContext, ) -> impl Future> { - let path = self.snapshot.path.join(relative_path); + let abs_path = self.snapshot.abs_path.join(path); ctx.background_executor().spawn(async move { - let mut file = std::fs::File::open(&path)?; + let mut file = std::fs::File::open(&abs_path)?; let mut base_text = String::new(); file.read_to_string(&mut base_text)?; Ok(History::new(Arc::from(base_text))) @@ -168,14 +168,14 @@ impl Worktree { pub fn save<'a>( &self, - relative_path: &Path, + path: &Path, content: BufferSnapshot, ctx: &AppContext, ) -> Task> { - let path = self.snapshot.path.join(relative_path); + let abs_path = self.snapshot.abs_path.join(path); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); - let file = std::fs::File::create(&path)?; + let file = std::fs::File::create(&abs_path)?; let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); for chunk in content.fragments() { writer.write(chunk.as_bytes())?; @@ -208,7 +208,7 @@ impl fmt::Debug for Worktree { pub struct Snapshot { id: usize, scan_id: usize, - path: Arc, + abs_path: Arc, ignores: BTreeMap, (Arc, usize)>, entries: SumTree, } @@ -226,16 +226,23 @@ impl Snapshot { FileIter::all(self, start) } + #[cfg(test)] + pub fn paths(&self) -> impl Iterator> { + let mut cursor = self.entries.cursor::<(), ()>(); + cursor.next(); + cursor.map(|entry| entry.path()) + } + pub fn visible_files(&self, start: usize) -> FileIter { FileIter::visible(self, start) } pub fn root_entry(&self) -> Entry { - self.entry_for_path(&self.path).unwrap() + self.entry_for_path(&self.abs_path).unwrap() } pub fn root_name(&self) -> Option<&OsStr> { - self.path.file_name() + self.abs_path.file_name() } fn entry_for_path(&self, path: impl AsRef) -> Option { @@ -252,6 +259,8 @@ impl Snapshot { } fn is_path_ignored(&self, path: &Path) -> Result { + dbg!(path); + let mut entry = self .entry_for_path(path) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; @@ -263,6 +272,7 @@ impl Snapshot { entry.path().parent().and_then(|p| self.entry_for_path(p)) { let parent_path = parent_entry.path(); + dbg!(parent_path); if let Some((ignore, _)) = self.ignores.get(parent_path) { let relative_path = path.strip_prefix(parent_path).unwrap(); match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { @@ -322,8 +332,7 @@ impl Snapshot { } fn insert_ignore_file(&mut self, path: &Path) { - let root_path = self.path.parent().unwrap_or(Path::new("")); - let (ignore, err) = Gitignore::new(root_path.join(path)); + let (ignore, err) = Gitignore::new(self.abs_path.join(path)); if let Some(err) = err { log::error!("error in ignore file {:?} - {:?}", path, err); } @@ -573,7 +582,7 @@ impl BackgroundScanner { } fn update_other_mount_paths(&mut self) { - let path = self.snapshot.lock().path.clone(); + let path = self.snapshot.lock().abs_path.clone(); self.other_mount_paths.clear(); self.other_mount_paths.extend( mounted_volume_paths() @@ -582,8 +591,8 @@ impl BackgroundScanner { ); } - fn path(&self) -> Arc { - self.snapshot.lock().path.clone() + fn abs_path(&self) -> Arc { + self.snapshot.lock().abs_path.clone() } fn snapshot(&self) -> Snapshot { @@ -625,16 +634,15 @@ impl BackgroundScanner { fn scan_dirs(&self) -> io::Result<()> { self.snapshot.lock().scan_id += 1; - let path = self.path(); - let metadata = fs::metadata(&path)?; + let path: Arc = Arc::from(Path::new("")); + let abs_path = self.abs_path(); + let metadata = fs::metadata(&abs_path)?; let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path)?.file_type().is_symlink(); - let name: Arc = path.file_name().unwrap_or(OsStr::new("/")).into(); - let relative_path: Arc = Arc::from((*name).as_ref()); + let is_symlink = fs::symlink_metadata(&abs_path)?.file_type().is_symlink(); if metadata.file_type().is_dir() { let dir_entry = Entry::Dir { - path: relative_path.clone(), + path: path.clone(), inode, is_symlink, pending: true, @@ -645,8 +653,8 @@ impl BackgroundScanner { let (tx, rx) = crossbeam_channel::unbounded(); tx.send(ScanJob { - path: path.to_path_buf(), - relative_path, + abs_path: abs_path.to_path_buf(), + path, scan_queue: tx.clone(), }) .unwrap(); @@ -657,7 +665,7 @@ impl BackgroundScanner { pool.execute(|| { while let Ok(job) = rx.recv() { if let Err(err) = self.scan_dir(&job) { - log::error!("error scanning {:?}: {}", job.path, err); + log::error!("error scanning {:?}: {}", job.abs_path, err); } } }); @@ -665,8 +673,8 @@ impl BackgroundScanner { }); } else { self.snapshot.lock().insert_entry(Entry::File { - path_entry: PathEntry::new(inode, relative_path.clone()), - path: relative_path, + path_entry: PathEntry::new(inode, path.clone()), + path, inode, is_symlink, is_ignored: None, @@ -682,37 +690,37 @@ impl BackgroundScanner { let mut new_entries = Vec::new(); let mut new_jobs = Vec::new(); - for child_entry in fs::read_dir(&job.path)? { + for child_entry in fs::read_dir(&job.abs_path)? { let child_entry = child_entry?; - let child_name: Arc = child_entry.file_name().into(); - let child_relative_path: Arc = job.relative_path.join(child_name.as_ref()).into(); + let child_name = child_entry.file_name(); + let child_abs_path = job.abs_path.join(&child_name); + let child_path: Arc = job.path.join(&child_name).into(); let child_metadata = child_entry.metadata()?; let child_inode = child_metadata.ino(); let child_is_symlink = child_metadata.file_type().is_symlink(); - let child_path = job.path.join(child_name.as_ref()); // Disallow mount points outside the file system containing the root of this worktree - if self.other_mount_paths.contains(&child_path) { + if self.other_mount_paths.contains(&child_abs_path) { continue; } if child_metadata.is_dir() { new_entries.push(Entry::Dir { - path: child_relative_path.clone(), + path: child_path.clone(), inode: child_inode, is_symlink: child_is_symlink, pending: true, is_ignored: None, }); new_jobs.push(ScanJob { + abs_path: child_abs_path, path: child_path, - relative_path: child_relative_path, scan_queue: job.scan_queue.clone(), }); } else { new_entries.push(Entry::File { - path_entry: PathEntry::new(child_inode, child_relative_path.clone()), - path: child_relative_path, + path_entry: PathEntry::new(child_inode, child_path.clone()), + path: child_path, inode: child_inode, is_symlink: child_is_symlink, is_ignored: None, @@ -722,7 +730,7 @@ impl BackgroundScanner { self.snapshot .lock() - .populate_dir(job.relative_path.clone(), new_entries); + .populate_dir(job.path.clone(), new_entries); for new_job in new_jobs { job.scan_queue.send(new_job).unwrap(); } @@ -736,40 +744,44 @@ impl BackgroundScanner { let mut snapshot = self.snapshot(); snapshot.scan_id += 1; - let root_path = if let Ok(path) = snapshot.path.canonicalize() { - path + let root_abs_path = if let Ok(abs_path) = snapshot.abs_path.canonicalize() { + abs_path } else { return false; }; events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - let mut paths = events.into_iter().map(|e| e.path).peekable(); + let mut abs_paths = events.into_iter().map(|e| e.path).peekable(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); - while let Some(path) = paths.next() { - let relative_path = - match path.strip_prefix(&root_path.parent().unwrap_or(Path::new(""))) { - Ok(relative_path) => relative_path.to_path_buf(), - Err(_) => { - log::error!("unexpected event {:?} for root path {:?}", path, root_path); - continue; - } - }; - while paths.peek().map_or(false, |p| p.starts_with(&path)) { - paths.next(); + while let Some(abs_path) = abs_paths.next() { + let path = match abs_path.strip_prefix(&root_abs_path) { + Ok(path) => Arc::from(path.to_path_buf()), + Err(_) => { + log::error!( + "unexpected event {:?} for root path {:?}", + abs_path, + root_abs_path + ); + continue; + } + }; + + while abs_paths.peek().map_or(false, |p| p.starts_with(&abs_path)) { + abs_paths.next(); } - snapshot.remove_path(&relative_path); + snapshot.remove_path(&path); - match self.fs_entry_for_path(&root_path, &path) { + match self.fs_entry_for_path(path.clone(), &abs_path) { Ok(Some(fs_entry)) => { let is_dir = fs_entry.is_dir(); snapshot.insert_entry(fs_entry); if is_dir { scan_queue_tx .send(ScanJob { + abs_path, path, - relative_path: relative_path.into(), scan_queue: scan_queue_tx.clone(), }) .unwrap(); @@ -792,7 +804,7 @@ impl BackgroundScanner { pool.execute(|| { while let Ok(job) = scan_queue_rx.recv() { if let Err(err) = self.scan_dir(&job) { - log::error!("error scanning {:?}: {}", job.path, err); + log::error!("error scanning {:?}: {}", job.abs_path, err); } } }); @@ -919,8 +931,8 @@ impl BackgroundScanner { }); } - fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result> { - let metadata = match fs::metadata(&path) { + fn fs_entry_for_path(&self, path: Arc, abs_path: &Path) -> Result> { + let metadata = match fs::metadata(&abs_path) { Err(err) => { return match (err.kind(), err.raw_os_error()) { (io::ErrorKind::NotFound, _) => Ok(None), @@ -930,20 +942,15 @@ impl BackgroundScanner { } Ok(metadata) => metadata, }; - let inode = metadata.ino(); - let is_symlink = fs::symlink_metadata(&path) + let is_symlink = fs::symlink_metadata(&abs_path) .context("failed to read symlink metadata")? .file_type() .is_symlink(); - let relative_path_with_root = root_path - .parent() - .map_or(path, |parent| path.strip_prefix(parent).unwrap()) - .into(); let entry = if metadata.file_type().is_dir() { Entry::Dir { - path: relative_path_with_root, + path, inode, is_symlink, pending: true, @@ -951,8 +958,8 @@ impl BackgroundScanner { } } else { Entry::File { - path_entry: PathEntry::new(inode, relative_path_with_root.clone()), - path: relative_path_with_root, + path_entry: PathEntry::new(inode, path.clone()), + path, inode, is_symlink, is_ignored: None, @@ -964,8 +971,8 @@ impl BackgroundScanner { } struct ScanJob { - path: PathBuf, - relative_path: Arc, + abs_path: PathBuf, + path: Arc, scan_queue: crossbeam_channel::Sender, } @@ -1149,7 +1156,7 @@ mod tests { } #[test] - fn test_rescan() { + fn test_rescan_simple() { App::test_async((), |mut app| async move { let dir = temp_tree(json!({ "a": { @@ -1173,10 +1180,23 @@ mod tests { }); std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - tree.condition(&app, move |_, _| { - file2.path().as_ref() == Path::new("d/file2") - }) - .await; + + app.read(|ctx| tree.read(ctx).next_scan_complete()).await; + + app.read(|ctx| { + assert_eq!( + tree.read(ctx) + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(), + vec!["a", "a/file1", "b", "d", "d/file2"] + ) + }); + + // tree.condition(&app, move |_, _| { + // file2.path().as_ref() == Path::new("d/file2") + // }) + // .await; }); } @@ -1196,6 +1216,16 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); app.read(|ctx| tree.read(ctx).scan_complete()).await; + + app.read(|ctx| { + let paths = tree + .read(ctx) + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(); + println!("paths {:?}", paths); + }); + app.read(|ctx| { let tree = tree.read(ctx); let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); @@ -1255,7 +1285,7 @@ mod tests { Arc::new(Mutex::new(Snapshot { id: 0, scan_id: 0, - path: root_dir.path().into(), + abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), })), @@ -1288,7 +1318,7 @@ mod tests { Arc::new(Mutex::new(Snapshot { id: 0, scan_id: 0, - path: root_dir.path().into(), + abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), })), diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 5b1a61e8c0..f8307b81ef 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -523,7 +523,7 @@ mod tests { &Snapshot { id: 0, scan_id: 0, - path: PathBuf::new().into(), + abs_path: PathBuf::new().into(), ignores: Default::default(), entries: Default::default(), }, From 054203d21cd5eb7fc8dd7f391176b80ac27d7af9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 23 Apr 2021 15:22:47 -0700 Subject: [PATCH 090/102] Fix fuzzy matching after removing root dirname from stored paths Co-Authored-By: Nathan Sobo --- zed/src/file_finder.rs | 25 +++++-- zed/src/worktree.rs | 38 ++++++---- zed/src/worktree/char_bag.rs | 10 ++- zed/src/worktree/fuzzy.rs | 134 ++++++++++++++++++++++------------- 4 files changed, 139 insertions(+), 68 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index cfddd00a84..f975c3406f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -24,6 +24,7 @@ pub struct FileFinder { search_count: usize, latest_search_id: usize, matches: Vec, + include_root_name: bool, selected: usize, list_state: UniformListState, } @@ -138,7 +139,12 @@ impl FileFinder { ) -> Option { let tree_id = path_match.tree_id; - self.worktree(tree_id, app).map(|_| { + self.worktree(tree_id, app).map(|tree| { + let prefix = if self.include_root_name { + tree.root_name_chars() + } else { + &[] + }; let path = path_match.path.clone(); let path_string = &path_match.path_string; let file_name = Path::new(&path_string) @@ -148,7 +154,8 @@ impl FileFinder { .to_string(); let path_positions = path_match.positions.clone(); - let file_name_start = path_string.chars().count() - file_name.chars().count(); + let file_name_start = + prefix.len() + path_string.chars().count() - file_name.chars().count(); let mut file_name_positions = Vec::new(); file_name_positions.extend(path_positions.iter().filter_map(|pos| { if pos >= &file_name_start { @@ -162,6 +169,9 @@ impl FileFinder { let highlight_color = ColorU::from_u32(0x304ee2ff); let bold = *Properties::new().weight(Weight::BOLD); + let mut full_path = prefix.iter().collect::(); + full_path.push_str(&path_string); + let mut container = Container::new( Flex::row() .with_child( @@ -191,7 +201,7 @@ impl FileFinder { ) .with_child( Label::new( - path_string.into(), + full_path, settings.ui_font_family, settings.ui_font_size, ) @@ -275,6 +285,7 @@ impl FileFinder { search_count: 0, latest_search_id: 0, matches: Vec::new(), + include_root_name: false, selected: 0, list_state: UniformListState::new(), } @@ -348,16 +359,17 @@ impl FileFinder { let search_id = util::post_inc(&mut self.search_count); let pool = ctx.as_ref().thread_pool().clone(); let task = ctx.background_executor().spawn(async move { + let include_root_name = snapshots.len() > 1; let matches = match_paths( snapshots.iter(), &query, - snapshots.len() > 1, + include_root_name, false, false, 100, pool, ); - (search_id, matches) + (search_id, include_root_name, matches) }); ctx.spawn(task, Self::update_matches).detach(); @@ -365,12 +377,13 @@ impl FileFinder { fn update_matches( &mut self, - (search_id, matches): (usize, Vec), + (search_id, include_root_name, matches): (usize, bool, Vec), ctx: &mut ViewContext, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; self.matches = matches; + self.include_root_name = include_root_name; self.selected = 0; self.list_state.scroll_to(0); ctx.notify(); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 7d42d5ca4f..107854e8a1 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -32,6 +32,8 @@ use std::{ time::Duration, }; +use self::char_bag::CharBag; + lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); } @@ -59,12 +61,17 @@ pub struct FileHandle { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { + let abs_path = path.into(); + let root_name_chars = abs_path.file_name().map_or(Vec::new(), |n| { + n.to_string_lossy().chars().chain(Some('/')).collect() + }); let (scan_state_tx, scan_state_rx) = smol::channel::unbounded(); let id = ctx.model_id(); let snapshot = Snapshot { id, scan_id: 0, - abs_path: path.into(), + abs_path, + root_name_chars, ignores: Default::default(), entries: Default::default(), }; @@ -209,6 +216,7 @@ pub struct Snapshot { id: usize, scan_id: usize, abs_path: Arc, + root_name_chars: Vec, ignores: BTreeMap, (Arc, usize)>, entries: SumTree, } @@ -238,13 +246,17 @@ impl Snapshot { } pub fn root_entry(&self) -> Entry { - self.entry_for_path(&self.abs_path).unwrap() + self.entry_for_path("").unwrap() } pub fn root_name(&self) -> Option<&OsStr> { self.abs_path.file_name() } + pub fn root_name_chars(&self) -> &[char] { + &self.root_name_chars + } + fn entry_for_path(&self, path: impl AsRef) -> Option { let mut cursor = self.entries.cursor::<_, ()>(); if cursor.seek(&PathSearch::Exact(path.as_ref()), SeekBias::Left) { @@ -259,8 +271,6 @@ impl Snapshot { } fn is_path_ignored(&self, path: &Path) -> Result { - dbg!(path); - let mut entry = self .entry_for_path(path) .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; @@ -272,7 +282,6 @@ impl Snapshot { entry.path().parent().and_then(|p| self.entry_for_path(p)) { let parent_path = parent_entry.path(); - dbg!(parent_path); if let Some((ignore, _)) = self.ignores.get(parent_path) { let relative_path = path.strip_prefix(parent_path).unwrap(); match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { @@ -567,11 +576,14 @@ struct BackgroundScanner { notify: Sender, other_mount_paths: HashSet, thread_pool: scoped_pool::Pool, + root_char_bag: CharBag, } impl BackgroundScanner { fn new(snapshot: Arc>, notify: Sender, worktree_id: usize) -> Self { + let root_char_bag = CharBag::from(snapshot.lock().root_name_chars.as_slice()); let mut scanner = Self { + root_char_bag, snapshot, notify, other_mount_paths: Default::default(), @@ -673,7 +685,7 @@ impl BackgroundScanner { }); } else { self.snapshot.lock().insert_entry(Entry::File { - path_entry: PathEntry::new(inode, path.clone()), + path_entry: PathEntry::new(inode, self.root_char_bag, path.clone()), path, inode, is_symlink, @@ -719,7 +731,7 @@ impl BackgroundScanner { }); } else { new_entries.push(Entry::File { - path_entry: PathEntry::new(child_inode, child_path.clone()), + path_entry: PathEntry::new(child_inode, self.root_char_bag, child_path.clone()), path: child_path, inode: child_inode, is_symlink: child_is_symlink, @@ -958,7 +970,7 @@ impl BackgroundScanner { } } else { Entry::File { - path_entry: PathEntry::new(inode, path.clone()), + path_entry: PathEntry::new(inode, self.root_char_bag, path.clone()), path, inode, is_symlink, @@ -1113,14 +1125,14 @@ mod tests { 10, ctx.thread_pool().clone(), ) - .iter() - .map(|result| result.path.clone()) + .into_iter() + .map(|result| result.path) .collect::>>(); assert_eq!( results, vec![ - PathBuf::from("root_link/banana/carrot/date").into(), - PathBuf::from("root_link/banana/carrot/endive").into(), + PathBuf::from("banana/carrot/date").into(), + PathBuf::from("banana/carrot/endive").into(), ] ); }) @@ -1288,6 +1300,7 @@ mod tests { abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), + root_name_chars: Default::default(), })), notify_tx, 0, @@ -1321,6 +1334,7 @@ mod tests { abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), + root_name_chars: Default::default(), })), notify_tx, 1, diff --git a/zed/src/worktree/char_bag.rs b/zed/src/worktree/char_bag.rs index 9e3c5314e9..b21949fe89 100644 --- a/zed/src/worktree/char_bag.rs +++ b/zed/src/worktree/char_bag.rs @@ -1,4 +1,4 @@ -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub struct CharBag(u64); impl CharBag { @@ -23,6 +23,14 @@ impl CharBag { } } +impl Extend for CharBag { + fn extend>(&mut self, iter: T) { + for c in iter { + self.insert(c); + } + } +} + impl From<&str> for CharBag { fn from(s: &str) -> Self { let mut bag = Self(0); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index f8307b81ef..b1b1a98b7f 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -21,11 +21,12 @@ pub struct PathEntry { } impl PathEntry { - pub fn new(ino: u64, path: Arc) -> Self { + pub fn new(ino: u64, root_char_bag: CharBag, path: Arc) -> Self { let path_str = path.to_string_lossy(); let lowercase_path = path_str.to_lowercase().chars().collect::>().into(); let path_chars: Arc<[char]> = path_str.chars().collect::>().into(); - let char_bag = CharBag::from(path_chars.as_ref()); + let mut char_bag = root_char_bag; + char_bag.extend(path_chars.iter().copied()); Self { ino, @@ -136,21 +137,9 @@ where } }); - let skipped_prefix_len = if include_root_name { - 0 - } else if let Entry::Dir { .. } = snapshot.root_entry() { - if let Some(name) = snapshot.root_name() { - name.to_string_lossy().chars().count() + 1 - } else { - 1 - } - } else { - 0 - }; - match_single_tree_paths( snapshot, - skipped_prefix_len, + include_root_name, path_entries, query, lowercase_query, @@ -186,7 +175,7 @@ where fn match_single_tree_paths<'a>( snapshot: &Snapshot, - skipped_prefix_len: usize, + include_root_name: bool, path_entries: impl Iterator, query: &[char], lowercase_query: &[char], @@ -200,6 +189,12 @@ fn match_single_tree_paths<'a>( score_matrix: &mut Vec>, best_position_matrix: &mut Vec, ) { + let prefix = if include_root_name { + snapshot.root_name_chars.as_slice() + } else { + &[] + }; + for path_entry in path_entries { if !path_entry.char_bag.is_superset(query_chars) { continue; @@ -207,25 +202,25 @@ fn match_single_tree_paths<'a>( if !find_last_positions( last_positions, - skipped_prefix_len, + prefix, &path_entry.lowercase_path, &lowercase_query[..], ) { continue; } - let matrix_len = query.len() * (path_entry.path_chars.len() - skipped_prefix_len); + let matrix_len = query.len() * (path_entry.path_chars.len() + prefix.len()); score_matrix.clear(); score_matrix.resize(matrix_len, None); best_position_matrix.clear(); - best_position_matrix.resize(matrix_len, skipped_prefix_len); + best_position_matrix.resize(matrix_len, 0); let score = score_match( &query[..], &lowercase_query[..], &path_entry.path_chars, &path_entry.lowercase_path, - skipped_prefix_len, + prefix, smart_case, &last_positions, score_matrix, @@ -237,11 +232,7 @@ fn match_single_tree_paths<'a>( if score > 0.0 { results.push(Reverse(PathMatch { tree_id: snapshot.id, - path_string: path_entry - .path_chars - .iter() - .skip(skipped_prefix_len) - .collect(), + path_string: path_entry.path_chars.iter().collect(), path: path_entry.path.clone(), score, positions: match_positions.clone(), @@ -255,18 +246,17 @@ fn match_single_tree_paths<'a>( fn find_last_positions( last_positions: &mut Vec, - skipped_prefix_len: usize, + prefix: &[char], path: &[char], query: &[char], ) -> bool { let mut path = path.iter(); + let mut prefix_iter = prefix.iter(); for (i, char) in query.iter().enumerate().rev() { if let Some(j) = path.rposition(|c| c == char) { - if j >= skipped_prefix_len { - last_positions[i] = j; - } else { - return false; - } + last_positions[i] = j + prefix.len(); + } else if let Some(j) = prefix_iter.rposition(|c| c == char) { + last_positions[i] = j; } else { return false; } @@ -279,7 +269,7 @@ fn score_match( query_cased: &[char], path: &[char], path_cased: &[char], - skipped_prefix_len: usize, + prefix: &[char], smart_case: bool, last_positions: &[usize], score_matrix: &mut [Option], @@ -292,14 +282,14 @@ fn score_match( query_cased, path, path_cased, - skipped_prefix_len, + prefix, smart_case, last_positions, score_matrix, best_position_matrix, min_score, 0, - skipped_prefix_len, + 0, query.len() as f64, ) * query.len() as f64; @@ -307,10 +297,10 @@ fn score_match( return 0.0; } - let path_len = path.len() - skipped_prefix_len; + let path_len = path.len() + prefix.len(); let mut cur_start = 0; for i in 0..query.len() { - match_positions[i] = best_position_matrix[i * path_len + cur_start] - skipped_prefix_len; + match_positions[i] = best_position_matrix[i * path_len + cur_start]; cur_start = match_positions[i] + 1; } @@ -322,7 +312,7 @@ fn recursive_score_match( query_cased: &[char], path: &[char], path_cased: &[char], - skipped_prefix_len: usize, + prefix: &[char], smart_case: bool, last_positions: &[usize], score_matrix: &mut [Option], @@ -336,9 +326,9 @@ fn recursive_score_match( return 1.0; } - let path_len = path.len() - skipped_prefix_len; + let path_len = prefix.len() + path.len(); - if let Some(memoized) = score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] { + if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] { return memoized; } @@ -350,7 +340,11 @@ fn recursive_score_match( let mut last_slash = 0; for j in path_idx..=limit { - let path_char = path_cased[j]; + let path_char = if j < prefix.len() { + prefix[j] + } else { + path_cased[j - prefix.len()] + }; let is_path_sep = path_char == '/' || path_char == '\\'; if query_idx == 0 && is_path_sep { @@ -358,10 +352,19 @@ fn recursive_score_match( } if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let curr = if j < prefix.len() { + prefix[j] + } else { + path[j - prefix.len()] + }; + let mut char_score = 1.0; if j > path_idx { - let last = path[j - 1]; - let curr = path[j]; + let last = if j - 1 < prefix.len() { + prefix[j - 1] + } else { + path[j - 1 - prefix.len()] + }; if last == '/' { char_score = 0.9; @@ -384,15 +387,15 @@ fn recursive_score_match( // Apply a severe penalty if the case doesn't match. // This will make the exact matches have higher score than the case-insensitive and the // path insensitive matches. - if (smart_case || path[j] == '/') && query[query_idx] != path[j] { + if (smart_case || curr == '/') && query[query_idx] != curr { char_score *= 0.001; } let mut multiplier = char_score; - // Scale the score based on how deep within the patch we found the match. + // Scale the score based on how deep within the path we found the match. if query_idx == 0 { - multiplier /= (path.len() - last_slash) as f64; + multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; } let mut next_score = 1.0; @@ -413,7 +416,7 @@ fn recursive_score_match( query_cased, path, path_cased, - skipped_prefix_len, + prefix, smart_case, last_positions, score_matrix, @@ -436,10 +439,10 @@ fn recursive_score_match( } if best_position != 0 { - best_position_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = best_position; + best_position_matrix[query_idx * path_len + path_idx] = best_position; } - score_matrix[query_idx * path_len + path_idx - skipped_prefix_len] = Some(score); + score_matrix[query_idx * path_len + path_idx] = Some(score); score } @@ -448,6 +451,38 @@ mod tests { use super::*; use std::path::PathBuf; + #[test] + fn test_get_last_positions() { + let mut last_positions = vec![0; 2]; + let result = find_last_positions( + &mut last_positions, + &['a', 'b', 'c'], + &['b', 'd', 'e', 'f'], + &['d', 'c'], + ); + assert_eq!(result, false); + + last_positions.resize(2, 0); + let result = find_last_positions( + &mut last_positions, + &['a', 'b', 'c'], + &['b', 'd', 'e', 'f'], + &['c', 'd'], + ); + assert_eq!(result, true); + assert_eq!(last_positions, vec![2, 4]); + + last_positions.resize(4, 0); + let result = find_last_positions( + &mut last_positions, + &['z', 'e', 'd', '/'], + &['z', 'e', 'd', '/', 'f'], + &['z', '/', 'z', 'f'], + ); + assert_eq!(result, true); + assert_eq!(last_positions, vec![0, 3, 4, 8]); + } + #[test] fn test_match_path_entries() { let paths = vec![ @@ -526,8 +561,9 @@ mod tests { abs_path: PathBuf::new().into(), ignores: Default::default(), entries: Default::default(), + root_name_chars: Vec::new(), }, - 0, + false, path_entries.iter(), &query[..], &lowercase_query[..], From 6a7308b87a30693e74128775137a3a4c30591891 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 23 Apr 2021 15:58:12 -0700 Subject: [PATCH 091/102] Avoid storing redundant copies of file paths Co-Authored-By: Nathan Sobo --- zed/src/file_finder.rs | 10 +-- zed/src/worktree.rs | 126 +++++++++++++++----------------------- zed/src/worktree/fuzzy.rs | 86 +++++++++++--------------- 3 files changed, 91 insertions(+), 131 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index f975c3406f..4302ece217 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -146,12 +146,12 @@ impl FileFinder { &[] }; let path = path_match.path.clone(); - let path_string = &path_match.path_string; - let file_name = Path::new(&path_string) + let path_string = path_match.path.to_string_lossy(); + let file_name = path_match + .path .file_name() .unwrap_or_default() - .to_string_lossy() - .to_string(); + .to_string_lossy(); let path_positions = path_match.positions.clone(); let file_name_start = @@ -192,7 +192,7 @@ impl FileFinder { Flex::column() .with_child( Label::new( - file_name, + file_name.to_string(), settings.ui_font_family, settings.ui_font_size, ) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 107854e8a1..76232643aa 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -6,7 +6,6 @@ use crate::{ sum_tree::{self, Cursor, Edit, SeekBias, SumTree}, }; use anyhow::{anyhow, Context, Result}; -use fuzzy::PathEntry; pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::gitignore::Gitignore; @@ -307,8 +306,8 @@ impl Snapshot { let mut edits = Vec::new(); let mut parent_entry = self.entries.get(&PathKey(parent_path)).unwrap().clone(); - if let Entry::Dir { pending, .. } = &mut parent_entry { - *pending = false; + if matches!(parent_entry.kind, EntryKind::PendingDir) { + parent_entry.kind = EntryKind::Dir; } else { unreachable!(); } @@ -384,54 +383,40 @@ impl FileHandle { } #[derive(Clone, Debug)] -pub enum Entry { - Dir { - path: Arc, - inode: u64, - is_symlink: bool, - pending: bool, - is_ignored: Option, - }, - File { - path: Arc, - inode: u64, - is_symlink: bool, - path_entry: PathEntry, - is_ignored: Option, - }, +pub struct Entry { + kind: EntryKind, + path: Arc, + inode: u64, + is_symlink: bool, + is_ignored: Option, +} + +#[derive(Clone, Debug)] +pub enum EntryKind { + PendingDir, + Dir, + File(CharBag), } impl Entry { pub fn path(&self) -> &Arc { - match self { - Entry::Dir { path, .. } => path, - Entry::File { path, .. } => path, - } + &self.path } pub fn inode(&self) -> u64 { - match self { - Entry::Dir { inode, .. } => *inode, - Entry::File { inode, .. } => *inode, - } + self.inode } fn is_ignored(&self) -> Option { - match self { - Entry::Dir { is_ignored, .. } => *is_ignored, - Entry::File { is_ignored, .. } => *is_ignored, - } + self.is_ignored } fn set_ignored(&mut self, ignored: bool) { - match self { - Entry::Dir { is_ignored, .. } => *is_ignored = Some(ignored), - Entry::File { is_ignored, .. } => *is_ignored = Some(ignored), - } + self.is_ignored = Some(ignored); } fn is_dir(&self) -> bool { - matches!(self, Entry::Dir { .. }) + matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) } } @@ -441,9 +426,9 @@ impl sum_tree::Item for Entry { fn summary(&self) -> Self::Summary { let file_count; let visible_file_count; - if let Entry::File { is_ignored, .. } = self { + if matches!(self.kind, EntryKind::File(_)) { file_count = 1; - if is_ignored.unwrap_or(false) { + if self.is_ignored.unwrap_or(false) { visible_file_count = 0; } else { visible_file_count = 1; @@ -653,11 +638,11 @@ impl BackgroundScanner { let is_symlink = fs::symlink_metadata(&abs_path)?.file_type().is_symlink(); if metadata.file_type().is_dir() { - let dir_entry = Entry::Dir { + let dir_entry = Entry { + kind: EntryKind::PendingDir, path: path.clone(), inode, is_symlink, - pending: true, is_ignored: None, }; self.snapshot.lock().insert_entry(dir_entry); @@ -684,8 +669,8 @@ impl BackgroundScanner { } }); } else { - self.snapshot.lock().insert_entry(Entry::File { - path_entry: PathEntry::new(inode, self.root_char_bag, path.clone()), + self.snapshot.lock().insert_entry(Entry { + kind: EntryKind::File(self.char_bag(&path)), path, inode, is_symlink, @@ -717,11 +702,11 @@ impl BackgroundScanner { } if child_metadata.is_dir() { - new_entries.push(Entry::Dir { + new_entries.push(Entry { + kind: EntryKind::PendingDir, path: child_path.clone(), inode: child_inode, is_symlink: child_is_symlink, - pending: true, is_ignored: None, }); new_jobs.push(ScanJob { @@ -730,8 +715,8 @@ impl BackgroundScanner { scan_queue: job.scan_queue.clone(), }); } else { - new_entries.push(Entry::File { - path_entry: PathEntry::new(child_inode, self.root_char_bag, child_path.clone()), + new_entries.push(Entry { + kind: EntryKind::File(self.char_bag(&child_path)), path: child_path, inode: child_inode, is_symlink: child_is_symlink, @@ -960,26 +945,26 @@ impl BackgroundScanner { .file_type() .is_symlink(); - let entry = if metadata.file_type().is_dir() { - Entry::Dir { - path, - inode, - is_symlink, - pending: true, - is_ignored: None, - } - } else { - Entry::File { - path_entry: PathEntry::new(inode, self.root_char_bag, path.clone()), - path, - inode, - is_symlink, - is_ignored: None, - } + let entry = Entry { + kind: if metadata.file_type().is_dir() { + EntryKind::PendingDir + } else { + EntryKind::File(self.char_bag(&path)) + }, + path, + inode, + is_symlink, + is_ignored: None, }; Ok(Some(entry)) } + + fn char_bag(&self, path: &Path) -> CharBag { + let mut result = self.root_char_bag; + result.extend(path.to_string_lossy().chars()); + result + } } struct ScanJob { @@ -1496,13 +1481,10 @@ mod tests { let mut files = self.files(0); let mut visible_files = self.visible_files(0); for entry in self.entries.cursor::<(), ()>() { - if let Entry::File { - inode, is_ignored, .. - } = entry - { - assert_eq!(files.next().unwrap().inode(), *inode); - if !is_ignored.unwrap() { - assert_eq!(visible_files.next().unwrap().inode(), *inode); + if matches!(entry.kind, EntryKind::File(_)) { + assert_eq!(files.next().unwrap().inode(), entry.inode); + if !entry.is_ignored.unwrap() { + assert_eq!(visible_files.next().unwrap().inode(), entry.inode); } } } @@ -1518,8 +1500,6 @@ mod tests { } fn to_vec(&self) -> Vec<(&Path, u64, bool)> { - use std::iter::FromIterator; - let mut paths = Vec::new(); for entry in self.entries.cursor::<(), ()>() { paths.push(( @@ -1527,12 +1507,6 @@ mod tests { entry.inode(), entry.is_ignored().unwrap(), )); - if let Entry::File { path_entry, .. } = entry { - assert_eq!( - String::from_iter(path_entry.path_chars.iter()), - entry.path().to_str().unwrap() - ); - } } paths.sort_by(|a, b| a.0.cmp(&b.0)); paths diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index b1b1a98b7f..b6d13f3288 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -1,4 +1,4 @@ -use super::{char_bag::CharBag, Entry, Snapshot}; +use super::{char_bag::CharBag, EntryKind, Snapshot}; use gpui::scoped_pool; use std::{ cmp::{max, min, Ordering, Reverse}, @@ -12,37 +12,15 @@ const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; #[derive(Clone, Debug)] -pub struct PathEntry { - pub ino: u64, +pub struct MatchCandidate<'a> { + pub path: &'a Arc, pub char_bag: CharBag, - pub path_chars: Arc<[char]>, - pub path: Arc, - pub lowercase_path: Arc<[char]>, -} - -impl PathEntry { - pub fn new(ino: u64, root_char_bag: CharBag, path: Arc) -> Self { - let path_str = path.to_string_lossy(); - let lowercase_path = path_str.to_lowercase().chars().collect::>().into(); - let path_chars: Arc<[char]> = path_str.chars().collect::>().into(); - let mut char_bag = root_char_bag; - char_bag.extend(path_chars.iter().copied()); - - Self { - ino, - char_bag, - path_chars, - path, - lowercase_path, - } - } } #[derive(Clone, Debug)] pub struct PathMatch { pub score: f64, pub positions: Vec, - pub path_string: String, pub tree_id: usize, pub path: Arc, } @@ -77,7 +55,7 @@ pub fn match_paths<'a, T>( pool: scoped_pool::Pool, ) -> Vec where - T: Clone + Send + Iterator, + T: Clone + Send + Iterator + 'a, { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -126,12 +104,12 @@ where } else { snapshot.visible_files(start).take(end - start) }; - let path_entries = entries.map(|entry| { - if let Entry::File { - path_entry: path, .. - } = entry - { - path + let paths = entries.map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + MatchCandidate { + path: &entry.path, + char_bag, + } } else { unreachable!() } @@ -140,7 +118,7 @@ where match_single_tree_paths( snapshot, include_root_name, - path_entries, + paths, query, lowercase_query, query_chars, @@ -176,7 +154,7 @@ where fn match_single_tree_paths<'a>( snapshot: &Snapshot, include_root_name: bool, - path_entries: impl Iterator, + path_entries: impl Iterator>, query: &[char], lowercase_query: &[char], query_chars: CharBag, @@ -189,6 +167,9 @@ fn match_single_tree_paths<'a>( score_matrix: &mut Vec>, best_position_matrix: &mut Vec, ) { + let mut path_chars = Vec::new(); + let mut lowercase_path_chars = Vec::new(); + let prefix = if include_root_name { snapshot.root_name_chars.as_slice() } else { @@ -200,16 +181,23 @@ fn match_single_tree_paths<'a>( continue; } + path_chars.clear(); + lowercase_path_chars.clear(); + for c in path_entry.path.to_string_lossy().chars() { + path_chars.push(c); + lowercase_path_chars.push(c.to_ascii_lowercase()); + } + if !find_last_positions( last_positions, prefix, - &path_entry.lowercase_path, + &lowercase_path_chars, &lowercase_query[..], ) { continue; } - let matrix_len = query.len() * (path_entry.path_chars.len() + prefix.len()); + let matrix_len = query.len() * (path_chars.len() + prefix.len()); score_matrix.clear(); score_matrix.resize(matrix_len, None); best_position_matrix.clear(); @@ -218,9 +206,9 @@ fn match_single_tree_paths<'a>( let score = score_match( &query[..], &lowercase_query[..], - &path_entry.path_chars, - &path_entry.lowercase_path, - prefix, + &path_chars, + &lowercase_path_chars, + &prefix, smart_case, &last_positions, score_matrix, @@ -232,7 +220,6 @@ fn match_single_tree_paths<'a>( if score > 0.0 { results.push(Reverse(PathMatch { tree_id: snapshot.id, - path_string: path_entry.path_chars.iter().collect(), path: path_entry.path.clone(), score, positions: match_positions.clone(), @@ -533,18 +520,17 @@ mod tests { let query = query.chars().collect::>(); let query_chars = CharBag::from(&lowercase_query[..]); + let path_arcs = paths + .iter() + .map(|path| Arc::from(PathBuf::from(path))) + .collect::>(); let mut path_entries = Vec::new(); for (i, path) in paths.iter().enumerate() { - let lowercase_path: Arc<[char]> = - path.to_lowercase().chars().collect::>().into(); - let char_bag = CharBag::from(lowercase_path.as_ref()); - let path_chars = path.chars().collect(); - path_entries.push(PathEntry { - ino: i as u64, + let lowercase_path = path.to_lowercase().chars().collect::>(); + let char_bag = CharBag::from(lowercase_path.as_slice()); + path_entries.push(MatchCandidate { char_bag, - path_chars, - path: Arc::from(PathBuf::from(path)), - lowercase_path, + path: path_arcs.get(i).unwrap(), }); } @@ -564,7 +550,7 @@ mod tests { root_name_chars: Vec::new(), }, false, - path_entries.iter(), + path_entries.into_iter(), &query[..], &lowercase_query[..], query_chars, From 9cd1d5e60772cd52b60b1c702d4e2c1c986a4a61 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 24 Apr 2021 10:14:17 +0200 Subject: [PATCH 092/102] Avoid cloning entry in `Snapshot::entry_for_path` --- zed/src/sum_tree/mod.rs | 2 +- zed/src/worktree.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/zed/src/sum_tree/mod.rs b/zed/src/sum_tree/mod.rs index 03a9430ef5..fd104a3b12 100644 --- a/zed/src/sum_tree/mod.rs +++ b/zed/src/sum_tree/mod.rs @@ -22,7 +22,7 @@ pub trait KeyedItem: Item { fn key(&self) -> Self::Key; } -pub trait Dimension<'a, Summary: Default>: 'a + Clone + fmt::Debug + Default { +pub trait Dimension<'a, Summary: Default>: Clone + fmt::Debug + Default { fn add_summary(&mut self, summary: &'a Summary); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 76232643aa..47166d35d2 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -244,7 +244,7 @@ impl Snapshot { FileIter::visible(self, start) } - pub fn root_entry(&self) -> Entry { + pub fn root_entry(&self) -> &Entry { self.entry_for_path("").unwrap() } @@ -256,10 +256,10 @@ impl Snapshot { &self.root_name_chars } - fn entry_for_path(&self, path: impl AsRef) -> Option { + fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { let mut cursor = self.entries.cursor::<_, ()>(); if cursor.seek(&PathSearch::Exact(path.as_ref()), SeekBias::Left) { - cursor.item().cloned() + cursor.item() } else { None } @@ -532,7 +532,7 @@ impl<'a> Default for PathSearch<'a> { } } -impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathSearch<'a> { +impl<'a: 'b, 'b> sum_tree::Dimension<'a, EntrySummary> for PathSearch<'b> { fn add_summary(&mut self, summary: &'a EntrySummary) { *self = Self::Exact(summary.max_path.as_ref()); } From f770a70929e8a21fc45ffbc63868f8b2ee56c1d1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 24 Apr 2021 23:59:03 -0600 Subject: [PATCH 093/102] WIP: Maintain an IgnoreStack while scanning All ignore files associated with ancestors of the directory currently being scanned are included in the stack. This allows us to compute ignore status for each entry as we initially scan it. If we encounter an ignored directory, we replace the stack with an "ignore all" variant that simply ignores every descendant of the ignored directory. This is incomplete. We still need to construct an ignore stack in an appropriate state when rescanning subtrees in response to events. It also doesn't deal with individual ignore files being added, removed, or changed. I think we could potentially use the ignore stack while reconstructing the tree for this purpose. --- zed/src/worktree.rs | 373 +++++++++++++++++++++---------------- zed/src/worktree/ignore.rs | 66 +++++++ 2 files changed, 275 insertions(+), 164 deletions(-) create mode 100644 zed/src/worktree/ignore.rs diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 47166d35d2..39e4fe0741 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,14 +1,15 @@ mod char_bag; mod fuzzy; +mod ignore; use crate::{ editor::{History, Snapshot as BufferSnapshot}, sum_tree::{self, Cursor, Edit, SeekBias, SumTree}, }; +use ::ignore::gitignore::Gitignore; use anyhow::{anyhow, Context, Result}; pub use fuzzy::{match_paths, PathMatch}; use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; -use ignore::gitignore::Gitignore; use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ @@ -23,7 +24,6 @@ use std::{ fmt, fs, future::Future, io::{self, Read, Write}, - mem, ops::{AddAssign, Deref}, os::unix::{ffi::OsStrExt, fs::MetadataExt}, path::{Path, PathBuf}, @@ -31,7 +31,7 @@ use std::{ time::Duration, }; -use self::char_bag::CharBag; +use self::{char_bag::CharBag, ignore::IgnoreStack}; lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); @@ -216,7 +216,7 @@ pub struct Snapshot { scan_id: usize, abs_path: Arc, root_name_chars: Vec, - ignores: BTreeMap, (Arc, usize)>, + ignores: BTreeMap>, entries: SumTree, } @@ -270,29 +270,31 @@ impl Snapshot { } fn is_path_ignored(&self, path: &Path) -> Result { - let mut entry = self - .entry_for_path(path) - .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; + todo!(); + Ok(false) + // let mut entry = self + // .entry_for_path(path) + // .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - if path.starts_with(".git") { - Ok(true) - } else { - while let Some(parent_entry) = - entry.path().parent().and_then(|p| self.entry_for_path(p)) - { - let parent_path = parent_entry.path(); - if let Some((ignore, _)) = self.ignores.get(parent_path) { - let relative_path = path.strip_prefix(parent_path).unwrap(); - match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { - ignore::Match::Whitelist(_) => return Ok(false), - ignore::Match::Ignore(_) => return Ok(true), - ignore::Match::None => {} - } - } - entry = parent_entry; - } - Ok(false) - } + // if path.starts_with(".git") { + // Ok(true) + // } else { + // while let Some(parent_entry) = + // entry.path().parent().and_then(|p| self.entry_for_path(p)) + // { + // let parent_path = parent_entry.path(); + // if let Some((ignore, _)) = self.ignores.get(parent_path) { + // let relative_path = path.strip_prefix(parent_path).unwrap(); + // match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { + // ::ignore::Match::Whitelist(_) => return Ok(false), + // ::ignore::Match::Ignore(_) => return Ok(true), + // ::ignore::Match::None => {} + // } + // } + // entry = parent_entry; + // } + // Ok(false) + // } } fn insert_entry(&mut self, entry: Entry) { @@ -302,10 +304,18 @@ impl Snapshot { self.entries.insert(entry); } - fn populate_dir(&mut self, parent_path: Arc, entries: impl IntoIterator) { + fn populate_dir( + &mut self, + parent_path: Arc, + entries: impl IntoIterator, + ignore: Option>, + ) { let mut edits = Vec::new(); let mut parent_entry = self.entries.get(&PathKey(parent_path)).unwrap().clone(); + if let Some(ignore) = ignore { + self.ignores.insert(parent_entry.inode, ignore); + } if matches!(parent_entry.kind, EntryKind::PendingDir) { parent_entry.kind = EntryKind::Dir; } else { @@ -332,22 +342,22 @@ impl Snapshot { }; self.entries = new_entries; - if path.file_name() == Some(&GITIGNORE) { - if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { - *scan_id = self.scan_id; - } - } + // if path.file_name() == Some(&GITIGNORE) { + // if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { + // *scan_id = self.scan_id; + // } + // } } - fn insert_ignore_file(&mut self, path: &Path) { + fn insert_ignore_file(&mut self, path: &Path) -> Arc { let (ignore, err) = Gitignore::new(self.abs_path.join(path)); if let Some(err) = err { log::error!("error in ignore file {:?} - {:?}", path, err); } - - let ignore_parent_path = path.parent().unwrap().into(); - self.ignores - .insert(ignore_parent_path, (Arc::new(ignore), self.scan_id)); + let ignore = Arc::new(ignore); + let ignore_parent_inode = self.entry_for_path(path.parent().unwrap()).unwrap().inode; + self.ignores.insert(ignore_parent_inode, ignore.clone()); + ignore } } @@ -388,7 +398,7 @@ pub struct Entry { path: Arc, inode: u64, is_symlink: bool, - is_ignored: Option, + is_ignored: bool, } #[derive(Clone, Debug)] @@ -407,12 +417,12 @@ impl Entry { self.inode } - fn is_ignored(&self) -> Option { + fn is_ignored(&self) -> bool { self.is_ignored } fn set_ignored(&mut self, ignored: bool) { - self.is_ignored = Some(ignored); + self.is_ignored = ignored; } fn is_dir(&self) -> bool { @@ -428,7 +438,7 @@ impl sum_tree::Item for Entry { let visible_file_count; if matches!(self.kind, EntryKind::File(_)) { file_count = 1; - if self.is_ignored.unwrap_or(false) { + if self.is_ignored { visible_file_count = 0; } else { visible_file_count = 1; @@ -442,7 +452,6 @@ impl sum_tree::Item for Entry { max_path: self.path().clone(), file_count, visible_file_count, - recompute_ignore_status: self.is_ignored().is_none(), } } } @@ -460,7 +469,6 @@ pub struct EntrySummary { max_path: Arc, file_count: usize, visible_file_count: usize, - recompute_ignore_status: bool, } impl Default for EntrySummary { @@ -469,7 +477,6 @@ impl Default for EntrySummary { max_path: Arc::from(Path::new("")), file_count: 0, visible_file_count: 0, - recompute_ignore_status: false, } } } @@ -479,7 +486,6 @@ impl<'a> AddAssign<&'a EntrySummary> for EntrySummary { self.max_path = rhs.max_path.clone(); self.file_count += rhs.file_count; self.visible_file_count += rhs.visible_file_count; - self.recompute_ignore_status |= rhs.recompute_ignore_status; } } @@ -611,6 +617,8 @@ impl BackgroundScanner { return; } + return; + event_stream.run(move |events| { if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return false; @@ -643,7 +651,7 @@ impl BackgroundScanner { path: path.clone(), inode, is_symlink, - is_ignored: None, + is_ignored: false, }; self.snapshot.lock().insert_entry(dir_entry); @@ -652,6 +660,7 @@ impl BackgroundScanner { tx.send(ScanJob { abs_path: abs_path.to_path_buf(), path, + ignore_stack: IgnoreStack::none(), scan_queue: tx.clone(), }) .unwrap(); @@ -674,7 +683,7 @@ impl BackgroundScanner { path, inode, is_symlink, - is_ignored: None, + is_ignored: false, }); } @@ -684,8 +693,10 @@ impl BackgroundScanner { } fn scan_dir(&self, job: &ScanJob) -> io::Result<()> { - let mut new_entries = Vec::new(); - let mut new_jobs = Vec::new(); + let mut new_entries: Vec = Vec::new(); + let mut new_jobs: Vec = Vec::new(); + let mut ignore_stack = job.ignore_stack.clone(); + let mut new_ignore = None; for child_entry in fs::read_dir(&job.abs_path)? { let child_entry = child_entry?; @@ -701,33 +712,67 @@ impl BackgroundScanner { continue; } + // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored + if child_name == *GITIGNORE { + let (ignore, err) = Gitignore::new(&child_abs_path); + if let Some(err) = err { + log::error!("error in ignore file {:?} - {:?}", child_path, err); + } + let ignore = Arc::new(ignore); + ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); + new_ignore = Some(ignore); + + // Update ignore status of any child entries we've already processed to reflect the + // ignore file in the current directory. Because `.gitignore` starts with a `.`, + // there should rarely be too numerous. Update the ignore stack associated with any + // 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()); + if entry.is_dir() { + new_jobs.next().unwrap().ignore_stack = if entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }; + } + } + } + if child_metadata.is_dir() { + let is_ignored = ignore_stack.is_path_ignored(&child_path, true); new_entries.push(Entry { kind: EntryKind::PendingDir, path: child_path.clone(), inode: child_inode, is_symlink: child_is_symlink, - is_ignored: None, + is_ignored, }); new_jobs.push(ScanJob { abs_path: child_abs_path, path: child_path, + ignore_stack: if is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }, scan_queue: job.scan_queue.clone(), }); } else { + let is_ignored = ignore_stack.is_path_ignored(&child_path, false); new_entries.push(Entry { kind: EntryKind::File(self.char_bag(&child_path)), path: child_path, inode: child_inode, is_symlink: child_is_symlink, - is_ignored: None, + is_ignored, }); }; } self.snapshot .lock() - .populate_dir(job.path.clone(), new_entries); + .populate_dir(job.path.clone(), new_entries, new_ignore); for new_job in new_jobs { job.scan_queue.send(new_job).unwrap(); } @@ -779,6 +824,7 @@ impl BackgroundScanner { .send(ScanJob { abs_path, path, + ignore_stack: todo!(), scan_queue: scan_queue_tx.clone(), }) .unwrap(); @@ -819,113 +865,113 @@ impl BackgroundScanner { } fn compute_ignore_status_for_new_ignores(&self) { - let mut snapshot = self.snapshot(); + // let mut snapshot = self.snapshot(); - let mut ignores_to_delete = Vec::new(); - let mut changed_ignore_parents = Vec::new(); - for (parent_path, (_, scan_id)) in &snapshot.ignores { - let prev_ignore_parent = changed_ignore_parents.last(); - if *scan_id == snapshot.scan_id - && prev_ignore_parent.map_or(true, |l| !parent_path.starts_with(l)) - { - changed_ignore_parents.push(parent_path.clone()); - } + // let mut ignores_to_delete = Vec::new(); + // let mut changed_ignore_parents = Vec::new(); + // for (parent_path, (_, scan_id)) in &snapshot.ignores { + // let prev_ignore_parent = changed_ignore_parents.last(); + // if *scan_id == snapshot.scan_id + // && prev_ignore_parent.map_or(true, |l| !parent_path.starts_with(l)) + // { + // changed_ignore_parents.push(parent_path.clone()); + // } - let ignore_parent_exists = snapshot.entry_for_path(parent_path).is_some(); - let ignore_exists = snapshot - .entry_for_path(parent_path.join(&*GITIGNORE)) - .is_some(); - if !ignore_parent_exists || !ignore_exists { - ignores_to_delete.push(parent_path.clone()); - } - } + // let ignore_parent_exists = snapshot.entry_for_path(parent_path).is_some(); + // let ignore_exists = snapshot + // .entry_for_path(parent_path.join(&*GITIGNORE)) + // .is_some(); + // if !ignore_parent_exists || !ignore_exists { + // ignores_to_delete.push(parent_path.clone()); + // } + // } - for parent_path in ignores_to_delete { - snapshot.ignores.remove(&parent_path); - self.snapshot.lock().ignores.remove(&parent_path); - } + // for parent_path in ignores_to_delete { + // snapshot.ignores.remove(&parent_path); + // self.snapshot.lock().ignores.remove(&parent_path); + // } - 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); - } - self.snapshot.lock().entries.edit(mem::take(&mut edits)); - } - }); + // 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); + // } + // self.snapshot.lock().entries.edit(mem::take(&mut edits)); + // } + // }); - scope.execute(|| { - let entries_tx = entries_tx; - let mut cursor = snapshot.entries.cursor::<_, ()>(); - for ignore_parent_path in &changed_ignore_parents { - cursor.seek(&PathSearch::Exact(ignore_parent_path), SeekBias::Right); - while let Some(entry) = cursor.item() { - if entry.path().starts_with(ignore_parent_path) { - entries_tx.send(entry.clone()).unwrap(); - cursor.next(); - } else { - break; - } - } - } - }); + // scope.execute(|| { + // let entries_tx = entries_tx; + // let mut cursor = snapshot.entries.cursor::<_, ()>(); + // for ignore_parent_path in &changed_ignore_parents { + // cursor.seek(&PathSearch::Exact(ignore_parent_path), SeekBias::Right); + // while let Some(entry) = cursor.item() { + // if entry.path().starts_with(ignore_parent_path) { + // entries_tx.send(entry.clone()).unwrap(); + // cursor.next(); + // } else { + // break; + // } + // } + // } + // }); - 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_path_ignored(entry.path()).unwrap()); - edits_tx.send(Edit::Insert(entry)).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_path_ignored(entry.path()).unwrap()); + // edits_tx.send(Edit::Insert(entry)).unwrap(); + // } + // }); + // } + // }); } fn compute_ignore_status_for_new_entries(&self) { - let snapshot = self.snapshot.lock().clone(); + // 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); - } - self.snapshot.lock().entries.edit(mem::take(&mut edits)); - } - }); + // 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); + // } + // 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_ignore_status) - { - entries_tx.send(entry.clone()).unwrap(); - } - }); + // scope.execute(|| { + // let entries_tx = entries_tx; + // for entry in snapshot + // .entries + // .filter::<_, ()>(|e| e.recompute_ignore_status) + // { + // 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_path_ignored(entry.path()).unwrap()); - edits_tx.send(Edit::Insert(entry)).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_path_ignored(entry.path()).unwrap()); + // edits_tx.send(Edit::Insert(entry)).unwrap(); + // } + // }); + // } + // }); } fn fs_entry_for_path(&self, path: Arc, abs_path: &Path) -> Result> { @@ -954,7 +1000,7 @@ impl BackgroundScanner { path, inode, is_symlink, - is_ignored: None, + is_ignored: false, }; Ok(Some(entry)) @@ -970,6 +1016,7 @@ impl BackgroundScanner { struct ScanJob { abs_path: PathBuf, path: Arc, + ignore_stack: Arc, scan_queue: crossbeam_channel::Sender, } @@ -1227,10 +1274,12 @@ mod tests { let tree = tree.read(ctx); 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)); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); }); + return; + fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); app.read(|ctx| tree.read(ctx).next_scan_complete()).await; @@ -1238,8 +1287,8 @@ mod tests { let tree = tree.read(ctx); 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)); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); }); }); } @@ -1483,7 +1532,7 @@ mod tests { for entry in self.entries.cursor::<(), ()>() { if matches!(entry.kind, EntryKind::File(_)) { assert_eq!(files.next().unwrap().inode(), entry.inode); - if !entry.is_ignored.unwrap() { + if !entry.is_ignored { assert_eq!(visible_files.next().unwrap().inode(), entry.inode); } } @@ -1491,22 +1540,18 @@ mod tests { assert!(files.next().is_none()); assert!(visible_files.next().is_none()); - for (ignore_parent_path, _) in &self.ignores { - assert!(self.entry_for_path(ignore_parent_path).is_some()); - assert!(self - .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) - .is_some()); - } + // for (ignore_parent_path, _) in &self.ignores { + // assert!(self.entry_for_path(ignore_parent_path).is_some()); + // assert!(self + // .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) + // .is_some()); + // } } fn to_vec(&self) -> Vec<(&Path, u64, bool)> { let mut paths = Vec::new(); for entry in self.entries.cursor::<(), ()>() { - paths.push(( - entry.path().as_ref(), - entry.inode(), - entry.is_ignored().unwrap(), - )); + paths.push((entry.path().as_ref(), entry.inode(), entry.is_ignored())); } paths.sort_by(|a, b| a.0.cmp(&b.0)); paths diff --git a/zed/src/worktree/ignore.rs b/zed/src/worktree/ignore.rs new file mode 100644 index 0000000000..5bd4389850 --- /dev/null +++ b/zed/src/worktree/ignore.rs @@ -0,0 +1,66 @@ +use std::{path::Path, sync::Arc}; + +use ignore::gitignore::Gitignore; + +pub enum IgnoreStack { + None, + Some { + base: Arc, + ignore: Arc, + parent: Arc, + }, + All, +} + +impl IgnoreStack { + pub fn none() -> Arc { + Arc::new(Self::None) + } + + pub fn all() -> Arc { + Arc::new(Self::All) + } + + pub fn append(self: Arc, base: Arc, ignore: Arc) -> Arc { + log::info!("appending ignore {:?}", base); + match self.as_ref() { + IgnoreStack::All => self, + _ => Arc::new(Self::Some { + base, + ignore, + parent: self, + }), + } + } + + pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool { + println!("is_path_ignored? {:?} {}", path, is_dir); + match self { + Self::None => { + println!("none case"); + false + } + Self::All => { + println!("all case"); + true + } + Self::Some { + base, + ignore, + parent: prev, + } => { + println!( + "some case {:?} {:?}", + base, + path.strip_prefix(base).unwrap() + ); + + match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) { + ignore::Match::None => prev.is_path_ignored(path, is_dir), + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + } + } + } + } +} From 111d98d6c16a40a8fedde22f786db2f47888a429 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Apr 2021 15:26:16 +0200 Subject: [PATCH 094/102] Keep ignore status up-to-date as events are processed --- zed/src/worktree.rs | 386 +++++++++++++++++++------------------ zed/src/worktree/ignore.rs | 34 ++-- 2 files changed, 209 insertions(+), 211 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 39e4fe0741..72929d58b9 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -19,7 +19,7 @@ use postage::{ use smol::{channel::Sender, Timer}; use std::{ cmp, - collections::{BTreeMap, HashSet}, + collections::{HashMap, HashSet}, ffi::{CStr, OsStr}, fmt, fs, future::Future, @@ -216,7 +216,7 @@ pub struct Snapshot { scan_id: usize, abs_path: Arc, root_name_chars: Vec, - ignores: BTreeMap>, + ignores: HashMap, (Arc, usize)>, entries: SumTree, } @@ -244,6 +244,10 @@ impl Snapshot { FileIter::visible(self, start) } + fn child_entries<'a>(&'a self, path: &'a Path) -> ChildEntriesIter<'a> { + ChildEntriesIter::new(path, self) + } + pub fn root_entry(&self) -> &Entry { self.entry_for_path("").unwrap() } @@ -269,37 +273,16 @@ impl Snapshot { self.entry_for_path(path.as_ref()).map(|e| e.inode()) } - fn is_path_ignored(&self, path: &Path) -> Result { - todo!(); - Ok(false) - // let mut entry = self - // .entry_for_path(path) - // .ok_or_else(|| anyhow!("entry does not exist in worktree"))?; - - // if path.starts_with(".git") { - // Ok(true) - // } else { - // while let Some(parent_entry) = - // entry.path().parent().and_then(|p| self.entry_for_path(p)) - // { - // let parent_path = parent_entry.path(); - // if let Some((ignore, _)) = self.ignores.get(parent_path) { - // let relative_path = path.strip_prefix(parent_path).unwrap(); - // match ignore.matched_path_or_any_parents(relative_path, entry.is_dir()) { - // ::ignore::Match::Whitelist(_) => return Ok(false), - // ::ignore::Match::Ignore(_) => return Ok(true), - // ::ignore::Match::None => {} - // } - // } - // entry = parent_entry; - // } - // Ok(false) - // } - } - fn insert_entry(&mut self, entry: Entry) { if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { - self.insert_ignore_file(entry.path()); + let (ignore, err) = Gitignore::new(self.abs_path.join(entry.path())); + if let Some(err) = err { + log::error!("error in ignore file {:?} - {:?}", entry.path(), err); + } + + let ignore_dir_path = entry.path().parent().unwrap(); + self.ignores + .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); } self.entries.insert(entry); } @@ -312,9 +295,13 @@ impl Snapshot { ) { let mut edits = Vec::new(); - let mut parent_entry = self.entries.get(&PathKey(parent_path)).unwrap().clone(); + let mut parent_entry = self + .entries + .get(&PathKey(parent_path.clone())) + .unwrap() + .clone(); if let Some(ignore) = ignore { - self.ignores.insert(parent_entry.inode, ignore); + self.ignores.insert(parent_path, (ignore, self.scan_id)); } if matches!(parent_entry.kind, EntryKind::PendingDir) { parent_entry.kind = EntryKind::Dir; @@ -324,9 +311,6 @@ impl Snapshot { edits.push(Edit::Insert(parent_entry)); for entry in entries { - if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { - self.insert_ignore_file(entry.path()); - } edits.push(Edit::Insert(entry)); } self.entries.edit(edits); @@ -342,22 +326,38 @@ impl Snapshot { }; self.entries = new_entries; - // if path.file_name() == Some(&GITIGNORE) { - // if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { - // *scan_id = self.scan_id; - // } - // } + if path.file_name() == Some(&GITIGNORE) { + if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { + *scan_id = self.scan_id; + } + } } - fn insert_ignore_file(&mut self, path: &Path) -> Arc { - let (ignore, err) = Gitignore::new(self.abs_path.join(path)); - if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", path, err); + fn ignore_stack_for_path(&self, 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) { + new_ignores.push((ancestor, Some(ignore.clone()))); + } else { + new_ignores.push((ancestor, None)); + } } - let ignore = Arc::new(ignore); - let ignore_parent_inode = self.entry_for_path(path.parent().unwrap()).unwrap().inode; - self.ignores.insert(ignore_parent_inode, ignore.clone()); - ignore + + 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) { + ignore_stack = IgnoreStack::all(); + break; + } else if let Some(ignore) = ignore { + ignore_stack = ignore_stack.append(Arc::from(parent_path), ignore); + } + } + + if ignore_stack.is_path_ignored(path, is_dir) { + ignore_stack = IgnoreStack::all(); + } + + ignore_stack } } @@ -417,14 +417,10 @@ impl Entry { self.inode } - fn is_ignored(&self) -> bool { + pub fn is_ignored(&self) -> bool { self.is_ignored } - fn set_ignored(&mut self, ignored: bool) { - self.is_ignored = ignored; - } - fn is_dir(&self) -> bool { matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) } @@ -617,8 +613,6 @@ impl BackgroundScanner { return; } - return; - event_stream.run(move |events| { if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { return false; @@ -687,8 +681,6 @@ impl BackgroundScanner { }); } - self.recompute_ignore_statuses(); - Ok(()) } @@ -816,15 +808,17 @@ impl BackgroundScanner { snapshot.remove_path(&path); match self.fs_entry_for_path(path.clone(), &abs_path) { - Ok(Some(fs_entry)) => { + Ok(Some(mut fs_entry)) => { let is_dir = fs_entry.is_dir(); + let ignore_stack = snapshot.ignore_stack_for_path(&path, is_dir); + fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry); if is_dir { scan_queue_tx .send(ScanJob { abs_path, path, - ignore_stack: todo!(), + ignore_stack, scan_queue: scan_queue_tx.clone(), }) .unwrap(); @@ -854,124 +848,95 @@ impl BackgroundScanner { } }); - self.recompute_ignore_statuses(); + self.update_ignore_statuses(); true } - fn recompute_ignore_statuses(&self) { - self.compute_ignore_status_for_new_ignores(); - self.compute_ignore_status_for_new_entries(); + fn update_ignore_statuses(&self) { + let mut snapshot = self.snapshot(); + + let mut ignores_to_update = Vec::new(); + let mut ignores_to_delete = Vec::new(); + for (parent_path, (_, scan_id)) in &snapshot.ignores { + if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() { + ignores_to_update.push(parent_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()); + } + } + + for parent_path in ignores_to_delete { + snapshot.ignores.remove(&parent_path); + self.snapshot.lock().ignores.remove(&parent_path); + } + + let (ignore_queue_tx, ignore_queue_rx) = crossbeam_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 ignores_to_update + .peek() + .map_or(false, |p| p.starts_with(&parent_path)) + { + ignores_to_update.next().unwrap(); + } + + let ignore_stack = snapshot.ignore_stack_for_path(&parent_path, true); + ignore_queue_tx + .send(UpdateIgnoreStatusJob { + path: parent_path, + ignore_stack, + ignore_queue: ignore_queue_tx.clone(), + }) + .unwrap(); + } + drop(ignore_queue_tx); + + self.thread_pool.scoped(|scope| { + for _ in 0..self.thread_pool.thread_count() { + scope.execute(|| { + while let Ok(job) = ignore_queue_rx.recv() { + self.update_ignore_status(job, &snapshot); + } + }); + } + }); } - fn compute_ignore_status_for_new_ignores(&self) { - // let mut snapshot = self.snapshot(); + fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &Snapshot) { + 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()); + } - // let mut ignores_to_delete = Vec::new(); - // let mut changed_ignore_parents = Vec::new(); - // for (parent_path, (_, scan_id)) in &snapshot.ignores { - // let prev_ignore_parent = changed_ignore_parents.last(); - // if *scan_id == snapshot.scan_id - // && prev_ignore_parent.map_or(true, |l| !parent_path.starts_with(l)) - // { - // changed_ignore_parents.push(parent_path.clone()); - // } + let mut edits = Vec::new(); + for mut entry in snapshot.child_entries(&job.path).cloned() { + let was_ignored = entry.is_ignored; + entry.is_ignored = ignore_stack.is_path_ignored(entry.path(), entry.is_dir()); + if entry.is_dir() { + let child_ignore_stack = if entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }; + job.ignore_queue + .send(UpdateIgnoreStatusJob { + path: entry.path().clone(), + ignore_stack: child_ignore_stack, + ignore_queue: job.ignore_queue.clone(), + }) + .unwrap(); + } - // let ignore_parent_exists = snapshot.entry_for_path(parent_path).is_some(); - // let ignore_exists = snapshot - // .entry_for_path(parent_path.join(&*GITIGNORE)) - // .is_some(); - // if !ignore_parent_exists || !ignore_exists { - // ignores_to_delete.push(parent_path.clone()); - // } - // } - - // for parent_path in ignores_to_delete { - // snapshot.ignores.remove(&parent_path); - // self.snapshot.lock().ignores.remove(&parent_path); - // } - - // 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); - // } - // self.snapshot.lock().entries.edit(mem::take(&mut edits)); - // } - // }); - - // scope.execute(|| { - // let entries_tx = entries_tx; - // let mut cursor = snapshot.entries.cursor::<_, ()>(); - // for ignore_parent_path in &changed_ignore_parents { - // cursor.seek(&PathSearch::Exact(ignore_parent_path), SeekBias::Right); - // while let Some(entry) = cursor.item() { - // if entry.path().starts_with(ignore_parent_path) { - // entries_tx.send(entry.clone()).unwrap(); - // cursor.next(); - // } else { - // break; - // } - // } - // } - // }); - - // 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_path_ignored(entry.path()).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); - // } - // 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_ignore_status) - // { - // 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_path_ignored(entry.path()).unwrap()); - // edits_tx.send(Edit::Insert(entry)).unwrap(); - // } - // }); - // } - // }); + if entry.is_ignored != was_ignored { + edits.push(Edit::Insert(entry)); + } + } + self.snapshot.lock().entries.edit(edits); } fn fs_entry_for_path(&self, path: Arc, abs_path: &Path) -> Result> { @@ -1020,6 +985,12 @@ struct ScanJob { scan_queue: crossbeam_channel::Sender, } +struct UpdateIgnoreStatusJob { + path: Arc, + ignore_stack: Arc, + ignore_queue: crossbeam_channel::Sender, +} + pub trait WorktreeHandle { fn file(&self, path: impl AsRef, app: &AppContext) -> Result; } @@ -1088,6 +1059,40 @@ impl<'a> Iterator for FileIter<'a> { } } +struct ChildEntriesIter<'a> { + parent_path: &'a Path, + cursor: Cursor<'a, Entry, PathSearch<'a>, ()>, +} + +impl<'a> ChildEntriesIter<'a> { + fn new(parent_path: &'a Path, snapshot: &'a Snapshot) -> Self { + let mut cursor = snapshot.entries.cursor(); + cursor.seek(&PathSearch::Exact(parent_path), SeekBias::Right); + Self { + parent_path, + cursor, + } + } +} + +impl<'a> Iterator for ChildEntriesIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.cursor.item() { + if item.path().starts_with(self.parent_path) { + self.cursor + .seek_forward(&PathSearch::Successor(item.path()), SeekBias::Left); + Some(item) + } else { + None + } + } else { + None + } + } +} + fn mounted_volume_paths() -> Vec { unsafe { let mut stat_ptr: *mut libc::statfs = std::ptr::null_mut(); @@ -1260,16 +1265,6 @@ mod tests { let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); app.read(|ctx| tree.read(ctx).scan_complete()).await; - - app.read(|ctx| { - let paths = tree - .read(ctx) - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(); - println!("paths {:?}", paths); - }); - app.read(|ctx| { let tree = tree.read(ctx); let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); @@ -1278,8 +1273,6 @@ mod tests { assert_eq!(ignored.is_ignored(), true); }); - return; - fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); app.read(|ctx| tree.read(ctx).next_scan_complete()).await; @@ -1540,12 +1533,29 @@ mod tests { assert!(files.next().is_none()); assert!(visible_files.next().is_none()); - // for (ignore_parent_path, _) in &self.ignores { - // assert!(self.entry_for_path(ignore_parent_path).is_some()); - // assert!(self - // .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) - // .is_some()); - // } + let mut bfs_paths = Vec::new(); + let mut stack = vec![Path::new("")]; + while let Some(path) = stack.pop() { + bfs_paths.push(path); + let ix = stack.len(); + for child_entry in self.child_entries(path) { + stack.insert(ix, child_entry.path()); + } + } + + let dfs_paths = self + .entries + .cursor::<(), ()>() + .map(|e| e.path().as_ref()) + .collect::>(); + assert_eq!(bfs_paths, dfs_paths); + + for (ignore_parent_path, _) in &self.ignores { + assert!(self.entry_for_path(ignore_parent_path).is_some()); + assert!(self + .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) + .is_some()); + } } fn to_vec(&self) -> Vec<(&Path, u64, bool)> { diff --git a/zed/src/worktree/ignore.rs b/zed/src/worktree/ignore.rs index 5bd4389850..0b2528b981 100644 --- a/zed/src/worktree/ignore.rs +++ b/zed/src/worktree/ignore.rs @@ -21,8 +21,11 @@ impl IgnoreStack { Arc::new(Self::All) } + pub fn is_all(&self) -> bool { + matches!(self, IgnoreStack::All) + } + pub fn append(self: Arc, base: Arc, ignore: Arc) -> Arc { - log::info!("appending ignore {:?}", base); match self.as_ref() { IgnoreStack::All => self, _ => Arc::new(Self::Some { @@ -34,33 +37,18 @@ impl IgnoreStack { } pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool { - println!("is_path_ignored? {:?} {}", path, is_dir); match self { - Self::None => { - println!("none case"); - false - } - Self::All => { - println!("all case"); - true - } + Self::None => false, + Self::All => true, Self::Some { base, ignore, parent: prev, - } => { - println!( - "some case {:?} {:?}", - base, - path.strip_prefix(base).unwrap() - ); - - match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) { - ignore::Match::None => prev.is_path_ignored(path, is_dir), - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - } - } + } => match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) { + ignore::Match::None => prev.is_path_ignored(path, is_dir), + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + }, } } } From 6535304da8ce03bdd460079eb0c40b47848165df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Apr 2021 20:10:15 +0200 Subject: [PATCH 095/102] WIP: Start on updating file handles Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer/mod.rs | 2 +- zed/src/worktree.rs | 59 +++++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 19d2af917b..d8f808334c 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -429,7 +429,7 @@ impl Buffer { } } - pub fn path(&self) -> Option<&Arc> { + pub fn path(&self) -> Option> { self.file.as_ref().map(|file| file.path()) } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 72929d58b9..56aea09e9a 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -27,7 +27,7 @@ use std::{ ops::{AddAssign, Deref}, os::unix::{ffi::OsStrExt, fs::MetadataExt}, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Weak}, time::Duration, }; @@ -47,6 +47,7 @@ enum ScanState { pub struct Worktree { snapshot: Snapshot, background_snapshot: Arc>, + handles: Arc, Weak>>>>, scan_state: (watch::Sender, watch::Receiver), _event_stream_handle: fsevent::Handle, poll_scheduled: bool, @@ -55,7 +56,12 @@ pub struct Worktree { #[derive(Clone)] pub struct FileHandle { worktree: ModelHandle, + state: Arc>, +} + +struct FileHandleState { path: Arc, + is_deleted: bool, } impl Worktree { @@ -78,17 +84,19 @@ impl Worktree { fsevent::EventStream::new(&[snapshot.abs_path.as_ref()], Duration::from_millis(100)); let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); + let handles = Arc::new(Mutex::new(Default::default())); let tree = Self { snapshot, background_snapshot: background_snapshot.clone(), + handles: handles.clone(), scan_state: watch::channel_with(ScanState::Scanning), _event_stream_handle: event_stream_handle, poll_scheduled: false, }; std::thread::spawn(move || { - let scanner = BackgroundScanner::new(background_snapshot, scan_state_tx, id); + let scanner = BackgroundScanner::new(background_snapshot, handles, scan_state_tx, id); scanner.run(event_stream) }); @@ -374,21 +382,21 @@ impl fmt::Debug for Snapshot { } impl FileHandle { - pub fn path(&self) -> &Arc { - &self.path + pub fn path(&self) -> Arc { + self.state.lock().path.clone() } pub fn load_history(&self, ctx: &AppContext) -> impl Future> { - self.worktree.read(ctx).load_history(&self.path, ctx) + self.worktree.read(ctx).load_history(&self.path(), ctx) } pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task> { let worktree = self.worktree.read(ctx); - worktree.save(&self.path, content, ctx) + worktree.save(&self.path(), content, ctx) } pub fn entry_id(&self) -> (usize, Arc) { - (self.worktree.id(), self.path.clone()) + (self.worktree.id(), self.path()) } } @@ -561,18 +569,25 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { struct BackgroundScanner { snapshot: Arc>, notify: Sender, + handles: Arc, Weak>>>>, other_mount_paths: HashSet, thread_pool: scoped_pool::Pool, root_char_bag: CharBag, } impl BackgroundScanner { - fn new(snapshot: Arc>, notify: Sender, worktree_id: usize) -> Self { + fn new( + snapshot: Arc>, + handles: Arc, Weak>>>>, + notify: Sender, + worktree_id: usize, + ) -> Self { let root_char_bag = CharBag::from(snapshot.lock().root_name_chars.as_slice()); let mut scanner = Self { root_char_bag, snapshot, notify, + handles, other_mount_paths: Default::default(), thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)), }; @@ -997,13 +1012,27 @@ pub trait WorktreeHandle { impl WorktreeHandle for ModelHandle { fn file(&self, path: impl AsRef, app: &AppContext) -> Result { - self.read(app) + let tree = self.read(app); + let entry = tree .entry_for_path(&path) - .map(|entry| FileHandle { - worktree: self.clone(), - path: entry.path().clone(), - }) - .ok_or_else(|| anyhow!("path does not exist in tree")) + .ok_or_else(|| anyhow!("path does not exist in tree"))?; + let path = entry.path().clone(); + let mut handles = tree.handles.lock(); + let state = if let Some(state) = handles.get(&path).and_then(Weak::upgrade) { + state + } else { + let state = Arc::new(Mutex::new(FileHandleState { + path: path.clone(), + is_deleted: false, + })); + handles.insert(path, Arc::downgrade(&state)); + state + }; + + Ok(FileHandle { + worktree: self.clone(), + state, + }) } } @@ -1329,6 +1358,7 @@ mod tests { ignores: Default::default(), root_name_chars: Default::default(), })), + Arc::new(Mutex::new(Default::default())), notify_tx, 0, ); @@ -1363,6 +1393,7 @@ mod tests { ignores: Default::default(), root_name_chars: Default::default(), })), + Arc::new(Mutex::new(Default::default())), notify_tx, 1, ); From 9753e6741045e1efdf8ed0cf1797cf3d73210894 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 26 Apr 2021 12:56:13 -0700 Subject: [PATCH 096/102] Update filehandle paths when renames occur Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 101 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 56aea09e9a..b623c0e0e3 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -59,6 +59,7 @@ pub struct FileHandle { state: Arc>, } +#[derive(Debug)] struct FileHandleState { path: Arc, is_deleted: bool, @@ -386,6 +387,10 @@ impl FileHandle { self.state.lock().path.clone() } + pub fn is_deleted(&self) -> bool { + self.state.lock().is_deleted + } + pub fn load_history(&self, ctx: &AppContext) -> impl Future> { self.worktree.read(ctx).load_history(&self.path(), ctx) } @@ -799,6 +804,38 @@ impl BackgroundScanner { return false; }; + let mut renamed_paths: HashMap = HashMap::new(); + let mut updated_handles = HashMap::new(); + for event in &events { + if event.flags.contains(fsevent::StreamFlags::ITEM_RENAMED) { + if let Ok(path) = event.path.strip_prefix(&root_abs_path) { + if let Some(inode) = snapshot.inode_for_path(path) { + renamed_paths.insert(inode, path.to_path_buf()); + } else if let Ok(metadata) = fs::metadata(&event.path) { + let new_path = path; + let mut handles = self.handles.lock(); + if let Some(old_path) = renamed_paths.get(&metadata.ino()) { + handles.retain(|handle_path, handle_state| { + if let Ok(path_suffix) = handle_path.strip_prefix(&old_path) { + let new_handle_path: Arc = + new_path.join(path_suffix).into(); + if let Some(handle_state) = Weak::upgrade(&handle_state) { + handle_state.lock().path = new_handle_path.clone(); + updated_handles + .insert(new_handle_path, Arc::downgrade(&handle_state)); + } + false + } else { + true + } + }); + handles.extend(updated_handles.drain()); + } + } + } + } + } + events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut abs_paths = events.into_iter().map(|e| e.path).peekable(); let (scan_queue_tx, scan_queue_rx) = crossbeam_channel::unbounded(); @@ -865,6 +902,19 @@ impl BackgroundScanner { self.update_ignore_statuses(); + let mut handles = self.handles.lock(); + let snapshot = self.snapshot.lock(); + handles.retain(|path, handle_state| { + if let Some(handle_state) = Weak::upgrade(&handle_state) { + if snapshot.entry_for_path(&path).is_none() { + handle_state.lock().is_deleted = true; + } + true + } else { + false + } + }); + true } @@ -1239,26 +1289,34 @@ mod tests { let dir = temp_tree(json!({ "a": { "file1": "", + "file2": "", + "file3": "", }, "b": { "c": { - "file2": "", + "file4": "", + "file5": "", } } })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 2)); + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 5)); - let file2 = app.read(|ctx| { - let file2 = tree.file("b/c/file2", ctx).unwrap(); - assert_eq!(file2.path().as_ref(), Path::new("b/c/file2")); - file2 + let (file2, file3, file4, file5) = app.read(|ctx| { + ( + tree.file("a/file2", ctx).unwrap(), + tree.file("a/file3", ctx).unwrap(), + tree.file("b/c/file4", ctx).unwrap(), + tree.file("b/c/file5", ctx).unwrap(), + ) }); + std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); + std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); + std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - app.read(|ctx| tree.read(ctx).next_scan_complete()).await; app.read(|ctx| { @@ -1267,14 +1325,29 @@ mod tests { .paths() .map(|p| p.to_str().unwrap()) .collect::>(), - vec!["a", "a/file1", "b", "d", "d/file2"] - ) - }); + vec![ + "a", + "a/file1", + "a/file2.new", + "b", + "d", + "d/file3", + "d/file4" + ] + ); + assert_eq!(file2.path().as_ref(), Path::new("a/file2.new")); + assert_eq!(file4.path().as_ref(), Path::new("d/file4")); + assert_eq!(file5.path().as_ref(), Path::new("d/file5")); + assert!(!file2.is_deleted()); + assert!(!file4.is_deleted()); + assert!(file5.is_deleted()); - // tree.condition(&app, move |_, _| { - // file2.path().as_ref() == Path::new("d/file2") - // }) - // .await; + // Right now, this rename isn't detected because the target path + // no longer exists on the file system by the time we process the + // rename event. + assert_eq!(file3.path().as_ref(), Path::new("a/file3")); + assert!(file3.is_deleted()); + }); }); } From 870925e2ac64c1777b7151f77079285c44485273 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Apr 2021 14:29:33 -0600 Subject: [PATCH 097/102] Rerender tabs when buffers' file handles change Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer/mod.rs | 979 ++++++++++++++----------- zed/src/editor/buffer_view.rs | 29 +- zed/src/editor/display_map/fold_map.rs | 12 +- zed/src/editor/display_map/mod.rs | 4 +- zed/src/workspace/workspace.rs | 14 +- zed/src/worktree.rs | 50 +- 6 files changed, 615 insertions(+), 473 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index d8f808334c..597990fbb3 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -353,15 +353,29 @@ pub struct UndoOperation { } impl Buffer { - pub fn new>>(replica_id: ReplicaId, base_text: T) -> Self { - Self::build(replica_id, None, History::new(base_text.into())) + pub fn new>>( + replica_id: ReplicaId, + base_text: T, + ctx: &mut ModelContext, + ) -> Self { + Self::build(replica_id, None, History::new(base_text.into()), ctx) } - pub fn from_history(replica_id: ReplicaId, file: FileHandle, history: History) -> Self { - Self::build(replica_id, Some(file), history) + pub fn from_history( + replica_id: ReplicaId, + file: FileHandle, + history: History, + ctx: &mut ModelContext, + ) -> Self { + Self::build(replica_id, Some(file), history, ctx) } - fn build(replica_id: ReplicaId, file: Option, history: History) -> Self { + fn build( + replica_id: ReplicaId, + file: Option, + history: History, + ctx: &mut ModelContext, + ) -> Self { let mut insertion_splits = HashMap::default(); let mut fragments = SumTree::new(); @@ -410,6 +424,10 @@ impl Buffer { }); } + if let Some(file) = file.as_ref() { + file.observe_from_model(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged)); + } + Self { file, fragments, @@ -445,6 +463,8 @@ impl Buffer { pub fn save(&mut self, ctx: &mut ModelContext) -> LocalBoxFuture<'static, Result<()>> { if let Some(file) = &self.file { + dbg!(file.path()); + let snapshot = self.snapshot(); let version = self.version.clone(); let save_task = file.save(snapshot, ctx.as_ref()); @@ -1772,6 +1792,7 @@ pub enum Event { Edited(Vec), Dirtied, Saved, + FileHandleChanged, } impl Entity for Buffer { @@ -2305,21 +2326,24 @@ mod tests { use std::{cell::RefCell, rc::Rc}; #[test] - fn test_edit() -> Result<()> { - let mut buffer = Buffer::new(0, "abc"); - assert_eq!(buffer.text(), "abc"); - buffer.edit(vec![3..3], "def", None)?; - assert_eq!(buffer.text(), "abcdef"); - buffer.edit(vec![0..0], "ghi", None)?; - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit(vec![5..5], "jkl", None)?; - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit(vec![6..7], "", None)?; - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit(vec![4..9], "mno", None)?; - assert_eq!(buffer.text(), "ghiamnoef"); - - Ok(()) + fn test_edit() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "abc", ctx); + assert_eq!(buffer.text(), "abc"); + buffer.edit(vec![3..3], "def", None).unwrap(); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit(vec![5..5], "jkl", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit(vec![6..7], "", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit(vec![4..9], "mno", None).unwrap(); + assert_eq!(buffer.text(), "ghiamnoef"); + buffer + }); + }) } #[test] @@ -2329,8 +2353,8 @@ mod tests { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - let buffer1 = app.add_model(|_| Buffer::new(0, "abcdef")); - let buffer2 = app.add_model(|_| Buffer::new(1, "abcdef")); + let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx)); + let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); let mut buffer_ops = Vec::new(); buffer1.update(app, |buffer, ctx| { let buffer_1_events = buffer_1_events.clone(); @@ -2408,187 +2432,207 @@ mod tests { #[test] fn test_random_edits() { for seed in 0..100 { - println!("{:?}", seed); - let mut rng = &mut StdRng::seed_from_u64(seed); + App::test((), |ctx| { + println!("{:?}", seed); + let mut rng = &mut StdRng::seed_from_u64(seed); - let reference_string_len = rng.gen_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - let mut buffer = Buffer::new(0, reference_string.as_str()); - let mut buffer_versions = Vec::new(); + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); + let mut buffer_versions = Vec::new(); + for _i in 0..10 { + let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); + for old_range in old_ranges.iter().rev() { + reference_string = [ + &reference_string[0..old_range.start], + new_text.as_str(), + &reference_string[old_range.end..], + ] + .concat(); + } + assert_eq!(buffer.text(), reference_string); - for _i in 0..10 { - let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); - for old_range in old_ranges.iter().rev() { - reference_string = [ - &reference_string[0..old_range.start], - new_text.as_str(), - &reference_string[old_range.end..], - ] - .concat(); - } - assert_eq!(buffer.text(), reference_string); + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(rng); + reference_string = buffer.text(); + } - if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(rng); - reference_string = buffer.text(); - } + { + let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); - { - let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); + for (len, rows) in &line_lengths { + for row in rows { + assert_eq!(buffer.line_len(*row).unwrap(), *len); + } + } - for (len, rows) in &line_lengths { - for row in rows { - assert_eq!(buffer.line_len(*row).unwrap(), *len); + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let rightmost_point = buffer.rightmost_point(); + assert_eq!(rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&rightmost_point.row)); + } + + for _ in 0..5 { + let end = rng.gen_range(0..buffer.len() + 1); + let start = rng.gen_range(0..end + 1); + + let line_lengths = line_lengths_in_range(&buffer, start..end); + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let range_sum = buffer.text_summary_for_range(start..end); + assert_eq!(range_sum.rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&range_sum.rightmost_point.row)); + let range_text = &buffer.text()[start..end]; + assert_eq!(range_sum.chars, range_text.chars().count()); + assert_eq!(range_sum.bytes, range_text.len()); + } + + if rng.gen_bool(0.3) { + buffer_versions.push(buffer.clone()); } } - let (longest_column, longest_rows) = line_lengths.iter().next_back().unwrap(); - let rightmost_point = buffer.rightmost_point(); - assert_eq!(rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&rightmost_point.row)); - } + for mut old_buffer in buffer_versions { + let mut delta = 0_isize; + for Edit { + old_range, + new_range, + } in buffer.edits_since(old_buffer.version.clone()) + { + let old_len = old_range.end - old_range.start; + let new_len = new_range.end - new_range.start; + let old_start = (old_range.start as isize + delta) as usize; + let new_text: String = + buffer.text_for_range(new_range).unwrap().collect(); + old_buffer + .edit(Some(old_start..old_start + old_len), new_text, None) + .unwrap(); - for _ in 0..5 { - let end = rng.gen_range(0..buffer.len() + 1); - let start = rng.gen_range(0..end + 1); + delta += new_len as isize - old_len as isize; + } + assert_eq!(old_buffer.text(), buffer.text()); + } - let line_lengths = line_lengths_in_range(&buffer, start..end); - let (longest_column, longest_rows) = line_lengths.iter().next_back().unwrap(); - let range_sum = buffer.text_summary_for_range(start..end); - assert_eq!(range_sum.rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&range_sum.rightmost_point.row)); - let range_text = &buffer.text()[start..end]; - assert_eq!(range_sum.chars, range_text.chars().count()); - assert_eq!(range_sum.bytes, range_text.len()); - } - - if rng.gen_bool(0.3) { - buffer_versions.push(buffer.clone()); - } - } - - for mut old_buffer in buffer_versions { - let mut delta = 0_isize; - for Edit { - old_range, - new_range, - } in buffer.edits_since(old_buffer.version.clone()) - { - let old_len = old_range.end - old_range.start; - let new_len = new_range.end - new_range.start; - let old_start = (old_range.start as isize + delta) as usize; - let new_text: String = buffer.text_for_range(new_range).unwrap().collect(); - old_buffer - .edit(Some(old_start..old_start + old_len), new_text, None) - .unwrap(); - - delta += new_len as isize - old_len as isize; - } - assert_eq!(old_buffer.text(), buffer.text()); - } + buffer + }) + }); } } #[test] - fn test_line_len() -> Result<()> { - let mut buffer = Buffer::new(0, ""); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None)?; - buffer.edit(vec![12..12], "kl\nmno", None)?; - buffer.edit(vec![18..18], "\npqrs\n", None)?; - buffer.edit(vec![18..21], "\nPQ", None)?; + fn test_line_len() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + buffer.edit(vec![18..18], "\npqrs\n", None).unwrap(); + buffer.edit(vec![18..21], "\nPQ", None).unwrap(); - assert_eq!(buffer.line_len(0)?, 4); - assert_eq!(buffer.line_len(1)?, 3); - assert_eq!(buffer.line_len(2)?, 5); - assert_eq!(buffer.line_len(3)?, 3); - assert_eq!(buffer.line_len(4)?, 4); - assert_eq!(buffer.line_len(5)?, 0); - assert!(buffer.line_len(6).is_err()); - - Ok(()) + assert_eq!(buffer.line_len(0).unwrap(), 4); + assert_eq!(buffer.line_len(1).unwrap(), 3); + assert_eq!(buffer.line_len(2).unwrap(), 5); + assert_eq!(buffer.line_len(3).unwrap(), 3); + assert_eq!(buffer.line_len(4).unwrap(), 4); + assert_eq!(buffer.line_len(5).unwrap(), 0); + assert!(buffer.line_len(6).is_err()); + buffer + }); + }); } #[test] - fn test_rightmost_point() -> Result<()> { - let mut buffer = Buffer::new(0, ""); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None)?; - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![12..12], "kl\nmno", None)?; - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![18..18], "\npqrs", None)?; - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![10..12], "", None)?; - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![24..24], "tuv", None)?; - assert_eq!(buffer.rightmost_point().row, 4); - - println!("{:?}", buffer.text()); - - Ok(()) + fn test_rightmost_point() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![18..18], "\npqrs", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![10..12], "", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![24..24], "tuv", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 4); + buffer + }); + }); } #[test] fn test_text_summary_for_range() { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz"); - let text = Text::from(buffer.text()); - - assert_eq!( - buffer.text_summary_for_range(1..3), - text.slice(1..3).summary() - ); - assert_eq!( - buffer.text_summary_for_range(1..12), - text.slice(1..12).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..20), - text.slice(0..20).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..22), - text.slice(0..22).summary() - ); - assert_eq!( - buffer.text_summary_for_range(7..22), - text.slice(7..22).summary() - ); + App::test((), |ctx| { + ctx.add_model(|ctx| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); + let text = Text::from(buffer.text()); + assert_eq!( + buffer.text_summary_for_range(1..3), + text.slice(1..3).summary() + ); + assert_eq!( + buffer.text_summary_for_range(1..12), + text.slice(1..12).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..20), + text.slice(0..20).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..22), + text.slice(0..22).summary() + ); + assert_eq!( + buffer.text_summary_for_range(7..22), + text.slice(7..22).summary() + ); + buffer + }); + }); } #[test] - fn test_chars_at() -> Result<()> { - let mut buffer = Buffer::new(0, ""); - buffer.edit(vec![0..0], "abcd\nefgh\nij", None)?; - buffer.edit(vec![12..12], "kl\nmno", None)?; - buffer.edit(vec![18..18], "\npqrs", None)?; - buffer.edit(vec![18..21], "\nPQ", None)?; + fn test_chars_at() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap(); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + buffer.edit(vec![18..18], "\npqrs", None).unwrap(); + buffer.edit(vec![18..21], "\nPQ", None).unwrap(); - let chars = buffer.chars_at(Point::new(0, 0))?; - assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); + let chars = buffer.chars_at(Point::new(0, 0)).unwrap(); + assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); - let chars = buffer.chars_at(Point::new(1, 0))?; - assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); + let chars = buffer.chars_at(Point::new(1, 0)).unwrap(); + assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); - let chars = buffer.chars_at(Point::new(2, 0))?; - assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); + let chars = buffer.chars_at(Point::new(2, 0)).unwrap(); + assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); - let chars = buffer.chars_at(Point::new(3, 0))?; - assert_eq!(chars.collect::(), "mno\nPQrs"); + let chars = buffer.chars_at(Point::new(3, 0)).unwrap(); + assert_eq!(chars.collect::(), "mno\nPQrs"); - let chars = buffer.chars_at(Point::new(4, 0))?; - assert_eq!(chars.collect::(), "PQrs"); + let chars = buffer.chars_at(Point::new(4, 0)).unwrap(); + assert_eq!(chars.collect::(), "PQrs"); - // Regression test: - let mut buffer = Buffer::new(0, ""); - buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", None)?; - buffer.edit(vec![60..60], "\n", None)?; + // Regression test: + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", None).unwrap(); + buffer.edit(vec![60..60], "\n", None).unwrap(); - let chars = buffer.chars_at(Point::new(6, 0))?; - assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); + let chars = buffer.chars_at(Point::new(6, 0)).unwrap(); + assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); - Ok(()) + buffer + }); + }); } // #[test] @@ -2706,177 +2750,202 @@ mod tests { } #[test] - fn test_anchors() -> Result<()> { - let mut buffer = Buffer::new(0, ""); - buffer.edit(vec![0..0], "abc", None)?; - let left_anchor = buffer.anchor_before(2).unwrap(); - let right_anchor = buffer.anchor_after(2).unwrap(); + fn test_anchors() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abc", None).unwrap(); + let left_anchor = buffer.anchor_before(2).unwrap(); + let right_anchor = buffer.anchor_after(2).unwrap(); - buffer.edit(vec![1..1], "def\n", None)?; - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); + buffer.edit(vec![1..1], "def\n", None).unwrap(); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); - buffer.edit(vec![2..3], "", None)?; - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); + buffer.edit(vec![2..3], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); - buffer.edit(vec![5..5], "ghi\n", None)?; - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 2, column: 0 } - ); + buffer.edit(vec![5..5], "ghi\n", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 2, column: 0 } + ); - buffer.edit(vec![7..9], "", None)?; - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 }, - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 3 } - ); + buffer.edit(vec![7..9], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 }, + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 3 } + ); - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 })?, - buffer.anchor_before(0)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 })?, - buffer.anchor_before(1)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 })?, - buffer.anchor_before(2)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 })?, - buffer.anchor_before(3)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 })?, - buffer.anchor_before(4)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 })?, - buffer.anchor_before(5)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 })?, - buffer.anchor_before(6)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 })?, - buffer.anchor_before(7)? - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 })?, - buffer.anchor_before(8)? - ); + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }).unwrap(), + buffer.anchor_before(0).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }).unwrap(), + buffer.anchor_before(1).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }).unwrap(), + buffer.anchor_before(2).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }).unwrap(), + buffer.anchor_before(3).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }).unwrap(), + buffer.anchor_before(4).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }).unwrap(), + buffer.anchor_before(5).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }).unwrap(), + buffer.anchor_before(6).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }).unwrap(), + buffer.anchor_before(7).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }).unwrap(), + buffer.anchor_before(8).unwrap() + ); - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0).unwrap(); - let anchor_at_offset_1 = buffer.anchor_before(1).unwrap(); - let anchor_at_offset_2 = buffer.anchor_before(2).unwrap(); + // Comparison between anchors. + let anchor_at_offset_0 = buffer.anchor_before(0).unwrap(); + let anchor_at_offset_1 = buffer.anchor_before(1).unwrap(); + let anchor_at_offset_2 = buffer.anchor_before(2).unwrap(); - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer)?, - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer)?, - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer)?, - Ordering::Equal - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Equal + ); - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer)?, - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer)?, - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer)?, - Ordering::Less - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer)?, - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer)?, - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer)?, - Ordering::Greater - ); - Ok(()) + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + buffer + }); + }); } #[test] - fn test_anchors_at_start_and_end() -> Result<()> { - let mut buffer = Buffer::new(0, ""); - let before_start_anchor = buffer.anchor_before(0).unwrap(); - let after_end_anchor = buffer.anchor_after(0).unwrap(); + fn test_anchors_at_start_and_end() { + App::test((), |ctx| { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + let before_start_anchor = buffer.anchor_before(0).unwrap(); + let after_end_anchor = buffer.anchor_after(0).unwrap(); - buffer.edit(vec![0..0], "abc", None)?; - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); + buffer.edit(vec![0..0], "abc", None).unwrap(); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); - let after_start_anchor = buffer.anchor_after(0).unwrap(); - let before_end_anchor = buffer.anchor_before(3).unwrap(); + let after_start_anchor = buffer.anchor_after(0).unwrap(); + let before_end_anchor = buffer.anchor_before(3).unwrap(); - buffer.edit(vec![3..3], "def", None)?; - buffer.edit(vec![0..0], "ghi", None)?; - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); - assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); - - Ok(()) + buffer.edit(vec![3..3], "def", None).unwrap(); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); + assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); + buffer + }); + }); } #[test] - fn test_is_modified() -> Result<()> { + fn test_is_modified() { App::test((), |app| { - let model = app.add_model(|_| Buffer::new(0, "abc")); + let model = app.add_model(|ctx| Buffer::new(0, "abc", ctx)); let events = Rc::new(RefCell::new(Vec::new())); // initially, the buffer isn't dirty. @@ -2958,94 +3027,113 @@ mod tests { ); }); }); - Ok(()) } #[test] - fn test_undo_redo() -> Result<()> { - let mut buffer = Buffer::new(0, "1234"); + fn test_undo_redo() { + App::test((), |app| { + app.add_model(|ctx| { + let mut buffer = Buffer::new(0, "1234", ctx); - let edit1 = buffer.edit(vec![1..1], "abx", None)?; - let edit2 = buffer.edit(vec![3..4], "yzef", None)?; - let edit3 = buffer.edit(vec![3..5], "cd", None)?; - assert_eq!(buffer.text(), "1abcdef234"); + let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); + let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); + let edit3 = buffer.edit(vec![3..5], "cd", None).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abcdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abcdef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; - assert_eq!(buffer.text(), "1234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1234"); - Ok(()) + buffer + }); + }); } #[test] - fn test_history() -> Result<()> { - let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456"); + fn test_history() { + App::test((), |app| { + app.add_model(|ctx| { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, "123456", ctx); - let (set_id, _) = - buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4])?, None); - buffer.start_transaction_at(Some(set_id), now)?; - buffer.edit(vec![2..4], "cd", None)?; - buffer.end_transaction_at(Some(set_id), now, None)?; - assert_eq!(buffer.text(), "12cd56"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![4..4]); + let (set_id, _) = buffer + .add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer.edit(vec![2..4], "cd", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cd56"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - buffer.start_transaction_at(Some(set_id), now)?; - buffer.update_selection_set(set_id, buffer.selections_from_ranges(vec![1..3])?, None)?; - buffer.edit(vec![4..5], "e", None)?; - buffer.end_transaction_at(Some(set_id), now, None)?; - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![1..3]); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![1..3]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![4..5], "e", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); - buffer.start_transaction_at(Some(set_id), now)?; - buffer.update_selection_set(set_id, buffer.selections_from_ranges(vec![2..2])?, None)?; - buffer.edit(vec![0..1], "a", None)?; - buffer.edit(vec![1..1], "b", None)?; - buffer.end_transaction_at(Some(set_id), now, None)?; - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![3..3]); + now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![2..2]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![0..1], "a", None).unwrap(); + buffer.edit(vec![1..1], "b", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - // Last transaction happened past the group interval, undo it on its - // own. - buffer.undo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![1..3]); + // Last transaction happened past the group interval, undo it on its + // own. + buffer.undo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - // First two transactions happened within the group interval, undo them - // together. - buffer.undo(None); - assert_eq!(buffer.text(), "123456"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![4..4]); + // First two transactions happened within the group interval, undo them + // together. + buffer.undo(None); + assert_eq!(buffer.text(), "123456"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - // Redo the first two transactions together. - buffer.redo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![1..3]); + // Redo the first two transactions together. + buffer.redo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - // Redo the last transaction on its own. - buffer.redo(None); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id)?, vec![3..3]); + // Redo the last transaction on its own. + buffer.redo(None); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - Ok(()) + buffer + }); + }); } #[test] @@ -3058,61 +3146,66 @@ mod tests { println!("{:?}", seed); let mut rng = &mut StdRng::seed_from_u64(seed); - let base_text_len = rng.gen_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(); - for i in 0..PEERS { - let buffer = Buffer::new(i as ReplicaId, base_text.as_str()); - buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); - } - - let mut mutation_count = 10; - loop { - let replica_index = rng.gen_range(0..PEERS); - let replica_id = replica_ids[replica_index]; - let buffer = &mut buffers[replica_index]; - - match rng.gen_range(0..=100) { - 0..=50 if mutation_count != 0 => { - let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - buffer - .apply_ops(network.receive(replica_id, &mut rng), None) - .unwrap(); - } - _ => {} + App::test((), |ctx| { + let base_text_len = rng.gen_range(0..10); + let base_text = RandomCharIter::new(&mut rng) + .take(base_text_len) + .collect::(); + let mut replica_ids = Vec::new(); + let mut buffers = Vec::new(); + let mut network = Network::new(); + for i in 0..PEERS { + let buffer = + ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); } - if mutation_count == 0 && network.is_idle() { - break; - } - } + let mut mutation_count = 10; + loop { + let replica_index = rng.gen_range(0..PEERS); + let replica_id = replica_ids[replica_index]; + buffers[replica_index].update(ctx, |buffer, _| match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 51..=70 if mutation_count != 0 => { + let ops = buffer.randomly_undo_redo(&mut rng); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + buffer + .apply_ops(network.receive(replica_id, &mut rng), None) + .unwrap(); + } + _ => {} + }); - for buffer in &buffers[1..] { - assert_eq!(buffer.text(), buffers[0].text()); - assert_eq!( - buffer.all_selections().collect::>(), - buffers[0].all_selections().collect::>() - ); - assert_eq!( - buffer.all_selection_ranges().collect::>(), - buffers[0].all_selection_ranges().collect::>() - ); - } + if mutation_count == 0 && network.is_idle() { + break; + } + } + + let first_buffer = buffers[0].read(ctx); + for buffer in &buffers[1..] { + let buffer = buffer.read(ctx); + assert_eq!(buffer.text(), first_buffer.text()); + assert_eq!( + buffer.all_selections().collect::>(), + first_buffer.all_selections().collect::>() + ); + assert_eq!( + buffer.all_selection_ranges().collect::>(), + first_buffer + .all_selection_ranges() + .collect::>() + ); + } + }); } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index b3c2eb0496..c13198f7a6 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -119,7 +119,7 @@ struct ClipboardSelection { impl BufferView { pub fn single_line(settings: watch::Receiver, ctx: &mut ViewContext) -> Self { - let buffer = ctx.add_model(|_| Buffer::new(0, String::new())); + let buffer = ctx.add_model(|ctx| Buffer::new(0, String::new(), ctx)); let mut view = Self::for_buffer(buffer, settings, ctx); view.single_line = true; view @@ -1316,6 +1316,7 @@ impl BufferView { buffer::Event::Edited(_) => ctx.emit(Event::Edited), buffer::Event::Dirtied => ctx.emit(Event::Dirtied), buffer::Event::Saved => ctx.emit(Event::Saved), + buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged), } } } @@ -1326,6 +1327,7 @@ pub enum Event { Blurred, Dirtied, Saved, + FileHandleChanged, } impl Entity for BufferView { @@ -1372,7 +1374,10 @@ impl workspace::ItemView for BufferView { } fn should_update_tab_on_event(event: &Self::Event) -> bool { - matches!(event, Event::Saved | Event::Dirtied) + matches!( + event, + Event::Saved | Event::Dirtied | Event::FileHandleChanged + ) } fn title(&self, app: &AppContext) -> std::string::String { @@ -1419,7 +1424,8 @@ mod tests { #[test] fn test_selection_with_mouse() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n")); + let buffer = + app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, buffer_view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); @@ -1533,7 +1539,7 @@ mod tests { let layout_cache = TextLayoutCache::new(app.platform().fonts()); let font_cache = app.font_cache().clone(); - let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6))); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); let settings = settings::channel(&font_cache).unwrap().1; let (_, view) = @@ -1550,7 +1556,7 @@ mod tests { #[test] fn test_fold() { App::test((), |app| { - let buffer = app.add_model(|_| { + let buffer = app.add_model(|ctx| { Buffer::new( 0, " @@ -1571,6 +1577,7 @@ mod tests { } " .unindent(), + ctx, ) }); let settings = settings::channel(&app.font_cache()).unwrap().1; @@ -1644,7 +1651,7 @@ mod tests { #[test] fn test_move_cursor() -> Result<()> { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6))); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); @@ -1681,8 +1688,12 @@ mod tests { #[test] fn test_backspace() { App::test((), |app| { - let buffer = app.add_model(|_| { - Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n") + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + "one two three\nfour five six\nseven eight nine\nten\n", + ctx, + ) }); let settings = settings::channel(&app.font_cache()).unwrap().1; let (_, view) = @@ -1714,7 +1725,7 @@ mod tests { #[test] fn test_clipboard() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six ")); + let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); let settings = settings::channel(&app.font_cache()).unwrap().1; let view = app .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 58f51cee94..536971a87d 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -471,7 +471,7 @@ mod tests { #[test] fn test_basic_folds() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); let mut map = FoldMap::new(buffer.clone(), app.as_ref()); map.fold( @@ -522,7 +522,7 @@ mod tests { #[test] fn test_overlapping_folds() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); let mut map = FoldMap::new(buffer.clone(), app.as_ref()); map.fold( vec![ @@ -541,7 +541,7 @@ mod tests { #[test] fn test_merging_folds_via_edit() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6))); + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); let mut map = FoldMap::new(buffer.clone(), app.as_ref()); map.fold( @@ -589,10 +589,10 @@ mod tests { let mut rng = StdRng::seed_from_u64(seed); App::test((), |app| { - let buffer = app.add_model(|_| { + let buffer = app.add_model(|ctx| { let len = rng.gen_range(0..10); let text = RandomCharIter::new(&mut rng).take(len).collect::(); - Buffer::new(0, text) + Buffer::new(0, text, ctx) }); let mut map = FoldMap::new(buffer.clone(), app.as_ref()); @@ -664,7 +664,7 @@ mod tests { fn test_buffer_rows() { App::test((), |app| { let text = sample_text(6, 6) + "\n"; - let buffer = app.add_model(|_| Buffer::new(0, text)); + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); let mut map = FoldMap::new(buffer.clone(), app.as_ref()); diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index 8142a8925c..c44f3ca302 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -298,7 +298,7 @@ mod tests { fn test_chars_at() { App::test((), |app| { let text = sample_text(6, 6); - let buffer = app.add_model(|_| Buffer::new(0, text)); + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); let map = app.add_model(|ctx| DisplayMap::new(buffer.clone(), 4, ctx)); buffer .update(app, |buffer, ctx| { @@ -365,7 +365,7 @@ mod tests { #[test] fn test_max_point() { App::test((), |app| { - let buffer = app.add_model(|_| Buffer::new(0, "aaa\n\t\tbbb")); + let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); let map = app.add_model(|ctx| DisplayMap::new(buffer.clone(), 4, ctx)); assert_eq!( map.read(app).max_point(app.as_ref()), diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index d98bd9c798..925781e340 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -1,6 +1,6 @@ use super::{ItemView, ItemViewHandle}; use crate::{ - editor::Buffer, + editor::{Buffer, History}, settings::Settings, time::ReplicaId, watch, @@ -174,15 +174,17 @@ impl Workspace { let replica_id = self.replica_id; let file = worktree.file(path.clone(), ctx.as_ref())?; let history = file.load_history(ctx.as_ref()); - let buffer = async move { Ok(Buffer::from_history(replica_id, file, history.await?)) }; + // let buffer = async move { Ok(Buffer::from_history(replica_id, file, history.await?)) }; let (mut tx, rx) = watch::channel(None); self.items.insert(item_key, OpenedItem::Loading(rx)); ctx.spawn( - buffer, - move |me, buffer: anyhow::Result, ctx| match buffer { - Ok(buffer) => { - let handle = Box::new(ctx.add_model(|_| buffer)) as Box; + history, + move |me, history: anyhow::Result, ctx| match history { + Ok(history) => { + let handle = Box::new( + ctx.add_model(|ctx| Buffer::from_history(replica_id, file, history, ctx)), + ) as Box; me.items .insert(item_key, OpenedItem::Loaded(handle.clone())); ctx.spawn( diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index b623c0e0e3..fb7ea918f1 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -59,7 +59,7 @@ pub struct FileHandle { state: Arc>, } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] struct FileHandleState { path: Arc, is_deleted: bool, @@ -403,6 +403,32 @@ impl FileHandle { pub fn entry_id(&self) -> (usize, Arc) { (self.worktree.id(), self.path()) } + + pub fn observe_from_model( + &self, + ctx: &mut ModelContext, + mut callback: impl FnMut(&mut T, FileHandle, &mut ModelContext) + 'static, + ) { + let mut prev_state = self.state.lock().clone(); + let cur_state = Arc::downgrade(&self.state); + ctx.observe(&self.worktree, move |observer, worktree, ctx| { + if let Some(cur_state) = cur_state.upgrade() { + let cur_state_unlocked = cur_state.lock(); + if *cur_state_unlocked != prev_state { + prev_state = cur_state_unlocked.clone(); + drop(cur_state_unlocked); + callback( + observer, + FileHandle { + worktree, + state: cur_state, + }, + ctx, + ); + } + } + }); + } } #[derive(Clone, Debug)] @@ -818,7 +844,12 @@ impl BackgroundScanner { handles.retain(|handle_path, handle_state| { if let Ok(path_suffix) = handle_path.strip_prefix(&old_path) { let new_handle_path: Arc = - new_path.join(path_suffix).into(); + if path_suffix.file_name().is_some() { + new_path.join(path_suffix) + } else { + new_path.to_path_buf() + } + .into(); if let Some(handle_state) = Weak::upgrade(&handle_state) { handle_state.lock().path = new_handle_path.clone(); updated_handles @@ -1266,20 +1297,24 @@ mod tests { app.read(|ctx| tree.read(ctx).scan_complete()).await; app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); - let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); + let buffer = + app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); let path = tree.update(&mut app, |tree, ctx| { let path = tree.files(0).next().unwrap().path().clone(); assert_eq!(path.file_name().unwrap(), "file1"); - smol::block_on(tree.save(&path, buffer.snapshot(), ctx.as_ref())).unwrap(); + smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())) + .unwrap(); path }); - let loaded_history = app + let history = app .read(|ctx| tree.read(ctx).load_history(&path, ctx)) .await .unwrap(); - assert_eq!(loaded_history.base_text.as_ref(), buffer.text()); + app.read(|ctx| { + assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()); + }); }); } @@ -1335,7 +1370,8 @@ mod tests { "d/file4" ] ); - assert_eq!(file2.path().as_ref(), Path::new("a/file2.new")); + + assert_eq!(file2.path().to_str().unwrap(), "a/file2.new"); assert_eq!(file4.path().as_ref(), Path::new("d/file4")); assert_eq!(file5.path().as_ref(), Path::new("d/file5")); assert!(!file2.is_deleted()); From e7c594262f503edd1e436c5263d0c854dc888dfd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 26 Apr 2021 15:04:26 -0700 Subject: [PATCH 098/102] Fix handling of uppercase characters in fuzzy finding --- zed/src/file_finder.rs | 6 +++--- zed/src/worktree.rs | 37 +++++++++++++++++++++--------------- zed/src/worktree/char_bag.rs | 10 ++++++++++ zed/src/worktree/fuzzy.rs | 23 ++++++++++++++++------ 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 4302ece217..a57730fa28 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -141,9 +141,9 @@ impl FileFinder { self.worktree(tree_id, app).map(|tree| { let prefix = if self.include_root_name { - tree.root_name_chars() + tree.root_name() } else { - &[] + "" }; let path = path_match.path.clone(); let path_string = path_match.path.to_string_lossy(); @@ -169,7 +169,7 @@ impl FileFinder { let highlight_color = ColorU::from_u32(0x304ee2ff); let bold = *Properties::new().weight(Weight::BOLD); - let mut full_path = prefix.iter().collect::(); + let mut full_path = prefix.to_string(); full_path.push_str(&path_string); let mut container = Container::new( diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index fb7ea918f1..6dc5bcec02 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -68,16 +68,16 @@ struct FileHandleState { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let abs_path = path.into(); - let root_name_chars = abs_path.file_name().map_or(Vec::new(), |n| { - n.to_string_lossy().chars().chain(Some('/')).collect() - }); + let root_name = abs_path + .file_name() + .map_or(String::new(), |n| n.to_string_lossy().to_string() + "/"); let (scan_state_tx, scan_state_rx) = smol::channel::unbounded(); let id = ctx.model_id(); let snapshot = Snapshot { id, scan_id: 0, abs_path, - root_name_chars, + root_name, ignores: Default::default(), entries: Default::default(), }; @@ -224,7 +224,7 @@ pub struct Snapshot { id: usize, scan_id: usize, abs_path: Arc, - root_name_chars: Vec, + root_name: String, ignores: HashMap, (Arc, usize)>, entries: SumTree, } @@ -261,12 +261,10 @@ impl Snapshot { self.entry_for_path("").unwrap() } - pub fn root_name(&self) -> Option<&OsStr> { - self.abs_path.file_name() - } - - pub fn root_name_chars(&self) -> &[char] { - &self.root_name_chars + /// Returns the filename of the snapshot's root directory, + /// with a trailing slash. + pub fn root_name(&self) -> &str { + &self.root_name } fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { @@ -613,7 +611,12 @@ impl BackgroundScanner { notify: Sender, worktree_id: usize, ) -> Self { - let root_char_bag = CharBag::from(snapshot.lock().root_name_chars.as_slice()); + let root_char_bag = snapshot + .lock() + .root_name + .chars() + .map(|c| c.to_ascii_lowercase()) + .collect(); let mut scanner = Self { root_char_bag, snapshot, @@ -1069,7 +1072,11 @@ impl BackgroundScanner { fn char_bag(&self, path: &Path) -> CharBag { let mut result = self.root_char_bag; - result.extend(path.to_string_lossy().chars()); + result.extend( + path.to_string_lossy() + .chars() + .map(|c| c.to_ascii_lowercase()), + ); result } } @@ -1465,7 +1472,7 @@ mod tests { abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), - root_name_chars: Default::default(), + root_name: Default::default(), })), Arc::new(Mutex::new(Default::default())), notify_tx, @@ -1500,7 +1507,7 @@ mod tests { abs_path: root_dir.path().into(), entries: Default::default(), ignores: Default::default(), - root_name_chars: Default::default(), + root_name: Default::default(), })), Arc::new(Mutex::new(Default::default())), notify_tx, diff --git a/zed/src/worktree/char_bag.rs b/zed/src/worktree/char_bag.rs index b21949fe89..e2f68a27d1 100644 --- a/zed/src/worktree/char_bag.rs +++ b/zed/src/worktree/char_bag.rs @@ -1,3 +1,5 @@ +use std::iter::FromIterator; + #[derive(Copy, Clone, Debug, Default)] pub struct CharBag(u64); @@ -31,6 +33,14 @@ impl Extend for CharBag { } } +impl FromIterator for CharBag { + fn from_iter>(iter: T) -> Self { + let mut result = Self::default(); + result.extend(iter); + result + } +} + impl From<&str> for CharBag { fn from(s: &str) -> Self { let mut bag = Self(0); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index b6d13f3288..849e6989b8 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -171,10 +171,16 @@ fn match_single_tree_paths<'a>( let mut lowercase_path_chars = Vec::new(); let prefix = if include_root_name { - snapshot.root_name_chars.as_slice() + snapshot.root_name() } else { - &[] - }; + "" + } + .chars() + .collect::>(); + let lowercase_prefix = prefix + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); for path_entry in path_entries { if !path_entry.char_bag.is_superset(query_chars) { @@ -190,7 +196,7 @@ fn match_single_tree_paths<'a>( if !find_last_positions( last_positions, - prefix, + &lowercase_prefix, &lowercase_path_chars, &lowercase_query[..], ) { @@ -209,6 +215,7 @@ fn match_single_tree_paths<'a>( &path_chars, &lowercase_path_chars, &prefix, + &lowercase_prefix, smart_case, &last_positions, score_matrix, @@ -257,6 +264,7 @@ fn score_match( path: &[char], path_cased: &[char], prefix: &[char], + lowercase_prefix: &[char], smart_case: bool, last_positions: &[usize], score_matrix: &mut [Option], @@ -270,6 +278,7 @@ fn score_match( path, path_cased, prefix, + lowercase_prefix, smart_case, last_positions, score_matrix, @@ -300,6 +309,7 @@ fn recursive_score_match( path: &[char], path_cased: &[char], prefix: &[char], + lowercase_prefix: &[char], smart_case: bool, last_positions: &[usize], score_matrix: &mut [Option], @@ -328,7 +338,7 @@ fn recursive_score_match( let mut last_slash = 0; for j in path_idx..=limit { let path_char = if j < prefix.len() { - prefix[j] + lowercase_prefix[j] } else { path_cased[j - prefix.len()] }; @@ -404,6 +414,7 @@ fn recursive_score_match( path, path_cased, prefix, + lowercase_prefix, smart_case, last_positions, score_matrix, @@ -547,7 +558,7 @@ mod tests { abs_path: PathBuf::new().into(), ignores: Default::default(), entries: Default::default(), - root_name_chars: Vec::new(), + root_name: Default::default(), }, false, path_entries.into_iter(), From 733dc15c320d4461e73d0e23b92cc3f88760dad8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 26 Apr 2021 15:46:06 -0700 Subject: [PATCH 099/102] Ignore .git directories --- zed/src/worktree.rs | 2 ++ zed/src/worktree/ignore.rs | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6dc5bcec02..196e62bb19 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1423,10 +1423,12 @@ mod tests { app.read(|ctx| tree.read(ctx).next_scan_complete()).await; app.read(|ctx| { let tree = tree.read(ctx); + 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); }); }); } diff --git a/zed/src/worktree/ignore.rs b/zed/src/worktree/ignore.rs index 0b2528b981..9eb605eaf8 100644 --- a/zed/src/worktree/ignore.rs +++ b/zed/src/worktree/ignore.rs @@ -1,6 +1,5 @@ -use std::{path::Path, sync::Arc}; - use ignore::gitignore::Gitignore; +use std::{ffi::OsStr, path::Path, sync::Arc}; pub enum IgnoreStack { None, @@ -37,6 +36,10 @@ impl IgnoreStack { } pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool { + if is_dir && path.file_name() == Some(OsStr::new(".git")) { + return true; + } + match self { Self::None => false, Self::All => true, From 09f5c7c23e36210ecf17de25a01434f3de17d738 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 26 Apr 2021 15:53:23 -0700 Subject: [PATCH 100/102] Fix scrollwheel events in uniformlist Co-Authored-By: Nathan Sobo --- gpui/src/elements/uniform_list.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index c1b9b86ca7..161b465ac8 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -74,10 +74,6 @@ where scroll_max: f32, ctx: &mut EventContext, ) -> bool { - if !self.rect().unwrap().contains_point(position) { - return false; - } - if !precise { todo!("still need to handle non-precise scroll events from a mouse wheel"); } @@ -111,11 +107,6 @@ where fn scroll_top(&self) -> f32 { self.state.0.lock().scroll_top } - - fn rect(&self) -> Option { - todo!() - // try_rect(self.origin, self.size) - } } impl Element for UniformList @@ -213,7 +204,7 @@ where fn dispatch_event( &mut self, event: &Event, - _: RectF, + bounds: RectF, layout: &mut Self::LayoutState, _: &mut Self::PaintState, ctx: &mut EventContext, @@ -229,8 +220,10 @@ where delta, precise, } => { - if self.scroll(*position, *delta, *precise, layout.scroll_max, ctx) { - handled = true; + if bounds.contains_point(*position) { + if self.scroll(*position, *delta, *precise, layout.scroll_max, ctx) { + handled = true; + } } } _ => {} From 55fcc586bca4e4ada4cca181eea264e641ada963 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 26 Apr 2021 16:14:43 -0700 Subject: [PATCH 101/102] Cancel outstanding fuzzy-matching calls before starting a new one Co-Authored-By: Nathan Sobo --- gpui/src/elements/uniform_list.rs | 2 +- zed/src/file_finder.rs | 15 ++++++++++++++- zed/src/worktree.rs | 1 + zed/src/worktree/fuzzy.rs | 11 +++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 161b465ac8..0b1d51d9ac 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -68,7 +68,7 @@ where fn scroll( &self, - position: Vector2F, + _: Vector2F, delta: Vector2F, precise: bool, scroll_max: f32, diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index a57730fa28..71abeb5ae4 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -14,7 +14,14 @@ use gpui::{ AppContext, Axis, Border, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use std::{cmp, path::Path, sync::Arc}; +use std::{ + cmp, + path::Path, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; pub struct FileFinder { handle: WeakViewHandle, @@ -26,6 +33,7 @@ pub struct FileFinder { matches: Vec, include_root_name: bool, selected: usize, + cancel_flag: Arc, list_state: UniformListState, } @@ -287,6 +295,7 @@ impl FileFinder { matches: Vec::new(), include_root_name: false, selected: 0, + cancel_flag: Arc::new(AtomicBool::new(false)), list_state: UniformListState::new(), } } @@ -358,6 +367,9 @@ impl FileFinder { .collect::>(); let search_id = util::post_inc(&mut self.search_count); let pool = ctx.as_ref().thread_pool().clone(); + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); let task = ctx.background_executor().spawn(async move { let include_root_name = snapshots.len() > 1; let matches = match_paths( @@ -367,6 +379,7 @@ impl FileFinder { false, false, 100, + cancel_flag, pool, ); (search_id, include_root_name, matches) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 196e62bb19..e475e3e733 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1277,6 +1277,7 @@ mod tests { false, false, 10, + Default::default(), ctx.thread_pool().clone(), ) .into_iter() diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 849e6989b8..0097a44268 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -4,6 +4,7 @@ use std::{ cmp::{max, min, Ordering, Reverse}, collections::BinaryHeap, path::Path, + sync::atomic::{self, AtomicBool}, sync::Arc, }; @@ -52,6 +53,7 @@ pub fn match_paths<'a, T>( include_ignored: bool, smart_case: bool, max_results: usize, + cancel_flag: Arc, pool: scoped_pool::Pool, ) -> Vec where @@ -77,6 +79,7 @@ where pool.scoped(|scope| { for (segment_idx, results) in segment_results.iter_mut().enumerate() { let trees = snapshots.clone(); + let cancel_flag = &cancel_flag; scope.execute(move || { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; @@ -130,6 +133,7 @@ where &mut last_positions, &mut score_matrix, &mut best_position_matrix, + &cancel_flag, ); } if tree_end >= segment_end { @@ -166,6 +170,7 @@ fn match_single_tree_paths<'a>( last_positions: &mut Vec, score_matrix: &mut Vec>, best_position_matrix: &mut Vec, + cancel_flag: &AtomicBool, ) { let mut path_chars = Vec::new(); let mut lowercase_path_chars = Vec::new(); @@ -187,6 +192,10 @@ fn match_single_tree_paths<'a>( continue; } + if cancel_flag.load(atomic::Ordering::Relaxed) { + break; + } + path_chars.clear(); lowercase_path_chars.clear(); for c in path_entry.path.to_string_lossy().chars() { @@ -550,6 +559,7 @@ mod tests { match_positions.resize(query.len(), 0); last_positions.resize(query.len(), 0); + let cancel_flag = AtomicBool::new(false); let mut results = BinaryHeap::new(); match_single_tree_paths( &Snapshot { @@ -573,6 +583,7 @@ mod tests { &mut last_positions, &mut Vec::new(), &mut Vec::new(), + &cancel_flag, ); results From f29f1b073d823be36ffa01d65381e4bbe787dd3a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Apr 2021 20:23:56 -0600 Subject: [PATCH 102/102] Preserve selected file finder path when Worktree changes --- zed/src/file_finder.rs | 43 ++++++++++++++++++++++++++------------- zed/src/worktree/fuzzy.rs | 8 ++++---- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 71abeb5ae4..d6ec299593 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -32,7 +32,7 @@ pub struct FileFinder { latest_search_id: usize, matches: Vec, include_root_name: bool, - selected: usize, + selected: Option>, cancel_flag: Arc, list_state: UniformListState, } @@ -224,12 +224,13 @@ impl FileFinder { ) .with_uniform_padding(6.0); - if index == self.selected || index < self.matches.len() - 1 { + let selected_index = self.selected_index(); + if index == selected_index || index < self.matches.len() - 1 { container = container.with_border(Border::bottom(1.0, ColorU::from_u32(0xdbdbdcff))); } - if index == self.selected { + if index == selected_index { container = container.with_background_color(ColorU::from_u32(0xdbdbdcff)); } @@ -294,7 +295,7 @@ impl FileFinder { latest_search_id: 0, matches: Vec::new(), include_root_name: false, - selected: 0, + selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), list_state: UniformListState::new(), } @@ -327,19 +328,34 @@ impl FileFinder { } } - fn select_prev(&mut self, _: &(), ctx: &mut ViewContext) { - if self.selected > 0 { - self.selected -= 1; + fn selected_index(&self) -> usize { + if let Some(selected) = self.selected.as_ref() { + for (ix, path_match) in self.matches.iter().enumerate() { + if path_match.path.as_ref() == selected.as_ref() { + return ix; + } + } } - self.list_state.scroll_to(self.selected); + 0 + } + + fn select_prev(&mut self, _: &(), ctx: &mut ViewContext) { + let mut selected_index = self.selected_index(); + if selected_index > 0 { + selected_index -= 1; + self.selected = Some(self.matches[selected_index].path.clone()); + } + self.list_state.scroll_to(selected_index); ctx.notify(); } fn select_next(&mut self, _: &(), ctx: &mut ViewContext) { - if self.selected + 1 < self.matches.len() { - self.selected += 1; + let mut selected_index = self.selected_index(); + if selected_index + 1 < self.matches.len() { + selected_index += 1; + self.selected = Some(self.matches[selected_index].path.clone()); } - self.list_state.scroll_to(self.selected); + self.list_state.scroll_to(selected_index); ctx.notify(); } @@ -348,7 +364,7 @@ impl FileFinder { } fn confirm(&mut self, _: &(), ctx: &mut ViewContext) { - if let Some(m) = self.matches.get(self.selected) { + if let Some(m) = self.matches.get(self.selected_index()) { ctx.emit(Event::Selected(m.tree_id, m.path.clone())); } } @@ -397,8 +413,7 @@ impl FileFinder { self.latest_search_id = search_id; self.matches = matches; self.include_root_name = include_root_name; - self.selected = 0; - self.list_state.scroll_to(0); + self.list_state.scroll_to(self.selected_index()); ctx.notify(); } } diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 0097a44268..10a1cace31 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -187,8 +187,8 @@ fn match_single_tree_paths<'a>( .map(|c| c.to_ascii_lowercase()) .collect::>(); - for path_entry in path_entries { - if !path_entry.char_bag.is_superset(query_chars) { + for candidate in path_entries { + if !candidate.char_bag.is_superset(query_chars) { continue; } @@ -198,7 +198,7 @@ fn match_single_tree_paths<'a>( path_chars.clear(); lowercase_path_chars.clear(); - for c in path_entry.path.to_string_lossy().chars() { + for c in candidate.path.to_string_lossy().chars() { path_chars.push(c); lowercase_path_chars.push(c.to_ascii_lowercase()); } @@ -236,7 +236,7 @@ fn match_single_tree_paths<'a>( if score > 0.0 { results.push(Reverse(PathMatch { tree_id: snapshot.id, - path: path_entry.path.clone(), + path: candidate.path.clone(), score, positions: match_positions.clone(), }));