Add rs-notify implementation of `fs::watch` (#9040)

Mikayla Maki created

This PR simplifies the Zed file system abstraction and implements
`Fs::watch` for linux and windows.

TODO:
- [x] Figure out why this fails to initialize the file watchers when we
have to initialize the config directory paths, but succeeds on
subsequent runs.
- [x] Fix macOS dependencies on old fsevents::Event crate

Release Notes:

- N/A

Change summary

Cargo.lock                                       |   1 
crates/extension/src/extension_store.rs          |   8 
crates/fs/Cargo.toml                             |   4 
crates/fs/src/fs.rs                              | 130 +++--
crates/fsevent/src/fsevent.rs                    | 389 +++++++++++++++++
crates/fsevent/src/mac_impl.rs                   | 382 -----------------
crates/gpui/src/platform/linux/platform.rs       |   7 
crates/gpui/src/platform/linux/wayland/client.rs |   2 
crates/gpui/src/platform/linux/x11/client.rs     |   2 
crates/project_core/src/worktree.rs              |  20 
crates/zed/Cargo.toml                            |   1 
crates/zed/src/main.rs                           |  19 
tooling/xtask/src/main.rs                        |   6 
13 files changed, 478 insertions(+), 493 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12790,7 +12790,6 @@ dependencies = [
  "feedback",
  "file_finder",
  "fs",
- "fsevent",
  "futures 0.3.28",
  "go_to_line",
  "gpui",

crates/extension/src/extension_store.rs 🔗

@@ -296,10 +296,10 @@ impl ExtensionStore {
             let reload_tx = this.reload_tx.clone();
             let installed_dir = this.installed_dir.clone();
             async move {
-                let mut events = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
-                while let Some(events) = events.next().await {
-                    for event in events {
-                        let Ok(event_path) = event.path.strip_prefix(&installed_dir) else {
+                let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
+                while let Some(paths) = paths.next().await {
+                    for path in paths {
+                        let Ok(event_path) = path.strip_prefix(&installed_dir) else {
                             continue;
                         };
 

crates/fs/Cargo.toml 🔗

@@ -13,7 +13,6 @@ path = "src/fs.rs"
 
 [dependencies]
 collections.workspace = true
-fsevent.workspace = true
 rope.workspace = true
 text.workspace = true
 util.workspace = true
@@ -37,6 +36,9 @@ time.workspace = true
 
 gpui = { workspace = true, optional = true }
 
+[target.'cfg(target_os = "macos")'.dependencies]
+fsevent.workspace = true
+
 [target.'cfg(not(target_os = "macos"))'.dependencies]
 notify = "6.1.1"
 

crates/fs/src/fs.rs 🔗

@@ -1,15 +1,6 @@
 pub mod repository;
 
 use anyhow::{anyhow, Result};
-pub use fsevent::Event;
-#[cfg(target_os = "macos")]
-use fsevent::EventStream;
-
-#[cfg(not(target_os = "macos"))]
-use fsevent::StreamFlags;
-
-#[cfg(not(target_os = "macos"))]
-use notify::{Config, EventKind, Watcher};
 
 #[cfg(unix)]
 use std::os::unix::fs::MetadataExt;
@@ -76,7 +67,7 @@ pub trait Fs: Send + Sync {
         &self,
         path: &Path,
         latency: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>>;
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
 
     fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
     fn is_fake(&self) -> bool;
@@ -327,12 +318,18 @@ impl Fs for RealFs {
         &self,
         path: &Path,
         latency: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> {
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
+        use fsevent::EventStream;
+
         let (tx, rx) = smol::channel::unbounded();
         let (stream, handle) = EventStream::new(&[path], latency);
         std::thread::spawn(move || {
-            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
+            stream.run(move |events| {
+                smol::block_on(tx.send(events.into_iter().map(|event| event.path).collect()))
+                    .is_ok()
+            });
         });
+
         Box::pin(rx.chain(futures::stream::once(async move {
             drop(handle);
             vec![]
@@ -343,49 +340,66 @@ impl Fs for RealFs {
     async fn watch(
         &self,
         path: &Path,
-        latency: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> {
-        let (tx, rx) = smol::channel::unbounded();
+        _latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
+        use notify::{event::EventKind, Watcher};
+        // todo(linux): This spawns two threads, while the macOS impl
+        // only spawns one. Can we use a OnceLock or some such to make
+        // this better
 
-        if !path.exists() {
-            log::error!("watch path does not exist: {}", path.display());
-            return Box::pin(rx);
-        }
+        let (tx, rx) = smol::channel::unbounded();
 
-        let mut watcher =
-            notify::recommended_watcher(move |res: Result<notify::Event, _>| match res {
-                Ok(event) => {
-                    let flags = match event.kind {
-                        // ITEM_REMOVED is currently the only flag we care about
-                        EventKind::Remove(_) => StreamFlags::ITEM_REMOVED,
-                        _ => StreamFlags::NONE,
-                    };
-                    let events = event
-                        .paths
-                        .into_iter()
-                        .map(|path| Event {
-                            event_id: 0,
-                            flags,
-                            path,
-                        })
-                        .collect::<Vec<_>>();
-                    let _ = tx.try_send(events);
+        let mut file_watcher = notify::recommended_watcher({
+            let tx = tx.clone();
+            move |event: Result<notify::Event, _>| {
+                if let Some(event) = event.log_err() {
+                    tx.try_send(event.paths).ok();
                 }
-                Err(err) => {
-                    log::error!("watch error: {}", err);
-                }
-            })
-            .unwrap();
-
-        watcher
-            .configure(Config::default().with_poll_interval(latency))
-            .unwrap();
+            }
+        })
+        .expect("Could not start file watcher");
 
-        watcher
+        file_watcher
             .watch(path, notify::RecursiveMode::Recursive)
-            .unwrap();
+            .ok(); // It's ok if this fails, the parent watcher will add it.
+
+        let mut parent_watcher = notify::recommended_watcher({
+            let watched_path = path.to_path_buf();
+            let tx = tx.clone();
+            move |event: Result<notify::Event, _>| {
+                if let Some(event) = event.ok() {
+                    if event.paths.into_iter().any(|path| *path == watched_path) {
+                        match event.kind {
+                            EventKind::Create(_) => {
+                                file_watcher
+                                    .watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
+                                    .log_err();
+                                let _ = tx.try_send(vec![watched_path.clone()]).ok();
+                            }
+                            EventKind::Remove(_) => {
+                                file_watcher.unwatch(&watched_path).log_err();
+                                let _ = tx.try_send(vec![watched_path.clone()]).ok();
+                            }
+                            _ => {}
+                        }
+                    }
+                }
+            }
+        })
+        .expect("Could not start file watcher");
+
+        parent_watcher
+            .watch(
+                path.parent()
+                    .expect("Watching root is probably not what you want"),
+                notify::RecursiveMode::NonRecursive,
+            )
+            .expect("Could not start watcher on parent directory");
 
-        Box::pin(rx)
+        Box::pin(rx.chain(futures::stream::once(async move {
+            drop(parent_watcher);
+            vec![]
+        })))
     }
 
     fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
@@ -443,10 +457,6 @@ impl Fs for RealFs {
     }
 }
 
-pub fn fs_events_paths(events: Vec<Event>) -> Vec<PathBuf> {
-    events.into_iter().map(|event| event.path).collect()
-}
-
 #[cfg(any(test, feature = "test-support"))]
 pub struct FakeFs {
     // Use an unfair lock to ensure tests are deterministic.
@@ -459,9 +469,9 @@ struct FakeFsState {
     root: Arc<Mutex<FakeFsEntry>>,
     next_inode: u64,
     next_mtime: SystemTime,
-    event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+    event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
     events_paused: bool,
-    buffered_events: Vec<fsevent::Event>,
+    buffered_events: Vec<PathBuf>,
     metadata_call_count: usize,
     read_dir_call_count: usize,
 }
@@ -569,11 +579,7 @@ impl FakeFsState {
         T: Into<PathBuf>,
     {
         self.buffered_events
-            .extend(paths.into_iter().map(|path| fsevent::Event {
-                event_id: 0,
-                flags: fsevent::StreamFlags::empty(),
-                path: path.into(),
-            }));
+            .extend(paths.into_iter().map(Into::into));
 
         if !self.events_paused {
             self.flush_events(self.buffered_events.len());
@@ -1328,14 +1334,14 @@ impl Fs for FakeFs {
         &self,
         path: &Path,
         _: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
         self.simulate_random_delay().await;
         let (tx, rx) = smol::channel::unbounded();
         self.state.lock().event_txs.push(tx);
         let path = path.to_path_buf();
         let executor = self.executor.clone();
         Box::pin(futures::StreamExt::filter(rx, move |events| {
-            let result = events.iter().any(|event| event.path.starts_with(&path));
+            let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
             let executor = executor.clone();
             async move {
                 executor.simulate_random_delay().await;

crates/fsevent/src/fsevent.rs 🔗

@@ -1,11 +1,17 @@
-#[cfg(target_os = "macos")]
-pub use mac_impl::*;
+#![cfg(target_os = "macos")]
 
 use bitflags::bitflags;
-use std::path::PathBuf;
-
-#[cfg(target_os = "macos")]
-mod mac_impl;
+use fsevent_sys::{self as fs, core_foundation as cf};
+use parking_lot::Mutex;
+use std::{
+    convert::AsRef,
+    ffi::{c_void, CStr, OsStr},
+    os::unix::ffi::OsStrExt,
+    path::{Path, PathBuf},
+    ptr, slice,
+    sync::Arc,
+    time::Duration,
+};
 
 #[derive(Clone, Debug)]
 pub struct Event {
@@ -14,10 +20,244 @@ pub struct Event {
     pub path: PathBuf,
 }
 
+pub struct EventStream {
+    lifecycle: Arc<Mutex<Lifecycle>>,
+    state: Box<State>,
+}
+
+struct State {
+    latency: Duration,
+    paths: cf::CFMutableArrayRef,
+    callback: Option<Box<dyn FnMut(Vec<Event>) -> bool>>,
+    last_valid_event_id: Option<fs::FSEventStreamEventId>,
+    stream: fs::FSEventStreamRef,
+}
+
+impl Drop for State {
+    fn drop(&mut self) {
+        unsafe {
+            cf::CFRelease(self.paths);
+            fs::FSEventStreamStop(self.stream);
+            fs::FSEventStreamInvalidate(self.stream);
+            fs::FSEventStreamRelease(self.stream);
+        }
+    }
+}
+
+enum Lifecycle {
+    New,
+    Running(cf::CFRunLoopRef),
+    Stopped,
+}
+
+pub struct Handle(Arc<Mutex<Lifecycle>>);
+
+unsafe impl Send for EventStream {}
+unsafe impl Send for Lifecycle {}
+
+impl EventStream {
+    pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
+        unsafe {
+            let cf_paths =
+                cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
+            assert!(!cf_paths.is_null());
+
+            for path in paths {
+                let path_bytes = path.as_os_str().as_bytes();
+                let cf_url = cf::CFURLCreateFromFileSystemRepresentation(
+                    cf::kCFAllocatorDefault,
+                    path_bytes.as_ptr() as *const i8,
+                    path_bytes.len() as cf::CFIndex,
+                    false,
+                );
+                let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle);
+                cf::CFArrayAppendValue(cf_paths, cf_path);
+                cf::CFRelease(cf_path);
+                cf::CFRelease(cf_url);
+            }
+
+            let mut state = Box::new(State {
+                latency,
+                paths: cf_paths,
+                callback: None,
+                last_valid_event_id: None,
+                stream: ptr::null_mut(),
+            });
+            let stream_context = fs::FSEventStreamContext {
+                version: 0,
+                info: state.as_ref() as *const _ as *mut c_void,
+                retain: None,
+                release: None,
+                copy_description: None,
+            };
+            let stream = fs::FSEventStreamCreate(
+                cf::kCFAllocatorDefault,
+                Self::trampoline,
+                &stream_context,
+                cf_paths,
+                FSEventsGetCurrentEventId(),
+                latency.as_secs_f64(),
+                fs::kFSEventStreamCreateFlagFileEvents
+                    | fs::kFSEventStreamCreateFlagNoDefer
+                    | fs::kFSEventStreamCreateFlagWatchRoot,
+            );
+            state.stream = stream;
+
+            let lifecycle = Arc::new(Mutex::new(Lifecycle::New));
+            (
+                EventStream {
+                    lifecycle: lifecycle.clone(),
+                    state,
+                },
+                Handle(lifecycle),
+            )
+        }
+    }
+
+    pub fn run<F>(mut self, f: F)
+    where
+        F: FnMut(Vec<Event>) -> bool + 'static,
+    {
+        self.state.callback = Some(Box::new(f));
+        unsafe {
+            let run_loop = cf::CFRunLoopGetCurrent();
+            {
+                let mut state = self.lifecycle.lock();
+                match *state {
+                    Lifecycle::New => *state = Lifecycle::Running(run_loop),
+                    Lifecycle::Running(_) => unreachable!(),
+                    Lifecycle::Stopped => return,
+                }
+            }
+            fs::FSEventStreamScheduleWithRunLoop(
+                self.state.stream,
+                run_loop,
+                cf::kCFRunLoopDefaultMode,
+            );
+            fs::FSEventStreamStart(self.state.stream);
+            cf::CFRunLoopRun();
+        }
+    }
+
+    extern "C" fn trampoline(
+        stream_ref: fs::FSEventStreamRef,
+        info: *mut ::std::os::raw::c_void,
+        num: usize,                                 // size_t numEvents
+        event_paths: *mut ::std::os::raw::c_void,   // void *eventPaths
+        event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[]
+        event_ids: *const ::std::os::raw::c_void,   // const FSEventStreamEventId eventIds[]
+    ) {
+        unsafe {
+            let event_paths = event_paths as *const *const ::std::os::raw::c_char;
+            let e_ptr = event_flags as *mut u32;
+            let i_ptr = event_ids as *mut u64;
+            let state = (info as *mut State).as_mut().unwrap();
+            let callback = if let Some(callback) = state.callback.as_mut() {
+                callback
+            } else {
+                return;
+            };
+
+            let paths = slice::from_raw_parts(event_paths, num);
+            let flags = slice::from_raw_parts_mut(e_ptr, num);
+            let ids = slice::from_raw_parts_mut(i_ptr, num);
+            let mut stream_restarted = false;
+
+            // Sometimes FSEvents reports a "dropped" event, an indication that either the kernel
+            // or our code couldn't keep up with the sheer volume of file-system events that were
+            // generated. If we observed a valid event before this happens, we'll try to read the
+            // file-system journal by stopping the current stream and creating a new one starting at
+            // such event. Otherwise, we'll let invoke the callback with the dropped event, which
+            // will likely perform a re-scan of one of the root directories.
+            if flags
+                .iter()
+                .copied()
+                .filter_map(StreamFlags::from_bits)
+                .any(|flags| {
+                    flags.contains(StreamFlags::USER_DROPPED)
+                        || flags.contains(StreamFlags::KERNEL_DROPPED)
+                })
+            {
+                if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
+                    fs::FSEventStreamStop(state.stream);
+                    fs::FSEventStreamInvalidate(state.stream);
+                    fs::FSEventStreamRelease(state.stream);
+
+                    let stream_context = fs::FSEventStreamContext {
+                        version: 0,
+                        info,
+                        retain: None,
+                        release: None,
+                        copy_description: None,
+                    };
+                    let stream = fs::FSEventStreamCreate(
+                        cf::kCFAllocatorDefault,
+                        Self::trampoline,
+                        &stream_context,
+                        state.paths,
+                        last_valid_event_id,
+                        state.latency.as_secs_f64(),
+                        fs::kFSEventStreamCreateFlagFileEvents
+                            | fs::kFSEventStreamCreateFlagNoDefer
+                            | fs::kFSEventStreamCreateFlagWatchRoot,
+                    );
+
+                    state.stream = stream;
+                    fs::FSEventStreamScheduleWithRunLoop(
+                        state.stream,
+                        cf::CFRunLoopGetCurrent(),
+                        cf::kCFRunLoopDefaultMode,
+                    );
+                    fs::FSEventStreamStart(state.stream);
+                    stream_restarted = true;
+                }
+            }
+
+            if !stream_restarted {
+                let mut events = Vec::with_capacity(num);
+                for p in 0..num {
+                    if let Some(flag) = StreamFlags::from_bits(flags[p]) {
+                        if !flag.contains(StreamFlags::HISTORY_DONE) {
+                            let path_c_str = CStr::from_ptr(paths[p]);
+                            let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
+                            let event = Event {
+                                event_id: ids[p],
+                                flags: flag,
+                                path,
+                            };
+                            state.last_valid_event_id = Some(event.event_id);
+                            events.push(event);
+                        }
+                    } else {
+                        debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
+                    }
+                }
+
+                if !events.is_empty() && !callback(events) {
+                    fs::FSEventStreamStop(stream_ref);
+                    cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
+                }
+            }
+        }
+    }
+}
+
+impl Drop for Handle {
+    fn drop(&mut self) {
+        let mut state = self.0.lock();
+        if let Lifecycle::Running(run_loop) = *state {
+            unsafe {
+                cf::CFRunLoopStop(run_loop);
+            }
+        }
+        *state = Lifecycle::Stopped;
+    }
+}
+
 // Synchronize with
 // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h
 bitflags! {
-  #[derive(Debug, PartialEq, Eq, Clone, Copy)]
+    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
   #[repr(C)]
   pub struct StreamFlags: u32 {
     const NONE = 0x00000000;
@@ -121,3 +361,138 @@ impl std::fmt::Display for StreamFlags {
         write!(f, "")
     }
 }
+
+#[link(name = "CoreServices", kind = "framework")]
+extern "C" {
+    pub fn FSEventsGetCurrentEventId() -> u64;
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::{fs, sync::mpsc, thread, time::Duration};
+
+    #[test]
+    fn test_event_stream_simple() {
+        for _ in 0..3 {
+            let dir = tempfile::Builder::new()
+                .prefix("test-event-stream")
+                .tempdir()
+                .unwrap();
+            let path = dir.path().canonicalize().unwrap();
+            for i in 0..10 {
+                fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
+            }
+            flush_historical_events();
+
+            let (tx, rx) = mpsc::channel();
+            let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
+            thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
+
+            fs::write(path.join("new-file"), "").unwrap();
+            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
+            let event = events.last().unwrap();
+            assert_eq!(event.path, path.join("new-file"));
+            assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
+
+            fs::remove_file(path.join("existing-file-5")).unwrap();
+            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
+            let event = events.last().unwrap();
+            assert_eq!(event.path, path.join("existing-file-5"));
+            assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
+            drop(handle);
+        }
+    }
+
+    #[test]
+    fn test_event_stream_delayed_start() {
+        for _ in 0..3 {
+            let dir = tempfile::Builder::new()
+                .prefix("test-event-stream")
+                .tempdir()
+                .unwrap();
+            let path = dir.path().canonicalize().unwrap();
+            for i in 0..10 {
+                fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
+            }
+            flush_historical_events();
+
+            let (tx, rx) = mpsc::channel();
+            let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
+
+            // Delay the call to `run` in order to make sure we don't miss any events that occur
+            // between creating the `EventStream` and calling `run`.
+            thread::spawn(move || {
+                thread::sleep(Duration::from_millis(100));
+                stream.run(move |events| tx.send(events.to_vec()).is_ok())
+            });
+
+            fs::write(path.join("new-file"), "").unwrap();
+            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
+            let event = events.last().unwrap();
+            assert_eq!(event.path, path.join("new-file"));
+            assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
+
+            fs::remove_file(path.join("existing-file-5")).unwrap();
+            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
+            let event = events.last().unwrap();
+            assert_eq!(event.path, path.join("existing-file-5"));
+            assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
+            drop(handle);
+        }
+    }
+
+    #[test]
+    fn test_event_stream_shutdown_by_dropping_handle() {
+        let dir = tempfile::Builder::new()
+            .prefix("test-event-stream")
+            .tempdir()
+            .unwrap();
+        let path = dir.path().canonicalize().unwrap();
+        flush_historical_events();
+
+        let (tx, rx) = mpsc::channel();
+        let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
+        thread::spawn(move || {
+            stream.run({
+                let tx = tx.clone();
+                move |_| {
+                    tx.send("running").unwrap();
+                    true
+                }
+            });
+            tx.send("stopped").unwrap();
+        });
+
+        fs::write(path.join("new-file"), "").unwrap();
+        assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
+
+        // Dropping the handle causes `EventStream::run` to return.
+        drop(handle);
+        assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
+    }
+
+    #[test]
+    fn test_event_stream_shutdown_before_run() {
+        let dir = tempfile::Builder::new()
+            .prefix("test-event-stream")
+            .tempdir()
+            .unwrap();
+        let path = dir.path().canonicalize().unwrap();
+
+        let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
+        drop(handle);
+
+        // This returns immediately because the handle was already dropped.
+        stream.run(|_| true);
+    }
+
+    fn flush_historical_events() {
+        let duration = if std::env::var("CI").is_ok() {
+            Duration::from_secs(2)
+        } else {
+            Duration::from_millis(500)
+        };
+        thread::sleep(duration);
+    }
+}

crates/fsevent/src/mac_impl.rs 🔗

@@ -1,382 +0,0 @@
-use fsevent_sys::{self as fs, core_foundation as cf};
-use parking_lot::Mutex;
-use std::{
-    convert::AsRef,
-    ffi::{c_void, CStr, OsStr},
-    os::unix::ffi::OsStrExt,
-    path::{Path, PathBuf},
-    ptr, slice,
-    sync::Arc,
-    time::Duration,
-};
-
-use crate::{Event, StreamFlags};
-
-pub struct EventStream {
-    lifecycle: Arc<Mutex<Lifecycle>>,
-    state: Box<State>,
-}
-
-struct State {
-    latency: Duration,
-    paths: cf::CFMutableArrayRef,
-    callback: Option<Box<dyn FnMut(Vec<Event>) -> bool>>,
-    last_valid_event_id: Option<fs::FSEventStreamEventId>,
-    stream: fs::FSEventStreamRef,
-}
-
-impl Drop for State {
-    fn drop(&mut self) {
-        unsafe {
-            cf::CFRelease(self.paths);
-            fs::FSEventStreamStop(self.stream);
-            fs::FSEventStreamInvalidate(self.stream);
-            fs::FSEventStreamRelease(self.stream);
-        }
-    }
-}
-
-enum Lifecycle {
-    New,
-    Running(cf::CFRunLoopRef),
-    Stopped,
-}
-
-pub struct Handle(Arc<Mutex<Lifecycle>>);
-
-unsafe impl Send for EventStream {}
-unsafe impl Send for Lifecycle {}
-
-impl EventStream {
-    pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
-        unsafe {
-            let cf_paths =
-                cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
-            assert!(!cf_paths.is_null());
-
-            for path in paths {
-                let path_bytes = path.as_os_str().as_bytes();
-                let cf_url = cf::CFURLCreateFromFileSystemRepresentation(
-                    cf::kCFAllocatorDefault,
-                    path_bytes.as_ptr() as *const i8,
-                    path_bytes.len() as cf::CFIndex,
-                    false,
-                );
-                let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle);
-                cf::CFArrayAppendValue(cf_paths, cf_path);
-                cf::CFRelease(cf_path);
-                cf::CFRelease(cf_url);
-            }
-
-            let mut state = Box::new(State {
-                latency,
-                paths: cf_paths,
-                callback: None,
-                last_valid_event_id: None,
-                stream: ptr::null_mut(),
-            });
-            let stream_context = fs::FSEventStreamContext {
-                version: 0,
-                info: state.as_ref() as *const _ as *mut c_void,
-                retain: None,
-                release: None,
-                copy_description: None,
-            };
-            let stream = fs::FSEventStreamCreate(
-                cf::kCFAllocatorDefault,
-                Self::trampoline,
-                &stream_context,
-                cf_paths,
-                FSEventsGetCurrentEventId(),
-                latency.as_secs_f64(),
-                fs::kFSEventStreamCreateFlagFileEvents
-                    | fs::kFSEventStreamCreateFlagNoDefer
-                    | fs::kFSEventStreamCreateFlagWatchRoot,
-            );
-            state.stream = stream;
-
-            let lifecycle = Arc::new(Mutex::new(Lifecycle::New));
-            (
-                EventStream {
-                    lifecycle: lifecycle.clone(),
-                    state,
-                },
-                Handle(lifecycle),
-            )
-        }
-    }
-
-    pub fn run<F>(mut self, f: F)
-    where
-        F: FnMut(Vec<Event>) -> bool + 'static,
-    {
-        self.state.callback = Some(Box::new(f));
-        unsafe {
-            let run_loop = cf::CFRunLoopGetCurrent();
-            {
-                let mut state = self.lifecycle.lock();
-                match *state {
-                    Lifecycle::New => *state = Lifecycle::Running(run_loop),
-                    Lifecycle::Running(_) => unreachable!(),
-                    Lifecycle::Stopped => return,
-                }
-            }
-            fs::FSEventStreamScheduleWithRunLoop(
-                self.state.stream,
-                run_loop,
-                cf::kCFRunLoopDefaultMode,
-            );
-            fs::FSEventStreamStart(self.state.stream);
-            cf::CFRunLoopRun();
-        }
-    }
-
-    extern "C" fn trampoline(
-        stream_ref: fs::FSEventStreamRef,
-        info: *mut ::std::os::raw::c_void,
-        num: usize,                                 // size_t numEvents
-        event_paths: *mut ::std::os::raw::c_void,   // void *eventPaths
-        event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[]
-        event_ids: *const ::std::os::raw::c_void,   // const FSEventStreamEventId eventIds[]
-    ) {
-        unsafe {
-            let event_paths = event_paths as *const *const ::std::os::raw::c_char;
-            let e_ptr = event_flags as *mut u32;
-            let i_ptr = event_ids as *mut u64;
-            let state = (info as *mut State).as_mut().unwrap();
-            let callback = if let Some(callback) = state.callback.as_mut() {
-                callback
-            } else {
-                return;
-            };
-
-            let paths = slice::from_raw_parts(event_paths, num);
-            let flags = slice::from_raw_parts_mut(e_ptr, num);
-            let ids = slice::from_raw_parts_mut(i_ptr, num);
-            let mut stream_restarted = false;
-
-            // Sometimes FSEvents reports a "dropped" event, an indication that either the kernel
-            // or our code couldn't keep up with the sheer volume of file-system events that were
-            // generated. If we observed a valid event before this happens, we'll try to read the
-            // file-system journal by stopping the current stream and creating a new one starting at
-            // such event. Otherwise, we'll let invoke the callback with the dropped event, which
-            // will likely perform a re-scan of one of the root directories.
-            if flags
-                .iter()
-                .copied()
-                .filter_map(StreamFlags::from_bits)
-                .any(|flags| {
-                    flags.contains(StreamFlags::USER_DROPPED)
-                        || flags.contains(StreamFlags::KERNEL_DROPPED)
-                })
-            {
-                if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
-                    fs::FSEventStreamStop(state.stream);
-                    fs::FSEventStreamInvalidate(state.stream);
-                    fs::FSEventStreamRelease(state.stream);
-
-                    let stream_context = fs::FSEventStreamContext {
-                        version: 0,
-                        info,
-                        retain: None,
-                        release: None,
-                        copy_description: None,
-                    };
-                    let stream = fs::FSEventStreamCreate(
-                        cf::kCFAllocatorDefault,
-                        Self::trampoline,
-                        &stream_context,
-                        state.paths,
-                        last_valid_event_id,
-                        state.latency.as_secs_f64(),
-                        fs::kFSEventStreamCreateFlagFileEvents
-                            | fs::kFSEventStreamCreateFlagNoDefer
-                            | fs::kFSEventStreamCreateFlagWatchRoot,
-                    );
-
-                    state.stream = stream;
-                    fs::FSEventStreamScheduleWithRunLoop(
-                        state.stream,
-                        cf::CFRunLoopGetCurrent(),
-                        cf::kCFRunLoopDefaultMode,
-                    );
-                    fs::FSEventStreamStart(state.stream);
-                    stream_restarted = true;
-                }
-            }
-
-            if !stream_restarted {
-                let mut events = Vec::with_capacity(num);
-                for p in 0..num {
-                    if let Some(flag) = StreamFlags::from_bits(flags[p]) {
-                        if !flag.contains(StreamFlags::HISTORY_DONE) {
-                            let path_c_str = CStr::from_ptr(paths[p]);
-                            let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
-                            let event = Event {
-                                event_id: ids[p],
-                                flags: flag,
-                                path,
-                            };
-                            state.last_valid_event_id = Some(event.event_id);
-                            events.push(event);
-                        }
-                    } else {
-                        debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
-                    }
-                }
-
-                if !events.is_empty() && !callback(events) {
-                    fs::FSEventStreamStop(stream_ref);
-                    cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
-                }
-            }
-        }
-    }
-}
-
-impl Drop for Handle {
-    fn drop(&mut self) {
-        let mut state = self.0.lock();
-        if let Lifecycle::Running(run_loop) = *state {
-            unsafe {
-                cf::CFRunLoopStop(run_loop);
-            }
-        }
-        *state = Lifecycle::Stopped;
-    }
-}
-
-#[link(name = "CoreServices", kind = "framework")]
-extern "C" {
-    pub fn FSEventsGetCurrentEventId() -> u64;
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::{fs, sync::mpsc, thread, time::Duration};
-
-    #[test]
-    fn test_event_stream_simple() {
-        for _ in 0..3 {
-            let dir = tempfile::Builder::new()
-                .prefix("test-event-stream")
-                .tempdir()
-                .unwrap();
-            let path = dir.path().canonicalize().unwrap();
-            for i in 0..10 {
-                fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
-            }
-            flush_historical_events();
-
-            let (tx, rx) = mpsc::channel();
-            let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
-            thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
-
-            fs::write(path.join("new-file"), "").unwrap();
-            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
-            let event = events.last().unwrap();
-            assert_eq!(event.path, path.join("new-file"));
-            assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
-
-            fs::remove_file(path.join("existing-file-5")).unwrap();
-            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
-            let event = events.last().unwrap();
-            assert_eq!(event.path, path.join("existing-file-5"));
-            assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
-            drop(handle);
-        }
-    }
-
-    #[test]
-    fn test_event_stream_delayed_start() {
-        for _ in 0..3 {
-            let dir = tempfile::Builder::new()
-                .prefix("test-event-stream")
-                .tempdir()
-                .unwrap();
-            let path = dir.path().canonicalize().unwrap();
-            for i in 0..10 {
-                fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
-            }
-            flush_historical_events();
-
-            let (tx, rx) = mpsc::channel();
-            let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
-
-            // Delay the call to `run` in order to make sure we don't miss any events that occur
-            // between creating the `EventStream` and calling `run`.
-            thread::spawn(move || {
-                thread::sleep(Duration::from_millis(100));
-                stream.run(move |events| tx.send(events.to_vec()).is_ok())
-            });
-
-            fs::write(path.join("new-file"), "").unwrap();
-            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
-            let event = events.last().unwrap();
-            assert_eq!(event.path, path.join("new-file"));
-            assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
-
-            fs::remove_file(path.join("existing-file-5")).unwrap();
-            let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
-            let event = events.last().unwrap();
-            assert_eq!(event.path, path.join("existing-file-5"));
-            assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
-            drop(handle);
-        }
-    }
-
-    #[test]
-    fn test_event_stream_shutdown_by_dropping_handle() {
-        let dir = tempfile::Builder::new()
-            .prefix("test-event-stream")
-            .tempdir()
-            .unwrap();
-        let path = dir.path().canonicalize().unwrap();
-        flush_historical_events();
-
-        let (tx, rx) = mpsc::channel();
-        let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
-        thread::spawn(move || {
-            stream.run({
-                let tx = tx.clone();
-                move |_| {
-                    tx.send("running").unwrap();
-                    true
-                }
-            });
-            tx.send("stopped").unwrap();
-        });
-
-        fs::write(path.join("new-file"), "").unwrap();
-        assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
-
-        // Dropping the handle causes `EventStream::run` to return.
-        drop(handle);
-        assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
-    }
-
-    #[test]
-    fn test_event_stream_shutdown_before_run() {
-        let dir = tempfile::Builder::new()
-            .prefix("test-event-stream")
-            .tempdir()
-            .unwrap();
-        let path = dir.path().canonicalize().unwrap();
-
-        let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
-        drop(handle);
-
-        // This returns immediately because the handle was already dropped.
-        stream.run(|_| true);
-    }
-
-    fn flush_historical_events() {
-        let duration = if std::env::var("CI").is_ok() {
-            Duration::from_secs(2)
-        } else {
-            Duration::from_millis(500)
-        };
-        thread::sleep(duration);
-    }
-}

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -324,7 +324,7 @@ impl Platform for LinuxPlatform {
         })
     }
 
-    //TODO linux
+    //todo(linux)
     fn app_path(&self) -> Result<PathBuf> {
         Err(anyhow::Error::msg(
             "Platform<LinuxPlatform>::app_path is not implemented yet",
@@ -338,7 +338,7 @@ impl Platform for LinuxPlatform {
         UtcOffset::UTC
     }
 
-    //TODO linux
+    //todo(linux)
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         Err(anyhow::Error::msg(
             "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
@@ -390,8 +390,7 @@ impl Platform for LinuxPlatform {
         })
     }
 
-    //TODO linux: add trait methods for accessing the primary selection
-
+    //todo(linux): add trait methods for accessing the primary selection
     fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
         let url = url.to_string();
         self.background_executor().spawn(async move {

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -281,7 +281,7 @@ impl Client for WaylandClient {
             CursorStyle::ResizeUp => "n-resize".to_string(),
             CursorStyle::ResizeDown => "s-resize".to_string(),
             CursorStyle::ResizeUpDown => "ns-resize".to_string(),
-            CursorStyle::DisappearingItem => "grabbing".to_string(), // TODO linux - couldn't find equivalent icon in linux
+            CursorStyle::DisappearingItem => "grabbing".to_string(), // todo(linux) - couldn't find equivalent icon in linux
             CursorStyle::IBeamCursorForVerticalLayout => "vertical-text".to_string(),
             CursorStyle::OperationNotAllowed => "not-allowed".to_string(),
             CursorStyle::DragLink => "dnd-link".to_string(),

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -361,7 +361,7 @@ impl Client for X11Client {
         Box::new(X11Window(window_ptr))
     }
 
-    // TODO linux
+    //todo(linux)
     fn set_cursor_style(&self, _style: CursorStyle) {}
 
     fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {

crates/project_core/src/worktree.rs 🔗

@@ -3336,7 +3336,7 @@ impl BackgroundScanner {
         }
     }
 
-    async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fs::Event>>>>) {
+    async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>) {
         use futures::FutureExt as _;
 
         // Populate ignores above the root.
@@ -3389,11 +3389,9 @@ impl BackgroundScanner {
         // For these events, update events cannot be as precise, because we didn't
         // have the previous state loaded yet.
         self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
-        if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) {
-            let mut paths = fs::fs_events_paths(events);
-
-            while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
-                paths.extend(fs::fs_events_paths(more_events));
+        if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) {
+            while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
+                paths.extend(more_paths);
             }
             self.process_events(paths).await;
         }
@@ -3430,12 +3428,10 @@ impl BackgroundScanner {
                     }
                 }
 
-                events = fs_events_rx.next().fuse() => {
-                    let Some(events) = events else { break };
-                    let mut paths = fs::fs_events_paths(events);
-
-                    while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
-                        paths.extend(fs::fs_events_paths(more_events));
+                paths = fs_events_rx.next().fuse() => {
+                    let Some(mut paths) = paths else { break };
+                    while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
+                        paths.extend(more_paths);
                     }
                     self.process_events(paths.clone()).await;
                 }

crates/zed/Cargo.toml 🔗

@@ -48,7 +48,6 @@ extensions_ui.workspace = true
 feedback.workspace = true
 file_finder.workspace = true
 fs.workspace = true
-fsevent.workspace = true
 futures.workspace = true
 go_to_line.workspace = true
 gpui.workspace = true

crates/zed/src/main.rs 🔗

@@ -11,8 +11,6 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use env_logger::Builder;
 use fs::RealFs;
-#[cfg(target_os = "macos")]
-use fsevent::StreamFlags;
 use futures::{future, StreamExt};
 use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
 use isahc::{prelude::Configurable, Request};
@@ -184,7 +182,6 @@ fn main() {
         );
 
         load_user_themes_in_background(fs.clone(), cx);
-        #[cfg(target_os = "macos")]
         watch_themes(fs.clone(), cx);
 
         cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
@@ -465,10 +462,11 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppConte
 
 fn init_paths() {
     std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
+    std::fs::create_dir_all(&*util::paths::EXTENSIONS_DIR)
+        .expect("could not create extensions path");
     std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
     std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
     std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
-    #[cfg(target_os = "linux")]
     std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path");
 }
 
@@ -974,9 +972,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
     .detach_and_log_err(cx);
 }
 
-// todo(linux): Port fsevents to linux
 /// Spawns a background task to watch the themes directory for changes.
-#[cfg(target_os = "macos")]
 fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
     use std::time::Duration;
     cx.spawn(|cx| async move {
@@ -984,17 +980,14 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
             .watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100))
             .await;
 
-        while let Some(events) = events.next().await {
-            for event in events {
-                if event.flags.contains(StreamFlags::ITEM_REMOVED) {
-                    // Theme was removed, don't need to reload.
-                    // We may want to remove the theme from the registry, in this case.
-                } else {
+        while let Some(paths) = events.next().await {
+            for path in paths {
+                if fs.metadata(&path).await.ok().flatten().is_some() {
                     if let Some(theme_registry) =
                         cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
                     {
                         if let Some(()) = theme_registry
-                            .load_user_theme(&event.path, fs.clone())
+                            .load_user_theme(&path, fs.clone())
                             .await
                             .log_err()
                         {

tooling/xtask/src/main.rs 🔗

@@ -47,10 +47,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
         clippy_command.arg("--workspace");
     }
 
-    clippy_command
-        .arg("--release")
-        .arg("--all-targets")
-        .arg("--all-features");
+    clippy_command.arg("--release").arg("--all-features");
 
     if args.fix {
         clippy_command.arg("--fix");
@@ -60,6 +57,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
 
     // Deny all warnings.
     // We don't do this yet on Windows, as it still has some warnings present.
+    // todo(windows)
     #[cfg(not(target_os = "windows"))]
     clippy_command.args(["--deny", "warnings"]);