ethersync 0.8.0

Enables real-time co-editing of local text files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
// SPDX-FileCopyrightText: 2024 blinry <mail@blinry.org>
// SPDX-FileCopyrightText: 2024 zormit <nt4u@kpvn.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

#![allow(dead_code)]

use crate::types::{EditorTextDelta, RevisionedEditorTextDelta, RevisionedTextDelta, TextDelta};
use operational_transform::OperationSeq;
use tracing::{debug, warn};

///    `OTServer` receives operations from both the CRDT world, and one editor and makes sure that
///    the editor operations (which might be based on an older document) are applicable to the
///    state that the CRDT is tracking.
///
///    Here's a broader example of how it works, when employed in a context of a daemon (the
///    caller, which needs to make sure, that operations are communicated both ways).
///
///    1. The daemon starts with an empty list of operations (corresponding to an empty/unchanged document)
///    2. It applies the d1 operation to it, which the editor also receives and applies.
///    3. The daemon applies d2 and d3, and sends them to the editor (thinking these would put it
///       into the same state).
///       It also sends along the editor revision, which is the number of ops received by the editor,
///       which is basically the column, and specifies the point the ops apply to. (Here: 0).
///    4. But the editor has made concurrent edits e1 and e2 in the meantime. It rejects d2 and d3.
///       It sends e1 and e2 to the daemon, along with the daemon revision, which is the number of ops
///       it has received from the daemon (the row).
///    5. The daemon transforms e1 and e2 through d2 and d3, creating e1' and e2', and applies them
///       to the document. It sends d2' and d3' to the editor, along with the editor revision (2).
///    6. The editor receives d2' and applies it, but then makes edit e3, and sends it to the daemon.
///       The editor rejects d3', because it is received after e3 was created.
///    7. The daemon meanwhile makes edit d4. Upon reciving e3, it transforms it against d3' and d4,
///       and sends d3'' and d4' to the editor. It applies d4 and e3'' to the document.
///    8. The editor receives d3'' and d4', and applies them. Both sides now have the same document.
///
///
///     ---- the right axis is the editor revision --->
///    |
///    | the down axis is the daemon revision
///    |
///    v
///        *
///        |
///     d1 |
///        v  e1      e2
///        * ----> * ----> *
///        |       |       |
///     d2 |       |       | d2'
///        v       v       v  e3
///        * ----> * ----> * ----> *          Ops in the rightmost column need
///        |       |       |       |          to be queued by us, because
///     d3 |       |       | d3'   | d3''     we don't know whether the
///        v  e1'  v  e2'  v  e3'  v          editor accepted them. (d3'' and d4')
///        * ----> * ----> * ----> *
///                        |       |
///    The lower        d4 |       | d4'
///    zig-zag is          v  e3'' v
///    the operations      * ----> *
///    log saved by the
///    daemon.
///    (d1, d2, d3, e1', e2', d4, e3'')
///
///
#[derive(Debug, Default)]
#[must_use]
pub struct OTServer {
    editor_revision: usize,
    daemon_revision: usize,
    /// "Source of truth" operations.
    operations: Vec<OperationSeq>,
    /// Operations that we have sent to the editor, but we're not sure whether it has
    /// accepted them. We have to keep them around until we know for sure, so that we
    /// can correctly transform operations for the editor.
    ///
    /// Design Note: The daemon should do the transformation because we want to spare
    /// the overhead of implementing the tranformation per editor plugin. In our case
    /// there's a small number of editors, so transforming it in the daemon is feasible.
    editor_queue: Vec<OperationSeq>,
    current_content: String,
    /// That's the content we assume the editor has, where the operations it sends us apply.
    /// (which is the content "in front" of the `editor_queue`)
    last_confirmed_editor_content: String,
}

impl OTServer {
    pub fn new(initial_content: String) -> Self {
        Self {
            last_confirmed_editor_content: initial_content.clone(),
            current_content: initial_content,
            ..Default::default()
        }
    }

    /// Called when the CRDT world makes a change to the document.
    pub fn apply_crdt_change(&mut self, delta: &TextDelta) -> RevisionedEditorTextDelta {
        // We can apply the change immediately.
        self.operations.push(delta.clone().into());
        self.editor_queue.push(delta.clone().into());
        self.daemon_revision += 1;
        // Use "previous" content to transform into editor text delta.
        let editor_delta = EditorTextDelta::from_delta(delta.clone(), &self.current_content);
        self.current_content = Self::force_apply(&self.current_content, delta.clone().into());

        // We assume that the editor is up-to-date, and send the operation to it.
        // If it can't accept it, we will transform and send it later.
        RevisionedEditorTextDelta {
            revision: self.editor_revision,
            delta: editor_delta,
        }
    }

