Cargo.lock 🔗
@@ -12790,7 +12790,6 @@ dependencies = [
"feedback",
"file_finder",
"fs",
- "fsevent",
"futures 0.3.28",
"go_to_line",
"gpui",
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
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(-)
@@ -12790,7 +12790,6 @@ dependencies = [
"feedback",
"file_finder",
"fs",
- "fsevent",
"futures 0.3.28",
"go_to_line",
"gpui",
@@ -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;
};
@@ -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"
@@ -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;
@@ -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);
+ }
+}
@@ -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);
- }
-}
@@ -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 {
@@ -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(),
@@ -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>> {
@@ -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;
}
@@ -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
@@ -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()
{
@@ -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"]);