Read file's mtime in background when getting a FileHandle

Max Brunsfeld and Antonio Scandurra created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

zed/src/editor/buffer/mod.rs | 14 +++--
zed/src/workspace.rs         | 23 +++++----
zed/src/worktree.rs          | 93 ++++++++++++++++++++-----------------
3 files changed, 73 insertions(+), 57 deletions(-)

Detailed changes

zed/src/editor/buffer/mod.rs 🔗

@@ -376,7 +376,9 @@ impl Buffer {
         file: Option<FileHandle>,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
+        let saved_mtime;
         if let Some(file) = file.as_ref() {
+            saved_mtime = file.mtime();
             file.observe_from_model(ctx, |this, file, ctx| {
                 let version = this.version.clone();
                 if this.version == this.saved_version {
@@ -408,6 +410,8 @@ impl Buffer {
                 }
                 ctx.emit(Event::FileHandleChanged);
             });
+        } else {
+            saved_mtime = UNIX_EPOCH;
         }
 
         let mut insertion_splits = HashMap::default();
@@ -472,11 +476,11 @@ impl Buffer {
             insertion_splits,
             version: time::Global::new(),
             saved_version: time::Global::new(),
-            saved_mtime: UNIX_EPOCH,
             last_edit: time::Local::default(),
             undo_map: Default::default(),
             history,
             file,
+            saved_mtime,
             selections: HashMap::default(),
             selections_last_update: 0,
             deferred_ops: OperationQueue::new(),
@@ -3073,7 +3077,7 @@ mod tests {
             tree.flush_fs_events(&app).await;
             app.read(|ctx| tree.read(ctx).scan_complete()).await;
 
-            let file1 = app.read(|ctx| tree.file("file1", ctx));
+            let file1 = app.update(|ctx| tree.file("file1", ctx)).await;
             let buffer1 = app.add_model(|ctx| {
                 Buffer::from_history(0, History::new("abc".into()), Some(file1), ctx)
             });
@@ -3133,7 +3137,7 @@ mod tests {
 
             // When a file is deleted, the buffer is considered dirty.
             let events = Rc::new(RefCell::new(Vec::new()));
-            let file2 = app.read(|ctx| tree.file("file2", ctx));
+            let file2 = app.update(|ctx| tree.file("file2", ctx)).await;
             let buffer2 = app.add_model(|ctx: &mut ModelContext<Buffer>| {
                 ctx.subscribe(&ctx.handle(), {
                     let events = events.clone();
@@ -3154,7 +3158,7 @@ mod tests {
 
             // When a file is already dirty when deleted, we don't emit a Dirtied event.
             let events = Rc::new(RefCell::new(Vec::new()));
-            let file3 = app.read(|ctx| tree.file("file3", ctx));
+            let file3 = app.update(|ctx| tree.file("file3", ctx)).await;
             let buffer3 = app.add_model(|ctx: &mut ModelContext<Buffer>| {
                 ctx.subscribe(&ctx.handle(), {
                     let events = events.clone();
@@ -3185,7 +3189,7 @@ mod tests {
         app.read(|ctx| tree.read(ctx).scan_complete()).await;
 
         let abs_path = dir.path().join("the-file");
-        let file = app.read(|ctx| tree.file("the-file", ctx));
+        let file = app.update(|ctx| tree.file("the-file", ctx)).await;
         let buffer = app.add_model(|ctx| {
             Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx)
         });

zed/src/workspace.rs 🔗

@@ -369,6 +369,7 @@ impl Workspace {
             .map(|(abs_path, file)| {
                 let is_file = bg.spawn(async move { abs_path.is_file() });
                 ctx.spawn(|this, mut ctx| async move {
+                    let file = file.await;
                     let is_file = is_file.await;
                     this.update(&mut ctx, |this, ctx| {
                         if is_file {
@@ -389,14 +390,14 @@ impl Workspace {
         }
     }
 
-    fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> FileHandle {
+    fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> Task<FileHandle> {
         for tree in self.worktrees.iter() {
             if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) {
-                return tree.file(relative_path, ctx.as_ref());
+                return tree.file(relative_path, ctx.as_mut());
             }
         }
         let worktree = self.add_worktree(&abs_path, ctx);
-        worktree.file(Path::new(""), ctx.as_ref())
+        worktree.file(Path::new(""), ctx.as_mut())
     }
 
     pub fn add_worktree(
@@ -497,18 +498,19 @@ impl Workspace {
             }
         };
 
-        let file = worktree.file(path.clone(), ctx.as_ref());
+        let file = worktree.file(path.clone(), ctx.as_mut());
         if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
             let (mut tx, rx) = postage::watch::channel();
             entry.insert(rx);
             let replica_id = self.replica_id;
-            let history = ctx
-                .background_executor()
-                .spawn(file.load_history(ctx.as_ref()));
 
             ctx.as_mut()
                 .spawn(|mut ctx| async move {
-                    *tx.borrow_mut() = Some(match history.await {
+                    let file = file.await;
+                    let history = ctx.read(|ctx| file.load_history(ctx));
+                    let history = ctx.background_executor().spawn(history).await;
+
+                    *tx.borrow_mut() = Some(match history {
                         Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
                             Buffer::from_history(replica_id, history, Some(file), ctx)
                         }))),
@@ -564,8 +566,9 @@ impl Workspace {
                 ctx.prompt_for_new_path(&start_path, move |path, ctx| {
                     if let Some(path) = path {
                         ctx.spawn(|mut ctx| async move {
-                            let file =
-                                handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx));
+                            let file = handle
+                                .update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx))
+                                .await;
                             if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await {
                                 error!("failed to save item: {:?}, ", error);
                             }

zed/src/worktree.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
 use ::ignore::gitignore::Gitignore;
 use anyhow::{Context, Result};
 pub use fuzzy::{match_paths, PathMatch};
-use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task};
+use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use postage::{
@@ -202,14 +202,12 @@ impl Worktree {
         path: &Path,
         ctx: &AppContext,
     ) -> impl Future<Output = Result<History>> {
-        let handles = self.handles.clone();
         let path = path.to_path_buf();
         let abs_path = self.absolutize(&path);
         ctx.background_executor().spawn(async move {
             let mut file = fs::File::open(&abs_path)?;
             let mut base_text = String::new();
             file.read_to_string(&mut base_text)?;
-            Self::update_file_handle(&file, &path, &handles)?;
             Ok(History::new(Arc::from(base_text)))
         })
     }
@@ -1228,7 +1226,7 @@ struct UpdateIgnoreStatusJob {
 }
 
 pub trait WorktreeHandle {
-    fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle;
+    fn file(&self, path: impl AsRef<Path>, app: &mut MutableAppContext) -> Task<FileHandle>;
 
     #[cfg(test)]
     fn flush_fs_events<'a>(
@@ -1238,36 +1236,51 @@ pub trait WorktreeHandle {
 }
 
 impl WorktreeHandle for ModelHandle<Worktree> {
-    fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle {
-        let path = path.as_ref();
+    fn file(&self, path: impl AsRef<Path>, app: &mut MutableAppContext) -> Task<FileHandle> {
+        let path = Arc::from(path.as_ref());
+        let handle = self.clone();
         let tree = self.read(app);
-        let mut handles = tree.handles.lock();
-        let state = if let Some(state) = handles.get(path).and_then(Weak::upgrade) {
-            state
-        } else {
-            let handle_state = if let Some(entry) = tree.entry_for_path(path) {
-                FileHandleState {
-                    path: entry.path().clone(),
-                    is_deleted: false,
-                    mtime: UNIX_EPOCH,
-                }
-            } else {
-                FileHandleState {
-                    path: path.into(),
-                    is_deleted: !tree.path_is_pending(path),
-                    mtime: UNIX_EPOCH,
-                }
-            };
-
-            let state = Arc::new(Mutex::new(handle_state.clone()));
-            handles.insert(handle_state.path, Arc::downgrade(&state));
-            state
-        };
+        let abs_path = tree.absolutize(&path);
+        app.spawn(|ctx| async move {
+            let mtime = ctx
+                .background_executor()
+                .spawn(async move {
+                    if let Ok(metadata) = fs::metadata(&abs_path) {
+                        metadata.modified().unwrap()
+                    } else {
+                        UNIX_EPOCH
+                    }
+                })
+                .await;
+            let state = handle.read_with(&ctx, |tree, _| {
+                let mut handles = tree.handles.lock();
+                if let Some(state) = handles.get(&path).and_then(Weak::upgrade) {
+                    state
+                } else {
+                    let handle_state = if let Some(entry) = tree.entry_for_path(&path) {
+                        FileHandleState {
+                            path: entry.path().clone(),
+                            is_deleted: false,
+                            mtime,
+                        }
+                    } else {
+                        FileHandleState {
+                            path: path.clone(),
+                            is_deleted: !tree.path_is_pending(path),
+                            mtime,
+                        }
+                    };
 
-        FileHandle {
-            worktree: self.clone(),
-            state,
-        }
+                    let state = Arc::new(Mutex::new(handle_state.clone()));
+                    handles.insert(handle_state.path, Arc::downgrade(&state));
+                    state
+                }
+            });
+            FileHandle {
+                worktree: handle.clone(),
+                state,
+            }
+        })
     }
 
     // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
@@ -1525,7 +1538,7 @@ mod tests {
         let buffer =
             app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
 
-        let file = app.read(|ctx| tree.file("", ctx));
+        let file = app.update(|ctx| tree.file("", ctx)).await;
         app.update(|ctx| {
             assert_eq!(file.path().file_name(), None);
             smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
@@ -1552,15 +1565,11 @@ mod tests {
         }));
 
         let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
-        let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| {
-            (
-                tree.file("a/file2", ctx),
-                tree.file("a/file3", ctx),
-                tree.file("b/c/file4", ctx),
-                tree.file("b/c/file5", ctx),
-                tree.file("a/filex", ctx),
-            )
-        });
+        let file2 = app.update(|ctx| tree.file("a/file2", ctx)).await;
+        let file3 = app.update(|ctx| tree.file("a/file3", ctx)).await;
+        let file4 = app.update(|ctx| tree.file("b/c/file4", ctx)).await;
+        let file5 = app.update(|ctx| tree.file("b/c/file5", ctx)).await;
+        let non_existent_file = app.update(|ctx| tree.file("a/file_x", ctx)).await;
 
         // The worktree hasn't scanned the directories containing these paths,
         // so it can't determine that the paths are deleted.