OT: changeset: Implement compose and apply.
This commit is contained in:
parent
44ff4d3c1f
commit
23109f1512
5 changed files with 330 additions and 22 deletions
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -172,6 +172,16 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futf"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
|
||||||
|
dependencies = [
|
||||||
|
"mac",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
|
@ -199,6 +209,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ropey",
|
"ropey",
|
||||||
"smallvec 1.4.0",
|
"smallvec 1.4.0",
|
||||||
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -232,6 +243,12 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-uninit"
|
name = "maybe-uninit"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -250,6 +267,12 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "new_debug_unreachable"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "5.1.1"
|
version = "5.1.1"
|
||||||
|
@ -458,10 +481,9 @@ checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ropey"
|
name = "ropey"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/cessen/ropey#083c34949274ef9800267e6bc64b76a45e401807"
|
||||||
checksum = "ba326a8508a4add47e7b260333aa2d896213a5f3572fde11ed6e9130241b7f71"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"smallvec 0.6.13",
|
"smallvec 1.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -554,6 +576,16 @@ dependencies = [
|
||||||
"unicode-xid 0.2.0",
|
"unicode-xid 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "git+https://github.com/servo/tendril#08f7f292ab82c00e9cf491b5918a76e53af92c8c"
|
||||||
|
dependencies = [
|
||||||
|
"futf",
|
||||||
|
"mac",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminfo"
|
name = "terminfo"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -639,6 +671,12 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
|
@ -7,7 +7,9 @@ edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ropey = "1.1.0"
|
# ropey = "1.1.0"
|
||||||
|
ropey = { git = "https://github.com/cessen/ropey" }
|
||||||
anyhow = "1.0.31"
|
anyhow = "1.0.31"
|
||||||
smallvec = "1.4.0"
|
smallvec = "1.4.0"
|
||||||
|
tendril = { git = "https://github.com/servo/tendril" }
|
||||||
# slab = "0.4.2"
|
# slab = "0.4.2"
|
||||||
|
|
|
@ -10,4 +10,4 @@ pub use selection::Selection;
|
||||||
|
|
||||||
pub use state::State;
|
pub use state::State;
|
||||||
|
|
||||||
pub use transaction::{Change, Transaction};
|
pub use transaction::{Change, ChangeSet, Transaction};
|
||||||
|
|
|
@ -83,7 +83,7 @@ impl Range {
|
||||||
/// A selection consists of one or more selection ranges.
|
/// A selection consists of one or more selection ranges.
|
||||||
pub struct Selection {
|
pub struct Selection {
|
||||||
// TODO: decide how many ranges to inline SmallVec<[Range; 1]>
|
// TODO: decide how many ranges to inline SmallVec<[Range; 1]>
|
||||||
ranges: Vec<Range>,
|
ranges: SmallVec<[Range; 1]>,
|
||||||
primary_index: usize,
|
primary_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ impl Selection {
|
||||||
self
|
self
|
||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
ranges: vec![self.ranges[self.primary_index]],
|
ranges: smallvec![self.ranges[self.primary_index]],
|
||||||
primary_index: 0,
|
primary_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,18 +112,18 @@ impl Selection {
|
||||||
/// Constructs a selection holding a single range.
|
/// Constructs a selection holding a single range.
|
||||||
pub fn single(anchor: usize, head: usize) -> Self {
|
pub fn single(anchor: usize, head: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ranges: vec![Range { anchor, head }],
|
ranges: smallvec![Range { anchor, head }],
|
||||||
primary_index: 0,
|
primary_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(ranges: Vec<Range>, primary_index: usize) -> Self {
|
pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
|
||||||
fn normalize(mut ranges: Vec<Range>, primary_index: usize) -> Selection {
|
fn normalize(mut ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Selection {
|
||||||
let primary = ranges[primary_index];
|
let primary = ranges[primary_index];
|
||||||
ranges.sort_unstable_by_key(|range| range.from());
|
ranges.sort_unstable_by_key(|range| range.from());
|
||||||
let mut primary_index = ranges.iter().position(|&range| range == primary).unwrap();
|
let mut primary_index = ranges.iter().position(|&range| range == primary).unwrap();
|
||||||
|
|
||||||
let mut result: Vec<Range> = Vec::new();
|
let mut result: SmallVec<[Range; 1]> = SmallVec::new();
|
||||||
|
|
||||||
// TODO: we could do with one vec by removing elements as we mutate
|
// TODO: we could do with one vec by removing elements as we mutate
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_normalizes_and_merges() {
|
fn test_create_normalizes_and_merges() {
|
||||||
let sel = Selection::new(
|
let sel = Selection::new(
|
||||||
vec![
|
smallvec![
|
||||||
Range::new(10, 12),
|
Range::new(10, 12),
|
||||||
Range::new(6, 7),
|
Range::new(6, 7),
|
||||||
Range::new(4, 5),
|
Range::new(4, 5),
|
||||||
|
@ -200,7 +200,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_merges_adjacent_points() {
|
fn test_create_merges_adjacent_points() {
|
||||||
let sel = Selection::new(
|
let sel = Selection::new(
|
||||||
vec![
|
smallvec![
|
||||||
Range::new(10, 12),
|
Range::new(10, 12),
|
||||||
Range::new(12, 12),
|
Range::new(12, 12),
|
||||||
Range::new(12, 12),
|
Range::new(12, 12),
|
||||||
|
|
|
@ -1,25 +1,293 @@
|
||||||
pub struct Change {
|
// pub struct Change {
|
||||||
from: usize,
|
// from: usize,
|
||||||
to: usize,
|
// to: usize,
|
||||||
insert: Option<String>,
|
// insert: Option<String>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 40 bytes (8 + 24 + 8) -> strings are really big 24 as String, 16 as &str
|
||||||
|
// pub struct Change {
|
||||||
|
// /// old extent
|
||||||
|
// old_extent: usize,
|
||||||
|
// /// inserted text, new extent equal to insert length
|
||||||
|
// insert: Option<String>,
|
||||||
|
// /// distance from the previous change
|
||||||
|
// distance: usize,
|
||||||
|
// }
|
||||||
|
|
||||||
|
use crate::{Buffer, Selection};
|
||||||
|
|
||||||
|
use ropey::Rope;
|
||||||
|
use tendril::StrTendril as Tendril;
|
||||||
|
|
||||||
|
// TODO: divided into three different operations, I sort of like having just
|
||||||
|
// Splice { extent, Option<text>, distance } better.
|
||||||
|
// insert: Splice { extent: 0, text: Some("a"), distance: 2 }
|
||||||
|
// delete: Splice { extent: 2, text: None, distance: 2 }
|
||||||
|
// replace: Splice { extent: 2, text: Some("abc"), distance: 2 }
|
||||||
|
// unchanged?: Splice { extent: 0, text: None, distance: 2 }
|
||||||
|
// harder to compose though.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Change {
|
||||||
|
/// Move cursor by n characters.
|
||||||
|
Retain(usize),
|
||||||
|
/// Delete n characters.
|
||||||
|
Delete(usize),
|
||||||
|
/// Insert text at position.
|
||||||
|
Insert(Tendril),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Change {
|
impl Change {
|
||||||
pub fn new(from: usize, to: usize, insert: Option<String>) {
|
pub fn new(from: usize, to: usize, insert: Option<Tendril>) {
|
||||||
// old_extent, new_extent, insert
|
// old_extent, new_extent, insert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Transaction {}
|
|
||||||
|
|
||||||
// ChangeSpec = Change | ChangeSet | Vec<Change>
|
// ChangeSpec = Change | ChangeSet | Vec<Change>
|
||||||
// ChangeDesc as a ChangeSet without text: can't be applied, cheaper to store.
|
// ChangeDesc as a ChangeSet without text: can't be applied, cheaper to store.
|
||||||
// ChangeSet = ChangeDesc with Text
|
// ChangeSet = ChangeDesc with Text
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ChangeSet {
|
pub struct ChangeSet {
|
||||||
// basically Vec<ChangeDesc> where ChangeDesc = (current len, replacement len?)
|
// basically Vec<ChangeDesc> where ChangeDesc = (current len, replacement len?)
|
||||||
// (0, n>0) for insertion, (n>0, 0) for deletion, (>0, >0) for replacement
|
// (0, n>0) for insertion, (n>0, 0) for deletion, (>0, >0) for replacement
|
||||||
sections: Vec<(usize, isize)>,
|
// sections: Vec<(usize, isize)>,
|
||||||
|
changes: Vec<Change>,
|
||||||
|
/// The required document length. Will refuse to apply changes unless it matches.
|
||||||
|
len: usize,
|
||||||
}
|
}
|
||||||
//
|
|
||||||
|
impl ChangeSet {
|
||||||
|
pub fn new(buf: &Buffer) -> Self {
|
||||||
|
let len = buf.contents.len_chars();
|
||||||
|
Self {
|
||||||
|
changes: vec![Change::Retain(len)],
|
||||||
|
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();
|
||||||
|
|
||||||
|
let mut changes: Vec<Change> = Vec::with_capacity(len); // TODO: max(a, b), shrink_to_fit() afterwards
|
||||||
|
|
||||||
|
loop {
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use Change::*;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
(None, _) => return Err(()),
|
||||||
|
(_, None) => return Err(()),
|
||||||
|
(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.
|
||||||
|
pub fn map(self, other: Self) -> Self {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new changeset that reverts this one. Useful for `undo` implementation.
|
||||||
|
pub fn invert(self) -> Self {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, text: &mut Rope) {
|
||||||
|
// TODO: validate text.chars() == self.len
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
for change in self.changes.iter() {
|
||||||
|
use Change::*;
|
||||||
|
match change {
|
||||||
|
Retain(n) => {
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
Delete(n) => {
|
||||||
|
text.remove(pos..pos + *n);
|
||||||
|
// pos += n;
|
||||||
|
}
|
||||||
|
Insert(s) => {
|
||||||
|
text.insert(pos, s);
|
||||||
|
pos += s.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iter over changes
|
||||||
|
}
|
||||||
|
|
||||||
// trait Transaction
|
// trait Transaction
|
||||||
// trait StrictTransaction
|
// trait StrictTransaction
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
changes: ChangeSet,
|
||||||
|
/// When set, explicitly updates the selection.
|
||||||
|
selection: Option<Selection>,
|
||||||
|
// effects, annotations
|
||||||
|
// scroll_into_view
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn composition() {
|
||||||
|
use Change::*;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
unimplemented!("{:?}", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map() {}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue