Add initial support for undo/redo stack

Antonio Scandurra created

We're still not capturing selections/anchors, that's right up next.

Change summary

zed/src/editor/buffer/mod.rs  | 127 +++++++++++++++++++++++++++++++++++++
zed/src/editor/buffer_view.rs |  14 ++++
2 files changed, 141 insertions(+)

Detailed changes

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

@@ -28,8 +28,11 @@ use std::{
     path::PathBuf,
     str,
     sync::Arc,
+    time::{Duration, Instant},
 };
 
+const UNDO_GROUP_INTERVAL: Duration = Duration::from_millis(300);
+
 pub type SelectionSetId = time::Lamport;
 pub type SelectionsVersion = usize;
 
@@ -65,6 +68,7 @@ pub struct Buffer {
     saved_version: time::Global,
     last_edit: time::Local,
     undo_map: UndoMap,
+    undo_history: UndoHistory,
     selections: HashMap<SelectionSetId, Vec<Selection>>,
     pub selections_last_update: SelectionsVersion,
     deferred_ops: OperationQueue<Operation>,
@@ -126,6 +130,67 @@ impl UndoMap {
     }
 }
 
+#[derive(Clone)]
+struct EditGroup {
+    edits: Vec<time::Local>,
+    last_edit_at: Instant,
+}
+
+#[derive(Clone)]
+struct UndoHistory {
+    group_interval: Duration,
+    undo_stack: Vec<EditGroup>,
+    redo_stack: Vec<EditGroup>,
+}
+
+impl UndoHistory {
+    fn new(group_interval: Duration) -> Self {
+        Self {
+            group_interval,
+            undo_stack: Vec::new(),
+            redo_stack: Vec::new(),
+        }
+    }
+
+    fn push(&mut self, edit_id: time::Local, now: Instant) {
+        self.redo_stack.clear();
+        if let Some(edit_group) = self.undo_stack.last_mut() {
+            if now - edit_group.last_edit_at <= self.group_interval {
+                edit_group.edits.push(edit_id);
+                edit_group.last_edit_at = now;
+            } else {
+                self.undo_stack.push(EditGroup {
+                    edits: vec![edit_id],
+                    last_edit_at: now,
+                });
+            }
+        } else {
+            self.undo_stack.push(EditGroup {
+                edits: vec![edit_id],
+                last_edit_at: now,
+            });
+        }
+    }
+
+    fn pop_undo(&mut self) -> Option<&EditGroup> {
+        if let Some(edit_group) = self.undo_stack.pop() {
+            self.redo_stack.push(edit_group);
+            self.redo_stack.last()
+        } else {
+            None
+        }
+    }
+
+    fn pop_redo(&mut self) -> Option<&EditGroup> {
+        if let Some(edit_group) = self.redo_stack.pop() {
+            self.undo_stack.push(edit_group);
+            self.undo_stack.last()
+        } else {
+            None
+        }
+    }
+}
+
 #[derive(Clone)]
 pub struct CharIter<'a> {
     fragments_cursor: Cursor<'a, Fragment, usize, usize>,
