Skip to main content

indigo_kernel/
edit.rs

1//! Operational transformation (OT) algorithm.
2//!
3//! <https://en.wikipedia.org/wiki/Operational_transformation>
4
5use ropey::Rope;
6use std::cmp::min;
7
8#[cfg_attr(test, derive(hegel::PrettyPrintable))]
9#[derive(Clone, Debug, Default, Eq, PartialEq)]
10pub struct Edit {
11    ops: Vec<Op>,
12    length_before: usize,
13    length_after: usize,
14}
15
16impl Edit {
17    #[must_use]
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    #[must_use]
23    pub fn identity(rope: &Rope) -> Self {
24        let mut edit = Self::new();
25        edit.retain_rest(rope).unwrap();
26        edit
27    }
28
29    #[must_use]
30    pub fn ops(&self) -> &[Op] {
31        &self.ops
32    }
33
34    #[must_use]
35    pub fn is_empty(&self) -> bool {
36        self.ops.is_empty()
37    }
38
39    pub fn retain(&mut self, byte_length: usize) {
40        if byte_length == 0 {
41            return;
42        }
43
44        self.length_before += byte_length;
45        self.length_after += byte_length;
46
47        if let Some(Op::Retain(last)) = self.ops.last_mut() {
48            *last += byte_length;
49        } else {
50            self.ops.push(Op::Retain(byte_length));
51        }
52    }
53
54    pub fn retain_rest(&mut self, rope: &Rope) -> Result<(), Error> {
55        if self.length_before > rope.len() {
56            return Err(Error::LengthMismatch {
57                expected: self.length_before,
58                actual: rope.len(),
59            });
60        }
61
62        self.retain(rope.len() - self.length_before);
63
64        Ok(())
65    }
66
67    pub fn delete(&mut self, text: &str) {
68        if text.is_empty() {
69            return;
70        }
71
72        self.length_before += text.len();
73
74        // Note [Canonical form]
75        let insert = match self.ops.pop() {
76            Some(Op::Insert(insert)) => Some(insert),
77            Some(op) => {
78                self.ops.push(op);
79                None
80            }
81            None => None,
82        };
83
84        if let Some(Op::Delete(last)) = self.ops.last_mut() {
85            last.push_str(text);
86        } else {
87            self.ops.push(Op::Delete(text.to_owned()));
88        }
89
90        if let Some(insert) = insert {
91            self.ops.push(Op::Insert(insert));
92        }
93    }
94
95    pub fn insert(&mut self, text: &str) {
96        if text.is_empty() {
97            return;
98        }
99
100        self.length_after += text.len();
101
102        if let Some(Op::Insert(last)) = self.ops.last_mut() {
103            last.push_str(text);
104        } else {
105            self.ops.push(Op::Insert(text.to_owned()));
106        }
107    }
108
109    pub fn compose(&self, other: &Self) -> Result<Self, Error> {
110        if self.length_after != other.length_before {
111            return Err(Error::LengthMismatch {
112                expected: self.length_after,
113                actual: other.length_before,
114            });
115        }
116
117        let mut a = Cursor::new(self);
118        let mut b = Cursor::new(other);
119        let mut edit = Self::new();
120
121        loop {
122            match (a.peek(), b.peek()) {
123                // `self` deleted text `other` never saw.
124                (Some(Op::Delete(_)), _) => {
125                    let n = a.remaining();
126                    edit.delete(a.text(n));
127                    a.advance(n);
128                }
129                // `other` inserted text `self` never saw.
130                (_, Some(Op::Insert(_))) => {
131                    let n = b.remaining();
132                    edit.insert(b.text(n));
133                    b.advance(n);
134                }
135                (None, None) => break,
136                (Some(a_op), Some(b_op)) => {
137                    let n = min(a.remaining(), b.remaining());
138                    match (a_op, b_op) {
139                        (Op::Retain(_), Op::Retain(_)) => edit.retain(n),
140                        (Op::Retain(_), Op::Delete(_)) => edit.delete(b.text(n)),
141                        (Op::Insert(_), Op::Retain(_)) => edit.insert(a.text(n)),
142                        // `other` deleted text `self` inserted; the ops cancel out
143                        (Op::Insert(_), Op::Delete(_)) => debug_assert_eq!(a.text(n), b.text(n)),
144                        _ => unreachable!(),
145                    }
146                    a.advance(n);
147                    b.advance(n);
148                }
149                (None, Some(_)) | (Some(_), None) => unreachable!(),
150            }
151        }
152
153        Ok(edit)
154    }
155
156    #[must_use]
157    pub fn invert(&self) -> Self {
158        let mut edit = Self::new();
159        for op in &self.ops {
160            match op {
161                Op::Retain(length) => edit.retain(*length),
162                Op::Delete(text) => edit.insert(text),
163                Op::Insert(text) => edit.delete(text),
164            }
165        }
166        edit
167    }
168
169    pub fn rebase(&self, onto: &Self, bias: Bias) -> Result<Self, Error> {
170        if self.length_before != onto.length_before {
171            return Err(Error::LengthMismatch {
172                expected: self.length_before,
173                actual: onto.length_before,
174            });
175        }
176
177        let mut a = Cursor::new(self);
178        let mut b = Cursor::new(onto);
179        let mut edit = Self::new();
180
181        loop {
182            match (a.peek(), b.peek()) {
183                // Both inserted at the same position; `bias` decides whose text comes first.
184                // Note [Bias].
185                (Some(Op::Insert(_)), Some(Op::Insert(_))) => match bias {
186                    Bias::Backward => {
187                        let n = a.remaining();
188                        edit.insert(a.text(n));
189                        a.advance(n);
190                    }
191                    Bias::Forward => {
192                        let n = b.remaining();
193                        edit.retain(n);
194                        b.advance(n);
195                    }
196                },
197                // `self` inserted text `onto` never saw.
198                (Some(Op::Insert(_)), _) => {
199                    let n = a.remaining();
200                    edit.insert(a.text(n));
201                    a.advance(n);
202                }
203                // `onto` inserted text `self` never saw; skip over it.
204                (_, Some(Op::Insert(_))) => {
205                    let n = b.remaining();
206                    edit.retain(n);
207                    b.advance(n);
208                }
209                (None, None) => break,
210                (Some(a_op), Some(b_op)) => {
211                    let n = min(a.remaining(), b.remaining());
212                    match (a_op, b_op) {
213                        (Op::Retain(_), Op::Retain(_)) => edit.retain(n),
214                        // `onto` deleted this text; there's nothing left to retain
215                        (Op::Retain(_), Op::Delete(_)) => {}
216                        (Op::Delete(_), Op::Retain(_)) => edit.delete(a.text(n)),
217                        // `onto` already deleted this text
218                        (Op::Delete(_), Op::Delete(_)) => debug_assert_eq!(a.text(n), b.text(n)),
219                        _ => unreachable!(),
220                    }
221                    a.advance(n);
222                    b.advance(n);
223                }
224                (None, Some(_)) | (Some(_), None) => unreachable!(),
225            }
226        }
227
228        Ok(edit)
229    }
230
231    #[must_use]
232    pub fn map_position(&self, byte_index: usize, bias: Bias) -> usize {
233        let mut byte_indexes = [byte_index];
234        self.map_positions(&mut byte_indexes, bias);
235        byte_indexes[0]
236    }
237
238    /// Input must be within the edit's input length.
239    pub fn map_positions(&self, byte_indexes: &mut [usize], bias: Bias) {
240        let mut indexed: Vec<(usize, usize)> = byte_indexes.iter().copied().enumerate().collect();
241        indexed.sort_by_key(|&(_, index)| index);
242
243        let mut sorted: Vec<usize> = indexed.iter().map(|&(_, index)| index).collect();
244        self.map_positions_sorted(&mut sorted, bias);
245
246        for (sorted_index, &(original_index, _)) in indexed.iter().enumerate() {
247            byte_indexes[original_index] = sorted[sorted_index];
248        }
249    }
250
251    /// Input must be sorted and within the edit's input length.
252    pub fn map_positions_sorted(&self, byte_indexes: &mut [usize], bias: Bias) {
253        debug_assert!(byte_indexes.is_sorted(), "input is not sorted");
254
255        let mut position: usize = 0;
256        let mut delta: isize = 0;
257        let mut i = 0;
258
259        for op in &self.ops {
260            if i >= byte_indexes.len() {
261                break;
262            }
263            match op {
264                Op::Retain(length) => {
265                    while i < byte_indexes.len() && byte_indexes[i] < position + length {
266                        byte_indexes[i] = byte_indexes[i].checked_add_signed(delta).unwrap();
267                        i += 1;
268                    }
269                    position += length;
270                }
271                Op::Insert(text) => {
272                    // Note [Bias].
273                    if let Bias::Backward = bias {
274                        while i < byte_indexes.len() && byte_indexes[i] == position {
275                            byte_indexes[i] = position.checked_add_signed(delta).unwrap();
276                            i += 1;
277                        }
278                    }
279                    delta += text.len().cast_signed();
280                }
281                Op::Delete(text) => {
282                    while i < byte_indexes.len() && byte_indexes[i] < position + text.len() {
283                        byte_indexes[i] = position.checked_add_signed(delta).unwrap();
284                        i += 1;
285                    }
286                    position += text.len();
287                    delta -= text.len().cast_signed();
288                }
289            }
290        }
291
292        while i < byte_indexes.len() {
293            byte_indexes[i] = byte_indexes[i].checked_add_signed(delta).unwrap();
294            i += 1;
295        }
296    }
297
298    pub fn apply(&self, rope: &mut Rope) -> Result<(), Error> {
299        if self.length_before != rope.len() {
300            return Err(Error::LengthMismatch {
301                expected: self.length_before,
302                actual: rope.len(),
303            });
304        }
305
306        let out = rope;
307        let mut rope = out.clone();
308
309        let mut index = 0;
310
311        for op in &self.ops {
312            match op {
313                Op::Retain(length) => index += length,
314                Op::Delete(text) => {
315                    debug_assert_eq!(rope.slice(index..index + text.len()), *text);
316                    rope.try_remove(index..index + text.len())?;
317                }
318                Op::Insert(text) => {
319                    rope.try_insert(index, text)?;
320                    index += text.len();
321                }
322            }
323        }
324
325        debug_assert!(
326            self.length_after == rope.len() && index == rope.len(),
327            "edit output length {} != rope length {}",
328            self.length_after,
329            rope.len()
330        );
331
332        *out = rope;
333
334        Ok(())
335    }
336}
337
338#[cfg_attr(test, derive(hegel::PrettyPrintable))]
339#[derive(Clone, Debug, Eq, PartialEq)]
340pub enum Op {
341    Retain(usize),
342    Delete(String),
343    Insert(String),
344}
345
346impl Op {
347    fn len(&self) -> usize {
348        match self {
349            Self::Retain(length) => *length,
350            Self::Delete(text) | Self::Insert(text) => text.len(),
351        }
352    }
353}
354
355// Note [Bias]
356#[derive(Clone, Copy)]
357pub enum Bias {
358    Backward,
359    Forward,
360}
361
362struct Cursor<'a> {
363    ops: &'a [Op],
364    op_index: usize,
365    consumed: usize,
366}
367
368impl<'a> Cursor<'a> {
369    fn new(edit: &'a Edit) -> Self {
370        Self {
371            ops: &edit.ops,
372            op_index: 0,
373            consumed: 0,
374        }
375    }
376
377    fn peek(&self) -> Option<&'a Op> {
378        self.ops.get(self.op_index)
379    }
380
381    fn remaining(&self) -> usize {
382        self.ops[self.op_index].len() - self.consumed
383    }
384
385    fn text(&self, n: usize) -> &'a str {
386        match &self.ops[self.op_index] {
387            Op::Retain(_) => unreachable!("text of a retain op"),
388            Op::Delete(text) | Op::Insert(text) => &text[self.consumed..self.consumed + n],
389        }
390    }
391
392    fn advance(&mut self, n: usize) {
393        self.consumed += n;
394        if self.consumed == self.ops[self.op_index].len() {
395            self.op_index += 1;
396            self.consumed = 0;
397        }
398    }
399}
400
401#[derive(Debug)]
402pub enum Error {
403    LengthMismatch { expected: usize, actual: usize },
404    Ropey(ropey::Error),
405}
406
407impl std::fmt::Display for Error {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        match self {
410            Self::LengthMismatch { expected, actual } => {
411                write!(f, "length mismatch: expected {expected}, got {actual}")
412            }
413            Self::Ropey(ropey) => write!(f, "{ropey}"),
414        }
415    }
416}
417
418impl std::error::Error for Error {
419    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
420        match self {
421            Self::LengthMismatch { .. } => None,
422            Self::Ropey(ropey) => Some(ropey),
423        }
424    }
425}
426
427impl From<ropey::Error> for Error {
428    fn from(ropey: ropey::Error) -> Self {
429        Self::Ropey(ropey)
430    }
431}
432
433/*
434Note [Canonical form]
435---------------------
436
437Claude: An edit is canonical when it has no zero-length ops, no adjacent ops of the same kind, and,
438at a given position, deletion before insertion. Canonical form makes the representation unique:
439equivalent edits are structurally equal.
440
441Uniqueness is more than tidiness. Deleting "d" and inserting "x" at one position produces the same
442document in either order, but when two concurrent edits are reconciled their ops are matched up
443positionally, and the two orderings interleave concurrent text on opposite sides of the same
444position. If both were representable, equivalent edits could reconcile to different documents
445depending on how they happened to be built. Delete-before-insert is an arbitrary choice; what
446matters is that exactly one order is representable.
447*/
448
449/*
450Note [Bias]
451-----------
452
453Claude: `Bias` picks a side of inserted text. A mapped position sitting exactly at an insertion
454point stays before the new text (`Backward`) or moves after it (`Forward`). Rebasing asks the same
455question of its only ambiguity — both edits inserting at the same point: `Backward` puts this edit's
456text first, `Forward` the other's, and rebasing each of two edits over the other with opposite
457biases produces the same document either way. The kernel always rebases drafts with `Forward`, so
458committed text wins ties.
459
460Replacements are the subtle case: canonical form makes delete-then-insert mean "the new text
461occupies the replaced range", so a concurrent insert at the range's start edge lands before the
462replacement text (bypassing the tie-break), and at its end edge after it. `Forward` rebasing and
463`Forward` mapping therefore agree — an edit's insertion point rebases the way a cursor at that
464position maps.
465*/
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use hegel::generators as gs;
471    use std::iter::zip;
472
473    #[test]
474    fn test() -> Result<(), Error> {
475        let mut rope = Rope::from("Hello, world!");
476        let mut edit = Edit::new();
477        edit.retain(7);
478        edit.delete("world");
479        edit.insert("Evan");
480        edit.delete("!");
481        edit.insert("...");
482        edit.insert("?");
483
484        edit.apply(&mut rope)?;
485        assert_eq!(rope, Rope::from("Hello, Evan...?"));
486
487        edit.invert().apply(&mut rope)?;
488        assert_eq!(rope, Rope::from("Hello, world!"));
489
490        edit.apply(&mut rope)?;
491        edit.invert().apply(&mut rope)?;
492        assert_eq!(rope, Rope::from("Hello, world!"));
493
494        let mut rope = Rope::from("Hello, world!");
495        edit.retain(1);
496        assert!(edit.apply(&mut rope).is_err());
497
498        Ok(())
499    }
500
501    #[test]
502    fn test_map_positions() -> Result<(), Error> {
503        let mut rope = Rope::from("Hello, world!");
504
505        let mut edit = Edit::new();
506        edit.delete("Hello, ");
507        edit.retain(5);
508        edit.insert("!!!");
509        edit.retain(1);
510
511        edit.apply(&mut rope)?;
512        assert_eq!(rope, Rope::from("world!!!!"));
513
514        // "H" and "," collapse to the deletion point; "r" follows the retained "world".
515        let mut indexes = [0, 5, 9];
516        edit.map_positions(&mut indexes, Bias::Forward);
517        assert_eq!(indexes, [0, 0, 2]);
518
519        // An index at an insertion point stays before or moves after the inserted text,
520        // depending on bias.
521        let mut edit = Edit::new();
522        edit.retain(5);
523        edit.insert("???");
524        edit.retain_rest(&rope)?;
525
526        let mut indexes = [5];
527        edit.map_positions(&mut indexes, Bias::Backward);
528        assert_eq!(indexes, [5]);
529
530        let mut indexes = [5];
531        edit.map_positions(&mut indexes, Bias::Forward);
532        assert_eq!(indexes, [8]);
533
534        Ok(())
535    }
536
537    #[test]
538    fn test_rebase() -> Result<(), Error> {
539        let rope = Rope::from("Hello, world!");
540
541        let mut a = Edit::new();
542        a.retain(7);
543        a.insert("brave ");
544        a.retain_rest(&rope)?;
545
546        let mut b = Edit::new();
547        b.retain(7);
548        b.insert("new ");
549        b.retain_rest(&rope)?;
550
551        let mut backward = rope.clone();
552        b.apply(&mut backward)?;
553        a.rebase(&b, Bias::Backward)?.apply(&mut backward)?;
554        assert_eq!(backward, Rope::from("Hello, brave new world!"));
555
556        let mut forward = rope.clone();
557        b.apply(&mut forward)?;
558        a.rebase(&b, Bias::Forward)?.apply(&mut forward)?;
559        assert_eq!(forward, Rope::from("Hello, new brave world!"));
560
561        Ok(())
562    }
563
564    /// Draws a byte index in `(index, doc.len()]` lying on a `char` boundary.
565    fn draw_str_end(tc: &hegel::TestCase, doc: &str, index: usize) -> usize {
566        let length = gs::integers::<usize>()
567            .min_value(1)
568            .max_value(doc.len() - index);
569        let mut end = index + tc.draw(length);
570        while !doc.is_char_boundary(end) {
571            end += 1;
572        }
573        end
574    }
575
576    #[hegel::composite]
577    fn gen_edit_and_text(tc: &hegel::TestCase, doc: String) -> (Edit, String) {
578        let mut edit = Edit::new();
579        let mut expected = String::new();
580        let mut index = 0;
581        while index < doc.len() {
582            match tc.draw(gs::integers::<u8>().max_value(2)) {
583                0 => {
584                    let end = draw_str_end(tc, &doc, index);
585                    edit.retain(end - index);
586                    expected.push_str(&doc[index..end]);
587                    index = end;
588                }
589                1 => {
590                    let end = draw_str_end(tc, &doc, index);
591                    edit.delete(&doc[index..end]);
592                    index = end;
593                }
594                _ => {
595                    let text: String = tc.draw(gs::text());
596                    edit.insert(&text);
597                    expected.push_str(&text);
598                }
599            }
600        }
601        if tc.draw(gs::booleans()) {
602            let text: String = tc.draw(gs::text());
603            edit.insert(&text);
604            expected.push_str(&text);
605        }
606        (edit, expected)
607    }
608
609    #[hegel::composite]
610    fn gen_edit(tc: &hegel::TestCase, doc: String) -> Edit {
611        tc.draw(gen_edit_and_text(doc.clone())).0
612    }
613
614    #[hegel::test(test_cases = 5_000)]
615    fn apply_produces_expected_document(tc: hegel::TestCase) {
616        let doc: String = tc.draw(gs::text());
617        let (edit, expected) = tc.draw(gen_edit_and_text(doc.clone()));
618
619        let mut rope = Rope::from(doc.as_str());
620        edit.apply(&mut rope).unwrap();
621
622        assert_eq!(rope, Rope::from(expected.as_str()));
623    }
624
625    #[hegel::test(test_cases = 5_000)]
626    fn invert_roundtrips(tc: hegel::TestCase) {
627        let doc: String = tc.draw(gs::text());
628        let edit = tc.draw(gen_edit(doc.clone()));
629
630        let before = Rope::from(doc.as_str());
631        let mut after = before.clone();
632        edit.apply(&mut after).unwrap();
633
634        // Applying the inverse undoes the edit...
635        let mut inverted = after.clone();
636        edit.invert().apply(&mut inverted).unwrap();
637        assert_eq!(inverted, before);
638
639        // ...and inverting is an involution.
640        assert_eq!(edit.invert().invert(), edit);
641    }
642
643    // Note [Canonical form]
644    #[hegel::test(test_cases = 5_000)]
645    fn canonical_form_is_unique(tc: hegel::TestCase) {
646        /// A `char` boundary near the middle of `s`.
647        fn midpoint(s: &str) -> usize {
648            let mut index = s.len() / 2;
649            while !s.is_char_boundary(index) {
650                index += 1;
651            }
652            index
653        }
654
655        let doc: String = tc.draw(gs::text());
656
657        // Build the same edit two ways: `one` deletes before inserting, in whole ops; `two`
658        // inserts before deleting, in split ops. Canonical form erases the difference.
659        let mut one = Edit::new();
660        let mut two = Edit::new();
661        let mut index = 0;
662        while index < doc.len() {
663            let end = draw_str_end(&tc, &doc, index);
664            if tc.draw(gs::booleans()) {
665                one.retain(end - index);
666                let middle = index + midpoint(&doc[index..end]);
667                two.retain(middle - index);
668                two.retain(end - middle);
669            } else {
670                let deleted = &doc[index..end];
671                let inserted: String = tc.draw(gs::text());
672                one.delete(deleted);
673                one.insert(&inserted);
674                let (delete_head, delete_tail) = deleted.split_at(midpoint(deleted));
675                let (insert_head, insert_tail) = inserted.split_at(midpoint(&inserted));
676                two.insert(insert_head);
677                two.insert(insert_tail);
678                two.delete(delete_head);
679                two.delete(delete_tail);
680            }
681            index = end;
682        }
683
684        assert_eq!(one, two);
685    }
686
687    #[hegel::test(test_cases = 5_000)]
688    fn compose_agrees_with_sequential_application(tc: hegel::TestCase) {
689        let doc: String = tc.draw(gs::text());
690        let a = tc.draw(gen_edit(doc.clone()));
691
692        let mut sequential = Rope::from(doc.as_str());
693        a.apply(&mut sequential).unwrap();
694        let b = tc.draw(gen_edit(sequential.to_string()));
695        b.apply(&mut sequential).unwrap();
696
697        let mut composed = Rope::from(doc.as_str());
698        a.compose(&b).unwrap().apply(&mut composed).unwrap();
699
700        assert_eq!(composed, sequential);
701    }
702
703    #[hegel::test(test_cases = 5_000)]
704    fn compose_is_associative(tc: hegel::TestCase) {
705        let doc: String = tc.draw(gs::text());
706        let mut rope = Rope::from(doc.as_str());
707
708        let a = tc.draw(gen_edit(rope.to_string()));
709        a.apply(&mut rope).unwrap();
710        let b = tc.draw(gen_edit(rope.to_string()));
711        b.apply(&mut rope).unwrap();
712        let c = tc.draw(gen_edit(rope.to_string()));
713
714        assert_eq!(
715            a.compose(&b).unwrap().compose(&c).unwrap(),
716            a.compose(&b.compose(&c).unwrap()).unwrap(),
717        );
718    }
719
720    #[hegel::test(test_cases = 5_000)]
721    fn compose_with_identity_is_identity(tc: hegel::TestCase) {
722        let doc: String = tc.draw(gs::text());
723        let edit = tc.draw(gen_edit(doc.clone()));
724
725        let mut before = Edit::new();
726        before.retain(edit.length_before);
727        assert_eq!(before.compose(&edit).unwrap(), edit);
728
729        let mut after = Edit::new();
730        after.retain(edit.length_after);
731        assert_eq!(edit.compose(&after).unwrap(), edit);
732    }
733
734    // TP1 convergence: rebasing with opposite biases converges regardless of which edit is applied
735    // first.
736    #[hegel::test(test_cases = 5_000)]
737    fn rebase_converges(tc: hegel::TestCase) {
738        let doc: String = tc.draw(gs::text());
739        let a = tc.draw(gen_edit(doc.clone()));
740        let b = tc.draw(gen_edit(doc.clone()));
741
742        let mut a_first = Rope::from(doc.as_str());
743        a.apply(&mut a_first).unwrap();
744        b.rebase(&a, Bias::Forward)
745            .unwrap()
746            .apply(&mut a_first)
747            .unwrap();
748
749        let mut b_first = Rope::from(doc.as_str());
750        b.apply(&mut b_first).unwrap();
751        a.rebase(&b, Bias::Backward)
752            .unwrap()
753            .apply(&mut b_first)
754            .unwrap();
755
756        assert_eq!(a_first, b_first);
757    }
758
759    #[hegel::test(test_cases = 5_000)]
760    fn transform_byte_indexes_preserves_order_and_validity(tc: hegel::TestCase) {
761        let doc: String = tc.draw(gs::text());
762        let edit = tc.draw(gen_edit(doc.clone()));
763
764        let mut indexes: Vec<usize> =
765            tc.draw(gs::vecs(gs::integers::<usize>().max_value(doc.len())));
766        for index in &mut indexes {
767            while !doc.is_char_boundary(*index) {
768                *index -= 1;
769            }
770        }
771        indexes.sort_unstable();
772
773        let mut rope = Rope::from(doc.as_str());
774        edit.apply(&mut rope).unwrap();
775        let after = rope.to_string();
776
777        let mut backward = indexes.clone();
778        edit.map_positions(&mut backward, Bias::Backward);
779        let mut forward = indexes;
780        edit.map_positions(&mut forward, Bias::Forward);
781
782        // Transformed indexes are valid, ordered positions in the new document, and backward bias
783        // never lands after forward bias.
784        assert!(backward.is_sorted());
785        assert!(forward.is_sorted());
786        for (backward, forward) in zip(&backward, &forward) {
787            assert!(after.is_char_boundary(*backward));
788            assert!(after.is_char_boundary(*forward));
789            assert!(backward <= forward);
790        }
791    }
792}