Use emacs-style undo.

Rather than truncating the undo stack when making an edit after undoing,
the undo steps themselves are pushed onto the undo stack before the
edit.  This means that the undo stack never loses any previous states
of the document, and those states are always accessible by just undoing
enough.
This commit is contained in:
Nathan Vegdahl 2024-10-04 20:42:15 +02:00
parent 693cdf078c
commit 5ad3f0ce8c
2 changed files with 42 additions and 1 deletions

View File

@ -15,7 +15,7 @@ impl History {
}
pub fn push_edit(&mut self, edit: Transaction) {
self.edits.truncate(self.position);
self.commit();
self.edits.push(edit);
self.position += 1;
}
@ -38,4 +38,19 @@ impl History {
None
}
}
/// Takes the current undo state, and pushes that as another transaction on
/// top of the existing undo stack, without deleting any existing items.
pub fn commit(&mut self) {
if self.position == self.edits.len() {
return;
}
let mut undo_trans = Transaction::new();
for trans in self.edits[self.position..].iter().rev() {
undo_trans = undo_trans.compose(&trans.inverse());
}
self.edits.push(undo_trans);
self.position = self.edits.len();
}
}

View File

@ -294,6 +294,32 @@ impl Transaction {
trans
}
/// Creates a transaction that is the inverse of this one (i.e. the "undo"
/// of this transaction).
pub fn inverse(&self) -> Transaction {
let mut inv = Transaction::new();
for op in self.ops.iter() {
match op {
Op::Retain(byte_count) => {
inv.retain(*byte_count);
}
Op::Replace {
old: old_range,
new: new_range,
} => {
// Swap old and new to do the inverse.
let old = &self.buffer[new_range.0..new_range.1];
let new = &self.buffer[old_range.0..old_range.1];
inv.replace(old, new);
}
}
}
inv
}
/// Applies the Transaction to a Rope.
pub fn apply(&self, text: &mut Rope) {
let mut i = 0;