    /// Called when the editor sends us an operation.
    /// daemonRevision is the revision this operation applies to.
    pub fn apply_editor_operation(
        &mut self,
        rev_editor_delta: RevisionedEditorTextDelta,
    ) -> (TextDelta, Vec<RevisionedEditorTextDelta>) {
        let daemon_revision = rev_editor_delta.revision;
        self.editor_revision += 1;

        let mut op_seq: OperationSeq;

        assert!(
            daemon_revision <= self.daemon_revision,
            "This must not happen, editor has seen a daemon revision from the future."
        );

        // The operation might apply to an older daemon revision.
        // We need to transform it through the daemon operations that have happened since then.

        // But we at least we know that the editor has seen all daemon operations until
        // daemon_revision. So we can remove them from the editor queue.
        let daemon_operations_to_transform = self.daemon_revision - daemon_revision;
        assert!(
            self.editor_queue.len() >= daemon_operations_to_transform,
            "Whoopsie, we don't have enough operations cached. Was this already processed?"
        );
        let seen_operations = self.editor_queue.len() - daemon_operations_to_transform;
        debug!(
            "Editor is confirming {} operations, there are still {} unconfirmed operations.",
            seen_operations, daemon_operations_to_transform,
        );
        if daemon_operations_to_transform > 50 {
            warn!(
                "Editor is {} operations behind, it might have trouble catching up?",
                daemon_operations_to_transform
            );
        }
        let confirmed_queue = self.editor_queue.drain(..seen_operations);
        for confirmed_editor_op in confirmed_queue {
            debug!(
                "Applying confirmed operation {:#?} to last confirmed editor content {:?}",
                &confirmed_editor_op, &self.last_confirmed_editor_content
            );
            self.last_confirmed_editor_content = Self::force_apply(
                &self.last_confirmed_editor_content,
                confirmed_editor_op.clone(),
            );
        }
        let rev_delta = RevisionedTextDelta::from_rev_ed_delta(
            rev_editor_delta,
            &self.last_confirmed_editor_content,
        );
        op_seq = rev_delta.delta.into();

        debug!(
            "Applying incoming editor operation {:#?} to last confirmed editor content {:?}",
            &op_seq, &self.last_confirmed_editor_content
        );
        self.last_confirmed_editor_content =
            Self::force_apply(&self.last_confirmed_editor_content, op_seq.clone());
        (op_seq, self.editor_queue) = transform_through_operations(op_seq, &self.editor_queue);
        self.operations.push(op_seq.clone());
        self.current_content = Self::force_apply(&self.current_content, op_seq.clone());

        let deltas_for_editor = self.deltas_for_editor();

        (op_seq.into(), deltas_for_editor)
    }

    fn deltas_for_editor(&self) -> Vec<RevisionedEditorTextDelta> {
        let mut to_editor = vec![];
        let mut document = self.last_confirmed_editor_content.clone();
        for editor_op in &self.editor_queue {
            let delta: TextDelta = editor_op.clone().into();
            let ed_delta = EditorTextDelta::from_delta(delta, &document);
            to_editor.push(RevisionedEditorTextDelta::new(
                self.editor_revision,
                ed_delta,
            ));

            //debug!("Applying unconfirmed operation from editor queue {:#?} to last confirmed editor content {:?}", &editor_op, &document);
            document = Self::force_apply(&document, editor_op.clone());
        }
        to_editor
    }

    #[must_use]
    pub fn current_content(&self) -> &str {
        &self.current_content
    }

    #[must_use]
    fn force_apply(document: &str, mut op_seq: OperationSeq) -> String {
        let doc_chars = document.chars().count();
        if op_seq.base_len() < doc_chars {
            op_seq.retain((doc_chars - op_seq.base_len()) as u64);
        }
        op_seq.apply(document).unwrap_or_else(|_| {
            panic!("Could not apply operation {op_seq:?} to string with length {doc_chars} ('{document:?}')")
        })
    }
}

