From 3f3cf5dd3a8863d955f6f943394a30abf782f3de Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Tue, 23 Aug 2022 12:30:14 -0700 Subject: [PATCH] Switch other backend code over to use transactions. --- sub_crates/backend/src/buffer.rs | 138 ++++++++++---------------- sub_crates/backend/src/history.rs | 17 ++-- sub_crates/backend/src/transaction.rs | 69 ++++++++----- 3 files changed, 104 insertions(+), 120 deletions(-) diff --git a/sub_crates/backend/src/buffer.rs b/sub_crates/backend/src/buffer.rs index ed08b96..c226d90 100644 --- a/sub_crates/backend/src/buffer.rs +++ b/sub_crates/backend/src/buffer.rs @@ -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, // 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; } diff --git a/sub_crates/backend/src/history.rs b/sub_crates/backend/src/history.rs index 5b06e6b..21245d9 100644 --- a/sub_crates/backend/src/history.rs +++ b/sub_crates/backend/src/history.rs @@ -1,6 +1,8 @@ +use crate::transaction::Transaction; + #[derive(Debug, Clone)] pub struct History { - edits: Vec, + edits: Vec, 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, -} diff --git a/sub_crates/backend/src/transaction.rs b/sub_crates/backend/src/transaction.rs index 1bc64ed..8a5e3a2 100644 --- a/sub_crates/backend/src/transaction.rs +++ b/sub_crates/backend/src/transaction.rs @@ -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.