1use ropey::Rope;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Assoc {
22 Before,
23 After,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum RangeEffect {
32 Untouched,
34 InsertedInside,
37 PartlyDeleted,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Operation {
45 Retain(usize),
47 Delete(usize),
49 Insert(String),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ChangeSet {
60 ops: Vec<Operation>,
61 len_before: usize,
62 len_after: usize,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct ChangedSpan {
73 pub before_start: usize,
74 pub before_end: usize,
75 pub after_start: usize,
76 pub after_end: usize,
77}
78
79impl ChangeSet {
80 pub fn builder(len_before: usize) -> ChangeSetBuilder {
81 ChangeSetBuilder { ops: Vec::new(), len_before, consumed: 0 }
82 }
83
84 pub fn identity(len: usize) -> Self {
86 ChangeSet::builder(len).build()
87 }
88
89 pub fn replace(len_before: usize, from: usize, to: usize, text: impl Into<String>) -> Self {
92 let mut b = ChangeSet::builder(len_before);
93 b.retain(from).delete(to - from).insert(text);
94 b.build()
95 }
96
97 pub fn len_before(&self) -> usize {
98 self.len_before
99 }
100
101 pub fn len_after(&self) -> usize {
102 self.len_after
103 }
104
105 pub fn ops(&self) -> &[Operation] {
106 &self.ops
107 }
108
109 pub fn is_identity(&self) -> bool {
111 self.ops.iter().all(|op| matches!(op, Operation::Retain(_)))
112 }
113
114 pub fn changed_span(&self) -> Option<ChangedSpan> {
115 let mut before = 0;
116 let mut after = 0;
117 let mut start = None;
118 let mut end = (0, 0);
119
120 for op in &self.ops {
121 match op {
122 Operation::Retain(n) => {
123 before += n;
124 after += n;
125 }
126 Operation::Delete(n) => {
127 start.get_or_insert((before, after));
128 before += n;
129 end = (before, after);
130 }
131 Operation::Insert(text) => {
132 start.get_or_insert((before, after));
133 after += text.chars().count();
134 end = (before, after);
135 }
136 }
137 }
138
139 start.map(|(before_start, after_start)| ChangedSpan {
140 before_start,
141 before_end: end.0,
142 after_start,
143 after_end: end.1,
144 })
145 }
146
147 pub fn apply(&self, text: &Rope) -> Rope {
150 debug_assert_eq!(
151 text.len_chars(),
152 self.len_before,
153 "changeset applied to a document it was not authored against"
154 );
155
156 let mut out = Rope::new();
157 let mut pos = 0;
158 for op in &self.ops {
159 match op {
160 Operation::Retain(n) => {
161 out.append(Rope::from(text.slice(pos..pos + n)));
162 pos += n;
163 }
164 Operation::Delete(n) => pos += n,
165 Operation::Insert(s) => {
166 let end = out.len_chars();
167 out.insert(end, s);
168 }
169 }
170 }
171 out
172 }
173
174 pub fn invert(&self, original: &Rope) -> ChangeSet {
180 debug_assert_eq!(original.len_chars(), self.len_before, "invert needs the pre-image");
181
182 let mut b = ChangeSet::builder(self.len_after);
183 let mut pos = 0;
184 for op in &self.ops {
185 match op {
186 Operation::Retain(n) => {
187 b.retain(*n);
188 pos += n;
189 }
190 Operation::Delete(n) => {
192 b.insert(original.slice(pos..pos + n).to_string());
193 pos += n;
194 }
195 Operation::Insert(s) => {
196 b.delete(s.chars().count());
197 }
198 }
199 }
200 b.build()
201 }
202
203 pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
210 let mut old = 0;
211 let mut new = 0;
212
213 for op in &self.ops {
214 match op {
215 Operation::Retain(n) => {
216 if pos < old + n {
217 return new + (pos - old);
218 }
219 old += n;
220 new += n;
221 }
222 Operation::Delete(n) => {
223 if pos < old + n {
224 return new;
225 }
226 old += n;
227 }
228 Operation::Insert(s) => {
229 let len = s.chars().count();
230 if pos == old {
233 return match assoc {
234 Assoc::Before => new,
235 Assoc::After => new + len,
236 };
237 }
238 new += len;
239 }
240 }
241 }
242 new + pos.saturating_sub(old)
244 }
245
246 pub fn touches(&self, from: usize, to: usize) -> RangeEffect {
258 debug_assert!(from <= to, "range bounds reversed");
259
260 let mut old = 0;
261 let mut effect = RangeEffect::Untouched;
262
263 for op in &self.ops {
264 match op {
265 Operation::Retain(n) => old += n,
266 Operation::Delete(n) => {
267 let (start, end) = (old, old + n);
268 let overlaps = if from == to {
269 start < from && from < end
270 } else {
271 start < to && from < end
274 };
275 if overlaps {
276 return RangeEffect::PartlyDeleted;
278 }
279 old = end;
280 }
281 Operation::Insert(_) => {
282 if from < old && old < to {
283 effect = RangeEffect::InsertedInside;
284 }
285 }
286 }
287 }
288 effect
289 }
290
291 pub fn compose(&self, other: &ChangeSet) -> ChangeSet {
296 assert_eq!(
297 self.len_after, other.len_before,
298 "cannot compose: the second changeset was authored against a different document"
299 );
300
301 let mut b = ChangeSet::builder(self.len_before);
302 let mut a_iter = self.ops.iter().cloned();
303 let mut b_iter = other.ops.iter().cloned();
304 let mut a = a_iter.next();
305 let mut c = b_iter.next();
306
307 loop {
308 match (a.take(), c.take()) {
309 (None, None) => break,
310
311 (Some(Operation::Delete(n)), rest) => {
315 b.delete(n);
316 a = a_iter.next();
317 c = rest;
318 }
319
320 (rest, Some(Operation::Insert(s))) => {
322 b.insert(s);
323 a = rest;
324 c = b_iter.next();
325 }
326
327 (Some(Operation::Retain(i)), Some(Operation::Retain(j))) => {
328 let n = i.min(j);
329 b.retain(n);
330 a = carry(Operation::Retain(i - n), &mut a_iter);
331 c = carry(Operation::Retain(j - n), &mut b_iter);
332 }
333
334 (Some(Operation::Retain(i)), Some(Operation::Delete(j))) => {
336 let n = i.min(j);
337 b.delete(n);
338 a = carry(Operation::Retain(i - n), &mut a_iter);
339 c = carry(Operation::Delete(j - n), &mut b_iter);
340 }
341
342 (Some(Operation::Insert(s)), Some(Operation::Retain(j))) => {
344 let len = s.chars().count();
345 let n = len.min(j);
346 b.insert(take_chars(&s, n));
347 a = carry_insert(&s, n, &mut a_iter);
348 c = carry(Operation::Retain(j - n), &mut b_iter);
349 }
350
351 (Some(Operation::Insert(s)), Some(Operation::Delete(j))) => {
354 let len = s.chars().count();
355 let n = len.min(j);
356 a = carry_insert(&s, n, &mut a_iter);
357 c = carry(Operation::Delete(j - n), &mut b_iter);
358 }
359
360 (None, Some(op)) | (Some(op), None) => {
363 unreachable!("changeset length mismatch, stranded {op:?}")
364 }
365 }
366 }
367
368 b.build()
369 }
370}
371
372fn carry(op: Operation, iter: &mut impl Iterator<Item = Operation>) -> Option<Operation> {
374 match &op {
375 Operation::Retain(0) | Operation::Delete(0) => iter.next(),
376 _ => Some(op),
377 }
378}
379
380fn carry_insert(
382 s: &str,
383 consumed: usize,
384 iter: &mut impl Iterator<Item = Operation>,
385) -> Option<Operation> {
386 let rest: String = s.chars().skip(consumed).collect();
387 if rest.is_empty() {
388 iter.next()
389 } else {
390 Some(Operation::Insert(rest))
391 }
392}
393
394fn take_chars(s: &str, n: usize) -> String {
395 s.chars().take(n).collect()
396}
397
398#[derive(Debug)]
405pub struct ChangeSetBuilder {
406 ops: Vec<Operation>,
407 len_before: usize,
408 consumed: usize,
409}
410
411impl ChangeSetBuilder {
412 pub fn retain(&mut self, n: usize) -> &mut Self {
413 if n == 0 {
414 return self;
415 }
416 self.consumed += n;
417 if let Some(Operation::Retain(prev)) = self.ops.last_mut() {
418 *prev += n;
419 } else {
420 self.ops.push(Operation::Retain(n));
421 }
422 self
423 }
424
425 pub fn delete(&mut self, n: usize) -> &mut Self {
426 if n == 0 {
427 return self;
428 }
429 self.consumed += n;
430
431 let at = match self.ops.last() {
434 Some(Operation::Insert(_)) => self.ops.len() - 1,
435 _ => self.ops.len(),
436 };
437 if at > 0 {
438 if let Some(Operation::Delete(prev)) = self.ops.get_mut(at - 1) {
439 *prev += n;
440 return self;
441 }
442 }
443 self.ops.insert(at, Operation::Delete(n));
444 self
445 }
446
447 pub fn insert(&mut self, text: impl Into<String>) -> &mut Self {
448 let text = text.into();
449 if text.is_empty() {
450 return self;
451 }
452 if let Some(Operation::Insert(prev)) = self.ops.last_mut() {
453 prev.push_str(&text);
454 } else {
455 self.ops.push(Operation::Insert(text));
456 }
457 self
458 }
459
460 pub fn build(mut self) -> ChangeSet {
462 let tail = self
463 .len_before
464 .checked_sub(self.consumed)
465 .expect("changeset consumed more of the document than it has");
466 self.retain(tail);
467
468 let len_after = self
469 .ops
470 .iter()
471 .map(|op| match op {
472 Operation::Retain(n) => *n,
473 Operation::Delete(_) => 0,
474 Operation::Insert(s) => s.chars().count(),
475 })
476 .sum();
477
478 ChangeSet { ops: self.ops, len_before: self.len_before, len_after }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 fn rope(s: &str) -> Rope {
487 Rope::from_str(s)
488 }
489
490 fn applied(text: &str, cs: &ChangeSet) -> String {
492 cs.apply(&rope(text)).to_string()
493 }
494
495 #[test]
496 fn identity_changes_nothing() {
497 let cs = ChangeSet::identity(5);
498 assert!(cs.is_identity());
499 assert_eq!(applied("hello", &cs), "hello");
500 assert_eq!(cs.len_after(), 5);
501 }
502
503 #[test]
504 fn replace_swaps_a_range() {
505 let cs = ChangeSet::replace(11, 6, 11, "there");
506 assert_eq!(applied("hello world", &cs), "hello there");
507 }
508
509 #[test]
510 fn insertion_and_deletion_report_their_output_length() {
511 let mut b = ChangeSet::builder(5);
512 b.retain(2).insert("XY").delete(3);
513 let cs = b.build();
514 assert_eq!(cs.len_before(), 5);
515 assert_eq!(cs.len_after(), 4); assert_eq!(applied("hello", &cs), "heXY");
517 }
518
519 #[test]
520 fn the_builder_merges_runs_and_orders_delete_before_insert() {
521 let mut b = ChangeSet::builder(10);
522 b.retain(1).retain(1).insert("a").insert("b").delete(2).delete(1);
523 let cs = b.build();
524
525 assert_eq!(
527 cs.ops(),
528 [
529 Operation::Retain(2),
530 Operation::Delete(3),
531 Operation::Insert("ab".into()),
532 Operation::Retain(5),
533 ]
534 );
535 }
536
537 #[test]
538 fn zero_length_operations_are_dropped() {
539 let mut b = ChangeSet::builder(3);
540 b.retain(0).delete(0).insert("");
541 assert!(b.build().is_identity());
542 }
543
544 #[test]
545 fn an_insert_reports_a_zero_width_span_at_the_insert_point() {
546 let mut builder = ChangeSet::builder(5);
547 builder.retain(2).insert("xy").retain(3);
548 let changes = builder.build();
549 let span = changes.changed_span().expect("not identity");
550 assert_eq!((span.before_start, span.before_end), (2, 2));
551 assert_eq!((span.after_start, span.after_end), (2, 4));
552 }
553
554 #[test]
555 fn a_delete_reports_the_removed_span_and_an_empty_replacement() {
556 let mut builder = ChangeSet::builder(5);
557 builder.retain(1).delete(2).retain(2);
558 let changes = builder.build();
559 let span = changes.changed_span().expect("not identity");
560 assert_eq!((span.before_start, span.before_end), (1, 3));
561 assert_eq!((span.after_start, span.after_end), (1, 1));
562 }
563
564 #[test]
565 fn a_replace_reports_both_spans() {
566 let changes = ChangeSet::replace(5, 1, 3, "abc");
567 let span = changes.changed_span().expect("not identity");
568 assert_eq!((span.before_start, span.before_end), (1, 3));
569 assert_eq!((span.after_start, span.after_end), (1, 4));
570 }
571
572 #[test]
573 fn several_edits_collapse_into_one_covering_span() {
574 let mut builder = ChangeSet::builder(9);
576 builder.retain(1).delete(1).retain(3).insert("zz").retain(4);
577 let changes = builder.build();
578 let span = changes.changed_span().expect("not identity");
579 assert_eq!(span.before_start, 1);
580 assert_eq!(span.before_end, 5);
581 }
582
583 #[test]
584 fn an_identity_changeset_has_no_span() {
585 assert!(ChangeSet::identity(4).changed_span().is_none());
586 }
587
588 #[test]
594 fn positions_are_chars_not_bytes() {
595 let cs = ChangeSet::replace(5, 1, 2, "e");
597 assert_eq!(applied("héllo", &cs), "hello");
598 }
599
600 #[test]
601 fn multibyte_text_survives_insertion_and_inversion() {
602 let original = rope("naïve");
603 let cs = ChangeSet::replace(5, 0, 0, "très ");
604 let changed = cs.apply(&original);
605 assert_eq!(changed.to_string(), "très naïve");
606 assert_eq!(cs.invert(&original).apply(&changed).to_string(), "naïve");
607 }
608
609 #[test]
612 fn invert_round_trips_every_edit_shape() {
613 for (text, cs) in [
614 ("hello world", ChangeSet::replace(11, 6, 11, "there")), ("hello", ChangeSet::replace(5, 5, 5, "!")), ("hello", ChangeSet::replace(5, 0, 2, "")), ("hello", ChangeSet::identity(5)), ] {
619 let original = rope(text);
620 let changed = cs.apply(&original);
621 let undone = cs.invert(&original).apply(&changed);
622 assert_eq!(undone.to_string(), text, "round-trip failed for {cs:?}");
623 }
624 }
625
626 #[test]
627 fn invert_of_invert_is_the_original_change() {
628 let original = rope("hello world");
629 let cs = ChangeSet::replace(11, 0, 5, "goodbye");
630 let changed = cs.apply(&original);
631
632 let back = cs.invert(&original);
633 let forward = back.invert(&changed);
634 assert_eq!(forward.apply(&original).to_string(), changed.to_string());
635 }
636
637 #[test]
640 fn compose_matches_applying_both_in_order() {
641 let original = rope("hello world");
642 let first = ChangeSet::replace(11, 0, 5, "goodbye"); let mid = first.apply(&original);
644 let second = ChangeSet::replace(mid.len_chars(), 8, 13, "everyone"); let composed = first.compose(&second);
647 assert_eq!(composed.apply(&original).to_string(), "goodbye everyone");
648 assert_eq!(composed.len_before(), 11);
649 assert_eq!(composed.len_after(), "goodbye everyone".chars().count());
650 }
651
652 #[test]
653 fn compose_drops_text_that_was_inserted_then_deleted() {
654 let original = rope("ac");
655 let first = ChangeSet::replace(2, 1, 1, "b"); let second = ChangeSet::replace(3, 1, 2, ""); let composed = first.compose(&second);
659 assert_eq!(composed.apply(&original).to_string(), "ac");
660 assert!(composed.is_identity(), "the round trip should compose away entirely");
661 }
662
663 #[test]
664 fn compose_is_associative() {
665 let original = rope("abcdef");
666 let a = ChangeSet::replace(6, 0, 1, "X"); let b = ChangeSet::replace(6, 2, 3, "Y"); let c = ChangeSet::replace(6, 4, 5, "Z"); let left = a.compose(&b).compose(&c);
671 let right = a.compose(&b.compose(&c));
672 assert_eq!(left.apply(&original).to_string(), right.apply(&original).to_string());
673 assert_eq!(left, right, "composition should be associative on the nose");
674 }
675
676 #[test]
677 fn compose_with_identity_is_a_no_op() {
678 let cs = ChangeSet::replace(5, 1, 3, "XY");
679 assert_eq!(ChangeSet::identity(5).compose(&cs), cs);
680 assert_eq!(cs.compose(&ChangeSet::identity(cs.len_after())), cs);
681 }
682
683 #[test]
684 fn typing_one_character_at_a_time_composes_into_one_change() {
685 let original = rope("() {}");
687 let mut composed = ChangeSet::identity(original.len_chars());
688 let mut text = original.clone();
689 for (i, ch) in "abc".chars().enumerate() {
690 let cs = ChangeSet::replace(text.len_chars(), 1 + i, 1 + i, ch.to_string());
691 text = cs.apply(&text);
692 composed = composed.compose(&cs);
693 }
694 assert_eq!(text.to_string(), "(abc) {}");
695 assert_eq!(composed.apply(&original).to_string(), "(abc) {}");
696 }
697
698 #[test]
699 #[should_panic(expected = "authored against a different document")]
700 fn composing_mismatched_changesets_is_a_programming_error() {
701 let a = ChangeSet::identity(5);
702 let b = ChangeSet::identity(9);
703 let _ = a.compose(&b);
704 }
705
706 #[test]
709 fn positions_before_a_change_are_untouched() {
710 let cs = ChangeSet::replace(11, 6, 11, "there");
711 assert_eq!(cs.map_pos(3, Assoc::After), 3);
712 }
713
714 #[test]
715 fn positions_after_an_insertion_shift_by_its_length() {
716 let cs = ChangeSet::replace(11, 0, 0, "abc");
717 assert_eq!(cs.map_pos(5, Assoc::After), 8);
718 }
719
720 #[test]
721 fn assoc_decides_only_at_the_insertion_point() {
722 let cs = ChangeSet::replace(10, 4, 4, "XY");
723 assert_eq!(cs.map_pos(4, Assoc::Before), 4, "Before stays put");
724 assert_eq!(cs.map_pos(4, Assoc::After), 6, "After moves past the insert");
725 assert_eq!(cs.map_pos(3, Assoc::Before), cs.map_pos(3, Assoc::After));
727 assert_eq!(cs.map_pos(5, Assoc::Before), cs.map_pos(5, Assoc::After));
728 }
729
730 #[test]
731 fn positions_inside_a_deleted_range_collapse_to_its_start() {
732 let cs = ChangeSet::replace(10, 2, 6, "");
733 for pos in 2..6 {
734 assert_eq!(cs.map_pos(pos, Assoc::After), 2, "pos {pos} should collapse");
735 }
736 assert_eq!(cs.map_pos(6, Assoc::After), 2, "the end of the range lands there too");
737 assert_eq!(cs.map_pos(7, Assoc::After), 3, "past it, positions shift back");
738 }
739
740 #[test]
741 fn the_end_of_the_document_maps_to_the_new_end() {
742 let cs = ChangeSet::replace(5, 2, 5, "XY");
743 assert_eq!(cs.map_pos(5, Assoc::After), cs.len_after());
744 }
745
746 #[test]
753 fn an_edit_outside_the_range_leaves_it_untouched() {
754 let hunk = (10, 20);
755 for elsewhere in [
756 ChangeSet::replace(40, 0, 5, "x"), ChangeSet::replace(40, 25, 30, "x"), ] {
759 assert_eq!(elsewhere.touches(hunk.0, hunk.1), RangeEffect::Untouched);
760 }
761 }
762
763 #[test]
766 fn an_insertion_inside_the_range_is_detected() {
767 let typed_inside = ChangeSet::replace(40, 15, 15, "hello");
768 assert_eq!(typed_inside.touches(10, 20), RangeEffect::InsertedInside);
769 }
770
771 #[test]
773 fn an_insertion_at_either_boundary_is_not_inside() {
774 for at in [10, 20] {
775 let cs = ChangeSet::replace(40, at, at, "x");
776 assert_eq!(
777 cs.touches(10, 20),
778 RangeEffect::Untouched,
779 "insertion at {at} is a boundary case, resolved by Assoc"
780 );
781 }
782 }
783
784 #[test]
786 fn a_deletion_overlapping_the_range_is_detected() {
787 for (from, to) in [
788 (5, 15), (15, 25), (12, 18), (5, 25), (10, 20), ] {
794 let cs = ChangeSet::replace(40, from, to, "");
795 assert_eq!(
796 cs.touches(10, 20),
797 RangeEffect::PartlyDeleted,
798 "deletion {from}..{to} should be seen"
799 );
800 }
801 }
802
803 #[test]
805 fn a_deletion_ending_or_starting_at_the_boundary_does_not_overlap() {
806 for (from, to) in [(5, 10), (20, 25)] {
807 let cs = ChangeSet::replace(40, from, to, "");
808 assert_eq!(
809 cs.touches(10, 20),
810 RangeEffect::Untouched,
811 "deletion {from}..{to} only touches the boundary"
812 );
813 }
814 }
815
816 #[test]
817 fn a_zero_width_anchor_survives_a_deletion_that_merely_starts_there() {
818 let cs = ChangeSet::replace(40, 10, 15, "");
819 assert_eq!(cs.touches(10, 10), RangeEffect::Untouched, "the anchor still stands");
820
821 let containing = ChangeSet::replace(40, 8, 15, "");
822 assert_eq!(containing.touches(10, 10), RangeEffect::PartlyDeleted, "swallowed");
823 }
824
825 #[test]
826 fn deletion_outranks_insertion_when_a_change_does_both() {
827 let cs = ChangeSet::replace(40, 12, 18, "replacement");
830 assert_eq!(cs.touches(10, 20), RangeEffect::PartlyDeleted);
831 }
832
833 #[test]
834 fn an_untouched_range_is_exactly_what_map_pos_can_be_trusted_for() {
835 let cs = ChangeSet::replace(40, 0, 5, "xx");
838 assert_eq!(cs.touches(10, 20), RangeEffect::Untouched);
839 assert_eq!((cs.map_pos(10, Assoc::After), cs.map_pos(20, Assoc::After)), (7, 17));
840 }
841
842 #[test]
849 fn a_pending_anchor_rides_forward_over_keystrokes_at_the_same_offset() {
850 let original = rope("0123456789tail");
851 let mut text = original.clone();
852 let mut anchor = 10;
853 let mut composed = ChangeSet::identity(original.len_chars());
854
855 for ch in "abc".chars() {
856 let cs = ChangeSet::replace(text.len_chars(), anchor, anchor, ch.to_string());
857 anchor = cs.map_pos(anchor, Assoc::After);
858 composed = composed.compose(&cs);
859 text = cs.apply(&text);
860 }
861
862 assert_eq!(text.to_string(), "0123456789abctail");
863 assert_eq!(anchor, 13, "the hunk sits after the typed text, not inside it");
864 assert_eq!(
865 composed.map_pos(10, Assoc::After),
866 anchor,
867 "per-keystroke and batched rebasing must agree"
868 );
869 }
870}