Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions crates/fs/src/fs_watcher.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,6 +15,8 @@ pub struct FsWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
registrations: Mutex<BTreeMap<Arc<std::path::Path>, WatcherRegistrationId>>,
#[cfg(target_os = "linux")]
removed_paths: Arc<Mutex<HashSet<Arc<std::path::Path>>>>,
}

impl FsWatcher {
Expand All @@ -24,6 +28,8 @@ impl FsWatcher {
tx,
pending_path_events,
registrations: Default::default(),
#[cfg(target_os = "linux")]
removed_paths: Arc::new(Default::default()),
}
}
}
Expand Down Expand Up @@ -70,31 +76,57 @@ impl Watcher for FsWatcher {
return Ok(());
}
}

let root_path = SanitizedPath::new_arc(path);
let path: Arc<std::path::Path> = 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<std::path::Path> = 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: &notify::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
Expand Down
72 changes: 71 additions & 1 deletion crates/project_panel/src/project_panel_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down