Maintain a different undo/redo stack in `MultiBuffer`

Antonio Scandurra created

This only applies to singleton mode.

Change summary

crates/editor/src/multi_buffer.rs | 293 ++++++++++++++++++++++++++++++--
crates/language/src/buffer.rs     |  34 +++
crates/text/src/text.rs           |  40 ++++
3 files changed, 345 insertions(+), 22 deletions(-)

Detailed changes

crates/editor/src/multi_buffer.rs 🔗

@@ -3,7 +3,7 @@ mod anchor;
 pub use anchor::{Anchor, AnchorRangeExt};
 use anyhow::Result;
 use clock::ReplicaId;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use gpui::{AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
 use language::{
     Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Selection,
@@ -15,7 +15,7 @@ use std::{
     iter::{self, FromIterator, Peekable},
     ops::{Range, Sub},
     sync::Arc,
-    time::{Instant, SystemTime},
+    time::{Duration, Instant, SystemTime},
 };
 use sum_tree::{Bias, Cursor, SumTree};
 use text::{
@@ -25,6 +25,7 @@ use text::{
     AnchorRangeExt as _, Edit, Point, PointUtf16, TextSummary,
 };
 use theme::SyntaxTheme;
+use util::post_inc;
 
 const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
 
@@ -36,6 +37,22 @@ pub struct MultiBuffer {
     subscriptions: Topic,
     singleton: bool,
     replica_id: ReplicaId,
+    history: History,
+}
+
+struct History {
+    next_transaction_id: usize,
+    undo_stack: Vec<Transaction>,
+    redo_stack: Vec<Transaction>,
+    transaction_depth: usize,
+    group_interval: Duration,
+}
+
+struct Transaction {
+    id: usize,
+    buffer_transactions: HashSet<(usize, text::TransactionId)>,
+    first_edit_at: Instant,
+    last_edit_at: Instant,
 }
 
 pub trait ToOffset: 'static {
@@ -110,6 +127,13 @@ impl MultiBuffer {
             subscriptions: Default::default(),
             singleton: false,
             replica_id,
+            history: History {
+                next_transaction_id: Default::default(),
+                undo_stack: Default::default(),
+                redo_stack: Default::default(),
+                transaction_depth: 0,
+                group_interval: Duration::from_millis(300),
+            },
         }
     }
 
@@ -310,17 +334,18 @@ impl MultiBuffer {
         now: Instant,
         cx: &mut ModelContext<Self>,
     ) -> Option<TransactionId> {
-        // TODO
-        self.as_singleton()
-            .unwrap()
-            .update(cx, |buffer, _| buffer.start_transaction_at(now))
+        if let Some(buffer) = self.as_singleton() {
+            return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
+        }
+
+        for BufferState { buffer, .. } in self.buffers.values() {
+            buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
+        }
+        self.history.start_transaction(now)
     }
 
     pub fn end_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
-        // TODO
-        self.as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| buffer.end_transaction(cx))
+        self.end_transaction_at(Instant::now(), cx)
     }
 
     pub(crate) fn end_transaction_at(
@@ -328,10 +353,25 @@ impl MultiBuffer {
         now: Instant,
         cx: &mut ModelContext<Self>,
     ) -> Option<TransactionId> {
-        // TODO
-        self.as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
+        if let Some(buffer) = self.as_singleton() {
+            return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx));
+        }
+
+        let mut buffer_transactions = HashSet::default();
+        for BufferState { buffer, .. } in self.buffers.values() {
+            if let Some(transaction_id) =
+                buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
+            {
+                buffer_transactions.insert((buffer.id(), transaction_id));
+            }
+        }
+
+        if self.history.end_transaction(now, buffer_transactions) {
+            let transaction_id = self.history.group().unwrap();
+            Some(transaction_id)
+        } else {
+            None
+        }
     }
 
     pub fn set_active_selections(
@@ -415,17 +455,49 @@ impl MultiBuffer {
     }
 
     pub fn undo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
-        // TODO
-        self.as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| buffer.undo(cx))
+        if let Some(buffer) = self.as_singleton() {
+            return buffer.update(cx, |buffer, cx| buffer.undo(cx));
+        }
+
+        while let Some(transaction) = self.history.pop_undo() {
+            let mut undone = false;
+            for (buffer_id, buffer_transaction_id) in &transaction.buffer_transactions {
+                if let Some(BufferState { buffer, .. }) = self.buffers.get(&buffer_id) {
+                    undone |= buffer.update(cx, |buf, cx| {
+                        buf.undo_transaction(*buffer_transaction_id, cx)
+                    });
+                }
+            }
+
+            if undone {
+                return Some(transaction.id);
+            }
+        }
+
+        None
     }
 
     pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
-        // TODO
-        self.as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| buffer.redo(cx))
+        if let Some(buffer) = self.as_singleton() {
+            return buffer.update(cx, |buffer, cx| buffer.redo(cx));
+        }
+
+        while let Some(transaction) = self.history.pop_redo() {
+            let mut redone = false;
+            for (buffer_id, buffer_transaction_id) in &transaction.buffer_transactions {
+                if let Some(BufferState { buffer, .. }) = self.buffers.get(&buffer_id) {
+                    redone |= buffer.update(cx, |buf, cx| {
+                        buf.redo_transaction(*buffer_transaction_id, cx)
+                    });
+                }
+            }
+
+            if redone {
+                return Some(transaction.id);
+            }
+        }
+
+        None
     }
 
     pub fn push_excerpt<O>(
@@ -436,6 +508,7 @@ impl MultiBuffer {
     where
         O: text::ToOffset,
     {
+        assert_eq!(self.history.transaction_depth, 0);
         self.sync(cx);
 
         let buffer = &props.buffer;
@@ -1211,6 +1284,93 @@ impl MultiBufferSnapshot {
     }
 }
 
+impl History {
+    fn start_transaction(&mut self, now: Instant) -> Option<TransactionId> {
+        self.transaction_depth += 1;
+        if self.transaction_depth == 1 {
+            let id = post_inc(&mut self.next_transaction_id);
+            self.undo_stack.push(Transaction {
+                id,
+                buffer_transactions: Default::default(),
+                first_edit_at: now,
+                last_edit_at: now,
+            });
+            Some(id)
+        } else {
+            None
+        }
+    }
+
+    fn end_transaction(
+        &mut self,
+        now: Instant,
+        buffer_transactions: HashSet<(usize, TransactionId)>,
+    ) -> bool {
+        assert_ne!(self.transaction_depth, 0);
+        self.transaction_depth -= 1;
+        if self.transaction_depth == 0 {
+            if buffer_transactions.is_empty() {
+                self.undo_stack.pop();
+                false
+            } else {
+                let transaction = self.undo_stack.last_mut().unwrap();
+                transaction.last_edit_at = now;
+                transaction.buffer_transactions.extend(buffer_transactions);
+                true
+            }
+        } else {
+            false
+        }
+    }
+
+    fn pop_undo(&mut self) -> Option<&Transaction> {
+        assert_eq!(self.transaction_depth, 0);
+        if let Some(transaction) = self.undo_stack.pop() {
+            self.redo_stack.push(transaction);
+            self.redo_stack.last()
+        } else {
+            None
+        }
+    }
+
+    fn pop_redo(&mut self) -> Option<&Transaction> {
+        assert_eq!(self.transaction_depth, 0);
+        if let Some(transaction) = self.redo_stack.pop() {
+            self.undo_stack.push(transaction);
+            self.undo_stack.last()
+        } else {
+            None
+        }
+    }
+
+    fn group(&mut self) -> Option<TransactionId> {
+        let mut new_len = self.undo_stack.len();
+        let mut transactions = self.undo_stack.iter_mut();
+
+        if let Some(mut transaction) = transactions.next_back() {
+            while let Some(prev_transaction) = transactions.next_back() {
+                if transaction.first_edit_at - prev_transaction.last_edit_at <= self.group_interval
+                {
+                    transaction = prev_transaction;
+                    new_len -= 1;
+                } else {
+                    break;
+                }
+            }
+        }
+
+        let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len);
+        if let Some(last_transaction) = transactions_to_keep.last_mut() {
+            if let Some(transaction) = transactions_to_merge.last() {
+                last_transaction.last_edit_at = transaction.last_edit_at;
+            }
+        }
+
+        self.undo_stack.truncate(new_len);
+        self.undo_stack.last().map(|t| t.id)
+    }
+}
+
 impl Excerpt {
     fn new(
         id: ExcerptId,
@@ -1848,4 +2008,93 @@ mod tests {
             assert_eq!(text.to_string(), snapshot.text());
         }
     }
+
+    #[gpui::test]
+    fn test_history(cx: &mut MutableAppContext) {
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        let group_interval = multibuffer.read(cx).history.group_interval;
+        multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.push_excerpt(
+                ExcerptProperties {
+                    buffer: &buffer_1,
+                    range: 0..buffer_1.read(cx).len(),
+                    header_height: 0,
+                },
+                cx,
+            );
+            multibuffer.push_excerpt(
+                ExcerptProperties {
+                    buffer: &buffer_2,
+                    range: 0..buffer_2.read(cx).len(),
+                    header_height: 0,
+                },
+                cx,
+            );
+        });
+
+        let mut now = Instant::now();
+
+        multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.start_transaction_at(now, cx);
+            multibuffer.edit(
+                [
+                    Point::new(0, 0)..Point::new(0, 0),
+                    Point::new(1, 0)..Point::new(1, 0),
+                ],
+                "A",
+                cx,
+            );
+            multibuffer.edit(
+                [
+                    Point::new(0, 1)..Point::new(0, 1),
+                    Point::new(1, 1)..Point::new(1, 1),
+                ],
+                "B",
+                cx,
+            );
+            multibuffer.end_transaction_at(now, cx);
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            now += 2 * group_interval;
+            multibuffer.start_transaction_at(now, cx);
+            multibuffer.edit([2..2], "C", cx);
+            multibuffer.end_transaction_at(now, cx);
+            assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678\n");
+
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "1234\n5678\n");
+
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678\n");
+
+            buffer_1.update(cx, |buffer_1, cx| buffer_1.undo(cx));
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "1234\n5678\n");
+
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678\n");
+
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678\n");
+
+            buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx));
+            assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678\n");
+
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678\n");
+        });
+    }
 }

