1use neco_textpatch::{TextPatch, TextPatchError};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum DiffOp {
8 Equal,
9 Insert,
10 Delete,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct ByteRange {
16 start: usize,
17 end: usize,
18}
19
20impl ByteRange {
21 pub fn new(start: usize, end: usize) -> Self {
22 Self { start, end }
23 }
24
25 pub fn start(&self) -> usize {
26 self.start
27 }
28
29 pub fn end(&self) -> usize {
30 self.end
31 }
32
33 pub fn len(&self) -> usize {
34 self.end - self.start
35 }
36
37 pub fn is_empty(&self) -> bool {
38 self.start == self.end
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct DiffLine {
45 op: DiffOp,
46 old_line: Option<u32>,
47 new_line: Option<u32>,
48 old_range: Option<ByteRange>,
49 new_range: Option<ByteRange>,
50}
51
52impl DiffLine {
53 pub fn op(&self) -> DiffOp {
54 self.op
55 }
56
57 pub fn old_line(&self) -> Option<u32> {
58 self.old_line
59 }
60
61 pub fn new_line(&self) -> Option<u32> {
62 self.new_line
63 }
64
65 pub fn old_range(&self) -> Option<ByteRange> {
66 self.old_range
67 }
68
69 pub fn new_range(&self) -> Option<ByteRange> {
70 self.new_range
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct DiffHunk {
77 old_start: u32,
78 old_count: u32,
79 new_start: u32,
80 new_count: u32,
81 lines: Vec<DiffLine>,
82}
83
84impl DiffHunk {
85 pub fn old_start(&self) -> u32 {
86 self.old_start
87 }
88
89 pub fn old_count(&self) -> u32 {
90 self.old_count
91 }
92
93 pub fn new_start(&self) -> u32 {
94 self.new_start
95 }
96
97 pub fn new_count(&self) -> u32 {
98 self.new_count
99 }
100
101 pub fn lines(&self) -> &[DiffLine] {
102 &self.lines
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct DiffResult {
109 lines: Vec<DiffLine>,
110}
111
112impl DiffResult {
113 pub fn lines(&self) -> &[DiffLine] {
114 &self.lines
115 }
116
117 pub fn to_hunks(&self, context_lines: u32) -> Vec<DiffHunk> {
118 let ctx = context_lines as usize;
119 let n = self.lines.len();
120 if n == 0 {
121 return Vec::new();
122 }
123
124 let change_indices: Vec<usize> = self
125 .lines
126 .iter()
127 .enumerate()
128 .filter(|(_, l)| l.op != DiffOp::Equal)
129 .map(|(i, _)| i)
130 .collect();
131
132 if change_indices.is_empty() {
133 return Vec::new();
134 }
135
136 let mut groups: Vec<(usize, usize)> = Vec::new();
137 let mut group_start = change_indices[0].saturating_sub(ctx);
138 let mut group_end = (change_indices[0] + ctx).min(n - 1);
139
140 for &ci in &change_indices[1..] {
141 let lo = ci.saturating_sub(ctx);
142 let hi = (ci + ctx).min(n - 1);
143 if lo <= group_end + 1 {
144 group_end = hi;
145 } else {
146 groups.push((group_start, group_end));
147 group_start = lo;
148 group_end = hi;
149 }
150 }
151 groups.push((group_start, group_end));
152
153 groups
154 .into_iter()
155 .map(|(start, end)| {
156 let hunk_lines: Vec<DiffLine> = self.lines[start..=end].to_vec();
157 let mut old_start = u32::MAX;
158 let mut old_count = 0u32;
159 let mut new_start = u32::MAX;
160 let mut new_count = 0u32;
161 for line in &hunk_lines {
162 if let Some(ol) = line.old_line {
163 if ol < old_start {
164 old_start = ol;
165 }
166 old_count += 1;
167 }
168 if let Some(nl) = line.new_line {
169 if nl < new_start {
170 new_start = nl;
171 }
172 new_count += 1;
173 }
174 }
175 if old_start == u32::MAX {
176 old_start = 0;
177 }
178 if new_start == u32::MAX {
179 new_start = 0;
180 }
181 DiffHunk {
182 old_start,
183 old_count,
184 new_start,
185 new_count,
186 lines: hunk_lines,
187 }
188 })
189 .collect()
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct IntraLineRange {
196 start: usize,
197 end: usize,
198 op: DiffOp,
199}
200
201impl IntraLineRange {
202 pub fn new(start: usize, end: usize, op: DiffOp) -> Self {
203 Self { start, end, op }
204 }
205
206 pub fn start(&self) -> usize {
207 self.start
208 }
209
210 pub fn end(&self) -> usize {
211 self.end
212 }
213
214 pub fn op(&self) -> DiffOp {
215 self.op
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct IntraLineDiff {
222 ranges: Vec<IntraLineRange>,
223}
224
225impl IntraLineDiff {
226 pub fn ranges(&self) -> &[IntraLineRange] {
227 &self.ranges
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SideLine {
234 line: u32,
235 op: DiffOp,
236 range: ByteRange,
237}
238
239impl SideLine {
240 pub fn new(line: u32, op: DiffOp, range: ByteRange) -> Self {
241 Self { line, op, range }
242 }
243
244 pub fn line(&self) -> u32 {
245 self.line
246 }
247
248 pub fn op(&self) -> DiffOp {
249 self.op
250 }
251
252 pub fn range(&self) -> ByteRange {
253 self.range
254 }
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct SideBySideLine {
260 left: Option<SideLine>,
261 right: Option<SideLine>,
262}
263
264impl SideBySideLine {
265 pub fn new(left: Option<SideLine>, right: Option<SideLine>) -> Self {
266 Self { left, right }
267 }
268
269 pub fn left(&self) -> Option<&SideLine> {
270 self.left.as_ref()
271 }
272
273 pub fn right(&self) -> Option<&SideLine> {
274 self.right.as_ref()
275 }
276}
277
278fn myers_diff<T: PartialEq>(old: &[T], new: &[T]) -> Vec<(DiffOp, usize)> {
280 let n = old.len();
281 let m = new.len();
282
283 if n == 0 && m == 0 {
284 return Vec::new();
285 }
286 if n == 0 {
287 return vec![(DiffOp::Insert, m)];
288 }
289 if m == 0 {
290 return vec![(DiffOp::Delete, n)];
291 }
292
293 let max = n + m;
294 let size = 2 * max + 1;
296 let mut v: Vec<usize> = vec![0; size];
297 let off = max; let mut trace: Vec<Vec<usize>> = Vec::new();
302
303 let mut found_d = 0;
304 'search: for d in 0..=max {
305 trace.push(v.clone());
306 let d_i = d as isize;
307 let mut k = -d_i;
308 while k <= d_i {
309 let ki = (k + off as isize) as usize;
310 let mut x = if k == -d_i
311 || (k != d_i
312 && v[(k - 1 + off as isize) as usize] < v[(k + 1 + off as isize) as usize])
313 {
314 v[(k + 1 + off as isize) as usize] } else {
316 v[(k - 1 + off as isize) as usize] + 1 };
318 let mut y = (x as isize - k) as usize;
319
320 while x < n && y < m && old[x] == new[y] {
322 x += 1;
323 y += 1;
324 }
325
326 v[ki] = x;
327
328 if x >= n && y >= m {
329 found_d = d;
330 break 'search;
331 }
332
333 k += 2;
334 }
335 }
336
337 let mut x = n;
339 let mut y = m;
340 let mut edits: Vec<DiffOp> = Vec::new();
341
342 for d in (0..=found_d).rev() {
343 let v_prev = &trace[d];
344 let k = x as isize - y as isize;
345 let d_i = d as isize;
346
347 if d == 0 {
356 while x > 0 && y > 0 {
358 x -= 1;
359 y -= 1;
360 edits.push(DiffOp::Equal);
361 }
362 break;
363 }
364
365 let prev_k = if k == -d_i
367 || (k != d_i
368 && v_prev[(k - 1 + off as isize) as usize]
369 < v_prev[(k + 1 + off as isize) as usize])
370 {
371 k + 1
372 } else {
373 k - 1
374 };
375
376 let prev_x = v_prev[(prev_k + off as isize) as usize];
377 let prev_y = (prev_x as isize - prev_k) as usize;
378
379 while x > prev_x && y > prev_y {
381 x -= 1;
382 y -= 1;
383 edits.push(DiffOp::Equal);
384 }
385
386 if prev_k == k + 1 {
388 y -= 1;
390 edits.push(DiffOp::Insert);
391 } else {
392 x -= 1;
394 edits.push(DiffOp::Delete);
395 }
396 }
397
398 edits.reverse();
399
400 let mut result: Vec<(DiffOp, usize)> = Vec::new();
402 for op in edits {
403 if let Some(last) = result.last_mut() {
404 if last.0 == op {
405 last.1 += 1;
406 continue;
407 }
408 }
409 result.push((op, 1));
410 }
411 result
412}
413
414struct LineInfo {
415 byte_start: usize,
416 byte_end: usize,
417}
418
419fn split_lines(text: &str) -> Vec<LineInfo> {
420 if text.is_empty() {
421 return Vec::new();
422 }
423 let mut lines = Vec::new();
424 let mut start = 0;
425 for (i, ch) in text.char_indices() {
426 if ch == '\n' {
427 lines.push(LineInfo {
428 byte_start: start,
429 byte_end: i + 1,
430 });
431 start = i + 1;
432 }
433 }
434 if start < text.len() {
435 lines.push(LineInfo {
436 byte_start: start,
437 byte_end: text.len(),
438 });
439 }
440 lines
441}
442
443pub fn diff(old: &str, new: &str) -> DiffResult {
445 let old_lines = split_lines(old);
446 let new_lines = split_lines(new);
447
448 let old_strs: Vec<&str> = old_lines
449 .iter()
450 .map(|l| &old[l.byte_start..l.byte_end])
451 .collect();
452 let new_strs: Vec<&str> = new_lines
453 .iter()
454 .map(|l| &new[l.byte_start..l.byte_end])
455 .collect();
456
457 let ops = myers_diff(&old_strs, &new_strs);
458
459 let mut result_lines = Vec::new();
460 let mut old_idx = 0usize;
461 let mut new_idx = 0usize;
462
463 for &(op, count) in &ops {
464 match op {
465 DiffOp::Equal => {
466 for _ in 0..count {
467 let ol = &old_lines[old_idx];
468 let nl = &new_lines[new_idx];
469 result_lines.push(DiffLine {
470 op: DiffOp::Equal,
471 old_line: Some(u32::try_from(old_idx).expect("line count fits u32")),
472 new_line: Some(u32::try_from(new_idx).expect("line count fits u32")),
473 old_range: Some(ByteRange::new(ol.byte_start, ol.byte_end)),
474 new_range: Some(ByteRange::new(nl.byte_start, nl.byte_end)),
475 });
476 old_idx += 1;
477 new_idx += 1;
478 }
479 }
480 DiffOp::Delete => {
481 for _ in 0..count {
482 let ol = &old_lines[old_idx];
483 result_lines.push(DiffLine {
484 op: DiffOp::Delete,
485 old_line: Some(u32::try_from(old_idx).expect("line count fits u32")),
486 new_line: None,
487 old_range: Some(ByteRange::new(ol.byte_start, ol.byte_end)),
488 new_range: None,
489 });
490 old_idx += 1;
491 }
492 }
493 DiffOp::Insert => {
494 for _ in 0..count {
495 let nl = &new_lines[new_idx];
496 result_lines.push(DiffLine {
497 op: DiffOp::Insert,
498 old_line: None,
499 new_line: Some(u32::try_from(new_idx).expect("line count fits u32")),
500 old_range: None,
501 new_range: Some(ByteRange::new(nl.byte_start, nl.byte_end)),
502 });
503 new_idx += 1;
504 }
505 }
506 }
507 }
508
509 DiffResult {
510 lines: result_lines,
511 }
512}
513
514pub fn diff_intra_line(old_line: &str, new_line: &str) -> IntraLineDiff {
516 let old_chars: Vec<char> = old_line.chars().collect();
517 let new_chars: Vec<char> = new_line.chars().collect();
518
519 let ops = myers_diff(&old_chars, &new_chars);
520
521 let mut ranges = Vec::new();
522 let mut old_byte = 0usize;
523 let mut new_byte = 0usize;
524 let mut old_ci = 0usize;
525 let mut new_ci = 0usize;
526
527 for &(op, count) in &ops {
528 match op {
529 DiffOp::Equal => {
530 for _ in 0..count {
531 old_byte += old_chars[old_ci].len_utf8();
532 new_byte += new_chars[new_ci].len_utf8();
533 old_ci += 1;
534 new_ci += 1;
535 }
536 }
537 DiffOp::Delete => {
538 let start = old_byte;
539 for _ in 0..count {
540 old_byte += old_chars[old_ci].len_utf8();
541 old_ci += 1;
542 }
543 ranges.push(IntraLineRange::new(start, old_byte, DiffOp::Delete));
544 }
545 DiffOp::Insert => {
546 let start = new_byte;
547 for _ in 0..count {
548 new_byte += new_chars[new_ci].len_utf8();
549 new_ci += 1;
550 }
551 ranges.push(IntraLineRange::new(start, new_byte, DiffOp::Insert));
552 }
553 }
554 }
555
556 IntraLineDiff { ranges }
557}
558
559pub fn to_side_by_side(result: &DiffResult) -> Vec<SideBySideLine> {
561 let lines = result.lines();
562 let mut output = Vec::new();
563 let mut i = 0;
564
565 while i < lines.len() {
566 match lines[i].op {
567 DiffOp::Equal => {
568 let l = &lines[i];
569 output.push(SideBySideLine::new(
570 Some(SideLine::new(
571 l.old_line.unwrap(),
572 DiffOp::Equal,
573 l.old_range.unwrap(),
574 )),
575 Some(SideLine::new(
576 l.new_line.unwrap(),
577 DiffOp::Equal,
578 l.new_range.unwrap(),
579 )),
580 ));
581 i += 1;
582 }
583 DiffOp::Delete => {
584 let mut deletes = Vec::new();
585 while i < lines.len() && lines[i].op == DiffOp::Delete {
586 deletes.push(&lines[i]);
587 i += 1;
588 }
589 let mut inserts = Vec::new();
590 while i < lines.len() && lines[i].op == DiffOp::Insert {
591 inserts.push(&lines[i]);
592 i += 1;
593 }
594 let max_len = deletes.len().max(inserts.len());
595 for j in 0..max_len {
596 let left = deletes.get(j).map(|d| {
597 SideLine::new(d.old_line.unwrap(), DiffOp::Delete, d.old_range.unwrap())
598 });
599 let right = inserts.get(j).map(|ins| {
600 SideLine::new(
601 ins.new_line.unwrap(),
602 DiffOp::Insert,
603 ins.new_range.unwrap(),
604 )
605 });
606 output.push(SideBySideLine::new(left, right));
607 }
608 }
609 DiffOp::Insert => {
610 let l = &lines[i];
611 output.push(SideBySideLine::new(
612 None,
613 Some(SideLine::new(
614 l.new_line.unwrap(),
615 DiffOp::Insert,
616 l.new_range.unwrap(),
617 )),
618 ));
619 i += 1;
620 }
621 }
622 }
623
624 output
625}
626
627pub fn diff_to_patches(new: &str, result: &DiffResult) -> Result<Vec<TextPatch>, TextPatchError> {
629 let mut patches = Vec::new();
630 let lines = result.lines();
631 let mut i = 0;
632
633 while i < lines.len() {
634 match lines[i].op {
635 DiffOp::Equal => {
636 i += 1;
637 }
638 DiffOp::Delete => {
639 let delete_start = i;
640 while i < lines.len() && lines[i].op == DiffOp::Delete {
641 i += 1;
642 }
643 let delete_end = i;
644
645 let insert_start = i;
646 while i < lines.len() && lines[i].op == DiffOp::Insert {
647 i += 1;
648 }
649 let insert_end = i;
650
651 let first_del = &lines[delete_start];
652 let last_del = &lines[delete_end - 1];
653 let del_byte_start = first_del.old_range.unwrap().start();
654 let del_byte_end = last_del.old_range.unwrap().end();
655
656 if insert_start < insert_end {
657 let first_ins = &lines[insert_start];
658 let last_ins = &lines[insert_end - 1];
659 let ins_byte_start = first_ins.new_range.unwrap().start();
660 let ins_byte_end = last_ins.new_range.unwrap().end();
661 let replacement = &new[ins_byte_start..ins_byte_end];
662 patches.push(TextPatch::replace(
663 del_byte_start,
664 del_byte_end,
665 replacement,
666 )?);
667 } else {
668 patches.push(TextPatch::delete(del_byte_start, del_byte_end)?);
669 }
670 }
671 DiffOp::Insert => {
672 let insert_start = i;
673 while i < lines.len() && lines[i].op == DiffOp::Insert {
674 i += 1;
675 }
676 let insert_end = i;
677
678 let first_ins = &lines[insert_start];
679 let last_ins = &lines[insert_end - 1];
680 let ins_byte_start = first_ins.new_range.unwrap().start();
681 let ins_byte_end = last_ins.new_range.unwrap().end();
682 let replacement = &new[ins_byte_start..ins_byte_end];
683
684 let offset = if insert_start > 0 {
685 let prev = &lines[insert_start - 1];
686 if let Some(r) = prev.old_range {
687 r.end()
688 } else {
689 0
690 }
691 } else {
692 0
693 };
694
695 patches.push(TextPatch::insert(offset, replacement));
696 }
697 }
698 }
699
700 Ok(patches)
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use neco_textpatch::apply_patches;
707
708 #[test]
709 fn diff_empty_texts() {
710 let result = diff("", "");
711 assert!(result.lines().is_empty());
712 }
713
714 #[test]
715 fn diff_identical_texts() {
716 let text = "hello\nworld\n";
717 let result = diff(text, text);
718 assert!(result.lines().iter().all(|l| l.op() == DiffOp::Equal));
719 assert_eq!(result.lines().len(), 2);
720 }
721
722 #[test]
723 fn diff_all_deleted() {
724 let result = diff("a\nb\n", "");
725 assert!(result.lines().iter().all(|l| l.op() == DiffOp::Delete));
726 assert_eq!(result.lines().len(), 2);
727 }
728
729 #[test]
730 fn diff_all_inserted() {
731 let result = diff("", "x\ny\n");
732 assert!(result.lines().iter().all(|l| l.op() == DiffOp::Insert));
733 assert_eq!(result.lines().len(), 2);
734 }
735
736 #[test]
737 fn diff_mixed_changes() {
738 let old = "a\nb\nc\nd\n";
739 let new = "a\nB\nc\nD\n";
740 let result = diff(old, new);
741 let ops: Vec<DiffOp> = result.lines().iter().map(|l| l.op()).collect();
742 assert_eq!(
743 ops,
744 vec![
745 DiffOp::Equal,
746 DiffOp::Delete,
747 DiffOp::Insert,
748 DiffOp::Equal,
749 DiffOp::Delete,
750 DiffOp::Insert,
751 ]
752 );
753 }
754
755 #[test]
756 fn diff_multibyte() {
757 let old = "こんにちは\n世界\n";
758 let new = "こんにちは\n宇宙\n";
759 let result = diff(old, new);
760 assert_eq!(result.lines().len(), 3);
761 assert_eq!(result.lines()[0].op(), DiffOp::Equal);
762 assert_eq!(result.lines()[1].op(), DiffOp::Delete);
763 assert_eq!(result.lines()[2].op(), DiffOp::Insert);
764 }
765
766 #[test]
767 fn to_hunks_splits_by_context() {
768 let old = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n";
769 let new = "1\n2\n3\n4\nFIVE\n6\n7\n8\n9\nTEN\n";
770 let result = diff(old, new);
771 let hunks = result.to_hunks(1);
772 assert_eq!(hunks.len(), 2);
773 assert!(hunks[0].lines().iter().any(|l| l.op() != DiffOp::Equal));
774 assert!(hunks[1].lines().iter().any(|l| l.op() != DiffOp::Equal));
775 }
776
777 #[test]
778 fn to_hunks_merges_close_changes() {
779 let old = "1\n2\n3\n4\n5\n";
780 let new = "ONE\n2\n3\n4\nFIVE\n";
781 let result = diff(old, new);
782 let hunks = result.to_hunks(3);
783 assert_eq!(hunks.len(), 1);
784 }
785
786 #[test]
787 fn diff_intra_line_detects_changes() {
788 let result = diff_intra_line("hello world", "hello there");
789 assert!(!result.ranges().is_empty());
790 let has_delete = result.ranges().iter().any(|r| r.op() == DiffOp::Delete);
791 let has_insert = result.ranges().iter().any(|r| r.op() == DiffOp::Insert);
792 assert!(has_delete);
793 assert!(has_insert);
794 }
795
796 #[test]
797 fn diff_intra_line_multibyte() {
798 let result = diff_intra_line("あいう", "あえう");
799 assert!(!result.ranges().is_empty());
800 let has_delete = result.ranges().iter().any(|r| r.op() == DiffOp::Delete);
801 let has_insert = result.ranges().iter().any(|r| r.op() == DiffOp::Insert);
802 assert!(has_delete);
803 assert!(has_insert);
804 }
805
806 #[test]
807 fn side_by_side_layout() {
808 let old = "a\nb\nc\n";
809 let new = "a\nB\nc\n";
810 let result = diff(old, new);
811 let sbs = to_side_by_side(&result);
812
813 assert!(sbs[0].left().is_some() && sbs[0].right().is_some());
814 assert_eq!(sbs[0].left().unwrap().op(), DiffOp::Equal);
815
816 let delete_line = sbs
817 .iter()
818 .find(|l| l.left().is_some_and(|s| s.op() == DiffOp::Delete));
819 assert!(delete_line.is_some());
820
821 let insert_line = sbs
822 .iter()
823 .find(|l| l.right().is_some_and(|s| s.op() == DiffOp::Insert));
824 assert!(insert_line.is_some());
825 }
826
827 #[test]
828 fn side_by_side_insert_only() {
829 let old = "a\n";
830 let new = "a\nb\n";
831 let result = diff(old, new);
832 let sbs = to_side_by_side(&result);
833 let insert_only = sbs
834 .iter()
835 .find(|l| l.left().is_none() && l.right().is_some());
836 assert!(insert_only.is_some());
837 }
838
839 #[test]
840 fn side_by_side_delete_only() {
841 let old = "a\nb\n";
842 let new = "a\n";
843 let result = diff(old, new);
844 let sbs = to_side_by_side(&result);
845 let delete_only = sbs
846 .iter()
847 .find(|l| l.left().is_some() && l.right().is_none());
848 assert!(delete_only.is_some());
849 }
850
851 fn roundtrip(old: &str, new: &str) {
852 let result = diff(old, new);
853 let patches = diff_to_patches(new, &result).expect("patches should be created");
854 let applied = apply_patches(old, &patches).expect("patches should apply");
855 assert_eq!(applied, new, "roundtrip failed for old={old:?} new={new:?}");
856 }
857
858 #[test]
859 fn roundtrip_empty_to_empty() {
860 roundtrip("", "");
861 }
862
863 #[test]
864 fn roundtrip_identical() {
865 roundtrip("hello\nworld\n", "hello\nworld\n");
866 }
867
868 #[test]
869 fn roundtrip_all_deleted() {
870 roundtrip("a\nb\nc\n", "");
871 }
872
873 #[test]
874 fn roundtrip_all_inserted() {
875 roundtrip("", "x\ny\nz\n");
876 }
877
878 #[test]
879 fn roundtrip_mixed_changes() {
880 roundtrip("a\nb\nc\nd\n", "a\nB\nc\nD\n");
881 }
882
883 #[test]
884 fn roundtrip_insert_at_beginning() {
885 roundtrip("b\nc\n", "a\nb\nc\n");
886 }
887
888 #[test]
889 fn roundtrip_insert_at_end() {
890 roundtrip("a\nb\n", "a\nb\nc\n");
891 }
892
893 #[test]
894 fn roundtrip_delete_at_beginning() {
895 roundtrip("a\nb\nc\n", "b\nc\n");
896 }
897
898 #[test]
899 fn roundtrip_delete_at_end() {
900 roundtrip("a\nb\nc\n", "a\nb\n");
901 }
902
903 #[test]
904 fn roundtrip_multibyte() {
905 roundtrip("こんにちは\n世界\n", "こんにちは\n宇宙\n");
906 }
907
908 #[test]
909 fn roundtrip_complex() {
910 let old = "line1\nline2\nline3\nline4\nline5\n";
911 let new = "line1\nmodified2\nline3\nnew_line\nline5\nextra\n";
912 roundtrip(old, new);
913 }
914
915 #[test]
916 fn roundtrip_no_trailing_newline() {
917 roundtrip("hello\nworld", "hello\nthere");
918 }
919
920 #[test]
921 fn roundtrip_consecutive_deletes_and_inserts() {
922 roundtrip("a\nb\nc\n", "x\ny\nz\n");
923 }
924}