2020-09-07 12:08:28 +04:00
|
|
|
use crate::{Rope, Selection, SelectionRange, State, Tendril};
|
2020-05-26 13:11:11 +04:00
|
|
|
|
2020-09-13 10:48:15 +04:00
|
|
|
/// (from, to, replacement)
|
2020-09-13 14:18:05 +04:00
|
|
|
pub type Change = (usize, usize, Option<Tendril>);
|
2020-09-13 10:48:15 +04:00
|
|
|
|
2020-05-26 13:11:11 +04:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
2020-09-17 09:57:49 +04:00
|
|
|
pub(crate) enum Operation {
|
2020-05-26 13:11:11 +04:00
|
|
|
/// Move cursor by n characters.
|
|
|
|
Retain(usize),
|
|
|
|
/// Delete n characters.
|
|
|
|
Delete(usize),
|
|
|
|
/// Insert text at position.
|
|
|
|
Insert(Tendril),
|
2020-05-25 08:02:21 +04:00
|
|
|
}
|
|
|
|
|
2020-05-28 09:46:00 +04:00
|
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
|
|
pub enum Assoc {
|
|
|
|
Before,
|
|
|
|
After,
|
|
|
|
}
|
|
|
|
|
2020-05-25 08:02:21 +04:00
|
|
|
// ChangeSpec = Change | ChangeSet | Vec<Change>
|
2020-05-26 13:11:11 +04:00
|
|
|
#[derive(Debug)]
|
2020-05-25 08:02:21 +04:00
|
|
|
pub struct ChangeSet {
|
2020-09-17 09:57:49 +04:00
|
|
|
pub(crate) changes: Vec<Operation>,
|
2020-05-26 13:11:11 +04:00
|
|
|
/// The required document length. Will refuse to apply changes unless it matches.
|
|
|
|
len: usize,
|
2020-05-25 08:02:21 +04:00
|
|
|
}
|
2020-05-26 13:11:11 +04:00
|
|
|
|
|
|
|
impl ChangeSet {
|
2020-05-28 09:45:44 +04:00
|
|
|
#[must_use]
|
2020-09-07 06:28:52 +04:00
|
|
|
pub fn new(doc: &Rope) -> Self {
|
|
|
|
let len = doc.len_chars();
|
2020-05-26 13:11:11 +04:00
|
|
|
Self {
|
2020-09-13 14:18:05 +04:00
|
|
|
changes: vec![Operation::Retain(len)],
|
2020-05-26 13:11:11 +04:00
|
|
|
len,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: from iter
|
|
|
|
|
|
|
|
/// 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: ChangeSet) -> Result<Self, ()> {
|
|
|
|
if self.len != other.len {
|
|
|
|
// length mismatch
|
|
|
|
return Err(());
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
2020-09-13 14:18:05 +04:00
|
|
|
let mut changes: Vec<Operation> = Vec::with_capacity(len); // TODO: max(a, b), shrink_to_fit() afterwards
|
2020-05-26 13:11:11 +04:00
|
|
|
|
|
|
|
loop {
|
|
|
|
use std::cmp::Ordering;
|
2020-09-13 14:18:05 +04:00
|
|
|
use Operation::*;
|
2020-05-26 13:11:11 +04:00
|
|
|
match (head_a, head_b) {
|
|
|
|
// we are done
|
|
|
|
(None, None) => {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// deletion in A
|
|
|
|
(Some(change @ Delete(..)), b) => {
|
|
|
|
changes.push(change);
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = b;
|
|
|
|
}
|
|
|
|
// insertion in B
|
|
|
|
(a, Some(change @ Insert(..))) => {
|
|
|
|
changes.push(change);
|
|
|
|
head_a = a;
|
|
|
|
head_b = changes_b.next();
|
|
|
|
}
|
2020-05-28 09:45:44 +04:00
|
|
|
(None, _) | (_, None) => return Err(()),
|
2020-05-26 13:11:11 +04:00
|
|
|
(Some(Retain(i)), Some(Retain(j))) => match i.cmp(&j) {
|
|
|
|
Ordering::Less => {
|
|
|
|
changes.push(Retain(i));
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = Some(Retain(j - i));
|
|
|
|
}
|
|
|
|
Ordering::Equal => {
|
|
|
|
changes.push(Retain(i));
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = changes_b.next();
|
|
|
|
}
|
|
|
|
Ordering::Greater => {
|
|
|
|
changes.push(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();
|
|
|
|
// calculate the difference
|
|
|
|
let to_drop = s.len() - pos;
|
|
|
|
s.pop_back(to_drop 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.push(Insert(s));
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = Some(Retain(j - len));
|
|
|
|
}
|
|
|
|
Ordering::Equal => {
|
|
|
|
changes.push(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();
|
|
|
|
// calculate the difference
|
|
|
|
let to_drop = s.len() - pos;
|
|
|
|
s.pop_back(to_drop as u32);
|
|
|
|
head_a = Some(Insert(s));
|
|
|
|
head_b = changes_b.next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
(Some(Retain(i)), Some(Delete(j))) => match i.cmp(&j) {
|
|
|
|
Ordering::Less => {
|
|
|
|
changes.push(Delete(i));
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = Some(Delete(j - i));
|
|
|
|
}
|
|
|
|
Ordering::Equal => {
|
|
|
|
changes.push(Delete(j));
|
|
|
|
head_a = changes_a.next();
|
|
|
|
head_b = changes_b.next();
|
|
|
|
}
|
|
|
|
Ordering::Greater => {
|
|
|
|
changes.push(Delete(j));
|
|
|
|
head_a = Some(Retain(i - j));
|
|
|
|
head_b = changes_b.next();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
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.
|
2020-06-02 05:57:01 +04:00
|
|
|
pub fn map(self, _other: Self) -> Self {
|
2020-05-26 13:11:11 +04:00
|
|
|
unimplemented!()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a new changeset that reverts this one. Useful for `undo` implementation.
|
|
|
|
pub fn invert(self) -> Self {
|
|
|
|
unimplemented!()
|
|
|
|
}
|
|
|
|
|
2020-09-07 12:08:28 +04:00
|
|
|
/// Returns true if applied successfully.
|
|
|
|
pub fn apply(&self, text: &mut Rope) -> bool {
|
|
|
|
if text.len_chars() != self.len {
|
|
|
|
return false;
|
|
|
|
}
|
2020-05-26 13:11:11 +04:00
|
|
|
|
|
|
|
let mut pos = 0;
|
|
|
|
|
2020-05-28 09:45:44 +04:00
|
|
|
for change in &self.changes {
|
2020-09-13 14:18:05 +04:00
|
|
|
use Operation::*;
|
2020-05-26 13:11:11 +04:00
|
|
|
match change {
|
|
|
|
Retain(n) => {
|
|
|
|
pos += n;
|
|
|
|
}
|
|
|
|
Delete(n) => {
|
|
|
|
text.remove(pos..pos + *n);
|
|
|
|
// pos += n;
|
|
|
|
}
|
|
|
|
Insert(s) => {
|
|
|
|
text.insert(pos, s);
|
|
|
|
pos += s.len();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-07 12:08:28 +04:00
|
|
|
true
|
2020-05-26 13:11:11 +04:00
|
|
|
}
|
|
|
|
|
2020-05-28 09:59:50 +04:00
|
|
|
/// `true` when the set is empty.
|
|
|
|
#[inline]
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
let len = self.changes.len();
|
2020-09-13 14:18:05 +04:00
|
|
|
len == 0 || (len == 1 && self.changes[0] == Operation::Retain(self.len))
|
2020-05-28 09:59:50 +04:00
|
|
|
}
|
|
|
|
|
2020-05-28 09:46:00 +04:00
|
|
|
/// 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 {
|
2020-09-13 14:18:05 +04:00
|
|
|
use Operation::*;
|
2020-05-28 09:46:00 +04:00
|
|
|
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 old_end = old_pos + len;
|
|
|
|
|
|
|
|
match change {
|
2020-09-19 06:55:15 +04:00
|
|
|
Retain(_) => {
|
2020-05-28 09:46:00 +04:00
|
|
|
if old_end > pos {
|
|
|
|
return new_pos + (pos - old_pos);
|
|
|
|
}
|
|
|
|
new_pos += len;
|
|
|
|
}
|
2020-09-19 06:55:15 +04:00
|
|
|
Delete(_) => {
|
2020-05-28 09:46:00 +04:00
|
|
|
// a subsequent ins means a replace, consume it
|
|
|
|
let ins = if let Some(Insert(s)) = iter.peek() {
|
|
|
|
iter.next();
|
|
|
|
s.chars().count()
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
|
|
|
|
|
|
|
// in range
|
|
|
|
if old_end > pos {
|
|
|
|
// at point or tracking before
|
|
|
|
if pos == old_pos || assoc == Assoc::Before {
|
|
|
|
return new_pos;
|
|
|
|
} else {
|
|
|
|
// place to end of delete
|
|
|
|
return new_pos + ins;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
new_pos += ins;
|
|
|
|
}
|
|
|
|
Insert(s) => {
|
|
|
|
let ins = s.chars().count();
|
|
|
|
// 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
|
|
|
|
}
|
2020-05-26 13:11:11 +04:00
|
|
|
}
|
|
|
|
|
2020-05-25 08:02:21 +04:00
|
|
|
// trait Transaction
|
|
|
|
// trait StrictTransaction
|
2020-05-26 13:11:11 +04:00
|
|
|
|
|
|
|
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
|
|
|
|
/// a single transaction.
|
|
|
|
pub struct Transaction {
|
|
|
|
/// Changes made to the buffer.
|
2020-09-17 09:57:49 +04:00
|
|
|
pub(crate) changes: ChangeSet,
|
2020-05-26 13:11:11 +04:00
|
|
|
/// When set, explicitly updates the selection.
|
|
|
|
selection: Option<Selection>,
|
|
|
|
// effects, annotations
|
|
|
|
// scroll_into_view
|
|
|
|
}
|
|
|
|
|
2020-09-07 12:08:28 +04:00
|
|
|
impl Transaction {
|
2020-09-13 15:04:16 +04:00
|
|
|
/// Create a new, empty transaction.
|
|
|
|
pub fn new(state: &mut State) -> Self {
|
|
|
|
Self {
|
|
|
|
changes: ChangeSet::new(&state.doc),
|
|
|
|
selection: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-07 12:08:28 +04:00
|
|
|
/// Returns true if applied successfully.
|
|
|
|
pub fn apply(&self, state: &mut State) -> bool {
|
|
|
|
// apply changes to the document
|
|
|
|
if !self.changes.apply(&mut state.doc) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// update the selection: either take the selection specified in the transaction, or map the
|
|
|
|
// current selection through changes.
|
|
|
|
state.selection = self
|
|
|
|
.selection
|
|
|
|
.clone()
|
|
|
|
.unwrap_or_else(|| state.selection.clone().map(&self.changes));
|
|
|
|
|
2020-09-17 09:57:49 +04:00
|
|
|
// TODO: no unwrap
|
|
|
|
state
|
|
|
|
.syntax
|
|
|
|
.as_mut()
|
|
|
|
.unwrap()
|
|
|
|
.update(&state.doc, &self.changes)
|
|
|
|
.unwrap();
|
|
|
|
|
2020-09-07 12:08:28 +04:00
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2020-09-13 14:51:42 +04:00
|
|
|
pub fn with_selection(mut self, selection: Selection) -> Self {
|
|
|
|
self.selection = Some(selection);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-09-13 10:48:15 +04:00
|
|
|
/// Generate a transaction from a set of changes.
|
2020-09-13 18:12:14 +04:00
|
|
|
pub fn change<I>(state: &State, changes: I) -> Self
|
|
|
|
where
|
|
|
|
I: IntoIterator<Item = Change> + ExactSizeIterator,
|
|
|
|
{
|
2020-09-07 12:08:28 +04:00
|
|
|
let len = state.doc.len_chars();
|
2020-09-13 10:48:15 +04:00
|
|
|
let mut acc = Vec::with_capacity(2 * changes.len() + 1);
|
2020-09-13 06:32:37 +04:00
|
|
|
|
|
|
|
// TODO: verify ranges are ordered and not overlapping.
|
2020-09-07 12:08:28 +04:00
|
|
|
|
2020-09-13 06:32:37 +04:00
|
|
|
let mut last = 0;
|
|
|
|
for (from, to, tendril) in changes {
|
|
|
|
// Retain from last "to" to current "from"
|
2020-09-13 14:18:05 +04:00
|
|
|
acc.push(Operation::Retain(from - last));
|
2020-09-17 09:57:49 +04:00
|
|
|
let span = to - from;
|
2020-09-13 06:32:37 +04:00
|
|
|
match tendril {
|
2020-09-17 09:57:49 +04:00
|
|
|
Some(text) => {
|
|
|
|
if span > 0 {
|
|
|
|
acc.push(Operation::Delete(span));
|
|
|
|
}
|
|
|
|
acc.push(Operation::Insert(text));
|
|
|
|
}
|
|
|
|
None => acc.push(Operation::Delete(span)),
|
2020-09-13 06:32:37 +04:00
|
|
|
}
|
|
|
|
last = to;
|
2020-09-07 12:08:28 +04:00
|
|
|
}
|
2020-09-13 14:18:05 +04:00
|
|
|
acc.push(Operation::Retain(len - last));
|
2020-09-07 12:08:28 +04:00
|
|
|
|
2020-09-13 06:32:37 +04:00
|
|
|
Self::from(ChangeSet { changes: acc, len })
|
2020-09-07 12:08:28 +04:00
|
|
|
}
|
|
|
|
|
2020-09-13 10:48:15 +04:00
|
|
|
/// Generate a transaction with a change per selection range.
|
|
|
|
pub fn change_by_selection<F>(state: &State, f: F) -> Self
|
|
|
|
where
|
2020-09-13 14:18:05 +04:00
|
|
|
F: Fn(&SelectionRange) -> Change,
|
2020-09-13 10:48:15 +04:00
|
|
|
{
|
2020-09-13 18:38:54 +04:00
|
|
|
Self::change(state, state.selection.ranges().iter().map(f))
|
2020-09-13 10:48:15 +04:00
|
|
|
}
|
|
|
|
|
2020-09-13 06:32:37 +04:00
|
|
|
/// Insert text at each selection head.
|
|
|
|
pub fn insert(state: &State, text: Tendril) -> Self {
|
|
|
|
Self::change_by_selection(state, |range| (range.head, range.head, Some(text.clone())))
|
2020-09-07 12:08:28 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ChangeSet> for Transaction {
|
|
|
|
fn from(changes: ChangeSet) -> Self {
|
|
|
|
Self {
|
|
|
|
changes,
|
|
|
|
selection: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-28 12:47:35 +04:00
|
|
|
|
2020-05-26 13:11:11 +04:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn composition() {
|
2020-09-13 14:18:05 +04:00
|
|
|
use Operation::*;
|
2020-05-26 13:11:11 +04:00
|
|
|
|
|
|
|
let a = ChangeSet {
|
|
|
|
changes: vec![
|
|
|
|
Retain(5),
|
|
|
|
Insert("!".into()),
|
|
|
|
Retain(1),
|
|
|
|
Delete(2),
|
|
|
|
Insert("ab".into()),
|
|
|
|
],
|
|
|
|
len: 7,
|
|
|
|
};
|
|
|
|
|
|
|
|
let b = ChangeSet {
|
|
|
|
changes: vec![Delete(5), Insert("world".into()), Retain(4)],
|
|
|
|
len: 7,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut text = Rope::from("hello xz");
|
|
|
|
|
|
|
|
// should probably return cloned text
|
|
|
|
a.compose(b).unwrap().apply(&mut text);
|
|
|
|
|
2020-05-28 09:46:00 +04:00
|
|
|
// unimplemented!("{:?}", text);
|
|
|
|
// TODO: assert
|
2020-05-26 13:11:11 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-28 09:46:00 +04:00
|
|
|
fn map_pos() {
|
2020-09-13 14:18:05 +04:00
|
|
|
use Operation::*;
|
2020-05-28 09:46:00 +04:00
|
|
|
|
|
|
|
// maps inserts
|
|
|
|
let cs = ChangeSet {
|
|
|
|
changes: vec![Retain(4), Insert("!!".into()), Retain(4)],
|
|
|
|
len: 8,
|
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
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![
|
|
|
|
Delete(2),
|
|
|
|
Insert("ab".into()),
|
|
|
|
Delete(2),
|
|
|
|
Insert("cd".into()),
|
|
|
|
],
|
|
|
|
len: 4,
|
|
|
|
};
|
|
|
|
assert_eq!(cs.map_pos(2, Assoc::Before), 2);
|
|
|
|
assert_eq!(cs.map_pos(2, Assoc::After), 2);
|
|
|
|
}
|
2020-09-17 09:57:49 +04:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn transaction_change() {
|
|
|
|
let mut state = State::new("hello world!\ntest 123".into());
|
|
|
|
let transaction = Transaction::change(
|
|
|
|
&state,
|
|
|
|
vec![(6, 11, Some("void".into())), (12, 17, None)].into_iter(),
|
|
|
|
);
|
|
|
|
transaction.apply(&mut state);
|
|
|
|
assert_eq!(state.doc, Rope::from_str("hello void! 123"));
|
|
|
|
}
|
2020-05-26 13:11:11 +04:00
|
|
|
}
|