crates/language/src/buffer.rs 🔗

@@ -1375,6 +1375,23 @@ impl Buffer {
         }
     }
 
+    pub fn undo_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        if let Some(operation) = self.text.undo_transaction(transaction_id) {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            true
+        } else {
+            false
+        }
+    }
+
     pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
         let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
@@ -1387,6 +1404,23 @@ impl Buffer {
             None
         }
     }
+
+    pub fn redo_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        if let Some(operation) = self.text.redo_transaction(transaction_id) {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            true
+        } else {
+            false
+        }
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/text/src/text.rs 🔗

@@ -240,6 +240,17 @@ impl History {
         }
     }
 
+    fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
+        assert_eq!(self.transaction_depth, 0);
+        if let Some(transaction_ix) = self.undo_stack.iter().rposition(|t| t.id == transaction_id) {
+            let transaction = self.undo_stack.remove(transaction_ix);
+            self.redo_stack.push(transaction);
+            self.redo_stack.last()
+        } else {
+            None
+        }
+    }
+
     fn pop_redo(&mut self) -> Option<&Transaction> {
         assert_eq!(self.transaction_depth, 0);
         if let Some(transaction) = self.redo_stack.pop() {
@@ -249,6 +260,17 @@ impl History {
             None
         }
     }
+
+    fn remove_from_redo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
+        assert_eq!(self.transaction_depth, 0);
+        if let Some(transaction_ix) = self.redo_stack.iter().rposition(|t| t.id == transaction_id) {
+            let transaction = self.redo_stack.remove(transaction_ix);
+            self.undo_stack.push(transaction);
+            self.undo_stack.last()
+        } else {
+            None
+        }
+    }
 }
 
 #[derive(Clone, Default, Debug)]
@@ -1108,6 +1130,15 @@ impl Buffer {
         }
     }
 
+    pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
+        if let Some(transaction) = self.history.remove_from_undo(transaction_id).cloned() {
+            let op = self.undo_or_redo(transaction).unwrap();
+            Some(op)
+        } else {
+            None
+        }
+    }
+
     pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
         if let Some(transaction) = self.history.pop_redo().cloned() {
             let transaction_id = transaction.id;
@@ -1118,6 +1149,15 @@ impl Buffer {
         }
     }
 
+    pub fn redo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
+        if let Some(transaction) = self.history.remove_from_redo(transaction_id).cloned() {
+            let op = self.undo_or_redo(transaction).unwrap();
+            Some(op)
+        } else {
+            None
+        }
+    }
+
     fn undo_or_redo(&mut self, transaction: Transaction) -> Result<Operation> {
         let mut counts = HashMap::default();
         for edit_id in transaction.edits {