Switch other backend code over to use transactions.

This commit is contained in:
Nathan Vegdahl 2022-08-23 12:30:14 -07:00
parent 696ecb732d
commit 3f3cf5dd3a
3 changed files with 104 additions and 120 deletions

View File

@ -1,11 +1,8 @@
use std::path::PathBuf;
use std::{borrow::Cow, path::PathBuf};
use ropey::Rope;
use crate::{
history::{Edit, History},
marks::MarkSet,
};
use crate::{history::History, marks::MarkSet, transaction::Transaction};
/// A path for an open text buffer.
///
@ -21,8 +18,8 @@ pub enum BufferPath {
#[derive(Debug, Clone)]
pub struct Buffer {
pub path: BufferPath,
pub is_dirty: bool, // Is this buffer currently out of sync with disk.
pub text: Rope, // The actual text content.
pub edits_since_saved: i64, // Tracks whether this buffer is out of sync with the last save.
pub text: Rope, // The actual text content.
pub mark_sets: Vec<MarkSet>, // MarkSets for cursors, view positions, etc.
history: History,
}
@ -31,19 +28,35 @@ impl Buffer {
pub fn new(text: Rope, path: BufferPath) -> Buffer {
Buffer {
path: path,
is_dirty: false,
edits_since_saved: 0,
text: text,
mark_sets: Vec::new(),
history: History::new(),
}
}
pub fn is_dirty(&self) -> bool {
self.edits_since_saved != 0
}
/// Saves the buffer to disk.
pub fn save(&mut self) {
self.edits_since_saved = 0;
todo!();
}
/// Replaces the given range of chars with the given text.
///
/// The range does not have to be ordered (i.e. the first component can be
/// greater than the second).
pub fn edit(&mut self, char_idx_range: (usize, usize), text: &str) {
self.is_dirty = true;
if self.edits_since_saved >= 0 {
self.edits_since_saved = self.edits_since_saved.wrapping_add(1);
} else {
// The undo state matching the last save is inaccessible, so
// set this to a similarly inaccessible number.
self.edits_since_saved = i64::MAX;
}
// Get the range, properly ordered.
let (start, end) = if char_idx_range.0 < char_idx_range.1 {
@ -52,114 +65,69 @@ impl Buffer {
(char_idx_range.1, char_idx_range.0)
};
// Update undo stack.
if char_idx_range.0 == char_idx_range.1 {
// Fast-path for insertion-only edits.
self.history.push_edit(Edit {
char_idx: start,
from: String::new(),
to: text.into(),
});
} else {
self.history.push_edit(Edit {
char_idx: start,
from: self.text.slice(start..end).into(),
to: text.into(),
});
}
// Build the transaction.
let trans = Transaction::from_edit(
self.text.char_to_byte(start),
&Cow::from(self.text.slice(start..end)),
text,
);
// Update mark sets.
let post_len = text.chars().count();
for mark_set in self.mark_sets.iter_mut() {
for mark in mark_set.iter_mut() {
*mark = mark.edit((start, end), post_len);
}
trans.apply_to_marks(mark_set);
mark_set.make_consistent();
}
// Do removal if needed.
if start != end {
self.text.remove(start..end);
}
// Apply the edit to the document.
trans.apply(&mut self.text);
// Do insertion if needed.
if !text.is_empty() {
self.text.insert(start, text);
}
// Update undo stack.
self.history.push_edit(trans);
}
/// Un-does the last edit if there is one, and returns the range of the
/// edited characters which can be used for e.g. placing a cursor or moving
/// the view.
/// Un-does the last edit if there is one, and returns the
/// Transaction that was inverse-applied, in case it's needed for
/// further processing.
///
/// Returns None if there is no edit to undo.
pub fn undo(&mut self) -> Option<(usize, usize)> {
if let Some(ed) = self.history.undo() {
self.is_dirty = true;
let pre_len = ed.to.chars().count();
let post_len = ed.from.chars().count();
let (start, end) = (ed.char_idx, ed.char_idx + pre_len);
pub fn undo(&mut self) -> Option<&Transaction> {
if let Some(trans) = self.history.undo() {
self.edits_since_saved = self.edits_since_saved.wrapping_sub(1);
// Update mark sets.
for mark_set in self.mark_sets.iter_mut() {
for mark in mark_set.iter_mut() {
*mark = mark.edit((start, end), post_len);
}
trans.apply_inverse_to_marks(mark_set);
mark_set.make_consistent();
}
// Do removal if needed.
if start != end {
self.text.remove(start..end);
}
// Apply the edit to the document.
trans.apply_inverse(&mut self.text);
// Do insertion if needed.
if !ed.from.is_empty() {
self.text.insert(start, &ed.from);
}
return Some((start, start + post_len));
return Some(trans);
} else {
return None;
}
}
/// Re-does the last edit if there is one, and returns the range of the
/// edited characters which can be used for e.g. placing a cursor or moving
/// the view.
/// Re-does the last edit if there is one, and returns the
/// Transaction that was applied, in case it's needed for further
/// processing.
///
/// Returns None if there is no edit to redo.
pub fn redo(&mut self) -> Option<(usize, usize)> {
if let Some(ed) = self.history.redo() {
self.is_dirty = true;
let pre_len = ed.from.chars().count();
let post_len = ed.to.chars().count();
let (start, end) = (ed.char_idx, ed.char_idx + pre_len);
pub fn redo(&mut self) -> Option<&Transaction> {
if let Some(trans) = self.history.redo() {
self.edits_since_saved = self.edits_since_saved.wrapping_add(1);
// Update mark sets.
for mark_set in self.mark_sets.iter_mut() {
for mark in mark_set.iter_mut() {
*mark = mark.edit((start, end), post_len);
}
trans.apply_to_marks(mark_set);
mark_set.make_consistent();
}
// Do removal if needed.
if start != end {
self.text.remove(start..end);
}
// Apply the edit to the document.
trans.apply(&mut self.text);
// Do insertion if needed.
if !ed.to.is_empty() {
self.text.insert(start, &ed.to);
}
return Some((start, start + post_len));
return Some(trans);
} else {
return None;
}

View File

@ -1,6 +1,8 @@
use crate::transaction::Transaction;
#[derive(Debug, Clone)]
pub struct History {
edits: Vec<Edit>,
edits: Vec<Transaction>,
position: usize, // Where we are in the history.
}
@ -12,13 +14,13 @@ impl History {
}
}
pub fn push_edit(&mut self, edit: Edit) {
pub fn push_edit(&mut self, edit: Transaction) {
self.edits.truncate(self.position);
self.edits.push(edit);
self.position += 1;
}
pub fn undo(&mut self) -> Option<&Edit> {
pub fn undo(&mut self) -> Option<&Transaction> {
if self.position > 0 {
self.position -= 1;
Some(&self.edits[self.position])
@ -27,7 +29,7 @@ impl History {
}
}
pub fn redo(&mut self) -> Option<&Edit> {
pub fn redo(&mut self) -> Option<&Transaction> {
if self.position < self.edits.len() {
let edit = &self.edits[self.position];
self.position += 1;
@ -37,10 +39,3 @@ impl History {
}
}
}
#[derive(Debug, Clone)]
pub struct Edit {
pub char_idx: usize,
pub from: String,
pub to: String,
}

View File

@ -293,29 +293,6 @@ impl Transaction {
trans
}
/// Build a Transaction that is functionally identical to undoing
/// this Transaction.
///
/// Note: the resulting Transaction will losslessly reverse the
/// original Transaction on text content, but will be lossy when
/// applied to Marks.
#[must_use]
pub fn invert(&self) -> Transaction {
let mut inverted = self.clone();
for op in inverted.ops.iter_mut() {
match *op {
Op::Retain(_) => {} // Do nothing.
Op::Replace {
ref mut old,
ref mut new,
} => {
std::mem::swap(old, new);
}
}
}
inverted
}
/// Applies the Transaction to a Rope.
pub fn apply(&self, text: &mut Rope) {
let mut i = 0;
@ -327,8 +304,43 @@ impl Transaction {
Op::Replace { old, new } => {
let old = &self.buffer[old.0..old.1];
let new = &self.buffer[new.0..new.1];
let char_i = text.byte_to_char(i);
let char_i = text.byte_to_char(i);
if !old.is_empty() {
let old_char_len = old.chars().count();
debug_assert_eq!(text.slice(char_i..(char_i + old_char_len)), old);
text.remove(char_i..(char_i + old_char_len));
}
if !new.is_empty() {
text.insert(char_i, new);
}
i = i + new.len();
}
}
}
debug_assert!(i <= text.len_bytes());
}
/// Applies the inverse of the Transaction to a Rope.
///
/// This is an "undo" of the Transaction.
pub fn apply_inverse(&self, text: &mut Rope) {
let mut i = 0;
for op in self.ops.iter() {
match op {
Op::Retain(byte_count) => {
i += 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];
let char_i = text.byte_to_char(i);
if !old.is_empty() {
let old_char_len = old.chars().count();
debug_assert_eq!(text.slice(char_i..(char_i + old_char_len)), old);
@ -350,6 +362,15 @@ impl Transaction {
todo!()
}
/// Applies the inverse of the Transaction to a set of Marks.
///
/// This is essentially an "undo" of the Transaction. However, on
/// marks this is a lossy operation and is not guaranteed to return
/// the marks to their original count and position.
pub fn apply_inverse_to_marks(&self, _marks: &mut MarkSet) {
todo!()
}
//---------------------------------------------------------
/// Pushes a retain op onto the transaction operations.