diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b485ef3f9e7eb561b276fec58c2cc5acd823407b..4901409310389bedc46840be4e058e97ba74ae24 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2131,9 +2131,9 @@ impl ModelHandle { let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); let duration = if std::env::var("CI").is_ok() { - Duration::from_secs(2) + Duration::from_secs(5) } else { - Duration::from_millis(500) + Duration::from_secs(1) }; async move { diff --git a/zed-rpc/proto/zed.proto b/zed-rpc/proto/zed.proto index f7e71fa3ed20943c3845933167dfb7c42c25fd6a..0190f1ca64461ca18f9c269d367a44b482f99583 100644 --- a/zed-rpc/proto/zed.proto +++ b/zed-rpc/proto/zed.proto @@ -95,8 +95,9 @@ message Entry { bool is_dir = 1; string path = 2; uint64 inode = 3; - bool is_symlink = 4; - bool is_ignored = 5; + Timestamp mtime = 4; + bool is_symlink = 5; + bool is_ignored = 6; } message Buffer { @@ -121,3 +122,8 @@ message Operation { uint32 timestamp = 2; } } + +message Timestamp { + uint64 seconds = 1; + uint32 nanos = 2; +} \ No newline at end of file diff --git a/zed-rpc/src/proto.rs b/zed-rpc/src/proto.rs index 4fe227cb4aa893b4db8881a30d9ee77584ac5cc8..1330728b907dbb3e910a6fb2f6cece1a41e78d65 100644 --- a/zed-rpc/src/proto.rs +++ b/zed-rpc/src/proto.rs @@ -1,6 +1,10 @@ use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt as _}; use prost::Message; -use std::{convert::TryInto, io}; +use std::{ + convert::TryInto, + io, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); @@ -122,6 +126,24 @@ where } } +impl Into for Timestamp { + fn into(self) -> SystemTime { + UNIX_EPOCH + .checked_add(Duration::new(self.seconds, self.nanos)) + .unwrap() + } +} + +impl From for Timestamp { + fn from(time: SystemTime) -> Self { + let duration = time.duration_since(UNIX_EPOCH).unwrap(); + Self { + seconds: duration.as_secs(), + nanos: duration.subsec_nanos(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 67c2c7429a65fdfd03f0b1b0ede6a4427d32a6f0..0ddf3a1a8137134313b8c4804ae91f9944e757ce 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -35,7 +35,7 @@ use std::{ atomic::{AtomicU64, Ordering::SeqCst}, Arc, Weak, }, - time::{Duration, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; lazy_static! { @@ -44,7 +44,7 @@ lazy_static! { #[derive(Clone, Debug)] enum ScanState { - Idle, + Idle(Option), Scanning, Err(Arc), } @@ -55,7 +55,7 @@ pub enum Worktree { } impl Entity for Worktree { - type Event = (); + type Event = Diff; } impl Worktree { @@ -104,7 +104,7 @@ impl Worktree { ) -> impl Future> { match self { Worktree::Local(worktree) => worktree.save(path, content, cx), - Worktree::Remote(worktree) => todo!(), + Worktree::Remote(_) => todo!(), } } } @@ -127,7 +127,7 @@ pub struct LocalWorktree { next_handle_id: AtomicU64, scan_state: (watch::Sender, watch::Receiver), _event_stream_handle: fsevent::Handle, - polling_snapshot: bool, + poll_scheduled: bool, rpc: Option, } @@ -191,7 +191,7 @@ impl LocalWorktree { next_handle_id: Default::default(), scan_state: watch::channel_with(ScanState::Scanning), _event_stream_handle: event_stream_handle, - polling_snapshot: false, + poll_scheduled: false, rpc: None, }; @@ -240,45 +240,32 @@ impl LocalWorktree { } } - fn observe_scan_state(&mut self, scan_state: ScanState, cx: &mut ModelContext) { - let _ = self.scan_state.0.blocking_send(scan_state); - if !self.polling_snapshot { - self.poll_snapshot(cx); + fn observe_scan_state(&mut self, mut scan_state: ScanState, cx: &mut ModelContext) { + if let ScanState::Idle(diff) = &mut scan_state { + if let Some(diff) = diff.take() { + cx.emit(diff); + } } + let _ = self.scan_state.0.blocking_send(scan_state); + self.poll_snapshot(cx); } fn poll_snapshot(&mut self, cx: &mut ModelContext) { - let poll_again = self.is_scanning(); - if poll_again { - self.polling_snapshot = true; - } - - // let prev_snapshot = self.snapshot.clone(); - let background_snapshot = self.background_snapshot.clone(); - let next_snapshot = cx.background_executor().spawn(async move { - let next_snapshot = background_snapshot.lock().clone(); - // TODO: Diff with next and prev snapshots - next_snapshot - }); + self.snapshot = self.background_snapshot.lock().clone(); + cx.notify(); - cx.spawn(|this, mut cx| async move { - let next_snapshot = next_snapshot.await; - this.update(&mut cx, |this, cx| { - let worktree = this.as_local_mut().unwrap(); - worktree.snapshot = next_snapshot; - cx.notify(); - }); - - if poll_again { + if self.is_scanning() && !self.poll_scheduled { + cx.spawn(|this, mut cx| async move { smol::Timer::after(Duration::from_millis(100)).await; this.update(&mut cx, |this, cx| { let worktree = this.as_local_mut().unwrap(); - worktree.polling_snapshot = false; + worktree.poll_scheduled = false; worktree.poll_snapshot(cx); }) - } - }) - .detach(); + }) + .detach(); + self.poll_scheduled = true; + } } fn is_scanning(&self) -> bool { @@ -353,6 +340,7 @@ impl LocalWorktree { is_dir: entry.is_dir(), path: entry.path.to_string_lossy().to_string(), inode: entry.inode, + mtime: Some(entry.mtime.into()), is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, }) @@ -419,7 +407,7 @@ impl RemoteWorktree { .collect(); let mut entries = SumTree::new(); entries.extend( - worktree.entries.into_iter().map(|entry| { + worktree.entries.into_iter().filter_map(|entry| { let kind = if entry.is_dir { EntryKind::Dir } else { @@ -427,12 +415,18 @@ impl RemoteWorktree { char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase())); EntryKind::File(char_bag) }; - Entry { - kind, - path: Path::new(&entry.path).into(), - inode: entry.inode, - is_symlink: entry.is_symlink, - is_ignored: entry.is_ignored, + if let Some(mtime) = entry.mtime { + Some(Entry { + kind, + path: Path::new(&entry.path).into(), + inode: entry.inode, + mtime: mtime.into(), + is_symlink: entry.is_symlink, + is_ignored: entry.is_ignored, + }) + } else { + log::warn!("missing mtime in worktree entry message"); + None } }), &(), @@ -627,34 +621,37 @@ impl Snapshot { let mut old = old.entries.cursor::<(), ()>().peekable(); let mut diff = Diff::default(); - let mut removed_inodes = HashMap::new(); - let mut added_inodes = HashMap::new(); + let mut removed = HashMap::new(); + let mut added = HashMap::new(); loop { - match (new.peek(), old.peek()) { + match (new.peek().copied(), old.peek().copied()) { (Some(new_entry), Some(old_entry)) => match new_entry.path.cmp(&old_entry.path) { cmp::Ordering::Equal => { + if new_entry.mtime > old_entry.mtime { + diff.modified.insert(new_entry.path.clone()); + } new.next(); old.next(); } cmp::Ordering::Less => { - added_inodes.insert(new_entry.inode, new_entry.path.clone()); + added.insert(new_entry.inode, new_entry); diff.added.insert(new_entry.path.clone()); new.next(); } cmp::Ordering::Greater => { - removed_inodes.insert(old_entry.path.clone(), old_entry.inode); + removed.insert(&old_entry.path, old_entry); diff.removed.insert(old_entry.path.clone()); old.next(); } }, (Some(new_entry), None) => { - added_inodes.insert(new_entry.inode, new_entry.path.clone()); + added.insert(new_entry.inode, new_entry); diff.added.insert(new_entry.path.clone()); new.next(); } (None, Some(old_entry)) => { - removed_inodes.insert(old_entry.path.clone(), old_entry.inode); + removed.insert(&old_entry.path, old_entry); diff.removed.insert(old_entry.path.clone()); old.next(); } @@ -662,11 +659,15 @@ impl Snapshot { } } - for (removed_path, inode) in removed_inodes { - if let Some(added_path) = added_inodes.remove(&inode) { - diff.removed.remove(&removed_path); - diff.added.remove(&added_path); - diff.moved.insert(removed_path, added_path); + for (removed_path, removed_entry) in removed { + if let Some(added_entry) = added.remove(&removed_entry.inode) { + diff.removed.remove(removed_path); + diff.added.remove(&added_entry.path); + diff.moved + .insert(removed_path.clone(), added_entry.path.clone()); + if added_entry.mtime > removed_entry.mtime { + diff.modified.insert(added_entry.path.clone()); + } } } @@ -686,12 +687,12 @@ impl fmt::Debug for Snapshot { } } -#[derive(Default)] -struct Diff { - moved: HashMap, Arc>, - removed: HashSet>, - added: HashSet>, - modified: HashSet>, +#[derive(Clone, Default, Debug, PartialEq)] +pub struct Diff { + pub moved: HashMap, Arc>, + pub removed: HashSet>, + pub added: HashSet>, + pub modified: HashSet>, } impl FileHandle { @@ -826,6 +827,7 @@ pub struct Entry { kind: EntryKind, path: Arc, inode: u64, + mtime: SystemTime, is_symlink: bool, is_ignored: bool, } @@ -1036,7 +1038,7 @@ impl BackgroundScanner { } } - if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { + if smol::block_on(self.notify.send(ScanState::Idle(None))).is_err() { return; } @@ -1045,11 +1047,13 @@ impl BackgroundScanner { return false; } + let prev_snapshot = self.snapshot.lock().clone(); if !self.process_events(events) { return false; } - if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { + let diff = self.snapshot.lock().diff(&prev_snapshot); + if smol::block_on(self.notify.send(ScanState::Idle(Some(diff)))).is_err() { return false; } @@ -1066,6 +1070,7 @@ impl BackgroundScanner { let inode = metadata.ino(); let is_symlink = fs::symlink_metadata(&abs_path)?.file_type().is_symlink(); let is_dir = metadata.file_type().is_dir(); + let mtime = metadata.modified()?; // After determining whether the root entry is a file or a directory, populate the // snapshot's "root name", which will be used for the purpose of fuzzy matching. @@ -1083,6 +1088,7 @@ impl BackgroundScanner { kind: EntryKind::PendingDir, path: path.clone(), inode, + mtime, is_symlink, is_ignored: false, }); @@ -1113,6 +1119,7 @@ impl BackgroundScanner { kind: EntryKind::File(self.char_bag(&path)), path, inode, + mtime, is_symlink, is_ignored: false, }); @@ -1142,6 +1149,7 @@ impl BackgroundScanner { }; let child_inode = child_metadata.ino(); + let child_mtime = child_metadata.modified()?; // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored if child_name == *GITIGNORE { @@ -1176,6 +1184,7 @@ impl BackgroundScanner { kind: EntryKind::PendingDir, path: child_path.clone(), inode: child_inode, + mtime: child_mtime, is_symlink: child_is_symlink, is_ignored, }); @@ -1195,6 +1204,7 @@ impl BackgroundScanner { kind: EntryKind::File(self.char_bag(&child_path)), path: child_path, inode: child_inode, + mtime: child_mtime, is_symlink: child_is_symlink, is_ignored, }); @@ -1469,6 +1479,7 @@ impl BackgroundScanner { Ok(metadata) => metadata, }; let inode = metadata.ino(); + let mtime = metadata.modified()?; let is_symlink = fs::symlink_metadata(&abs_path) .context("failed to read symlink metadata")? .file_type() @@ -1482,6 +1493,7 @@ impl BackgroundScanner { }, path, inode, + mtime, is_symlink, is_ignored: false, }; @@ -1754,10 +1766,7 @@ mod tests { use anyhow::Result; use rand::prelude::*; use serde_json::json; - use std::env; - use std::fmt::Write; - use std::os::unix; - use std::time::SystemTime; + use std::{cell::RefCell, env, fmt::Write, os::unix, rc::Rc, time::SystemTime}; #[gpui::test] async fn test_populate_and_search(mut cx: gpui::TestAppContext) { @@ -1998,6 +2007,7 @@ mod tests { path: Path::new("b").into(), kind: EntryKind::Dir, inode: 0, + mtime: UNIX_EPOCH, is_ignored: false, is_symlink: false, }), @@ -2005,6 +2015,7 @@ mod tests { path: Path::new("b/a").into(), kind: EntryKind::Dir, inode: 0, + mtime: UNIX_EPOCH, is_ignored: false, is_symlink: false, }), @@ -2012,6 +2023,7 @@ mod tests { path: Path::new("b/c").into(), kind: EntryKind::PendingDir, inode: 0, + mtime: UNIX_EPOCH, is_ignored: false, is_symlink: false, }), @@ -2019,6 +2031,7 @@ mod tests { path: Path::new("b/e").into(), kind: EntryKind::Dir, inode: 0, + mtime: UNIX_EPOCH, is_ignored: false, is_symlink: false, }), @@ -2034,6 +2047,80 @@ mod tests { assert!(!snapshot.path_is_pending("b/e")); } + #[gpui::test] + async fn test_file_change_events(mut cx: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "dir_a": { + "file1": "1", + "file2": "2", + "dir_b": { + "file3": "3", + } + }, + "dir_c": { + "dir_d": { + "file4": "4", + "file5": "5", + } + } + })); + + let root = dir.path(); + let tree = cx.add_model(|cx| Worktree::local(root, cx)); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(&cx).await; + + let events = Rc::new(RefCell::new(Vec::new())); + tree.update(&mut cx, { + let events = events.clone(); + |_, cx| { + cx.subscribe(&tree, move |_, event, _| { + events.borrow_mut().push(event.clone()); + }) + } + }); + + std::fs::remove_file(root.join("dir_a/file1")).unwrap(); + std::fs::rename(root.join("dir_a/file2"), root.join("dir_c/file20")).unwrap(); + std::fs::write(root.join("dir_c/dir_d/file4"), "modified 4").unwrap(); + std::fs::write(root.join("dir_c/file10"), "hi").unwrap(); + std::fs::rename( + root.join("dir_c/dir_d/file5"), + root.join("dir_c/dir_d/file50"), + ) + .unwrap(); + std::fs::write(root.join("dir_c/dir_d/file50"), "modified after rename").unwrap(); + tree.condition(&cx, |_, _| !events.borrow().is_empty()) + .await; + + assert_eq!( + *events.borrow(), + &[Diff { + moved: vec![ + ( + Path::new("dir_a/file2").into(), + Path::new("dir_c/file20").into(), + ), + ( + Path::new("dir_c/dir_d/file5").into(), + Path::new("dir_c/dir_d/file50").into(), + ) + ] + .into_iter() + .collect(), + added: vec![Path::new("dir_c/file10").into()].into_iter().collect(), + removed: vec![Path::new("dir_a/file1").into()].into_iter().collect(), + modified: vec![ + Path::new("dir_c/dir_d/file4").into(), + Path::new("dir_c/dir_d/file50").into() + ] + .into_iter() + .collect(), + }] + ); + } + #[test] fn test_random() { let iterations = env::var("ITERATIONS")