Unnamed repository; edit this file 'description' to name the repository.
fix: Support filesystems that don't send Create events
On some filesystems, particularly FUSE on Linux, we don't get
Create(...) events. We do get Access(Open(Any)) events, so handle
those consistently with create/modify/remove events.
This fixes missed file notifications when using Sapling SCM with
EdenFS, although I believe the problem can occur on other FUSE
environments.
Reproduction:
Commit a change with Sapling that adds a new file foo.rs and
references it with `mod foo;` in lib.rs. Configure rust-analyzer as
follows:
```
{
"rust-analyzer.files.watcher": "server",
"rust-analyzer.server.extraEnv": {
"RA_LOG": "vfs_notify=debug"
},
}
```
Go to the previous commit, restart rust-analyzer, then go to the next
commit. The logs only show:
```
2026-03-18T07:16:54.211788903-07:00 DEBUG vfs-notify event event=NotifyEvent(Ok(Event { kind: Access(Open(Any)), paths: ["/data/users/wilfred/scratch/src/foo.rs"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }))
2026-03-18T07:16:54.211906733-07:00 DEBUG vfs-notify event event=NotifyEvent(Ok(Event { kind: Access(Open(Any)), paths: ["/data/users/wilfred/scratch/src/foo.rs"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }))
2026-03-18T07:16:54.216467168-07:00 DEBUG vfs-notify event event=NotifyEvent(Ok(Event { kind: Access(Open(Any)), paths: ["/data/users/wilfred/scratch/src/lib.rs"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }))
2026-03-18T07:16:54.216811304-07:00 DEBUG vfs-notify event event=NotifyEvent(Ok(Event { kind: Access(Open(Any)), paths: ["/data/users/wilfred/scratch/src/lib.rs"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }))
```
Observe that `mod foo;` has a red squiggle and shows "unresolved
module, can't find module file: foo.rs, or foo/mod.rs". This
commit fixes that.
| -rw-r--r-- | crates/vfs-notify/src/lib.rs | 29 |
1 files changed, 26 insertions, 3 deletions
diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs index f91d830ca0..6465a85d2d 100644 --- a/crates/vfs-notify/src/lib.rs +++ b/crates/vfs-notify/src/lib.rs @@ -14,7 +14,7 @@ use std::{ }; use crossbeam_channel::{Receiver, Sender, select, unbounded}; -use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher, event::AccessKind}; use paths::{AbsPath, AbsPathBuf, Utf8PathBuf}; use rayon::iter::{IndexedParallelIterator as _, IntoParallelIterator as _, ParallelIterator}; use rustc_hash::FxHashSet; @@ -63,6 +63,7 @@ struct NotifyActor { sender: loader::Sender, watched_file_entries: FxHashSet<AbsPathBuf>, watched_dir_entries: Vec<loader::Directories>, + seen_paths: FxHashSet<AbsPathBuf>, // Drop order is significant. watcher: Option<(RecommendedWatcher, Receiver<NotifyEvent>)>, } @@ -79,6 +80,7 @@ impl NotifyActor { sender, watched_dir_entries: Vec::new(), watched_file_entries: FxHashSet::default(), + seen_paths: FxHashSet::default(), watcher: None, } } @@ -120,6 +122,7 @@ impl NotifyActor { let n_total = config.load.len(); self.watched_dir_entries.clear(); self.watched_file_entries.clear(); + self.seen_paths.clear(); self.send(loader::Message::Progress { n_total, @@ -195,8 +198,10 @@ impl NotifyActor { }, Event::NotifyEvent(event) => { if let Some(event) = log_notify_error(event) - && let EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) = - event.kind + && let EventKind::Create(_) + | EventKind::Modify(_) + | EventKind::Remove(_) + | EventKind::Access(AccessKind::Open(_)) = event.kind { let abs_paths: Vec<AbsPathBuf> = event .paths @@ -209,6 +214,24 @@ impl NotifyActor { }) .collect(); + let mut saw_new_file = false; + for abs_path in &abs_paths { + if self.seen_paths.insert(abs_path.clone()) { + saw_new_file = true; + } + } + + // Only consider access events for files that we haven't seen + // before. + // + // This is important on FUSE filesystems, where we may not get a + // Create event. In other cases we're about to access the file, so + // we don't want an infinite loop where processing an Access event + // creates another Access event. + if matches!(event.kind, EventKind::Access(_)) && !saw_new_file { + continue; + } + let files = abs_paths .into_iter() .filter_map(|path| -> Option<(AbsPathBuf, Option<Vec<u8>>)> { |