diff options
author | Blaž Hrastnik | 2021-04-06 10:01:48 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2021-04-06 10:01:48 +0000 |
commit | f00cb15137fdff72c2f08c4c6f43bfa96933433f (patch) | |
tree | da5390cdaab9df5d5d4099077f05ebc24c62502c | |
parent | cf7b19d7115b7c5a9c5516713478908d157bda61 (diff) |
core: Improve changeset composition behavior.
It would fail to combine with an empty set.
-rw-r--r-- | helix-core/src/transaction.rs | 101 |
1 files changed, 69 insertions, 32 deletions
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 6c60c9c5..77cb358f 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -27,15 +27,35 @@ pub struct ChangeSet { pub(crate) changes: Vec<Operation>, /// The required document length. Will refuse to apply changes unless it matches. len: usize, + len_after: usize, +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + changes: Vec::new(), + len: 0, + len_after: 0, + } + } } impl ChangeSet { + pub fn with_capacity(capacity: usize) -> Self { + Self { + changes: Vec::with_capacity(capacity), + len: 0, + len_after: 0, + } + } + #[must_use] pub fn new(doc: &Rope) -> Self { let len = doc.len_chars(); Self { - changes: vec![Operation::Retain(len)], + changes: Vec::new(), len, + len_after: len, } } @@ -47,21 +67,6 @@ impl ChangeSet { &self.changes } - #[must_use] - fn len_after(&self) -> usize { - use Operation::*; - - let mut len = 0; - for change in &self.changes { - match change { - Retain(i) => len += i, - Insert(s) => len += s.chars().count(), - Delete(_) => (), - } - } - len - } - // Changeset builder operations: delete/insert/retain fn delete(&mut self, n: usize) { use Operation::*; @@ -69,6 +74,8 @@ impl ChangeSet { return; } + self.len += n; + if let Some(Delete(count)) = self.changes.last_mut() { *count += n; } else { @@ -83,6 +90,8 @@ impl ChangeSet { return; } + self.len_after += fragment.len(); + let new_last = match self.changes.as_mut_slice() { [.., Insert(prev)] | [.., Insert(prev), Delete(_)] => { prev.push_tendril(&fragment); @@ -101,6 +110,9 @@ impl ChangeSet { return; } + self.len += n; + self.len_after += n; + if let Some(Retain(count)) = self.changes.last_mut() { *count += n; } else { @@ -112,7 +124,13 @@ impl ChangeSet { /// In other words, If `this` goes `docA` → `docB` and `other` represents `docB` → `docC`, the /// returned value will represent the change `docA` → `docC`. pub fn compose(self, other: Self) -> Self { - debug_assert!(self.len_after() == other.len); + debug_assert!(self.len_after == other.len); + + // composing fails in weird ways if one of the sets is empty + // a: [] len: 0 len_after: 1 | b: [Insert(Tendril<UTF8>(inline: "\n")), Retain(1)] len 1 + if self.changes.is_empty() { + return other; + } let len = self.changes.len(); @@ -122,10 +140,7 @@ impl ChangeSet { let mut head_a = changes_a.next(); let mut head_b = changes_b.next(); - let mut changes = Self { - len: self.len, - changes: Vec::with_capacity(len), // TODO: max(a, b), shrink_to_fit() afterwards - }; + let mut changes = Self::with_capacity(len); // TODO: max(a, b), shrink_to_fit() afterwards loop { use std::cmp::Ordering; @@ -228,6 +243,9 @@ impl ChangeSet { }; } + // starting len should still equal original starting len + debug_assert!(changes.len == self.len); + changes } @@ -251,7 +269,8 @@ impl ChangeSet { pub fn invert(&self, original_doc: &Rope) -> Self { assert!(original_doc.len_chars() == self.len); - let mut changes = Vec::with_capacity(self.changes.len()); + let mut changes = Self::with_capacity(self.changes.len()); + let mut pos = 0; let mut len = 0; @@ -259,24 +278,22 @@ impl ChangeSet { use Operation::*; match change { Retain(n) => { - changes.push(Retain(*n)); + changes.retain(*n); pos += n; - len += n; } Delete(n) => { let text = Cow::from(original_doc.slice(pos..pos + *n)); - changes.push(Insert(Tendril::from_slice(&text))); + changes.insert(Tendril::from_slice(&text)); pos += n; } Insert(s) => { let chars = s.chars().count(); - changes.push(Delete(chars)); - len += chars; + changes.delete(chars); } } } - Self { changes, len } + changes } /// Returns true if applied successfully. @@ -309,7 +326,7 @@ impl ChangeSet { /// `true` when the set is empty. #[inline] pub fn is_empty(&self) -> bool { - matches!(self.changes.as_slice(), [] | [Operation::Retain(_)]) + self.changes.is_empty() } /// Map a position through the changes. @@ -458,8 +475,8 @@ impl Transaction { I: IntoIterator<Item = Change> + ExactSizeIterator, { let len = doc.len_chars(); - let acc = Vec::with_capacity(2 * changes.len() + 1); // rough estimate - let mut changeset = ChangeSet { changes: acc, len }; + + let mut changeset = ChangeSet::with_capacity(2 * changes.len() + 1); // rough estimate // TODO: verify ranges are ordered and not overlapping or change will panic. @@ -576,11 +593,13 @@ mod test { Insert("abc".into()), ], len: 8, + len_after: 15, }; let b = ChangeSet { changes: vec![Delete(10), Insert("world".into()), Retain(5)], len: 15, + len_after: 10, }; let mut text = Rope::from("hello xz"); @@ -597,8 +616,9 @@ mod test { use Operation::*; let changes = ChangeSet { - changes: vec![Retain(4), Delete(5), Insert("test".into()), Retain(3)], + changes: vec![Retain(4), Insert("test".into()), Delete(5), Retain(3)], len: 12, + len_after: 11, }; let doc = Rope::from("世界3 hello xz"); @@ -627,6 +647,7 @@ mod test { let cs = ChangeSet { changes: vec![Retain(4), Insert("!!".into()), Retain(4)], len: 8, + len_after: 10, }; assert_eq!(cs.map_pos(0, Assoc::Before), 0); // before insert region @@ -638,6 +659,7 @@ mod test { let cs = ChangeSet { changes: vec![Retain(4), Delete(4), Retain(4)], len: 12, + len_after: 8, }; assert_eq!(cs.map_pos(0, Assoc::Before), 0); // at start assert_eq!(cs.map_pos(4, Assoc::Before), 4); // before a delete @@ -655,6 +677,7 @@ mod test { Delete(2), ], len: 4, + len_after: 4, }; assert_eq!(cs.map_pos(2, Assoc::Before), 2); assert_eq!(cs.map_pos(2, Assoc::After), 2); @@ -717,4 +740,18 @@ mod test { assert_eq!(changes.changes, &[Insert("hello".into())]); // instead of insert h, insert e, insert l, insert l, insert o } + + #[test] + fn combine_with_empty() { + let empty = Rope::from(""); + let mut a = ChangeSet::new(&empty); + + let mut b = ChangeSet::new(&empty); + b.insert("a".into()); + + let changes = a.compose(b); + + use Operation::*; + assert_eq!(changes.changes, &[Insert("a".into())]); + } } |