diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 2b6129280986a2398aabe42fbacf5f23f942ded0..a21ea8f639ef4e7d7311c8a1743dd7ba4d402e4c 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -140,6 +140,7 @@ pub struct LocalWorktree { settings: WorktreeSettings, share_private_files: bool, scanning_enabled: bool, + force_defer_watch: bool, } pub struct PathPrefixScanRequest { @@ -504,6 +505,7 @@ impl Worktree { visible, settings, scanning_enabled, + force_defer_watch: false, }; worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); Worktree::Local(worktree) @@ -1151,6 +1153,7 @@ impl LocalWorktree { let next_entry_id = self.next_entry_id.clone(); let fs = self.fs.clone(); let scanning_enabled = self.scanning_enabled; + let force_defer_watch = self.force_defer_watch; let track_git_repositories = self.visible; let settings = self.settings.clone(); let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); @@ -1158,7 +1161,10 @@ impl LocalWorktree { let abs_path = snapshot.abs_path.as_path().to_path_buf(); let background = cx.background_executor().clone(); async move { - let (events, watcher) = if scanning_enabled { + let defer_watch = + force_defer_watch || (scanning_enabled && fs::requires_poll_watcher(&abs_path)); + + let (events, watcher) = if scanning_enabled && !defer_watch { fs.watch(&abs_path, FS_WATCH_LATENCY).await } else { (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _) @@ -1192,11 +1198,10 @@ impl LocalWorktree { watcher, track_git_repositories, is_single_file, + defer_watch, }; - scanner - .run(Box::pin(events.map(|events| events.into_iter().collect()))) - .await; + scanner.run(events).await; } }); let scan_state_updater = cx.spawn(async move |this, cx| { @@ -2053,6 +2058,12 @@ impl LocalWorktree { self.snapshot.update_abs_path(new_path, root_name); self.restart_background_scanners(cx); } + #[cfg(feature = "test-support")] + pub fn set_defer_watch(&mut self, defer: bool, cx: &mut Context) { + self.force_defer_watch = defer; + self.restart_background_scanners(cx); + } + #[cfg(feature = "test-support")] pub fn repositories(&self) -> Vec> { self.git_repositories @@ -3946,6 +3957,7 @@ struct BackgroundScanner { /// Whether this is a single-file worktree (root is a file, not a directory). /// Used to determine if we should give up after repeated canonicalization failures. is_single_file: bool, + defer_watch: bool, } #[derive(Copy, Clone, PartialEq)] @@ -4087,6 +4099,37 @@ impl BackgroundScanner { self.send_status_update(false, SmallVec::new(), &[]).await; + if self.defer_watch { + let (events, watcher) = self + .fs + .watch(root_abs_path.as_path(), FS_WATCH_LATENCY) + .await; + self.watcher = watcher; + fs_events_rx = Box::pin(events.map(|events| events.into_iter().collect())); + + let state = self.state.lock().await; + for target in state.symlink_paths_by_target.keys() { + if !target.starts_with(root_abs_path.as_path()) { + self.watcher.add(target).log_err(); + } + } + for repo in state.snapshot.git_repositories.values() { + if !repo + .common_dir_abs_path + .starts_with(root_abs_path.as_path()) + { + self.watcher.add(&repo.common_dir_abs_path).log_err(); + } + if !repo + .repository_dir_abs_path + .starts_with(root_abs_path.as_path()) + { + self.watcher.add(&repo.repository_dir_abs_path).log_err(); + } + } + drop(state); + } + // 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 // have the previous state loaded yet. diff --git a/crates/worktree/tests/integration/main.rs b/crates/worktree/tests/integration/main.rs index 4fa1fa9a1e4ca73859e907cd84f42b1e6c361938..ef5180a7083bc57b196bd7dfdf408dd6f09aff68 100644 --- a/crates/worktree/tests/integration/main.rs +++ b/crates/worktree/tests/integration/main.rs @@ -4237,3 +4237,170 @@ async fn test_remote_worktree_with_git_emits_root_repo_event_when_repo_info_arri "should fire exactly once, not duplicate" ); } + +#[gpui::test] +async fn test_deferred_watch_repository_above_root( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/root"), + json!({ + ".git": {}, + "subproject": { + "a.txt": "A" + } + }), + ) + .await; + let worktree = Worktree::local( + path!("/root/subproject").as_ref(), + true, + fs.clone(), + Arc::default(), + true, + WorktreeId::from_proto(0), + &mut cx.to_async(), + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().set_defer_watch(true, cx); + }); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + let repos = worktree.update(cx, |worktree, _| { + worktree.as_local().unwrap().repositories() + }); + pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); +} + +#[gpui::test] +async fn test_deferred_watch_symlinks_pointing_outside(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "deps": {}, + "src": { + "a.rs": "", + }, + }, + "dir2": { + "src": { + "c.rs": "", + } + }, + }), + ) + .await; + + fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into()) + .await + .unwrap(); + + let tree = Worktree::local( + Path::new("/root/dir1"), + true, + fs.clone(), + Default::default(), + true, + WorktreeId::from_proto(0), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.run_until_parked(); + + tree.update(cx, |tree, cx| { + tree.as_local_mut().unwrap().set_defer_watch(true, cx); + }); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.run_until_parked(); + + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true, 0) + .map(|entry| (entry.path.as_ref(), entry.is_external)) + .collect::>(), + vec![ + (rel_path(""), false), + (rel_path("deps"), false), + (rel_path("deps/dep-dir2"), true), + (rel_path("src"), false), + (rel_path("src/a.rs"), false), + ] + ); + }); + + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2").into()]) + }) + .recv() + .await; + + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true, 0) + .map(|entry| (entry.path.as_ref(), entry.is_external)) + .collect::>(), + vec![ + (rel_path(""), false), + (rel_path("deps"), false), + (rel_path("deps/dep-dir2"), true), + (rel_path("deps/dep-dir2/src"), true), + (rel_path("src"), false), + (rel_path("src/a.rs"), false), + ] + ); + }); + + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2/src").into()]) + }) + .recv() + .await; + + tree.read_with(cx, |tree, _| { + assert!( + tree.entry_for_path(rel_path("deps/dep-dir2/src/c.rs")) + .is_some() + ); + }); + + fs.insert_file(Path::new("/root/dir2/src/new.rs"), b"".to_vec()) + .await; + + wait_for_condition(cx, |cx| { + tree.read_with(cx, |tree, _| { + tree.entry_for_path(rel_path("deps/dep-dir2/src/new.rs")) + .is_some() + }) + }) + .await; +}