Emit `Dirtied` event when a clean buffer's file is deleted

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs              |  2 
zed/src/editor/buffer/mod.rs | 71 ++++++++++++++++++++++++++++---------
2 files changed, 54 insertions(+), 19 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -1620,7 +1620,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
             });
     }
 
-    fn handle(&self) -> ModelHandle<T> {
+    pub fn handle(&self) -> ModelHandle<T> {
         ModelHandle::new(self.model_id, &self.app.ctx.ref_counts)
     }
 

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

@@ -377,7 +377,12 @@ impl Buffer {
         ctx: &mut ModelContext<Self>,
     ) -> Self {
         if let Some(file) = file.as_ref() {
-            file.observe_from_model(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged));
+            file.observe_from_model(ctx, |this, file, ctx| {
+                if this.version == this.saved_version && file.is_deleted() {
+                    ctx.emit(Event::Dirtied);
+                }
+                ctx.emit(Event::FileHandleChanged);
+            });
         }
 
         let mut insertion_splits = HashMap::default();
@@ -2989,19 +2994,21 @@ mod tests {
         App::test_async((), |mut app| async move {
             let dir = temp_tree(json!({
                 "file1": "",
+                "file2": "",
+                "file3": "",
             }));
             let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
             app.read(|ctx| tree.read(ctx).scan_complete()).await;
 
-            let file = app.read(|ctx| tree.file("file1", ctx));
-            let model = app.add_model(|ctx| {
-                Buffer::from_history(0, History::new("abc".into()), Some(file), ctx)
+            let file1 = app.read(|ctx| tree.file("file1", ctx));
+            let buffer1 = app.add_model(|ctx| {
+                Buffer::from_history(0, History::new("abc".into()), Some(file1), ctx)
             });
             let events = Rc::new(RefCell::new(Vec::new()));
 
             // initially, the buffer isn't dirty.
-            model.update(&mut app, |buffer, ctx| {
-                ctx.subscribe(&model, {
+            buffer1.update(&mut app, |buffer, ctx| {
+                ctx.subscribe(&buffer1, {
                     let events = events.clone();
                     move |_, event, _| events.borrow_mut().push(event.clone())
                 });
@@ -3013,7 +3020,7 @@ mod tests {
             });
 
             // after the first edit, the buffer is dirty, and emits a dirtied event.
-            model.update(&mut app, |buffer, ctx| {
+            buffer1.update(&mut app, |buffer, ctx| {
                 assert!(buffer.text() == "ac");
                 assert!(buffer.is_dirty());
                 assert_eq!(*events.borrow(), &[Event::Edited, Event::Dirtied]);
@@ -3023,7 +3030,7 @@ mod tests {
             });
 
             // after saving, the buffer is not dirty, and emits a saved event.
-            model.update(&mut app, |buffer, ctx| {
+            buffer1.update(&mut app, |buffer, ctx| {
                 assert!(!buffer.is_dirty());
                 assert_eq!(*events.borrow(), &[Event::Saved]);
                 events.borrow_mut().clear();
@@ -3033,7 +3040,7 @@ mod tests {
             });
 
             // after editing again, the buffer is dirty, and emits another dirty event.
-            model.update(&mut app, |buffer, ctx| {
+            buffer1.update(&mut app, |buffer, ctx| {
                 assert!(buffer.text() == "aBDc");
                 assert!(buffer.is_dirty());
                 assert_eq!(
@@ -3051,22 +3058,50 @@ mod tests {
 
             assert_eq!(*events.borrow(), &[Event::Edited]);
 
-            // When the file is deleted, the buffer is considered dirty.
-            model.update(&mut app, |buffer, ctx| {
-                buffer.did_save(buffer.version(), None, ctx);
-                assert!(!buffer.is_dirty());
+            // 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 buffer2 = app.add_model(|ctx: &mut ModelContext<Buffer>| {
+                ctx.subscribe(&ctx.handle(), {
+                    let events = events.clone();
+                    move |_, event, _| events.borrow_mut().push(event.clone())
+                });
+
+                Buffer::from_history(0, History::new("abc".into()), Some(file2), ctx)
             });
-            events.borrow_mut().clear();
 
             tree.flush_fs_events(&app).await;
-            fs::remove_file(dir.path().join("file1")).unwrap();
+            fs::remove_file(dir.path().join("file2")).unwrap();
             tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
                 .await;
+            assert_eq!(
+                *events.borrow(),
+                &[Event::Dirtied, Event::FileHandleChanged]
+            );
+            app.read(|ctx| assert!(buffer2.read(ctx).is_dirty()));
 
-            model.update(&mut app, |buffer, _| {
-                assert_eq!(*events.borrow(), &[Event::FileHandleChanged]);
-                assert!(buffer.is_dirty());
+            // 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 buffer3 = app.add_model(|ctx: &mut ModelContext<Buffer>| {
+                ctx.subscribe(&ctx.handle(), {
+                    let events = events.clone();
+                    move |_, event, _| events.borrow_mut().push(event.clone())
+                });
+
+                Buffer::from_history(0, History::new("abc".into()), Some(file3), ctx)
             });
+
+            tree.flush_fs_events(&app).await;
+            buffer3.update(&mut app, |buffer, ctx| {
+                buffer.edit(Some(0..0), "x", Some(ctx)).unwrap();
+            });
+            events.borrow_mut().clear();
+            fs::remove_file(dir.path().join("file3")).unwrap();
+            tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
+                .await;
+            assert_eq!(*events.borrow(), &[Event::FileHandleChanged]);
+            app.read(|ctx| assert!(buffer3.read(ctx).is_dirty()));
         });
     }