core: Improve changeset composition behavior.
It would fail to combine with an empty set.
This commit is contained in:
parent
cf7b19d711
commit
f00cb15137
1 changed files with 69 additions and 32 deletions
|
@ -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())]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue