Maintain a different undo/redo stack in MultiBuffer

This only applies to singleton mode.
This commit is contained in:
Antonio Scandurra 2021-12-14 17:43:41 +01:00
parent 523cbe781b
commit 08e9f3e1e3
3 changed files with 345 additions and 22 deletions

View File

@ -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");
});
}
}

View File

@ -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"))]

View File

@ -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 {