helix-mods/helix-core/src/transaction.rs
Ivan Tham f5f46b1fed Separate document history into Cell
As history is used separately from the rest of the edits, separating it
can avoid needless borrowing and cloning. But one need to be aware later.
2021-06-02 23:47:50 +08:00

757 lines
23 KiB
Rust

use crate::{Range, Rope, Selection, State, Tendril};
use std::{borrow::Cow, convert::TryFrom};
/// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>);
// TODO: pub(crate)
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operation {
/// Move cursor by n characters.
Retain(usize),
/// Delete n characters.
Delete(usize),
/// Insert text at position.
Insert(Tendril),
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Assoc {
Before,
After,
}
// ChangeSpec = Change | ChangeSet | Vec<Change>
#[derive(Debug, Clone, PartialEq, Eq)]
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::new(),
len,
len_after: len,
}
}
// TODO: from iter
//
#[doc(hidden)] // used by lsp to convert to LSP changes
pub fn changes(&self) -> &[Operation] {
&self.changes
}
// Changeset builder operations: delete/insert/retain
fn delete(&mut self, n: usize) {
use Operation::*;
if n == 0 {
return;
}
self.len += n;
if let Some(Delete(count)) = self.changes.last_mut() {
*count += n;
} else {
self.changes.push(Delete(n));
}
}
fn insert(&mut self, fragment: Tendril) {
use Operation::*;
if fragment.is_empty() {
return;
}
self.len_after += fragment.len();
let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
prev.push_tendril(&fragment);
return;
}
[.., last @ Delete(_)] => std::mem::replace(last, Insert(fragment)),
_ => Insert(fragment),
};
self.changes.push(new_last);
}
fn retain(&mut self, n: usize) {
use Operation::*;
if n == 0 {
return;
}
self.len += n;
self.len_after += n;
if let Some(Retain(count)) = self.changes.last_mut() {
*count += n;
} else {
self.changes.push(Retain(n));
}
}
/// Combine two changesets together.
/// 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);
// 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();
let mut changes_a = self.changes.into_iter();
let mut changes_b = other.changes.into_iter();
let mut head_a = changes_a.next();
let mut head_b = changes_b.next();
let mut changes = Self::with_capacity(len); // TODO: max(a, b), shrink_to_fit() afterwards
loop {
use std::cmp::Ordering;
use Operation::*;
match (head_a, head_b) {
// we are done
(None, None) => {
break;
}
// deletion in A
(Some(Delete(i)), b) => {
changes.delete(i);
head_a = changes_a.next();
head_b = b;
}
// insertion in B
(a, Some(Insert(current))) => {
changes.insert(current);
head_a = a;
head_b = changes_b.next();
}
(None, _) | (_, None) => return unreachable!(),
(Some(Retain(i)), Some(Retain(j))) => match i.cmp(&j) {
Ordering::Less => {
changes.retain(i);
head_a = changes_a.next();
head_b = Some(Retain(j - i));
}
Ordering::Equal => {
changes.retain(i);
head_a = changes_a.next();
head_b = changes_b.next();
}
Ordering::Greater => {
changes.retain(j);
head_a = Some(Retain(i - j));
head_b = changes_b.next();
}
},
(Some(Insert(mut s)), Some(Delete(j))) => {
let len = s.chars().count();
match len.cmp(&j) {
Ordering::Less => {
head_a = changes_a.next();
head_b = Some(Delete(j - len));
}
Ordering::Equal => {
head_a = changes_a.next();
head_b = changes_b.next();
}
Ordering::Greater => {
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(len - j).unwrap();
s.pop_front(s.len() as u32 - pos as u32);
head_a = Some(Insert(s));
head_b = changes_b.next();
}
}
}
(Some(Insert(mut s)), Some(Retain(j))) => {
let len = s.chars().count();
match len.cmp(&j) {
Ordering::Less => {
changes.insert(s);
head_a = changes_a.next();
head_b = Some(Retain(j - len));
}
Ordering::Equal => {
changes.insert(s);
head_a = changes_a.next();
head_b = changes_b.next();
}
Ordering::Greater => {
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(j).unwrap();
let pos = pos as u32;
changes.insert(s.subtendril(0, pos));
head_a = Some(Insert(s.subtendril(pos, s.len() as u32 - pos)));
head_b = changes_b.next();
}
}
}
(Some(Retain(i)), Some(Delete(j))) => match i.cmp(&j) {
Ordering::Less => {
changes.delete(i);
head_a = changes_a.next();
head_b = Some(Delete(j - i));
}
Ordering::Equal => {
changes.delete(j);
head_a = changes_a.next();
head_b = changes_b.next();
}
Ordering::Greater => {
changes.delete(j);
head_a = Some(Retain(i - j));
head_b = changes_b.next();
}
},
};
}
// starting len should still equal original starting len
debug_assert!(changes.len == self.len);
changes
}
/// Given another change set starting in the same document, maps this
/// change set over the other, producing a new change set that can be
/// applied to the document produced by applying `other`. When
/// `before` is `true`, order changes as if `this` comes before
/// `other`, otherwise (the default) treat `other` as coming first.
///
/// Given two changes `A` and `B`, `A.compose(B.map(A))` and
/// `B.compose(A.map(B, true))` will produce the same document. This
/// provides a basic form of [operational
/// transformation](https://en.wikipedia.org/wiki/Operational_transformation),
/// and can be used for collaborative editing.
pub fn map(self, _other: Self) -> Self {
unimplemented!()
}
/// Returns a new changeset that reverts this one. Useful for `undo` implementation.
/// The document parameter expects the original document before this change was applied.
pub fn invert(&self, original_doc: &Rope) -> Self {
assert!(original_doc.len_chars() == self.len);
let mut changes = Self::with_capacity(self.changes.len());
let mut pos = 0;
let mut len = 0;
for change in &self.changes {
use Operation::*;
match change {
Retain(n) => {
changes.retain(*n);
pos += n;
}
Delete(n) => {
let text = Cow::from(original_doc.slice(pos..pos + *n));
changes.insert(Tendril::from_slice(&text));
pos += n;
}
Insert(s) => {
let chars = s.chars().count();
changes.delete(chars);
}
}
}
changes
}
/// Returns true if applied successfully.
pub fn apply(&self, text: &mut Rope) -> bool {
if text.len_chars() != self.len {
return false;
}
let mut pos = 0;
for change in &self.changes {
use Operation::*;
match change {
Retain(n) => {
pos += n;
}
Delete(n) => {
text.remove(pos..pos + *n);
// pos += n;
}
Insert(s) => {
text.insert(pos, s);
pos += s.chars().count();
}
}
}
true
}
/// `true` when the set is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
/// Map a position through the changes.
///
/// `assoc` indicates which size to associate the position with. `Before` will keep the
/// position close to the character before, and will place it before insertions over that
/// range, or at that point. `After` will move it forward, placing it at the end of such
/// insertions.
pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
use Operation::*;
let mut old_pos = 0;
let mut new_pos = 0;
let mut iter = self.changes.iter().peekable();
while let Some(change) = iter.next() {
let len = match change {
Delete(i) | Retain(i) => *i,
Insert(_) => 0,
};
let mut old_end = old_pos + len;
match change {
Retain(_) => {
if old_end > pos {
return new_pos + (pos - old_pos);
}
new_pos += len;
}
Delete(_) => {
// in range
if old_end > pos {
return new_pos;
}
}
Insert(s) => {
let ins = s.chars().count();
// a subsequent delete means a replace, consume it
if let Some(Delete(len)) = iter.peek() {
iter.next();
old_end = old_pos + len;
// in range of replaced text
if old_end > pos {
// at point or tracking before
if pos == old_pos || assoc == Assoc::Before {
return new_pos;
} else {
// place to end of insert
return new_pos + ins;
}
}
} else {
// at insert point
if old_pos == pos {
// return position before inserted text
if assoc == Assoc::Before {
return new_pos;
} else {
// after text
return new_pos + ins;
}
}
}
new_pos += ins;
}
}
old_pos = old_end;
}
if pos > old_pos {
panic!(
"Position {} is out of range for changeset len {}!",
pos, old_pos
)
}
new_pos
}
pub fn changes_iter(&self) -> ChangeIterator {
ChangeIterator::new(self)
}
}
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
#[derive(Debug, Default, Clone)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
// effects, annotations
// scroll_into_view
}
impl Transaction {
/// Create a new, empty transaction.
pub fn new(doc: &Rope) -> Self {
Self {
changes: ChangeSet::new(doc),
selection: None,
}
}
/// Changes made to the buffer.
pub fn changes(&self) -> &ChangeSet {
&self.changes
}
/// When set, explicitly updates the selection.
pub fn selection(&self) -> Option<&Selection> {
self.selection.as_ref()
}
/// Returns true if applied successfully.
pub fn apply(&self, doc: &mut Rope) -> bool {
if !self.changes.is_empty() {
// apply changes to the document
if !self.changes.apply(doc) {
return false;
}
}
true
}
/// Generate a transaction that reverts this one.
pub fn invert(&self, original: &Rope) -> Self {
let changes = self.changes.invert(original);
Self {
changes,
selection: None,
}
}
pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = Some(selection);
self
}
/// Generate a transaction from a set of changes.
pub fn change<I>(doc: &Rope, changes: I) -> Self
where
I: IntoIterator<Item = Change> + ExactSizeIterator,
{
let len = doc.len_chars();
let mut changeset = ChangeSet::with_capacity(2 * changes.len() + 1); // rough estimate
// TODO: verify ranges are ordered and not overlapping or change will panic.
// TODO: test for (pos, pos, None) to factor out as nothing
let mut last = 0;
for (from, to, tendril) in changes {
// Retain from last "to" to current "from"
changeset.retain(from - last);
let span = to - from;
match tendril {
Some(text) => {
changeset.insert(text);
changeset.delete(span);
}
None => changeset.delete(span),
}
last = to;
}
changeset.retain(len - last);
Self::from(changeset)
}
/// Generate a transaction with a change per selection range.
pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where
F: FnMut(&Range) -> Change,
{
Self::change(doc, selection.iter().map(f))
}
/// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| {
(range.head, range.head, Some(text.clone()))
})
}
pub fn changes_iter(&self) -> ChangeIterator {
self.changes.changes_iter()
}
}
impl From<ChangeSet> for Transaction {
fn from(changes: ChangeSet) -> Self {
Self {
changes,
selection: None,
}
}
}
pub struct ChangeIterator<'a> {
iter: std::iter::Peekable<std::slice::Iter<'a, Operation>>,
pos: usize,
}
impl<'a> ChangeIterator<'a> {
fn new(changeset: &'a ChangeSet) -> Self {
let iter = changeset.changes.iter().peekable();
Self { iter, pos: 0 }
}
}
impl<'a> Iterator for ChangeIterator<'a> {
type Item = Change;
fn next(&mut self) -> Option<Self::Item> {
use Operation::*;
loop {
match self.iter.next()? {
Retain(len) => {
self.pos += len;
}
Delete(len) => {
let start = self.pos;
self.pos += len;
return Some((start, self.pos, None));
}
Insert(s) => {
let start = self.pos;
// a subsequent delete means a replace, consume it
if let Some(Delete(len)) = self.iter.peek() {
self.iter.next();
self.pos += len;
return Some((start, self.pos, Some(s.clone())));
} else {
return Some((start, start, Some(s.clone())));
}
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn composition() {
use Operation::*;
let a = ChangeSet {
changes: vec![
Retain(5),
Insert(" test!".into()),
Retain(1),
Delete(2),
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");
// should probably return cloned text
let composed = a.compose(b);
assert_eq!(composed.len, 8);
assert!(composed.apply(&mut text));
assert_eq!(text, "world! abc");
}
#[test]
fn invert() {
use Operation::*;
let changes = ChangeSet {
changes: vec![Retain(4), Insert("test".into()), Delete(5), Retain(3)],
len: 12,
len_after: 11,
};
let doc = Rope::from("世界3 hello xz");
let revert = changes.invert(&doc);
let mut doc2 = doc.clone();
changes.apply(&mut doc2);
// a revert is different
assert_ne!(changes, revert);
assert_ne!(doc, doc2);
// but inverting a revert will give us the original
assert_eq!(changes, revert.invert(&doc2));
// applying a revert gives us back the original
revert.apply(&mut doc2);
assert_eq!(doc, doc2);
}
#[test]
fn map_pos() {
use Operation::*;
// maps inserts
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
assert_eq!(cs.map_pos(4, Assoc::Before), 4); // at insert, track before
assert_eq!(cs.map_pos(4, Assoc::After), 6); // at insert, track after
assert_eq!(cs.map_pos(5, Assoc::Before), 7); // after insert region
// maps deletes
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
assert_eq!(cs.map_pos(5, Assoc::Before), 4); // inside a delete
assert_eq!(cs.map_pos(5, Assoc::After), 4); // inside a delete
// TODO: delete tracking
// stays inbetween replacements
let cs = ChangeSet {
changes: vec![
Insert("ab".into()),
Delete(2),
Insert("cd".into()),
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);
}
#[test]
fn transaction_change() {
let mut state = State::new("hello world!\ntest 123".into());
let transaction = Transaction::change(
&state.doc,
// (1, 1, None) is a useless 0-width delete
vec![(1, 1, None), (6, 11, Some("void".into())), (12, 17, None)].into_iter(),
);
transaction.apply(&mut state.doc);
assert_eq!(state.doc, Rope::from_str("hello void! 123"));
}
#[test]
fn changes_iter() {
let mut state = State::new("hello world!\ntest 123".into());
let changes = vec![(6, 11, Some("void".into())), (12, 17, None)];
let transaction = Transaction::change(&state.doc, changes.clone().into_iter());
assert_eq!(transaction.changes_iter().collect::<Vec<_>>(), changes);
}
#[test]
fn optimized_composition() {
let mut state = State::new("".into());
let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('h'));
t1.apply(&mut state.doc);
state.selection = state.selection.clone().map(t1.changes());
let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('e'));
t2.apply(&mut state.doc);
state.selection = state.selection.clone().map(t2.changes());
let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
t3.apply(&mut state.doc);
state.selection = state.selection.clone().map(t3.changes());
let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
t4.apply(&mut state.doc);
state.selection = state.selection.clone().map(t4.changes());
let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('o'));
t5.apply(&mut state.doc);
state.selection = state.selection.clone().map(t5.changes());
assert_eq!(state.doc, Rope::from_str("hello"));
// changesets as follows:
// h
// retain 1, e
// retain 2, l
let mut changes = t1
.changes
.compose(t2.changes)
.compose(t3.changes)
.compose(t4.changes)
.compose(t5.changes);
use Operation::*;
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())]);
}
}