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))