@@ -307,6 +372,7 @@ impl Buffer {
             saved_version: time::Global::new(),
             last_edit: time::Local::default(),
             undo_map: Default::default(),
+            undo_history: UndoHistory::new(UNDO_GROUP_INTERVAL),
             selections: HashMap::default(),
             selections_last_update: 0,
             deferred_ops: OperationQueue::new(),
@@ -494,6 +560,21 @@ impl Buffer {
         new_text: T,
         ctx: Option<&mut ModelContext<Self>>,
     ) -> Result<Vec<Operation>>
+    where
+        I: IntoIterator<Item = Range<S>>,
+        S: ToOffset,
+        T: Into<Text>,
+    {
+        self.edit_at(old_ranges, new_text, Instant::now(), ctx)
+    }
+
+    fn edit_at<I, S, T>(
+        &mut self,
+        old_ranges: I,
+        new_text: T,
+        now: Instant,
+        ctx: Option<&mut ModelContext<Self>>,
+    ) -> Result<Vec<Operation>>
     where
         I: IntoIterator<Item = Range<S>>,
         S: ToOffset,
@@ -523,6 +604,7 @@ impl Buffer {
         for op in &ops {
             if let Operation::Edit { edit, .. } = op {
                 self.edit_ops.insert(edit.id, edit.clone());
+                self.undo_history.push(edit.id, now);
             }
         }
 
@@ -931,6 +1013,50 @@ impl Buffer {
         Ok(())
     }
 
+    pub fn undo(&mut self, ctx: Option<&mut ModelContext<Self>>) -> Vec<Operation> {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        let mut ops = Vec::new();
+        if let Some(edit_group) = self.undo_history.pop_undo() {
+            for edit_id in edit_group.edits.clone() {
+                ops.push(self.undo_or_redo(edit_id).unwrap());
+            }
+        }
+
+        if let Some(ctx) = ctx {
+            ctx.notify();
+            let changes = self.edits_since(old_version).collect::<Vec<_>>();
+            if !changes.is_empty() {
+                self.did_edit(changes, was_dirty, ctx);
+            }
+        }
+
+        ops
+    }
+
+    pub fn redo(&mut self, ctx: Option<&mut ModelContext<Self>>) -> Vec<Operation> {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        let mut ops = Vec::new();
+        if let Some(edit_group) = self.undo_history.pop_redo() {
+            for edit_id in edit_group.edits.clone() {
+                ops.push(self.undo_or_redo(edit_id).unwrap());
+            }
+        }
+
+        if let Some(ctx) = ctx {
+            ctx.notify();
+            let changes = self.edits_since(old_version).collect::<Vec<_>>();
+            if !changes.is_empty() {
+                self.did_edit(changes, was_dirty, ctx);
+            }
+        }
+
+        ops
+    }
+
     fn undo_or_redo(&mut self, edit_id: time::Local) -> Result<Operation> {
         let undo = UndoOperation {
             id: self.local_clock.tick(),
@@ -1580,6 +1706,7 @@ impl Clone for Buffer {
             saved_version: self.saved_version.clone(),
             last_edit: self.last_edit.clone(),
             undo_map: self.undo_map.clone(),
+            undo_history: self.undo_history.clone(),
             selections: self.selections.clone(),
             selections_last_update: self.selections_last_update.clone(),
             deferred_ops: self.deferred_ops.clone(),

zed/src/editor/buffer_view.rs 🔗

@@ -28,6 +28,8 @@ pub fn init(app: &mut App) {
     app.add_bindings(vec![
         Binding::new("backspace", "buffer:backspace", Some("BufferView")),
         Binding::new("enter", "buffer:newline", Some("BufferView")),
+        Binding::new("cmd-z", "buffer:undo", Some("BufferView")),
+        Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")),
         Binding::new("up", "buffer:move_up", Some("BufferView")),
         Binding::new("down", "buffer:move_down", Some("BufferView")),
         Binding::new("left", "buffer:move_left", Some("BufferView")),
@@ -52,6 +54,8 @@ pub fn init(app: &mut App) {
     app.add_action("buffer:insert", BufferView::insert);
     app.add_action("buffer:newline", BufferView::newline);
     app.add_action("buffer:backspace", BufferView::backspace);
+    app.add_action("buffer:undo", BufferView::undo);
+    app.add_action("buffer:redo", BufferView::redo);
     app.add_action("buffer:move_up", BufferView::move_up);
     app.add_action("buffer:move_down", BufferView::move_down);
     app.add_action("buffer:move_left", BufferView::move_left);
@@ -435,6 +439,16 @@ impl BufferView {
         self.insert(&String::new(), ctx);
     }
 
+    pub fn undo(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        self.buffer
+            .update(ctx, |buffer, ctx| buffer.undo(Some(ctx)));
+    }
+
+    pub fn redo(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        self.buffer
+            .update(ctx, |buffer, ctx| buffer.redo(Some(ctx)));
+    }
+
     pub fn move_left(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
         {
             let app = ctx.app();