diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index efb381c9a5480d..cd0eb915ab57b8 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -1,5 +1,7 @@ use notify::EventKind; use parking_lot::Mutex; +#[cfg(target_os = "linux")] +use std::collections::HashSet; use std::{ collections::{BTreeMap, HashMap}, ops::DerefMut, @@ -13,6 +15,8 @@ pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, registrations: Mutex, WatcherRegistrationId>>, + #[cfg(target_os = "linux")] + removed_paths: Arc>>>, } impl FsWatcher { @@ -24,6 +28,8 @@ impl FsWatcher { tx, pending_path_events, registrations: Default::default(), + #[cfg(target_os = "linux")] + removed_paths: Arc::new(Default::default()), } } } @@ -70,31 +76,57 @@ impl Watcher for FsWatcher { return Ok(()); } } + + let root_path = SanitizedPath::new_arc(path); + let path: Arc = path.into(); + #[cfg(target_os = "linux")] { - if self.registrations.lock().contains_key(path) { - log::trace!("path to watch is already watched: {path:?}"); - return Ok(()); + if self.registrations.lock().contains_key(&path) { + // In Linux, the watched path ended when the directory was removed. + // So if we are re-adding a path that is already being watched, + if self.removed_paths.lock().remove(&path) { + self.remove(&path).map_err(|e| { + log::trace!("path removed, but watcher remains registered; watcher unregistration failed: {path:?}, error: {e:?}"); + e + })?; + } else { + log::trace!("path to watch is already watched: {path:?}"); + return Ok(()); + } } } - let root_path = SanitizedPath::new_arc(path); - let path: Arc = path.into(); - #[cfg(any(target_os = "windows", target_os = "macos"))] let mode = notify::RecursiveMode::Recursive; #[cfg(target_os = "linux")] let mode = notify::RecursiveMode::NonRecursive; + #[cfg(target_os = "linux")] + let removed_paths = self.removed_paths.clone(); let registration_id = global({ let path = path.clone(); + #[cfg(target_os = "linux")] + let path_for_removal_check = path.clone(); |g| { g.add(path, mode, move |event: ¬ify::Event| { log::trace!("watcher received event: {event:?}"); let kind = match event.kind { EventKind::Create(_) => Some(PathEventKind::Created), EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), + EventKind::Remove(_) => { + #[cfg(target_os = "linux")] + if event + .paths + .iter() + .any(|p| p.as_path() == path_for_removal_check.as_ref()) + { + { + removed_paths.lock().insert(path_for_removal_check.clone()); + } + } + Some(PathEventKind::Removed) + } _ => None, }; let mut path_events = event diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 01d165174784f4..ece38d2ed0f3c5 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -4,7 +4,7 @@ use editor::MultiBufferOffset; use gpui::{Empty, Entity, TestAppContext, VisualTestContext}; use menu::Cancel; use pretty_assertions::assert_eq; -use project::FakeFs; +use project::{FakeFs, RealFs}; use serde_json::json; use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; use std::path::{Path, PathBuf}; @@ -108,6 +108,76 @@ async fn test_visible_list(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_watcher_for_recreated_directory_in_real_fs(cx: &mut gpui::TestAppContext) { + cx.executor().allow_parking(); + init_test(cx); + + let project_root = "test-recreated-root"; + let test_name = "test-recreateing"; + let test_dir = format!("{}/{}", project_root, test_name); + + let count = 200; + + let fs = Arc::new(RealFs::new(None, cx.executor())); + let temp_dir = tempfile::TempDir::new().unwrap(); + let project_root_dir = temp_dir.path().join(&project_root); + let dir = temp_dir.path().join(&test_dir); + + std::fs::create_dir(&project_root_dir).ok(); + assert_eq!( + std::fs::create_dir(&dir).is_ok(), + true, + "Failed to create test directory" + ); + + let project = Project::test(fs.clone(), [project_root_dir.as_ref()], cx).await; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); + cx.run_until_parked(); + + std::fs::remove_dir_all(&dir).ok(); + assert_eq!( + std::fs::create_dir(&dir).is_ok(), + true, + "Failed to create test directory" + ); + toggle_expand_dir(&panel, &test_dir, cx); + + let mut entries = Vec::with_capacity(count); + entries.push(format!("v {}", project_root)); + entries.push(format!(" v {} <== selected", test_name)); + for i in 1..=count { + let filename = format!("file-{}", i); + std::fs::File::create(dir.join(&filename)).unwrap(); + entries.push(format!(" {}", &filename)); + if i % 9 == 0 { + // this can simulate a realistic scenario and some file showing up in the panel + // instead of just an empty directory + // but the pause timer is just a magic number + cx.executor() + .timer(std::time::Duration::from_nanos(75)) + .await; + } + } + + // need sometime for watcher event + // run_until_parked cannot catch this + cx.executor() + .timer(std::time::Duration::from_millis(1)) + .await; + cx.run_until_parked(); + + assert_eq!( + entries, + visible_entries_as_strings(&panel, 0..usize::MAX, cx), + ); +} + #[gpui::test] async fn test_opening_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx);