core: Improve changeset composition behavior.

It would fail to combine with an empty set.
This commit is contained in:
Blaž Hrastnik 2021-04-06 19:01:48 +09:00
parent cf7b19d711
commit f00cb15137

View file

@ -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())]);
}
}