/// This function takes operations t1 and m1 ... `m_n`,
/// and returns operations t1' and m1' ... `m_n`'.
/// .
///        t1
///     * ----> *
///     |       |
///  m1 |       | m1'
///     v       v
///     * ----> *
///     |       |
///  m2 |       | m2'
///     v       v
///     * ----> *
///     |       |
///  m3 |       | m3'
///     v  t1'  v
///     * ----> *
///
fn transform_through_operations(
    mut their_op_seq: OperationSeq,
    my_operations: &[OperationSeq],
) -> (OperationSeq, Vec<OperationSeq>) {
    let mut transformed_my_operations = Vec::new();
    for my_op_seq in my_operations {
        let mut my_op_seq = my_op_seq.clone();
        // transform expects both operations to have the same base_len. See also:
        // https://docs.rs/operational-transform/0.6.1/src/operational_transform/lib.rs.html#345
        // Currently we are implementing this method on data that doesn't carry this 'global' knowledge.
        // So we'll workaround by manually fixing the base_len, if one of the operations is shorter.
        // We do so by simply retaining the required number of characters at the end
        if my_op_seq.base_len() < their_op_seq.base_len() {
            let diff = their_op_seq.base_len() - my_op_seq.base_len();
            my_op_seq.retain(diff as u64);
        } else {
            let diff = my_op_seq.base_len() - their_op_seq.base_len();
            their_op_seq.retain(diff as u64);
        }
        let (my_prime, their_prime) = my_op_seq.transform(&their_op_seq).unwrap_or_else(|_| {
            panic!(
                "Could not transform operations {:?} on top of {:?}.",
                &their_op_seq.ops(),
                &my_op_seq.ops()
            )
        });
        transformed_my_operations.push(my_prime);
        their_op_seq = their_prime;
    }
    (their_op_seq, transformed_my_operations)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::factories::*;
    use tracing_test::traced_test;

    fn compose(delta1: TextDelta, delta2: TextDelta) -> TextDelta {
        operational_transform_internals::ot_compose(delta1.into(), delta2.into()).into()
    }

    mod ot_server_public_interface {
        use super::*;

        #[traced_test]
        #[test]
        fn routes_operations_through_server() {
            let mut ot_server: OTServer = OTServer::new("hello".into());

            let to_editor = ot_server.apply_crdt_change(&insert(1, "x"));
            let expected = EditorTextDelta::from_delta(insert(1, "x"), "hello");
            assert_eq!(to_editor, rev_ed_delta(0, expected));

            let (to_crdt, to_editor) =
                ot_server.apply_editor_operation(rev_ed_delta_single(0, (0, 2), (0, 2), "y"));
            assert_eq!(to_crdt, insert(3, "y"));
            let expected = EditorTextDelta::from_delta(insert(1, "x"), "heyllo");
            assert_eq!(to_editor, vec![rev_ed_delta(1, expected)]);

            assert_eq!(
                ot_server.operations,
                vec![insert(1, "x").into(), insert(3, "y").into()]
            );
            assert_eq!(ot_server.current_content(), "hxeyllo");

            let to_editor = ot_server.apply_crdt_change(&insert(3, "z"));
            let expected = EditorTextDelta::from_delta(insert(3, "z"), "TODO");
            assert_eq!(to_editor, rev_ed_delta(1, expected));

            assert_eq!(ot_server.current_content(), "hxezyllo");

            // editor thinks: hxeyllo -> hlo
            let (to_crdt, to_editor) =
                ot_server.apply_editor_operation(rev_ed_delta_single(1, (0, 1), (0, 5), ""));
            assert_eq!(to_crdt, compose(delete(1, 2), delete(2, 2)));
            let expected = EditorTextDelta::from_delta(insert(1, "z"), "hlo");
            assert_eq!(to_editor, vec![rev_ed_delta(2, expected)]);

            assert_eq!(ot_server.current_content(), "hzlo");
            assert_eq!(
                ot_server.operations,
                vec![
                    insert(1, "x").into(),
                    insert(3, "y").into(),
                    insert(3, "z").into(),
                    compose(delete(1, 2), delete(2, 2)).into()
                ]
            );

            assert_eq!(ot_server.editor_queue, vec![insert(1, "z").into(),]);
            assert_eq!(ot_server.last_confirmed_editor_content, "hlo");

            // Note: We're not testing some of the returned values.
            // We're mostly interested in the created structure.
            let (_to_crdt, _to_editor) =
                ot_server.apply_editor_operation(rev_ed_delta_single(2, (0, 4), (0, 4), "!"));
            assert_eq!(ot_server.editor_queue, vec![]);
            assert_eq!(ot_server.last_confirmed_editor_content, "hzlo!");

            let _to_editor = ot_server.apply_crdt_change(&insert(1, "o"));
            let _to_editor = ot_server.apply_crdt_change(&insert(2, "\n"));
            let (_to_crdt, _to_editor) =
                ot_server.apply_editor_operation(rev_ed_delta_single(2, (0, 5), (0, 5), "\nblubb"));

            assert_eq!(ot_server.editor_queue.len(), 2);
            assert_eq!(ot_server.last_confirmed_editor_content, "hzlo!\nblubb");
            assert_eq!(ot_server.current_content(), "ho\nzlo!\nblubb");
        }

        #[test]
        fn can_do_newline_stuff() {
            let mut ot_server: OTServer = OTServer::default();
            ot_server.apply_crdt_change(&insert(0, "Let's say\nthis could be\na poem."));
            ot_server.apply_editor_operation(rev_ed_delta_single(
                ot_server.daemon_revision,
                (0, 0),
                (0, 0),
                "THE POEM\n",
            ));

            assert_eq!(
                ot_server.current_content(),
                "THE POEM\nLet's say\nthis could be\na poem."
            );

            // both do a replacemenet at the "same time", but not overlapping.

            // Note: using len because all is ascii and .chars().count() is more noise.
            ot_server.apply_crdt_change(&replace(
                "THE POEM\nLet's say\n".len(), // skip
                "this could".len(),            // replace
                "I want to",                   // by
            ));

            ot_server.apply_editor_operation(rev_ed_delta_single(
                ot_server.daemon_revision - 1, // same time
                (3, 0),
                (3, "a poem".len()),
                "the boss",
            ));

            assert_eq!(
                ot_server.current_content(),
                "THE POEM\nLet's say\nI want to be\nthe boss."
            );
        }

        #[test]
        fn newline_join_behavior() {
            let content = "hello\nworld\n";
            let mut ot_server: OTServer = OTServer::new(content.to_string());
            let (to_crdt_1, _) =
                ot_server.apply_editor_operation(rev_ed_delta_single(0, (0, 5), (0, 5), " world"));
            // Drop the newline (*could* be implicit, see below).
            let (to_crdt_2, _) =
                ot_server.apply_editor_operation(rev_ed_delta_single(0, (1, 0), (2, 0), ""));

            assert_eq!(to_crdt_1, insert(5, " world"));
            assert_eq!(to_crdt_2, delete(12, 6));

            let mut ot_server2: OTServer = OTServer::new(content.to_string());
            let to_2nd_editor = ot_server2.apply_crdt_change(&to_crdt_1);
            let to_2nd_editor_2 = ot_server2.apply_crdt_change(&to_crdt_2);

            assert_eq!(
                to_2nd_editor,
                rev_ed_delta_single(0, (0, 5), (0, 5), " world")
            );
            // If the content would be "hello\nworld", without a final newline, the following delta
            // would not correctly "drop" the implicit newline.
            // For details, check out ../../docs/decisions/02-working-with-vims-eol-behavior.md.
            assert_eq!(to_2nd_editor_2, rev_ed_delta_single(0, (1, 0), (2, 0), ""));
            assert_eq!(ot_server.current_content(), ot_server2.current_content());
        }
    }

    mod ot_server_internal_state {
        use super::*;

        fn dummy_insert(at: usize) -> TextDelta {
            insert(at, "foo")
        }

        #[test]
        fn crdt_change_increases_revision() {
            let mut ot_server: OTServer = OTServer::new("foobar".to_string());
            ot_server.apply_crdt_change(&dummy_insert(2));
            assert_eq!(ot_server.daemon_revision, 1);
            assert_eq!(ot_server.editor_revision, 0);
        }

        #[test]
        fn editor_operation_tracks_revision() {
            let mut ot_server: OTServer = OTServer::new("foobar".to_string());
            ot_server.apply_editor_operation(rev_ed_delta_single(0, (0, 0), (0, 0), "x"));
            assert_eq!(ot_server.editor_revision, 1);
            assert_eq!(ot_server.daemon_revision, 0);
        }

        #[test]
        fn crdt_change_tracks_in_queue() {
            let mut ot_server: OTServer = OTServer::new("foobar".to_string());
            ot_server.apply_crdt_change(&dummy_insert(2));
            assert_eq!(ot_server.editor_queue, vec![dummy_insert(2).into()]);
        }

        #[test]
        fn editor_operation_reduces_editor_queue() {
            let mut ot_server: OTServer = OTServer::new("xx".to_string());

            ot_server.apply_crdt_change(&dummy_insert(2));
            ot_server.apply_crdt_change(&dummy_insert(5));
            ot_server.apply_crdt_change(&dummy_insert(8));
            assert_eq!(ot_server.editor_queue.len(), 3);

            ot_server.apply_editor_operation(rev_ed_delta_single(1, (0, 2), (0, 2), "foo"));
            // we have already seen one op, so now the queue has only 2 left.
            assert_eq!(ot_server.editor_queue.len(), 2);
        }

        #[test]
        fn replace_single_character() {
            let mut ot_server: OTServer = OTServer::new("hello".into());

            let (to_crdt, _to_editor) =
                ot_server.apply_editor_operation(rev_ed_delta_single(0, (0, 1), (0, 2), "u"));

            let mut expected = TextDelta::default();
            expected.retain(1);
            expected.insert("u");
            expected.delete(1);

            assert_eq!(expected, to_crdt);
            //assert_eq!(Vec::, to_editor);

            assert_eq!(ot_server.current_content(), "hullo");
            assert_eq!(ot_server.last_confirmed_editor_content, "hullo");
        }
    }

    mod operational_transform_internals {
        use super::*;
        use operational_transform::Operation as OTOperation;

        fn ot_insert(at: usize, s: &str) -> OperationSeq {
            let mut op_seq = OperationSeq::default();
            op_seq.retain(at as u64);
            op_seq.insert(s);
            op_seq
        }

        fn ot_delete(from: usize, length: usize) -> OperationSeq {
            let mut op_seq = OperationSeq::default();
            op_seq.retain(from as u64);
            op_seq.delete(length as u64);
            op_seq
        }

        #[expect(clippy::needless_pass_by_value)] // only used in tests
        pub fn ot_compose(mut op1: OperationSeq, op2: OperationSeq) -> OperationSeq {
            if op1.target_len() < op2.base_len() {
                op1.retain((op2.base_len() - op1.target_len()) as u64);
            }
            op1.compose(&op2)
                .expect("Composition failed. Lengths messed up?")
        }

        #[test]
        fn transforms_operation_correctly() {
            let mut ours = vec![ot_insert(0, "foo"), ot_insert(3, "foo")];
            // an insert at the same position as the first operation => clash.
            let theirs = ot_insert(0, "bar");
            let (theirs, ours_prime) = transform_through_operations(theirs, &ours);
            // expect the insert to be moved to the end
            assert_eq!(theirs, ot_insert(6, "bar"));
            // check that ours hasn't changed (besides retains that had to be inserted)
            ours[0].retain(3);
            ours[1].retain(3);
            assert_eq!(ours_prime, ours);
        }

        #[test]
        fn transforms_operation_correctly_different_base_lengths() {
            let ours = vec![ot_insert(3, "foo")];
            let mut theirs = ot_insert(0, "bar");
            let (theirs_prime, ours_prime) = transform_through_operations(theirs.clone(), &ours);
            // position of the insert hasn't shifted, but we got a retain added.
            theirs.retain(6);
            assert_eq!(theirs, theirs_prime);
            assert_eq!(ours_prime, vec![ot_insert(6, "foo")]);
        }

        #[test]
        fn transforms_operation_correctly_splits_deletion() {
            let editor_op = ot_insert(2, "x");
            let unacknowledged_ops = vec![ot_delete(1, 3)];

            let (op_prime, queue_prime) =
                transform_through_operations(editor_op, &unacknowledged_ops);
            assert_eq!(op_prime, ot_insert(1, "x"));
            assert_eq!(
                queue_prime,
                vec![ot_compose(ot_delete(1, 1), ot_delete(2, 2))]
            );
        }

        #[test]
        fn ot_transform_does_what_we_think() {
            let mut a = OperationSeq::default();
            let mut b = OperationSeq::default();
            let mut c = OperationSeq::default();

            a.retain(2);
            a.insert("x");
            a.retain(1);

            b.retain(1);
            b.delete(2);

            // similar to a, but other character.
            c.retain(2);
            c.insert("y");
            c.retain(1);

            let (a_prime, b_prime) = a
                .transform(&b)
                .expect("Transform failed. Do the lengths fit?");
            assert_eq!(
                a_prime.ops(),
                vec![OTOperation::Retain(1), OTOperation::Insert("x".to_string())]
            );
            assert_eq!(
                b_prime.ops(),
                vec![
                    OTOperation::Retain(1),
                    OTOperation::Delete(1),
                    OTOperation::Retain(1),
                    OTOperation::Delete(1)
                ]
            );

            // With inserts at the same position,
            // the operation that is transformed is applied "after" the other one.
            // If you want it the other way around, you'll need to swap a and c.
            let (a_prime, c_prime) = a
                .transform(&c)
                .expect("Transform failed. Do the lengths fit?");
            assert_eq!(
                a_prime.ops(),
                vec![
                    OTOperation::Retain(2),
                    OTOperation::Insert("x".to_string()),
                    OTOperation::Retain(2)
                ]
            );
            assert_eq!(
                c_prime.ops(),
                vec![
                    OTOperation::Retain(3),
                    OTOperation::Insert("y".to_string()),
                    OTOperation::Retain(1)
                ]
            );
        }
    }
}