Update modified status by emitting event whenever buffer is dirtied or saved

Nathan Sobo and Max Brunsfeld created

I used the word "dirty" because it felt more expressive than "modified" to me, but not married to it. Tagging Max because we did a lot of this thinking together.

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

zed/src/editor/buffer/mod.rs        | 34 ++++++++++++++++++++----------
zed/src/editor/buffer_view.rs       | 14 +++++++-----
zed/src/workspace/pane.rs           |  2 
zed/src/workspace/workspace_view.rs | 18 ++++++++--------
4 files changed, 40 insertions(+), 28 deletions(-)

Detailed changes

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

@@ -266,8 +266,12 @@ impl Buffer {
         ctx.emit(Event::Saved);
     }
 
-    pub fn is_modified(&self) -> bool {
-        self.fragments.summary().max_version > self.persisted_version
+    pub fn is_dirty(&self) -> bool {
+        self.version > self.persisted_version
+    }
+
+    pub fn version(&self) -> time::Global {
+        self.version.clone()
     }
 
     pub fn text_summary(&self) -> TextSummary {
@@ -414,6 +418,7 @@ impl Buffer {
             None
         };
 
+        let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
         let old_ranges = old_ranges
             .into_iter()
@@ -432,7 +437,7 @@ impl Buffer {
                 ctx.notify();
                 let changes = self.edits_since(old_version).collect::<Vec<_>>();
                 if !changes.is_empty() {
-                    self.did_edit(changes, ctx);
+                    self.did_edit(changes, was_dirty, ctx);
                 }
             }
 
@@ -450,8 +455,11 @@ impl Buffer {
         Ok(ops)
     }
 
-    fn did_edit(&self, changes: Vec<Edit>, ctx: &mut ModelContext<Self>) {
-        ctx.emit(Event::Edited(changes))
+    fn did_edit(&self, changes: Vec<Edit>, was_dirty: bool, ctx: &mut ModelContext<Self>) {
+        ctx.emit(Event::Edited(changes));
+        if !was_dirty {
+            ctx.emit(Event::Dirtied);
+        }
     }
 
     pub fn simulate_typing<T: Rng>(&mut self, rng: &mut T) {
@@ -639,6 +647,7 @@ impl Buffer {
         ops: I,
         ctx: Option<&mut ModelContext<Self>>,
     ) -> Result<()> {
+        let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
 
         let mut deferred_ops = Vec::new();
@@ -657,7 +666,7 @@ impl Buffer {
             ctx.notify();
             let changes = self.edits_since(old_version).collect::<Vec<_>>();
             if !changes.is_empty() {
-                ctx.emit(Event::Edited(changes));
+                self.did_edit(changes, was_dirty, ctx);
             }
         }
 
@@ -1416,6 +1425,7 @@ impl Snapshot {
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum Event {
     Edited(Vec<Edit>),
+    Dirtied,
     Saved,
 }
 
@@ -2510,28 +2520,28 @@ mod tests {
             let model = app.add_model(|_| Buffer::new(0, "abc"));
             model.update(&mut app, |buffer, ctx| {
                 // initially, buffer isn't modified.
-                assert!(!buffer.is_modified());
+                assert!(!buffer.is_dirty());
 
                 // after editing, buffer is modified.
                 buffer.edit(vec![1..2], "", None).unwrap();
                 assert!(buffer.text() == "ac");
-                assert!(buffer.is_modified());
+                assert!(buffer.is_dirty());
 
                 // after saving, buffer is not modified.
-                buffer.did_save(ctx);
-                assert!(!buffer.is_modified());
+                buffer.did_save(buffer.version(), ctx);
+                assert!(!buffer.is_dirty());
 
                 // after editing again, buffer is modified.
                 buffer.edit(vec![1..1], "B", None).unwrap();
                 buffer.edit(vec![2..2], "D", None).unwrap();
                 assert!(buffer.text() == "aBDc");
-                assert!(buffer.is_modified());
+                assert!(buffer.is_dirty());
 
                 // TODO - currently, after restoring the buffer to its
                 // saved state, it is still considered modified.
                 buffer.edit(vec![1..3], "", None).unwrap();
                 assert!(buffer.text() == "ac");
-                assert!(buffer.is_modified());
+                assert!(buffer.is_dirty());
             });
         });
         Ok(())

zed/src/editor/buffer_view.rs 🔗

@@ -5,7 +5,7 @@ use super::{
 use crate::{
     settings::Settings,
     watch,
-    workspace::{self, WorkspaceEvent},
+    workspace::{self, ItemViewEvent},
 };
 use anyhow::Result;
 use futures_core::future::LocalBoxFuture;
@@ -1095,6 +1095,7 @@ impl BufferView {
     ) {
         match event {
             buffer::Event::Edited(_) => ctx.emit(Event::Edited),
+            buffer::Event::Dirtied => ctx.emit(Event::Dirtied),
             buffer::Event::Saved => ctx.emit(Event::Saved),
         }
     }
@@ -1111,6 +1112,7 @@ pub enum Event {
     Activate,
     Edited,
     Blurred,
+    Dirtied,
     Saved,
 }
 
@@ -1153,10 +1155,10 @@ impl workspace::Item for Buffer {
 }
 
 impl workspace::ItemView for BufferView {
-    fn to_workspace_event(event: &Self::Event) -> Option<WorkspaceEvent> {
+    fn to_workspace_event(event: &Self::Event) -> Option<ItemViewEvent> {
         match event {
-            Event::Activate => Some(WorkspaceEvent::Activate),
-            Event::Saved => Some(WorkspaceEvent::TabStateChanged),
+            Event::Activate => Some(ItemViewEvent::Activated),
+            Event::Dirtied | Event::Saved => Some(ItemViewEvent::TabStateChanged),
             _ => None,
         }
     }
@@ -1189,8 +1191,8 @@ impl workspace::ItemView for BufferView {
         self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx))
     }
 
-    fn is_modified(&self, ctx: &AppContext) -> bool {
-        self.buffer.as_ref(ctx).is_modified()
+    fn is_dirty(&self, ctx: &AppContext) -> bool {
+        self.buffer.as_ref(ctx).is_dirty()
     }
 }
 

zed/src/workspace/pane.rs 🔗

@@ -210,7 +210,7 @@ impl Pane {
                                     settings.ui_font_family,
                                     settings.ui_font_size,
                                     ConstrainedBox::new(Self::render_modified_icon(
-                                        item.is_modified(app),
+                                        item.is_dirty(app),
                                     ))
                                     .with_max_width(12.)
                                     .boxed(),

zed/src/workspace/workspace_view.rs 🔗

@@ -13,13 +13,13 @@ pub fn init(app: &mut App) {
     app.add_bindings(vec![Binding::new("cmd-s", "workspace:save", None)]);
 }
 
-pub enum WorkspaceEvent {
+pub enum ItemViewEvent {
     TabStateChanged,
-    Activate,
+    Activated,
 }
 
 pub trait ItemView: View {
-    fn to_workspace_event(event: &Self::Event) -> Option<WorkspaceEvent>;
+    fn to_workspace_event(event: &Self::Event) -> Option<ItemViewEvent>;
     fn title(&self, app: &AppContext) -> String;
     fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
@@ -28,7 +28,7 @@ pub trait ItemView: View {
     {
         None
     }
-    fn is_modified(&self, _: &AppContext) -> bool {
+    fn is_dirty(&self, _: &AppContext) -> bool {
         false
     }
     fn save(&self, _: &mut ViewContext<Self>) -> LocalBoxFuture<'static, anyhow::Result<()>> {
@@ -44,7 +44,7 @@ pub trait ItemViewHandle: Send + Sync {
     fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
-    fn is_modified(&self, ctx: &AppContext) -> bool;
+    fn is_dirty(&self, ctx: &AppContext) -> bool;
     fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>;
 }
 
@@ -72,13 +72,13 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         pane.update(app, |_, ctx| {
             ctx.subscribe_to_view(self, |pane, item, event, ctx| {
                 match T::to_workspace_event(event) {
-                    Some(WorkspaceEvent::Activate) => {
+                    Some(ItemViewEvent::Activated) => {
                         if let Some(ix) = pane.item_index(&item) {
                             pane.activate_item(ix, ctx);
                             pane.activate(ctx);
                         }
                     }
-                    Some(WorkspaceEvent::TabStateChanged) => ctx.notify(),
+                    Some(ItemViewEvent::TabStateChanged) => ctx.notify(),
                     _ => {}
                 }
             })
@@ -89,8 +89,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.update(ctx, |item, ctx| item.save(ctx))
     }
 
-    fn is_modified(&self, ctx: &AppContext) -> bool {
-        self.as_ref(ctx).is_modified(ctx)
+    fn is_dirty(&self, ctx: &AppContext) -> bool {
+        self.as_ref(ctx).is_dirty(ctx)
     }
 
     fn id(&self) -> usize {