1use notify::EventKind;
2use parking_lot::Mutex;
3use std::sync::{Arc, OnceLock};
4use util::{paths::SanitizedPath, ResultExt};
5
6use crate::{PathEvent, PathEventKind, Watcher};
7
8pub struct FsWatcher {
9 tx: smol::channel::Sender<()>,
10 pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
11}
12
13impl FsWatcher {
14 pub fn new(
15 tx: smol::channel::Sender<()>,
16 pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
17 ) -> Self {
18 Self {
19 tx,
20 pending_path_events,
21 }
22 }
23}
24
25impl Watcher for FsWatcher {
26 fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
27 let root_path = SanitizedPath::from(path);
28
29 let tx = self.tx.clone();
30 let pending_paths = self.pending_path_events.clone();
31
32 use notify::Watcher;
33
34 global({
35 |g| {
36 g.add(move |event: ¬ify::Event| {
37 let kind = match event.kind {
38 EventKind::Create(_) => Some(PathEventKind::Created),
39 EventKind::Modify(_) => Some(PathEventKind::Changed),
40 EventKind::Remove(_) => Some(PathEventKind::Removed),
41 // Adding this fix a weird bug on Linux after upgrading notify
42 // https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
43 EventKind::Access(_) => return,
44 _ => None,
45 };
46 let mut path_events = event
47 .paths
48 .iter()
49 .filter_map(|event_path| {
50 let event_path = SanitizedPath::from(event_path);
51 event_path.starts_with(&root_path).then(|| PathEvent {
52 path: event_path.as_path().to_path_buf(),
53 kind,
54 })
55 })
56 .collect::<Vec<_>>();
57
58 if !path_events.is_empty() {
59 path_events.sort();
60 let mut pending_paths = pending_paths.lock();
61 if pending_paths.is_empty() {
62 tx.try_send(()).ok();
63 }
64 util::extend_sorted(
65 &mut *pending_paths,
66 path_events,
67 usize::MAX,
68 |a, b| a.path.cmp(&b.path),
69 );
70 }
71 })
72 }
73 })?;
74
75 global(|g| {
76 g.watcher
77 .lock()
78 .watch(path, notify::RecursiveMode::NonRecursive)
79 })??;
80
81 Ok(())
82 }
83
84 fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
85 use notify::Watcher;
86 Ok(global(|w| w.watcher.lock().unwatch(path))??)
87 }
88}
89
90pub struct GlobalWatcher {
91 // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
92 #[cfg(target_os = "linux")]
93 pub(super) watcher: Mutex<notify::INotifyWatcher>,
94 #[cfg(target_os = "freebsd")]
95 pub(super) watcher: Mutex<notify::KqueueWatcher>,
96 #[cfg(target_os = "windows")]
97 pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
98 pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
99}
100
101impl GlobalWatcher {
102 pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) {
103 self.watchers.lock().push(Box::new(cb))
104 }
105}
106
107static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
108 OnceLock::new();
109
110fn handle_event(event: Result<notify::Event, notify::Error>) {
111 let Some(event) = event.log_err() else { return };
112 global::<()>(move |watcher| {
113 for f in watcher.watchers.lock().iter() {
114 f(&event)
115 }
116 })
117 .log_err();
118}
119
120pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
121 let result = FS_WATCHER_INSTANCE.get_or_init(|| {
122 notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
123 watcher: Mutex::new(file_watcher),
124 watchers: Default::default(),
125 })
126 });
127 match result {
128 Ok(g) => Ok(f(g)),
129 Err(e) => Err(anyhow::anyhow!("{}", e)),
130 }
131}