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

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>
This commit is contained in:
Nathan Sobo 2021-04-06 20:46:35 -06:00
parent d724387158
commit 2619bc4602
4 changed files with 40 additions and 28 deletions

View file

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

View file

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

View file

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

View file

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