From 9ad61961507255b2782d93bb285b6108341dab0f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:38:26 +0200 Subject: [PATCH] fs: Replace a bunch of uses of smol::fs with manual impls (#39906) smol::fs uses a separate threadpool, which is a bit yuck. Release Notes: - N/A --- Cargo.lock | 30 +- Cargo.toml | 1 + crates/editor/src/editor_tests.rs | 1 + crates/fs/src/fs.rs | 78 ++-- crates/worktree/Cargo.toml | 5 + crates/worktree/bin/bench_background_scan.rs | 47 +++ crates/worktree/src/worktree.rs | 355 +++++++++++-------- crates/worktree/src/worktree_tests.rs | 3 +- 8 files changed, 336 insertions(+), 184 deletions(-) create mode 100644 crates/worktree/bin/bench_background_scan.rs diff --git a/Cargo.lock b/Cargo.lock index 471a82d83f766b4a1b73e7c8442f05047430caea..2791789c1c9717775dde263c44f18585c3727d1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,7 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" dependencies = [ - "async-lock", + "async-lock 3.4.1", "blocking", "futures-lite 2.6.0", ] @@ -1151,7 +1151,7 @@ dependencies = [ "async-channel 2.3.1", "async-executor", "async-io", - "async-lock", + "async-lock 3.4.1", "blocking", "futures-lite 2.6.0", "once_cell", @@ -1163,7 +1163,7 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ - "async-lock", + "async-lock 3.4.1", "cfg-if", "concurrent-queue", "futures-io", @@ -1175,6 +1175,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + [[package]] name = "async-lock" version = "3.4.1" @@ -1214,7 +1223,7 @@ checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel 2.3.1", "async-io", - "async-lock", + "async-lock 3.4.1", "async-signal", "async-task", "blocking", @@ -1243,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ "async-io", - "async-lock", + "async-lock 3.4.1", "atomic-waker", "cfg-if", "futures-core", @@ -1264,7 +1273,7 @@ dependencies = [ "async-channel 1.9.0", "async-global-executor", "async-io", - "async-lock", + "async-lock 3.4.1", "async-process", "crossbeam-utils", "futures-channel", @@ -9036,7 +9045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -10736,7 +10745,7 @@ dependencies = [ "ashpd 0.12.0", "async-fs", "async-io", - "async-lock", + "async-lock 3.4.1", "blocking", "cbc", "cipher", @@ -14823,7 +14832,7 @@ dependencies = [ "async-executor", "async-fs", "async-io", - "async-lock", + "async-lock 3.4.1", "async-net", "async-process", "blocking", @@ -19868,6 +19877,7 @@ name = "worktree" version = "0.1.0" dependencies = [ "anyhow", + "async-lock 2.8.0", "clock", "collections", "fs", @@ -20163,7 +20173,7 @@ dependencies = [ "async-broadcast", "async-executor", "async-io", - "async-lock", + "async-lock 3.4.1", "async-process", "async-recursion", "async-task", diff --git a/Cargo.toml b/Cargo.toml index 3e4d3d18483583dd592df38deac4ed7b4e6dfee8..56b8fd4026c3aa3e138ee252d72be8882e07e8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -455,6 +455,7 @@ async-compat = "0.2.1" async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" async-fs = "2.1" +async-lock = "2.1" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" async-tar = "0.5.0" diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ede8d1c20d664ecaa5f26955de85a4dbc5313a5e..e5605d33f5c89143ccff72e836e86b89749e39c3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12509,6 +12509,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ) .await; + cx.run_until_parked(); // Set up a buffer white some trailing whitespace and no trailing newline. cx.set_state( &[ diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 03cf78d74eb0e0ed8caf22c710acc131960e97c0..bb947bcb93e350990b899cc210cbab3537795a97 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,6 +7,7 @@ pub mod fs_watcher; use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; +use futures::stream::iter; use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; @@ -562,12 +563,17 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - let text = smol::unblock(|| std::fs::read_to_string(path)).await?; - Ok(text) + self.executor + .spawn(async move { Ok(std::fs::read_to_string(path)?) }) + .await } + async fn load_bytes(&self, path: &Path) -> Result> { let path = path.to_path_buf(); - let bytes = smol::unblock(|| std::fs::read(path)).await?; + let bytes = self + .executor + .spawn(async move { std::fs::read(path) }) + .await?; Ok(bytes) } @@ -635,30 +641,46 @@ impl Fs for RealFs { if let Some(path) = path.parent() { self.create_dir(path).await?; } - smol::fs::write(path, content).await?; - Ok(()) + let path = path.to_owned(); + let contents = content.to_owned(); + self.executor + .spawn(async move { + std::fs::write(path, contents)?; + Ok(()) + }) + .await } async fn canonicalize(&self, path: &Path) -> Result { - Ok(smol::fs::canonicalize(path) + let path = path.to_owned(); + self.executor + .spawn(async move { + std::fs::canonicalize(&path).with_context(|| format!("canonicalizing {path:?}")) + }) .await - .with_context(|| format!("canonicalizing {path:?}"))?) } async fn is_file(&self, path: &Path) -> bool { - smol::fs::metadata(path) + let path = path.to_owned(); + self.executor + .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) }) .await - .is_ok_and(|metadata| metadata.is_file()) } async fn is_dir(&self, path: &Path) -> bool { - smol::fs::metadata(path) + let path = path.to_owned(); + self.executor + .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) }) .await - .is_ok_and(|metadata| metadata.is_dir()) } async fn metadata(&self, path: &Path) -> Result> { - let symlink_metadata = match smol::fs::symlink_metadata(path).await { + let path_buf = path.to_owned(); + let symlink_metadata = match self + .executor + .spawn(async move { std::fs::symlink_metadata(&path_buf) }) + .await + { Ok(metadata) => metadata, Err(err) => { return match (err.kind(), err.raw_os_error()) { @@ -670,17 +692,23 @@ impl Fs for RealFs { }; let path_buf = path.to_path_buf(); - let path_exists = smol::unblock(move || { - path_buf - .try_exists() - .with_context(|| format!("checking existence for path {path_buf:?}")) - }) - .await?; + let path_exists = self + .executor + .spawn(async move { + path_buf + .try_exists() + .with_context(|| format!("checking existence for path {path_buf:?}")) + }) + .await?; let is_symlink = symlink_metadata.file_type().is_symlink(); let metadata = match (is_symlink, path_exists) { - (true, true) => smol::fs::metadata(path) - .await - .with_context(|| "accessing symlink for path {path}")?, + (true, true) => { + let path_buf = path.to_path_buf(); + self.executor + .spawn(async move { std::fs::metadata(path_buf) }) + .await + .with_context(|| "accessing symlink for path {path}")? + } _ => symlink_metadata, }; @@ -715,7 +743,13 @@ impl Fs for RealFs { &self, path: &Path, ) -> Result>>>> { - let result = smol::fs::read_dir(path).await?.map(|entry| match entry { + let path = path.to_owned(); + let result = iter( + self.executor + .spawn(async move { std::fs::read_dir(path) }) + .await?, + ) + .map(|entry| match entry { Ok(entry) => Ok(entry.path()), Err(error) => Err(anyhow!("failed to read dir entry {error:?}")), }); diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index fdeca37b7ac73759fe9851f722985349e0a183b7..5bf60c02082ea375e87c875ed96853ff44059d3c 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -9,6 +9,10 @@ license = "GPL-3.0-or-later" path = "src/worktree.rs" doctest = false +[[bin]] +name = "bench_background_scan" +path = "bin/bench_background_scan.rs" + [lints] workspace = true @@ -24,6 +28,7 @@ test-support = [ [dependencies] anyhow.workspace = true +async-lock.workspace = true clock.workspace = true collections.workspace = true fs.workspace = true diff --git a/crates/worktree/bin/bench_background_scan.rs b/crates/worktree/bin/bench_background_scan.rs new file mode 100644 index 0000000000000000000000000000000000000000..8f789fd61624448b701624f3259ed36051e6393f --- /dev/null +++ b/crates/worktree/bin/bench_background_scan.rs @@ -0,0 +1,47 @@ +use std::{ + path::Path, + sync::{Arc, atomic::AtomicUsize}, +}; + +use fs::RealFs; +use gpui::Application; +use settings::Settings; +use worktree::{Worktree, WorktreeSettings}; + +fn main() { + let Some(worktree_root_path) = std::env::args().nth(1) else { + println!( + "Missing path to worktree root\nUsage: bench_background_scan PATH_TO_WORKTREE_ROOT" + ); + return; + }; + let app = Application::headless(); + + app.run(|cx| { + settings::init(cx); + WorktreeSettings::register(cx); + let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); + + cx.spawn(async move |cx| { + let worktree = Worktree::local( + Path::new(&worktree_root_path), + true, + fs, + Arc::new(AtomicUsize::new(0)), + cx, + ) + .await + .expect("Worktree initialization to succeed"); + let did_finish_scan = worktree + .update(cx, |this, _| this.as_local().unwrap().scan_complete()) + .unwrap(); + let start = std::time::Instant::now(); + did_finish_scan.await; + println!("{:?}", start.elapsed()); + cx.update(|cx| { + cx.quit(); + }) + }) + .detach(); + }) +} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0327c345a1ee90c51751fdc71fda668511b9fd16..29b0e99492d5f23c61dd53b51f55e5990587eccd 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -64,7 +64,7 @@ use std::{ use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ - ResultExt, debug_panic, + ResultExt, debug_panic, maybe, paths::{PathMatcher, PathStyle, SanitizedPath, home_dir}, rel_path::RelPath, }; @@ -226,7 +226,7 @@ impl Default for WorkDirectory { } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LocalSnapshot { snapshot: Snapshot, global_gitignore: Option>, @@ -239,6 +239,7 @@ pub struct LocalSnapshot { /// The file handle of the worktree root. `None` if the worktree is a directory. /// (so we can find it after it's been moved) root_file_handle: Option>, + executor: BackgroundExecutor, } struct BackgroundScannerState { @@ -321,7 +322,6 @@ impl DerefMut for LocalSnapshot { } } -#[derive(Debug)] enum ScanState { Started, Updated { @@ -402,6 +402,7 @@ impl Worktree { PathStyle::local(), ), root_file_handle, + executor: cx.background_executor().clone(), }; let worktree_id = snapshot.id(); @@ -1069,7 +1070,7 @@ impl LocalWorktree { scan_requests_rx, path_prefixes_to_scan_rx, next_entry_id, - state: Mutex::new(BackgroundScannerState { + state: async_lock::Mutex::new(BackgroundScannerState { prev_snapshot: snapshot.snapshot.clone(), snapshot, scanned_dirs: Default::default(), @@ -2441,7 +2442,7 @@ impl LocalSnapshot { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.absolutize(&entry.path); - match smol::block_on(build_gitignore(&abs_path, fs)) { + match self.executor.block(build_gitignore(&abs_path, fs)) { Ok(ignore) => { self.ignores_by_parent_abs_path .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); @@ -2492,7 +2493,12 @@ impl LocalSnapshot { inodes } - fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool, fs: &dyn Fs) -> IgnoreStack { + async fn ignore_stack_for_abs_path( + &self, + abs_path: &Path, + is_dir: bool, + fs: &dyn Fs, + ) -> IgnoreStack { let mut new_ignores = Vec::new(); let mut repo_root = None; for (index, ancestor) in abs_path.ancestors().enumerate() { @@ -2503,9 +2509,8 @@ impl LocalSnapshot { new_ignores.push((ancestor, None)); } } - let metadata = smol::block_on(fs.metadata(&ancestor.join(DOT_GIT))) - .ok() - .flatten(); + + let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten(); if metadata.is_some() { repo_root = Some(Arc::from(ancestor)); break; @@ -2651,7 +2656,7 @@ impl BackgroundScannerState { .any(|p| entry.path.starts_with(p)) } - fn enqueue_scan_dir( + async fn enqueue_scan_dir( &self, abs_path: Arc, entry: &Entry, @@ -2659,7 +2664,10 @@ impl BackgroundScannerState { fs: &dyn Fs, ) { let path = entry.path.clone(); - let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true, fs); + let ignore_stack = self + .snapshot + .ignore_stack_for_abs_path(&abs_path, true, fs) + .await; let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); if !ancestor_inodes.contains(&entry.inode) { @@ -2697,11 +2705,17 @@ impl BackgroundScannerState { } } - fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry { + async fn insert_entry( + &mut self, + mut entry: Entry, + fs: &dyn Fs, + watcher: &dyn Watcher, + ) -> Entry { self.reuse_entry_id(&mut entry); let entry = self.snapshot.insert_entry(entry, fs); if entry.path.file_name() == Some(&DOT_GIT) { - self.insert_git_repository(entry.path.clone(), fs, watcher); + self.insert_git_repository(entry.path.clone(), fs, watcher) + .await; } #[cfg(test)] @@ -2832,7 +2846,7 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn insert_git_repository( + async fn insert_git_repository( &mut self, dot_git_path: Arc, fs: &dyn Fs, @@ -2873,10 +2887,11 @@ impl BackgroundScannerState { fs, watcher, ) + .await .log_err(); } - fn insert_git_repository_for_path( + async fn insert_git_repository_for_path( &mut self, work_directory: WorkDirectory, dot_git_abs_path: Arc, @@ -2898,7 +2913,7 @@ impl BackgroundScannerState { let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory); let (repository_dir_abs_path, common_dir_abs_path) = - discover_git_paths(&dot_git_abs_path, fs); + discover_git_paths(&dot_git_abs_path, fs).await; watcher .add(&common_dir_abs_path) .context("failed to add common directory to watcher") @@ -3542,7 +3557,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { } struct BackgroundScanner { - state: Mutex, + state: async_lock::Mutex, fs: Arc, fs_case_sensitive: bool, status_updates_tx: UnboundedSender, @@ -3568,31 +3583,39 @@ impl BackgroundScanner { // If the worktree root does not contain a git repository, then find // the git repository in an ancestor directory. Find any gitignore files // in ancestor directories. - let root_abs_path = self.state.lock().snapshot.abs_path.clone(); + let root_abs_path = self.state.lock().await.snapshot.abs_path.clone(); let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await; self.state .lock() + .await .snapshot .ignores_by_parent_abs_path .extend(ignores); - let containing_git_repository = repo.and_then(|(ancestor_dot_git, work_directory)| { - self.state - .lock() - .insert_git_repository_for_path( - work_directory, - ancestor_dot_git.clone().into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .log_err()?; - Some(ancestor_dot_git) - }); + let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo { + maybe!(async { + self.state + .lock() + .await + .insert_git_repository_for_path( + work_directory, + ancestor_dot_git.clone().into(), + self.fs.as_ref(), + self.watcher.as_ref(), + ) + .await + .log_err()?; + Some(ancestor_dot_git) + }) + .await + } else { + None + }; log::trace!("containing git repository: {containing_git_repository:?}"); let mut global_gitignore_events = if let Some(global_gitignore_path) = &paths::global_gitignore_path() { - self.state.lock().snapshot.global_gitignore = + self.state.lock().await.snapshot.global_gitignore = if self.fs.is_file(&global_gitignore_path).await { build_gitignore(global_gitignore_path, self.fs.as_ref()) .await @@ -3606,31 +3629,34 @@ impl BackgroundScanner { .await .0 } else { - self.state.lock().snapshot.global_gitignore = None; + self.state.lock().await.snapshot.global_gitignore = None; Box::pin(futures::stream::empty()) }; let (scan_job_tx, scan_job_rx) = channel::unbounded(); { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; state.snapshot.scan_id += 1; if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { - let ignore_stack = state.snapshot.ignore_stack_for_abs_path( - root_abs_path.as_path(), - true, - self.fs.as_ref(), - ); + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref()) + .await; if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { root_entry.is_ignored = true; - state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); + state + .insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()) + .await; } if root_entry.is_dir() { - state.enqueue_scan_dir( - root_abs_path.as_path().into(), - &root_entry, - &scan_job_tx, - self.fs.as_ref(), - ); + state + .enqueue_scan_dir( + root_abs_path.as_path().into(), + &root_entry, + &scan_job_tx, + self.fs.as_ref(), + ) + .await; } } }; @@ -3639,11 +3665,11 @@ impl BackgroundScanner { drop(scan_job_tx); self.scan_dirs(true, scan_job_rx).await; { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; state.snapshot.completed_scan_id = state.snapshot.scan_id; } - self.send_status_update(false, SmallVec::new()); + self.send_status_update(false, SmallVec::new()).await; // Process any any FS events that occurred while performing the initial scan. // For these events, update events cannot be as precise, because we didn't @@ -3688,7 +3714,7 @@ impl BackgroundScanner { if did_scan { let abs_path = { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; state.path_prefixes_to_scan.insert(request.path.clone()); state.snapshot.absolutize(&request.path) }; @@ -3697,7 +3723,7 @@ impl BackgroundScanner { self.process_events(vec![abs_path]).await; } } - self.send_status_update(false, request.done); + self.send_status_update(false, request.done).await; } paths = fs_events_rx.next().fuse() => { @@ -3726,7 +3752,7 @@ impl BackgroundScanner { request.relative_paths.sort_unstable(); self.forcibly_load_paths(&request.relative_paths).await; - let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_path = self.state.lock().await.snapshot.abs_path.clone(); let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; let root_canonical_path = match &root_canonical_path { Ok(path) => SanitizedPath::new(path), @@ -3748,7 +3774,7 @@ impl BackgroundScanner { .collect::>(); { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id; state.snapshot.scan_id += 1; if is_idle { @@ -3765,11 +3791,11 @@ impl BackgroundScanner { ) .await; - self.send_status_update(scanning, request.done) + self.send_status_update(scanning, request.done).await } async fn process_events(&self, mut abs_paths: Vec) { - let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_path = self.state.lock().await.snapshot.abs_path.clone(); let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; let root_canonical_path = match &root_canonical_path { Ok(path) => SanitizedPath::new(path), @@ -3777,6 +3803,7 @@ impl BackgroundScanner { let new_path = self .state .lock() + .await .snapshot .root_file_handle .clone() @@ -3809,24 +3836,31 @@ impl BackgroundScanner { let mut dot_git_abs_paths = Vec::new(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(b)); - abs_paths.retain(|abs_path| { + { + let snapshot = &self.state.lock().await.snapshot; + abs_paths.retain(|abs_path| { let abs_path = &SanitizedPath::new(abs_path); - let snapshot = &self.state.lock().snapshot; + { let mut is_git_related = false; - let dot_git_paths = abs_path.as_path().ancestors().find_map(|ancestor| { - if smol::block_on(is_git_dir(ancestor, self.fs.as_ref())) { + let dot_git_paths = self.executor.block(maybe!(async { + let mut path = None; + for ancestor in abs_path.as_path().ancestors() { + + if is_git_dir(ancestor, self.fs.as_ref()).await { let path_in_git_dir = abs_path .as_path() .strip_prefix(ancestor) .expect("stripping off the ancestor"); - Some((ancestor.to_owned(), path_in_git_dir.to_owned())) - } else { - None + path = Some((ancestor.to_owned(), path_in_git_dir.to_owned())); + break; } - }); + } + path + + })); if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths { if skipped_files_in_dot_git @@ -3899,12 +3933,12 @@ impl BackgroundScanner { true } }); - + } if relative_paths.is_empty() && dot_git_abs_paths.is_empty() { return; } - self.state.lock().snapshot.scan_id += 1; + self.state.lock().await.snapshot.scan_id += 1; let (scan_job_tx, scan_job_rx) = channel::unbounded(); log::debug!("received fs events {:?}", relative_paths); @@ -3918,29 +3952,29 @@ impl BackgroundScanner { .await; let affected_repo_roots = if !dot_git_abs_paths.is_empty() { - self.update_git_repositories(dot_git_abs_paths) + self.update_git_repositories(dot_git_abs_paths).await } else { Vec::new() }; { - let mut ignores_to_update = self.ignores_needing_update(); + let mut ignores_to_update = self.ignores_needing_update().await; ignores_to_update.extend(affected_repo_roots); - let ignores_to_update = self.order_ignores(ignores_to_update); - let snapshot = self.state.lock().snapshot.clone(); + let ignores_to_update = self.order_ignores(ignores_to_update).await; + let snapshot = self.state.lock().await.snapshot.clone(); self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update) .await; self.scan_dirs(false, scan_job_rx).await; } { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry) in mem::take(&mut state.removed_entries) { state.scanned_dirs.remove(&entry.id); } } - self.send_status_update(false, SmallVec::new()); + self.send_status_update(false, SmallVec::new()).await; } async fn update_global_gitignore(&self, abs_path: &Path) { @@ -3949,30 +3983,30 @@ impl BackgroundScanner { .log_err() .map(Arc::new); let (prev_snapshot, ignore_stack, abs_path) = { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; state.snapshot.global_gitignore = ignore; let abs_path = state.snapshot.abs_path().clone(); - let ignore_stack = - state - .snapshot - .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()); + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()) + .await; (state.snapshot.clone(), ignore_stack, abs_path) }; let (scan_job_tx, scan_job_rx) = channel::unbounded(); self.update_ignore_statuses_for_paths( scan_job_tx, prev_snapshot, - vec![(abs_path, ignore_stack)].into_iter(), + vec![(abs_path, ignore_stack)], ) .await; self.scan_dirs(false, scan_job_rx).await; - self.send_status_update(false, SmallVec::new()); + self.send_status_update(false, SmallVec::new()).await; } async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { let (scan_job_tx, scan_job_rx) = channel::unbounded(); { - let mut state = self.state.lock(); + let mut state = self.state.lock().await; let root_path = state.snapshot.abs_path.clone(); for path in paths { for ancestor in path.ancestors() { @@ -3980,12 +4014,14 @@ impl BackgroundScanner { && entry.kind == EntryKind::UnloadedDir { let abs_path = root_path.join(ancestor.as_std_path()); - state.enqueue_scan_dir( - abs_path.into(), - entry, - &scan_job_tx, - self.fs.as_ref(), - ); + state + .enqueue_scan_dir( + abs_path.into(), + entry, + &scan_job_tx, + self.fs.as_ref(), + ) + .await; state.paths_to_scan.insert(path.clone()); break; } @@ -3997,7 +4033,7 @@ impl BackgroundScanner { self.scan_dir(&job).await.log_err(); } - !mem::take(&mut self.state.lock().paths_to_scan).is_empty() + !mem::take(&mut self.state.lock().await.paths_to_scan).is_empty() } async fn scan_dirs( @@ -4045,7 +4081,7 @@ impl BackgroundScanner { ) { Ok(_) => { last_progress_update_count += 1; - self.send_status_update(true, SmallVec::new()); + self.send_status_update(true, SmallVec::new()).await; } Err(count) => { last_progress_update_count = count; @@ -4070,8 +4106,12 @@ impl BackgroundScanner { .await; } - fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool { - let mut state = self.state.lock(); + async fn send_status_update( + &self, + scanning: bool, + barrier: SmallVec<[barrier::Sender; 1]>, + ) -> bool { + let mut state = self.state.lock().await; if state.changed_paths.is_empty() && scanning { return true; } @@ -4100,7 +4140,7 @@ impl BackgroundScanner { let root_abs_path; let root_char_bag; { - let snapshot = &self.state.lock().snapshot; + let snapshot = &self.state.lock().await.snapshot; if self.settings.is_path_excluded(&job.path) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); @@ -4153,12 +4193,14 @@ impl BackgroundScanner { }; if child_name == DOT_GIT { - let mut state = self.state.lock(); - state.insert_git_repository( - child_path.clone(), - self.fs.as_ref(), - self.watcher.as_ref(), - ); + let mut state = self.state.lock().await; + state + .insert_git_repository( + child_path.clone(), + self.fs.as_ref(), + self.watcher.as_ref(), + ) + .await; } else if child_name == GITIGNORE { match build_gitignore(&child_abs_path, self.fs.as_ref()).await { Ok(ignore) => { @@ -4178,7 +4220,7 @@ impl BackgroundScanner { if self.settings.is_path_excluded(&child_path) { log::debug!("skipping excluded child entry {child_path:?}"); - self.state.lock().remove_path(&child_path); + self.state.lock().await.remove_path(&child_path); continue; } @@ -4278,7 +4320,7 @@ impl BackgroundScanner { new_entries.push(child_entry); } - let mut state = self.state.lock(); + let mut state = self.state.lock().await; // Identify any subdirectories that should not be scanned. let mut job_ix = 0; @@ -4360,7 +4402,7 @@ impl BackgroundScanner { None }; - let mut state = self.state.lock(); + let mut state = self.state.lock().await; let doing_recursive_update = scan_queue_tx.is_some(); // Remove any entries for paths that no longer exist or are being recursively @@ -4377,11 +4419,10 @@ impl BackgroundScanner { let abs_path: Arc = root_abs_path.join(path.as_std_path()).into(); match metadata { Ok(Some((metadata, canonical_path))) => { - let ignore_stack = state.snapshot.ignore_stack_for_abs_path( - &abs_path, - metadata.is_dir, - self.fs.as_ref(), - ); + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref()) + .await; let is_external = !canonical_path.starts_with(&root_canonical_path); let mut fs_entry = Entry::new( path.clone(), @@ -4413,18 +4454,22 @@ impl BackgroundScanner { || (fs_entry.path.is_empty() && abs_path.file_name() == Some(OsStr::new(DOT_GIT))) { - state.enqueue_scan_dir( - abs_path, - &fs_entry, - scan_queue_tx, - self.fs.as_ref(), - ); + state + .enqueue_scan_dir( + abs_path, + &fs_entry, + scan_queue_tx, + self.fs.as_ref(), + ) + .await; } else { fs_entry.kind = EntryKind::UnloadedDir; } } - state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); + state + .insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()) + .await; if path.is_empty() && let Some((ignores, repo)) = new_ancestor_repo.take() @@ -4439,6 +4484,7 @@ impl BackgroundScanner { self.fs.as_ref(), self.watcher.as_ref(), ) + .await .log_err(); } } @@ -4477,11 +4523,11 @@ impl BackgroundScanner { &self, scan_job_tx: Sender, prev_snapshot: LocalSnapshot, - mut ignores_to_update: impl Iterator, IgnoreStack)>, + ignores_to_update: Vec<(Arc, IgnoreStack)>, ) { let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); { - while let Some((parent_abs_path, ignore_stack)) = ignores_to_update.next() { + for (parent_abs_path, ignore_stack) in ignores_to_update { ignore_queue_tx .send_blocking(UpdateIgnoreStatusJob { abs_path: parent_abs_path, @@ -4522,11 +4568,11 @@ impl BackgroundScanner { .await; } - fn ignores_needing_update(&self) -> Vec> { + async fn ignores_needing_update(&self) -> Vec> { let mut ignores_to_update = Vec::new(); { - let snapshot = &mut self.state.lock().snapshot; + let snapshot = &mut self.state.lock().await.snapshot; let abs_path = snapshot.abs_path.clone(); snapshot .ignores_by_parent_abs_path @@ -4554,26 +4600,27 @@ impl BackgroundScanner { ignores_to_update } - fn order_ignores( - &self, - mut ignores: Vec>, - ) -> impl use<> + Iterator, IgnoreStack)> { + async fn order_ignores(&self, mut ignores: Vec>) -> Vec<(Arc, IgnoreStack)> { let fs = self.fs.clone(); - let snapshot = self.state.lock().snapshot.clone(); + let snapshot = self.state.lock().await.snapshot.clone(); ignores.sort_unstable(); let mut ignores_to_update = ignores.into_iter().peekable(); - std::iter::from_fn(move || { - let parent_abs_path = ignores_to_update.next()?; + + let mut result = vec![]; + while let Some(parent_abs_path) = ignores_to_update.next() { while ignores_to_update .peek() .map_or(false, |p| p.starts_with(&parent_abs_path)) { ignores_to_update.next().unwrap(); } - let ignore_stack = - snapshot.ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref()); - Some((parent_abs_path, ignore_stack)) - }) + let ignore_stack = snapshot + .ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref()) + .await; + result.push((parent_abs_path, ignore_stack)); + } + + result } async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { @@ -4605,7 +4652,7 @@ impl BackgroundScanner { return; }; - if let Ok(Some(metadata)) = smol::block_on(self.fs.metadata(&job.abs_path.join(DOT_GIT))) + if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await && metadata.is_dir { ignore_stack.repo_root = Some(job.abs_path.clone()); @@ -4625,14 +4672,16 @@ impl BackgroundScanner { // Scan any directories that were previously ignored and weren't previously scanned. if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { - let state = self.state.lock(); + let state = self.state.lock().await; if state.should_scan_directory(&entry) { - state.enqueue_scan_dir( - abs_path.clone(), - &entry, - &job.scan_queue, - self.fs.as_ref(), - ); + state + .enqueue_scan_dir( + abs_path.clone(), + &entry, + &job.scan_queue, + self.fs.as_ref(), + ) + .await; } } @@ -4656,7 +4705,7 @@ impl BackgroundScanner { } } - let state = &mut self.state.lock(); + let state = &mut self.state.lock().await; for edit in &entries_by_path_edits { if let Edit::Insert(entry) = edit && let Err(ix) = state.changed_paths.binary_search(&entry.path) @@ -4672,9 +4721,9 @@ impl BackgroundScanner { state.snapshot.entries_by_id.edit(entries_by_id_edits, ()); } - fn update_git_repositories(&self, dot_git_paths: Vec) -> Vec> { + async fn update_git_repositories(&self, dot_git_paths: Vec) -> Vec> { log::trace!("reloading repositories: {dot_git_paths:?}"); - let mut state = self.state.lock(); + let mut state = self.state.lock().await; let scan_id = state.snapshot.scan_id; let mut affected_repo_roots = Vec::new(); for dot_git_dir in dot_git_paths { @@ -4704,13 +4753,15 @@ impl BackgroundScanner { return Vec::new(); }; affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); - state.insert_git_repository( - RelPath::new(relative, PathStyle::local()) - .unwrap() - .into_arc(), - self.fs.as_ref(), - self.watcher.as_ref(), - ); + state + .insert_git_repository( + RelPath::new(relative, PathStyle::local()) + .unwrap() + .into_arc(), + self.fs.as_ref(), + self.watcher.as_ref(), + ) + .await; } Some(local_repository) => { state.snapshot.git_repositories.update( @@ -4738,7 +4789,7 @@ impl BackgroundScanner { if exists_in_snapshot || matches!( - smol::block_on(self.fs.metadata(&entry.common_dir_abs_path)), + self.fs.metadata(&entry.common_dir_abs_path).await, Ok(Some(_)) ) { @@ -5497,11 +5548,13 @@ fn parse_gitfile(content: &str) -> anyhow::Result<&Path> { Ok(Path::new(path.trim())) } -fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, Arc) { +async fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, Arc) { let mut repository_dir_abs_path = dot_git_abs_path.clone(); let mut common_dir_abs_path = dot_git_abs_path.clone(); - if let Some(path) = smol::block_on(fs.load(dot_git_abs_path)) + if let Some(path) = fs + .load(dot_git_abs_path) + .await .ok() .as_ref() .and_then(|contents| parse_gitfile(contents).log_err()) @@ -5510,17 +5563,19 @@ fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, .parent() .unwrap_or(Path::new("")) .join(path); - if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() { + if let Some(path) = fs.canonicalize(&path).await.log_err() { repository_dir_abs_path = Path::new(&path).into(); common_dir_abs_path = repository_dir_abs_path.clone(); - if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok() - && let Some(commondir_path) = - smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err() + + if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok() + && let Some(commondir_path) = fs + .canonicalize(&path.join(commondir_contents.trim())) + .await + .log_err() { common_dir_abs_path = commondir_path.as_path().into(); } } }; - (repository_dir_abs_path, common_dir_abs_path) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 3c39d5c3ad70563f8f954ee9908c27cef17a752c..d89e1ef4e4df7dbef3cf51789c1f1fc8a5309eb1 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -734,7 +734,6 @@ async fn test_write_file(cx: &mut TestAppContext) { }) .await .unwrap(); - worktree.read_with(cx, |tree, _| { let tracked = tree .entry_for_path(rel_path("tracked-dir/file.txt")) @@ -1537,7 +1536,7 @@ async fn test_random_worktree_operations_during_initial_scan( assert_eq!( updated_snapshot.entries(true, 0).collect::>(), final_snapshot.entries(true, 0).collect::>(), - "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}", + "wrong updates after snapshot {i}: {updates:#?}", ); } }