From 08e9f3e1e3fe69c46aa25401a3c4da875a6c3c0d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Dec 2021 17:43:41 +0100 Subject: [PATCH] Maintain a different undo/redo stack in `MultiBuffer` This only applies to singleton mode. --- 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(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 655002f8b9909ed8ab31b071653db372f2798b65..beca029a9fab0928145709a05c6c0cb6300b9553 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/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, + redo_stack: Vec, + 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, ) -> Option { - // 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) -> Option { - // 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, ) -> Option { - // 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) -> Option { - // 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) -> Option { - // 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( @@ -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 { + 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 { + 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"); + }); + } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 92539c49ba0c89d1259ec4953a211cb9c0a4e942..d1683241b3ed4458b4d2ff6db7dc071864bc8f29 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1375,6 +1375,23 @@ impl Buffer { } } + pub fn undo_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) -> 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) -> Option { 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, + ) -> 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"))] diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2b2282474a81291a17ab9828c36df33270290b44..8114835b8219d83397d846802258f6302a73b5ec 100644 --- a/crates/text/src/text.rs +++ b/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 { + 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 { + 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 { let mut counts = HashMap::default(); for edit_id in transaction.edits {