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

View File

@ -1,6 +1,8 @@
use crate::transaction::Transaction;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct History { pub struct History {
edits: Vec<Edit>, edits: Vec<Transaction>,
position: usize, // Where we are in the history. 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.truncate(self.position);
self.edits.push(edit); self.edits.push(edit);
self.position += 1; self.position += 1;
} }
pub fn undo(&mut self) -> Option<&Edit> { pub fn undo(&mut self) -> Option<&Transaction> {
if self.position > 0 { if self.position > 0 {
self.position -= 1; self.position -= 1;
Some(&self.edits[self.position]) 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() { if self.position < self.edits.len() {
let edit = &self.edits[self.position]; let edit = &self.edits[self.position];
self.position += 1; 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 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. /// Applies the Transaction to a Rope.
pub fn apply(&self, text: &mut Rope) { pub fn apply(&self, text: &mut Rope) {
let mut i = 0; let mut i = 0;
@ -327,8 +304,43 @@ impl Transaction {
Op::Replace { old, new } => { Op::Replace { old, new } => {
let old = &self.buffer[old.0..old.1]; let old = &self.buffer[old.0..old.1];
let new = &self.buffer[new.0..new.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() { if !old.is_empty() {
let old_char_len = old.chars().count(); let old_char_len = old.chars().count();
debug_assert_eq!(text.slice(char_i..(char_i + old_char_len)), old); debug_assert_eq!(text.slice(char_i..(char_i + old_char_len)), old);
@ -350,6 +362,15 @@ impl Transaction {
todo!() 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. /// Pushes a retain op onto the transaction operations.