1use crate::cell::Cell;
23use crate::grid::Grid;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct DirtySpan {
30 pub start: u16,
32 pub end: u16,
34}
35
36impl DirtySpan {
37 pub fn new(start: u16, end: u16) -> Self {
39 Self { start, end }
40 }
41
42 #[inline]
44 pub fn len(&self) -> u16 {
45 self.end.saturating_sub(self.start)
46 }
47
48 #[inline]
50 pub fn is_empty(&self) -> bool {
51 self.end <= self.start
52 }
53
54 fn mergeable(&self, other: &Self, merge_gap: u16) -> bool {
56 let other_end = other.end.saturating_add(merge_gap);
58 let self_end = self.end.saturating_add(merge_gap);
59 !(self.start > other_end || other.start > self_end)
60 }
61
62 fn merge(&mut self, other: &Self) {
64 self.start = self.start.min(other.start);
65 self.end = self.end.max(other.end);
66 }
67}
68
69#[derive(Debug, Clone)]
73struct DirtyRow {
74 full: bool,
76 spans: Vec<DirtySpan>,
78}
79
80impl DirtyRow {
81 fn new() -> Self {
82 Self {
83 full: false,
84 spans: Vec::new(),
85 }
86 }
87
88 fn is_dirty(&self) -> bool {
89 self.full || !self.spans.is_empty()
90 }
91
92 fn mark_full(&mut self) {
93 self.full = true;
94 self.spans.clear();
95 }
96
97 fn mark_span(&mut self, start: u16, end: u16, merge_gap: u16) {
98 if self.full {
99 return;
100 }
101 let new_span = DirtySpan::new(start, end);
102 let mut merged = false;
104 for span in &mut self.spans {
105 if span.mergeable(&new_span, merge_gap) {
106 span.merge(&new_span);
107 merged = true;
108 break;
109 }
110 }
111 if !merged {
112 self.spans.push(new_span);
113 }
114 self.coalesce(merge_gap);
116 }
117
118 fn coalesce(&mut self, merge_gap: u16) {
119 if self.spans.len() < 2 {
120 return;
121 }
122 self.spans.sort_by_key(|s| s.start);
123 let mut write = 0;
124 for read in 1..self.spans.len() {
125 if self.spans[write].mergeable(&self.spans[read], merge_gap) {
126 let other = self.spans[read];
127 self.spans[write].merge(&other);
128 } else {
129 write += 1;
130 self.spans[write] = self.spans[read];
131 }
132 }
133 self.spans.truncate(write + 1);
134 }
135
136 fn clear(&mut self) {
137 self.full = false;
138 self.spans.clear();
139 }
140}
141
142#[derive(Debug, Clone)]
156pub struct DirtyTracker {
157 rows: Vec<DirtyRow>,
158 cols: u16,
159 merge_gap: u16,
160 dirty_count: usize,
162}
163
164impl DirtyTracker {
165 pub fn new(cols: u16, rows: u16) -> Self {
167 Self {
168 rows: (0..rows).map(|_| DirtyRow::new()).collect(),
169 cols,
170 merge_gap: 1,
171 dirty_count: 0,
172 }
173 }
174
175 pub fn row_count(&self) -> u16 {
177 self.rows.len() as u16
178 }
179
180 pub fn set_merge_gap(&mut self, gap: u16) {
182 self.merge_gap = gap;
183 }
184
185 pub fn is_dirty(&self) -> bool {
187 self.dirty_count > 0
188 }
189
190 pub fn dirty_row_count(&self) -> usize {
192 self.dirty_count
193 }
194
195 pub fn mark_cell(&mut self, row: u16, col: u16) {
197 if col >= self.cols {
198 return;
199 }
200 if let Some(dr) = self.rows.get_mut(row as usize) {
201 let was_dirty = dr.is_dirty();
202 let end = col.saturating_add(1).min(self.cols);
203 dr.mark_span(col, end, self.merge_gap);
204 if !was_dirty && dr.is_dirty() {
205 self.dirty_count += 1;
206 }
207 }
208 }
209
210 pub fn mark_span(&mut self, row: u16, start_col: u16, end_col: u16) {
212 if start_col >= self.cols || end_col <= start_col {
213 return;
214 }
215 let end = end_col.min(self.cols);
216 if end <= start_col {
217 return;
218 }
219 if let Some(dr) = self.rows.get_mut(row as usize) {
220 let was_dirty = dr.is_dirty();
221 dr.mark_span(start_col, end, self.merge_gap);
222 if !was_dirty && dr.is_dirty() {
223 self.dirty_count += 1;
224 }
225 }
226 }
227
228 pub fn mark_row(&mut self, row: u16) {
230 if let Some(dr) = self.rows.get_mut(row as usize) {
231 let was_dirty = dr.is_dirty();
232 dr.mark_full();
233 if !was_dirty {
234 self.dirty_count += 1;
235 }
236 }
237 }
238
239 pub fn mark_all(&mut self) {
241 for dr in &mut self.rows {
242 dr.mark_full();
243 }
244 self.dirty_count = self.rows.len();
245 }
246
247 pub fn clear(&mut self) {
249 for dr in &mut self.rows {
250 dr.clear();
251 }
252 self.dirty_count = 0;
253 }
254
255 pub fn is_row_dirty(&self, row: u16) -> bool {
257 self.rows.get(row as usize).is_some_and(|dr| dr.is_dirty())
258 }
259
260 pub fn row_spans(&self, row: u16) -> Option<&[DirtySpan]> {
266 self.rows.get(row as usize).and_then(|dr| {
267 if dr.full {
268 None } else {
270 Some(dr.spans.as_slice())
271 }
272 })
273 }
274
275 pub fn resize(&mut self, new_cols: u16, new_rows: u16) {
277 self.cols = new_cols;
278 self.rows.resize_with(new_rows as usize, DirtyRow::new);
279 self.mark_all();
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct ChangeRun {
292 pub row: u16,
294 pub start_col: u16,
296 pub end_col: u16,
298}
299
300impl ChangeRun {
301 #[inline]
303 pub fn len(&self) -> u16 {
304 self.end_col.saturating_sub(self.start_col)
305 }
306
307 #[inline]
309 pub fn is_empty(&self) -> bool {
310 self.end_col <= self.start_col
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct CellUpdate {
322 pub row: u16,
323 pub col: u16,
324 pub cell: Cell,
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Default)]
334pub struct Patch {
335 pub cols: u16,
337 pub rows: u16,
339 pub updates: Vec<CellUpdate>,
341}
342
343impl Patch {
344 #[must_use]
346 pub fn new(cols: u16, rows: u16) -> Self {
347 Self {
348 cols,
349 rows,
350 updates: Vec::new(),
351 }
352 }
353
354 #[inline]
356 #[must_use]
357 pub fn is_empty(&self) -> bool {
358 self.updates.is_empty()
359 }
360
361 #[inline]
363 #[must_use]
364 pub fn len(&self) -> usize {
365 self.updates.len()
366 }
367
368 pub fn push(&mut self, row: u16, col: u16, cell: Cell) {
370 self.updates.push(CellUpdate { row, col, cell });
371 }
372
373 #[must_use]
378 pub fn runs(&self) -> Vec<ChangeRun> {
379 let mut runs = Vec::new();
380 self.runs_into(&mut runs);
381 runs
382 }
383
384 pub fn runs_into(&self, out: &mut Vec<ChangeRun>) {
388 out.clear();
389 if self.updates.is_empty() {
390 return;
391 }
392
393 let mut current_row = self.updates[0].row;
394 let mut start_col = self.updates[0].col;
395 let mut end_col = self.updates[0].col.saturating_add(1);
396
397 for update in &self.updates[1..] {
398 if update.row == current_row && update.col == end_col {
399 end_col = update.col.saturating_add(1);
401 } else {
402 out.push(ChangeRun {
404 row: current_row,
405 start_col,
406 end_col,
407 });
408 current_row = update.row;
409 start_col = update.col;
410 end_col = update.col.saturating_add(1);
411 }
412 }
413 out.push(ChangeRun {
415 row: current_row,
416 start_col,
417 end_col,
418 });
419 }
420
421 #[must_use]
425 pub fn density(&self) -> f64 {
426 let total = self.cols as usize * self.rows as usize;
427 if total == 0 {
428 return 0.0;
429 }
430 self.updates.len() as f64 / total as f64
431 }
432}
433
434pub struct GridDiff;
438
439impl GridDiff {
440 pub fn diff(old: &Grid, new: &Grid) -> Patch {
444 let cols = new.cols();
445 let rows = new.rows();
446 let mut patch = Patch::new(cols, rows);
447
448 for r in 0..rows {
449 for c in 0..cols {
450 let old_cell = old.cell(r, c);
451 let new_cell = new.cell(r, c);
452 match (old_cell, new_cell) {
453 (Some(o), Some(n)) if o != n => {
454 patch.push(r, c, *n);
455 }
456 (None, Some(n)) => {
457 patch.push(r, c, *n);
458 }
459 _ => {}
460 }
461 }
462 }
463
464 patch
465 }
466
467 pub fn diff_dirty(old: &Grid, new: &Grid, tracker: &DirtyTracker) -> Patch {
477 let cols = new.cols();
478 let rows = new.rows();
479 let mut patch = Patch::new(cols, rows);
480
481 if !tracker.is_dirty() {
482 return patch;
483 }
484
485 for r in 0..rows {
486 if !tracker.is_row_dirty(r) {
487 continue;
488 }
489
490 match tracker.row_spans(r) {
491 None => {
492 for c in 0..cols {
494 let old_cell = old.cell(r, c);
495 let new_cell = new.cell(r, c);
496 match (old_cell, new_cell) {
497 (Some(o), Some(n)) if o != n => {
498 patch.push(r, c, *n);
499 }
500 (None, Some(n)) => {
501 patch.push(r, c, *n);
502 }
503 _ => {}
504 }
505 }
506 }
507 Some(spans) => {
508 for span in spans {
510 let start = span.start.min(cols);
511 let end = span.end.min(cols);
512 for c in start..end {
513 let old_cell = old.cell(r, c);
514 let new_cell = new.cell(r, c);
515 match (old_cell, new_cell) {
516 (Some(o), Some(n)) if o != n => {
517 patch.push(r, c, *n);
518 }
519 (None, Some(n)) => {
520 patch.push(r, c, *n);
521 }
522 _ => {}
523 }
524 }
525 }
526 }
527 }
528 }
529
530 patch
531 }
532
533 pub fn diff_into(old: &Grid, new: &Grid, patch: &mut Patch) {
535 patch.cols = new.cols();
536 patch.rows = new.rows();
537 patch.updates.clear();
538
539 let cols = new.cols();
540 let rows = new.rows();
541
542 for r in 0..rows {
543 for c in 0..cols {
544 let old_cell = old.cell(r, c);
545 let new_cell = new.cell(r, c);
546 match (old_cell, new_cell) {
547 (Some(o), Some(n)) if o != n => {
548 patch.push(r, c, *n);
549 }
550 (None, Some(n)) => {
551 patch.push(r, c, *n);
552 }
553 _ => {}
554 }
555 }
556 }
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 #[test]
566 fn empty_patch_is_empty() {
567 let p = Patch::new(80, 24);
568 assert!(p.is_empty());
569 assert_eq!(p.len(), 0);
570 assert_eq!(p.cols, 80);
571 assert_eq!(p.rows, 24);
572 }
573
574 #[test]
575 fn push_adds_update() {
576 let mut p = Patch::new(2, 2);
577 p.push(0, 1, Cell::new('X'));
578 assert_eq!(p.len(), 1);
579 assert_eq!(p.updates[0].row, 0);
580 assert_eq!(p.updates[0].col, 1);
581 assert_eq!(p.updates[0].cell.content(), 'X');
582 }
583
584 #[test]
585 fn density_calculation() {
586 let mut p = Patch::new(10, 10);
587 for i in 0..25u16 {
588 p.push(i / 10, i % 10, Cell::new('X'));
589 }
590 let d = p.density();
591 assert!((d - 0.25).abs() < f64::EPSILON);
592 }
593
594 #[test]
595 fn density_empty_grid() {
596 let p = Patch::new(0, 0);
597 assert_eq!(p.density(), 0.0);
598 }
599
600 #[test]
603 fn runs_empty_patch() {
604 let p = Patch::new(10, 5);
605 assert!(p.runs().is_empty());
606 }
607
608 #[test]
609 fn runs_single_cell() {
610 let mut p = Patch::new(10, 5);
611 p.push(2, 5, Cell::new('A'));
612 let runs = p.runs();
613 assert_eq!(runs.len(), 1);
614 assert_eq!(runs[0].row, 2);
615 assert_eq!(runs[0].start_col, 5);
616 assert_eq!(runs[0].end_col, 6);
617 assert_eq!(runs[0].len(), 1);
618 }
619
620 #[test]
621 fn runs_coalesces_adjacent_cells() {
622 let mut p = Patch::new(10, 5);
623 p.push(1, 3, Cell::new('A'));
624 p.push(1, 4, Cell::new('B'));
625 p.push(1, 5, Cell::new('C'));
626 let runs = p.runs();
627 assert_eq!(runs.len(), 1);
628 assert_eq!(runs[0].row, 1);
629 assert_eq!(runs[0].start_col, 3);
630 assert_eq!(runs[0].end_col, 6);
631 assert_eq!(runs[0].len(), 3);
632 }
633
634 #[test]
635 fn runs_gap_creates_separate_runs() {
636 let mut p = Patch::new(10, 5);
637 p.push(0, 1, Cell::new('A'));
638 p.push(0, 5, Cell::new('B'));
639 let runs = p.runs();
640 assert_eq!(runs.len(), 2);
641 assert_eq!(runs[0].start_col, 1);
642 assert_eq!(runs[0].end_col, 2);
643 assert_eq!(runs[1].start_col, 5);
644 assert_eq!(runs[1].end_col, 6);
645 }
646
647 #[test]
648 fn runs_different_rows() {
649 let mut p = Patch::new(10, 5);
650 p.push(0, 0, Cell::new('A'));
651 p.push(0, 1, Cell::new('B'));
652 p.push(2, 3, Cell::new('C'));
653 let runs = p.runs();
654 assert_eq!(runs.len(), 2);
655 assert_eq!(runs[0].row, 0);
656 assert_eq!(runs[0].start_col, 0);
657 assert_eq!(runs[0].end_col, 2);
658 assert_eq!(runs[1].row, 2);
659 assert_eq!(runs[1].start_col, 3);
660 assert_eq!(runs[1].end_col, 4);
661 }
662
663 #[test]
664 fn runs_into_reuses_buffer() {
665 let mut p = Patch::new(10, 5);
666 p.push(0, 0, Cell::new('A'));
667 p.push(0, 1, Cell::new('B'));
668
669 let mut buf = Vec::new();
670 p.runs_into(&mut buf);
671 assert_eq!(buf.len(), 1);
672 p.runs_into(&mut buf);
674 assert_eq!(buf.len(), 1); }
676
677 #[test]
680 fn tracker_new_is_clean() {
681 let t = DirtyTracker::new(80, 24);
682 assert!(!t.is_dirty());
683 assert_eq!(t.dirty_row_count(), 0);
684 assert_eq!(t.row_count(), 24);
685 }
686
687 #[test]
688 fn tracker_mark_cell() {
689 let mut t = DirtyTracker::new(80, 24);
690 t.mark_cell(5, 10);
691 assert!(t.is_dirty());
692 assert!(t.is_row_dirty(5));
693 assert!(!t.is_row_dirty(4));
694 assert_eq!(t.dirty_row_count(), 1);
695
696 let spans = t.row_spans(5).unwrap();
697 assert_eq!(spans.len(), 1);
698 assert_eq!(spans[0].start, 10);
699 assert_eq!(spans[0].end, 11);
700 }
701
702 #[test]
703 fn tracker_mark_span() {
704 let mut t = DirtyTracker::new(80, 24);
705 t.mark_span(3, 5, 15);
706 let spans = t.row_spans(3).unwrap();
707 assert_eq!(spans.len(), 1);
708 assert_eq!(spans[0].start, 5);
709 assert_eq!(spans[0].end, 15);
710 }
711
712 #[test]
713 fn tracker_mark_row() {
714 let mut t = DirtyTracker::new(80, 24);
715 t.mark_row(10);
716 assert!(t.is_row_dirty(10));
717 assert!(t.row_spans(10).is_none());
719 }
720
721 #[test]
722 fn tracker_mark_all() {
723 let mut t = DirtyTracker::new(80, 24);
724 t.mark_all();
725 assert_eq!(t.dirty_row_count(), 24);
726 for r in 0..24 {
727 assert!(t.is_row_dirty(r));
728 }
729 }
730
731 #[test]
732 fn tracker_clear() {
733 let mut t = DirtyTracker::new(80, 24);
734 t.mark_all();
735 t.clear();
736 assert!(!t.is_dirty());
737 assert_eq!(t.dirty_row_count(), 0);
738 }
739
740 #[test]
741 fn tracker_adjacent_spans_merge() {
742 let mut t = DirtyTracker::new(80, 24);
743 t.mark_span(0, 5, 10);
744 t.mark_span(0, 10, 15); let spans = t.row_spans(0).unwrap();
746 assert_eq!(spans.len(), 1);
747 assert_eq!(spans[0].start, 5);
748 assert_eq!(spans[0].end, 15);
749 }
750
751 #[test]
752 fn tracker_gap_within_merge_gap() {
753 let mut t = DirtyTracker::new(80, 24);
754 t.set_merge_gap(2);
755 t.mark_span(0, 5, 8);
756 t.mark_span(0, 10, 15); let spans = t.row_spans(0).unwrap();
758 assert_eq!(spans.len(), 1);
759 assert_eq!(spans[0].start, 5);
760 assert_eq!(spans[0].end, 15);
761 }
762
763 #[test]
764 fn tracker_gap_beyond_merge_gap() {
765 let mut t = DirtyTracker::new(80, 24);
766 t.set_merge_gap(1);
767 t.mark_span(0, 5, 8);
768 t.mark_span(0, 20, 25); let spans = t.row_spans(0).unwrap();
770 assert_eq!(spans.len(), 2);
771 }
772
773 #[test]
774 fn tracker_out_of_bounds_ignored() {
775 let mut t = DirtyTracker::new(80, 24);
776 t.mark_cell(99, 99);
777 assert!(!t.is_dirty());
778 }
779
780 #[test]
781 fn tracker_out_of_bounds_col_ignored() {
782 let mut t = DirtyTracker::new(80, 24);
783 t.mark_cell(0, u16::MAX);
784 t.mark_cell(0, 80); t.mark_span(0, 80, 81); t.mark_span(0, 10, 10); assert!(!t.is_dirty());
788 }
789
790 #[test]
791 fn tracker_resize_marks_all_dirty() {
792 let mut t = DirtyTracker::new(80, 24);
793 t.resize(120, 40);
794 assert_eq!(t.row_count(), 40);
795 assert_eq!(t.dirty_row_count(), 40);
796 }
797
798 #[test]
801 fn diff_identical_grids_empty_patch() {
802 let a = Grid::new(10, 5);
803 let b = Grid::new(10, 5);
804 let patch = GridDiff::diff(&a, &b);
805 assert!(patch.is_empty());
806 }
807
808 #[test]
809 fn diff_detects_single_change() {
810 let a = Grid::new(10, 5);
811 let mut b = Grid::new(10, 5);
812 b.cell_mut(2, 3).unwrap().set_content('X', 1);
813 let patch = GridDiff::diff(&a, &b);
814 assert_eq!(patch.len(), 1);
815 assert_eq!(patch.updates[0].row, 2);
816 assert_eq!(patch.updates[0].col, 3);
817 assert_eq!(patch.updates[0].cell.content(), 'X');
818 }
819
820 #[test]
821 fn diff_detects_multiple_changes() {
822 let a = Grid::new(5, 3);
823 let mut b = Grid::new(5, 3);
824 b.cell_mut(0, 0).unwrap().set_content('A', 1);
825 b.cell_mut(1, 2).unwrap().set_content('B', 1);
826 b.cell_mut(2, 4).unwrap().set_content('C', 1);
827 let patch = GridDiff::diff(&a, &b);
828 assert_eq!(patch.len(), 3);
829 assert_eq!(patch.updates[0].row, 0);
831 assert_eq!(patch.updates[1].row, 1);
832 assert_eq!(patch.updates[2].row, 2);
833 }
834
835 #[test]
836 fn diff_into_reuses_patch() {
837 let a = Grid::new(5, 3);
838 let mut b = Grid::new(5, 3);
839 b.cell_mut(1, 1).unwrap().set_content('Z', 1);
840
841 let mut patch = Patch::new(0, 0);
842 GridDiff::diff_into(&a, &b, &mut patch);
843 assert_eq!(patch.len(), 1);
844 assert_eq!(patch.cols, 5);
845 assert_eq!(patch.rows, 3);
846 }
847
848 #[test]
849 fn diff_dirty_skips_clean_rows() {
850 let a = Grid::new(10, 5);
851 let mut b = Grid::new(10, 5);
852 b.cell_mut(1, 0).unwrap().set_content('A', 1);
854 b.cell_mut(3, 5).unwrap().set_content('B', 1);
855
856 let mut tracker = DirtyTracker::new(10, 5);
858 tracker.mark_row(1);
859
860 let patch = GridDiff::diff_dirty(&a, &b, &tracker);
861 assert_eq!(patch.len(), 1);
863 assert_eq!(patch.updates[0].row, 1);
864 assert_eq!(patch.updates[0].col, 0);
865 }
866
867 #[test]
868 fn diff_dirty_uses_spans() {
869 let a = Grid::new(10, 5);
870 let mut b = Grid::new(10, 5);
871 b.cell_mut(0, 2).unwrap().set_content('A', 1);
873 b.cell_mut(0, 8).unwrap().set_content('B', 1);
874
875 let mut tracker = DirtyTracker::new(10, 5);
876 tracker.mark_span(0, 0, 5);
878
879 let patch = GridDiff::diff_dirty(&a, &b, &tracker);
880 assert_eq!(patch.len(), 1);
882 assert_eq!(patch.updates[0].col, 2);
883 }
884
885 #[test]
886 fn diff_dirty_clean_tracker_empty_patch() {
887 let a = Grid::new(10, 5);
888 let mut b = Grid::new(10, 5);
889 b.cell_mut(0, 0).unwrap().set_content('X', 1);
890
891 let tracker = DirtyTracker::new(10, 5); let patch = GridDiff::diff_dirty(&a, &b, &tracker);
893 assert!(patch.is_empty());
894 }
895
896 #[test]
897 fn diff_detects_attribute_changes() {
898 let a = Grid::new(5, 1);
899 let mut b = Grid::new(5, 1);
900 let cell = b.cell_mut(0, 2).unwrap();
902 cell.attrs.flags = crate::cell::SgrFlags::BOLD;
903
904 let patch = GridDiff::diff(&a, &b);
905 assert_eq!(patch.len(), 1);
906 assert_eq!(patch.updates[0].col, 2);
907 }
908
909 #[test]
910 fn diff_row_major_order_guaranteed() {
911 let a = Grid::new(3, 3);
912 let mut b = Grid::new(3, 3);
913 b.cell_mut(2, 2).unwrap().set_content('Z', 1);
915 b.cell_mut(1, 1).unwrap().set_content('Y', 1);
916 b.cell_mut(0, 0).unwrap().set_content('X', 1);
917
918 let patch = GridDiff::diff(&a, &b);
919 assert_eq!(patch.len(), 3);
920 assert!(patch.updates[0].row <= patch.updates[1].row);
922 assert!(patch.updates[1].row <= patch.updates[2].row);
923 }
924
925 #[test]
928 fn dirty_span_len() {
929 let span = DirtySpan::new(5, 10);
930 assert_eq!(span.len(), 5);
931 assert!(!span.is_empty());
932
933 let empty = DirtySpan::new(5, 5);
934 assert_eq!(empty.len(), 0);
935 assert!(empty.is_empty());
936 }
937
938 #[test]
941 fn change_run_len() {
942 let run = ChangeRun {
943 row: 0,
944 start_col: 3,
945 end_col: 7,
946 };
947 assert_eq!(run.len(), 4);
948 assert!(!run.is_empty());
949 }
950
951 #[test]
954 fn pipeline_track_diff_coalesce() {
955 let old = Grid::new(10, 3);
957 let mut new = Grid::new(10, 3);
958 let mut tracker = DirtyTracker::new(10, 3);
959
960 for (i, ch) in "hello".chars().enumerate() {
962 new.cell_mut(1, i as u16).unwrap().set_content(ch, 1);
963 tracker.mark_cell(1, i as u16);
964 }
965
966 let patch = GridDiff::diff_dirty(&old, &new, &tracker);
967 assert_eq!(patch.len(), 5);
968
969 let runs = patch.runs();
970 assert_eq!(runs.len(), 1);
971 assert_eq!(runs[0].row, 1);
972 assert_eq!(runs[0].start_col, 0);
973 assert_eq!(runs[0].end_col, 5);
974 assert_eq!(runs[0].len(), 5);
975 }
976
977 #[test]
978 fn pipeline_sparse_changes() {
979 let old = Grid::new(80, 24);
980 let mut new = Grid::new(80, 24);
981 let mut tracker = DirtyTracker::new(80, 24);
982
983 new.cell_mut(5, 10).unwrap().set_content('>', 1);
985 tracker.mark_cell(5, 10);
986
987 for c in 0..80u16 {
988 new.cell_mut(23, c).unwrap().set_content('-', 1);
989 }
990 tracker.mark_row(23);
991
992 let patch = GridDiff::diff_dirty(&old, &new, &tracker);
993 assert_eq!(patch.len(), 81); let runs = patch.runs();
996 assert_eq!(runs.len(), 2); assert_eq!(runs[0].row, 5);
998 assert_eq!(runs[0].len(), 1);
999 assert_eq!(runs[1].row, 23);
1000 assert_eq!(runs[1].len(), 80);
1001
1002 assert!(patch.density() < 0.05);
1004 }
1005
1006 #[test]
1011 fn dirty_span_reversed_is_empty() {
1012 let span = DirtySpan::new(10, 5);
1013 assert!(span.is_empty());
1014 assert_eq!(span.len(), 0);
1015 }
1016
1017 #[test]
1018 fn dirty_span_u16_max() {
1019 let span = DirtySpan::new(0, u16::MAX);
1020 assert_eq!(span.len(), u16::MAX);
1021 assert!(!span.is_empty());
1022 }
1023
1024 #[test]
1025 fn dirty_span_full_range() {
1026 let span = DirtySpan::new(0, u16::MAX);
1027 let other = DirtySpan::new(u16::MAX - 1, u16::MAX);
1028 assert!(span.mergeable(&other, 0));
1030 }
1031
1032 #[test]
1033 fn dirty_span_mergeable_exact_boundary() {
1034 let a = DirtySpan::new(0, 5);
1036 let b = DirtySpan::new(5, 10);
1037 assert!(a.mergeable(&b, 0));
1038 assert!(b.mergeable(&a, 0));
1039 }
1040
1041 #[test]
1042 fn dirty_span_not_mergeable_gap_1_no_merge_gap() {
1043 let a = DirtySpan::new(0, 5);
1045 let b = DirtySpan::new(6, 10);
1046 assert!(!a.mergeable(&b, 0));
1047 }
1048
1049 #[test]
1050 fn dirty_span_mergeable_gap_1_with_merge_gap_1() {
1051 let a = DirtySpan::new(0, 5);
1053 let b = DirtySpan::new(6, 10);
1054 assert!(a.mergeable(&b, 1));
1055 }
1056
1057 #[test]
1058 fn dirty_span_merge_union() {
1059 let mut a = DirtySpan::new(3, 7);
1060 let b = DirtySpan::new(5, 12);
1061 a.merge(&b);
1062 assert_eq!(a.start, 3);
1063 assert_eq!(a.end, 12);
1064 }
1065
1066 #[test]
1067 fn dirty_span_merge_disjoint() {
1068 let mut a = DirtySpan::new(0, 5);
1069 let b = DirtySpan::new(10, 20);
1070 a.merge(&b);
1071 assert_eq!(a.start, 0);
1073 assert_eq!(a.end, 20);
1074 }
1075
1076 #[test]
1077 fn dirty_span_merge_subset() {
1078 let mut a = DirtySpan::new(0, 20);
1079 let b = DirtySpan::new(5, 10);
1080 a.merge(&b);
1081 assert_eq!(a.start, 0);
1083 assert_eq!(a.end, 20);
1084 }
1085
1086 #[test]
1089 fn tracker_mark_cell_at_last_col() {
1090 let mut t = DirtyTracker::new(80, 24);
1091 t.mark_cell(0, 79); assert!(t.is_dirty());
1093 let spans = t.row_spans(0).unwrap();
1094 assert_eq!(spans.len(), 1);
1095 assert_eq!(spans[0].start, 79);
1096 assert_eq!(spans[0].end, 80);
1097 }
1098
1099 #[test]
1100 fn tracker_mark_span_clamped_to_cols() {
1101 let mut t = DirtyTracker::new(10, 1);
1102 t.mark_span(0, 5, 100); let spans = t.row_spans(0).unwrap();
1104 assert_eq!(spans.len(), 1);
1105 assert_eq!(spans[0].start, 5);
1106 assert_eq!(spans[0].end, 10);
1107 }
1108
1109 #[test]
1110 fn tracker_mark_row_then_mark_cell_stays_full() {
1111 let mut t = DirtyTracker::new(80, 24);
1112 t.mark_row(0);
1113 t.mark_cell(0, 10);
1114 assert!(t.row_spans(0).is_none());
1116 assert_eq!(t.dirty_row_count(), 1);
1117 }
1118
1119 #[test]
1120 fn tracker_mark_row_twice_no_double_count() {
1121 let mut t = DirtyTracker::new(80, 24);
1122 t.mark_row(5);
1123 t.mark_row(5);
1124 assert_eq!(t.dirty_row_count(), 1);
1125 }
1126
1127 #[test]
1128 fn tracker_mark_cell_twice_same_cell() {
1129 let mut t = DirtyTracker::new(80, 24);
1130 t.mark_cell(3, 10);
1131 t.mark_cell(3, 10);
1132 assert_eq!(t.dirty_row_count(), 1);
1133 let spans = t.row_spans(3).unwrap();
1134 assert_eq!(spans.len(), 1);
1135 }
1136
1137 #[test]
1138 fn tracker_resize_to_zero_rows() {
1139 let mut t = DirtyTracker::new(80, 24);
1140 t.resize(80, 0);
1141 assert_eq!(t.row_count(), 0);
1142 assert_eq!(t.dirty_row_count(), 0);
1143 assert!(!t.is_dirty());
1144 }
1145
1146 #[test]
1147 fn tracker_resize_to_zero_cols() {
1148 let mut t = DirtyTracker::new(80, 24);
1149 t.resize(0, 24);
1150 assert_eq!(t.row_count(), 24);
1151 assert_eq!(t.dirty_row_count(), 24);
1153 }
1154
1155 #[test]
1156 fn tracker_clear_then_mark_cell() {
1157 let mut t = DirtyTracker::new(80, 24);
1158 t.mark_all();
1159 t.clear();
1160 t.mark_cell(0, 0);
1161 assert_eq!(t.dirty_row_count(), 1);
1162 let spans = t.row_spans(0).unwrap();
1164 assert_eq!(spans.len(), 1);
1165 }
1166
1167 #[test]
1168 fn tracker_clone_independence() {
1169 let mut t = DirtyTracker::new(80, 24);
1170 t.mark_cell(0, 5);
1171 let mut t2 = t.clone();
1172 t2.mark_cell(1, 10);
1173 assert_eq!(t.dirty_row_count(), 1);
1174 assert_eq!(t2.dirty_row_count(), 2);
1175 }
1176
1177 #[test]
1178 fn tracker_row_spans_out_of_bounds() {
1179 let t = DirtyTracker::new(80, 24);
1180 assert_eq!(t.row_spans(24), None);
1181 assert_eq!(t.row_spans(100), None);
1182 }
1183
1184 #[test]
1185 fn tracker_is_row_dirty_out_of_bounds() {
1186 let t = DirtyTracker::new(80, 24);
1187 assert!(!t.is_row_dirty(24));
1188 assert!(!t.is_row_dirty(u16::MAX));
1189 }
1190
1191 #[test]
1192 fn tracker_multiple_spans_coalesce_three_way() {
1193 let mut t = DirtyTracker::new(80, 24);
1194 t.set_merge_gap(0);
1195 t.mark_span(0, 0, 3);
1196 t.mark_span(0, 6, 9);
1197 t.mark_span(0, 3, 6);
1199 let spans = t.row_spans(0).unwrap();
1200 assert_eq!(spans.len(), 1);
1201 assert_eq!(spans[0].start, 0);
1202 assert_eq!(spans[0].end, 9);
1203 }
1204
1205 #[test]
1206 fn tracker_mark_span_empty_range_no_op() {
1207 let mut t = DirtyTracker::new(80, 24);
1208 t.mark_span(0, 5, 5); assert!(!t.is_dirty());
1210 }
1211
1212 #[test]
1213 fn tracker_mark_span_reversed_range_no_op() {
1214 let mut t = DirtyTracker::new(80, 24);
1215 t.mark_span(0, 10, 5); assert!(!t.is_dirty());
1217 }
1218
1219 #[test]
1220 fn tracker_resize_shrink_then_grow() {
1221 let mut t = DirtyTracker::new(80, 24);
1222 t.mark_cell(20, 0);
1223 t.resize(80, 10); assert_eq!(t.row_count(), 10);
1225 t.clear();
1226 t.resize(80, 30); assert_eq!(t.row_count(), 30);
1228 assert_eq!(t.dirty_row_count(), 30); }
1230
1231 #[test]
1234 fn change_run_empty() {
1235 let run = ChangeRun {
1236 row: 0,
1237 start_col: 5,
1238 end_col: 5,
1239 };
1240 assert!(run.is_empty());
1241 assert_eq!(run.len(), 0);
1242 }
1243
1244 #[test]
1245 fn change_run_single_cell() {
1246 let run = ChangeRun {
1247 row: 0,
1248 start_col: 0,
1249 end_col: 1,
1250 };
1251 assert!(!run.is_empty());
1252 assert_eq!(run.len(), 1);
1253 }
1254
1255 #[test]
1256 fn change_run_clone_and_eq() {
1257 let run = ChangeRun {
1258 row: 5,
1259 start_col: 10,
1260 end_col: 20,
1261 };
1262 let run2 = run.clone();
1263 assert_eq!(run, run2);
1264 }
1265
1266 #[test]
1269 fn patch_default_trait() {
1270 let p = Patch::default();
1271 assert_eq!(p.cols, 0);
1272 assert_eq!(p.rows, 0);
1273 assert!(p.is_empty());
1274 }
1275
1276 #[test]
1277 fn patch_density_full_grid() {
1278 let mut p = Patch::new(2, 2);
1279 for r in 0..2u16 {
1280 for c in 0..2u16 {
1281 p.push(r, c, Cell::new('X'));
1282 }
1283 }
1284 assert!((p.density() - 1.0).abs() < f64::EPSILON);
1285 }
1286
1287 #[test]
1288 fn patch_density_zero_cols_nonzero_rows() {
1289 let p = Patch::new(0, 10);
1290 assert_eq!(p.density(), 0.0);
1291 }
1292
1293 #[test]
1294 fn patch_density_nonzero_cols_zero_rows() {
1295 let p = Patch::new(10, 0);
1296 assert_eq!(p.density(), 0.0);
1297 }
1298
1299 #[test]
1300 fn patch_runs_single_update() {
1301 let mut p = Patch::new(80, 24);
1302 p.push(12, 40, Cell::new('Z'));
1303 let runs = p.runs();
1304 assert_eq!(runs.len(), 1);
1305 assert_eq!(runs[0].row, 12);
1306 assert_eq!(runs[0].start_col, 40);
1307 assert_eq!(runs[0].end_col, 41);
1308 }
1309
1310 #[test]
1311 fn patch_runs_gap_of_one_splits() {
1312 let mut p = Patch::new(10, 1);
1313 p.push(0, 0, Cell::new('A'));
1314 p.push(0, 2, Cell::new('B'));
1316 let runs = p.runs();
1317 assert_eq!(runs.len(), 2);
1318 assert_eq!(runs[0].end_col, 1);
1319 assert_eq!(runs[1].start_col, 2);
1320 }
1321
1322 #[test]
1323 fn patch_runs_into_clears_buffer() {
1324 let mut p = Patch::new(10, 1);
1325 p.push(0, 0, Cell::new('A'));
1326
1327 let mut buf = vec![ChangeRun {
1328 row: 99,
1329 start_col: 99,
1330 end_col: 100,
1331 }];
1332 p.runs_into(&mut buf);
1333 assert_eq!(buf.len(), 1);
1335 assert_eq!(buf[0].row, 0);
1336 }
1337
1338 #[test]
1339 fn patch_clone_and_eq() {
1340 let mut p = Patch::new(5, 5);
1341 p.push(0, 0, Cell::new('X'));
1342 let p2 = p.clone();
1343 assert_eq!(p, p2);
1344 }
1345
1346 #[test]
1347 fn patch_runs_three_rows_one_cell_each() {
1348 let mut p = Patch::new(80, 24);
1349 p.push(0, 10, Cell::new('A'));
1350 p.push(5, 20, Cell::new('B'));
1351 p.push(10, 30, Cell::new('C'));
1352 let runs = p.runs();
1353 assert_eq!(runs.len(), 3);
1354 for run in &runs {
1355 assert_eq!(run.len(), 1);
1356 }
1357 }
1358
1359 #[test]
1362 fn diff_old_smaller_than_new() {
1363 let old = Grid::new(5, 3);
1364 let mut new = Grid::new(10, 5);
1365 new.cell_mut(4, 9).unwrap().set_content('Z', 1);
1366 let patch = GridDiff::diff(&old, &new);
1367 assert!(!patch.is_empty());
1369 assert!(patch.updates.iter().any(|u| u.row == 4 && u.col == 9));
1371 }
1372
1373 #[test]
1374 fn diff_new_smaller_than_old() {
1375 let mut old = Grid::new(10, 5);
1376 old.cell_mut(4, 9).unwrap().set_content('A', 1);
1377 let new = Grid::new(5, 3);
1378 let patch = GridDiff::diff(&old, &new);
1380 assert_eq!(patch.cols, 5);
1381 assert_eq!(patch.rows, 3);
1382 assert!(patch.updates.iter().all(|u| u.row < 3 && u.col < 5));
1384 }
1385
1386 #[test]
1387 fn diff_into_called_twice() {
1388 let a = Grid::new(5, 3);
1389 let mut b = Grid::new(5, 3);
1390 b.cell_mut(0, 0).unwrap().set_content('X', 1);
1391
1392 let mut patch = Patch::new(0, 0);
1393 GridDiff::diff_into(&a, &b, &mut patch);
1394 assert_eq!(patch.len(), 1);
1395
1396 GridDiff::diff_into(&b, &b, &mut patch);
1398 assert_eq!(patch.len(), 0);
1399 assert_eq!(patch.cols, 5);
1400 }
1401
1402 #[test]
1403 fn diff_dirty_multiple_spans_same_row() {
1404 let a = Grid::new(20, 1);
1405 let mut b = Grid::new(20, 1);
1406 b.cell_mut(0, 2).unwrap().set_content('A', 1);
1407 b.cell_mut(0, 15).unwrap().set_content('B', 1);
1408
1409 let mut tracker = DirtyTracker::new(20, 1);
1410 tracker.set_merge_gap(0);
1411 tracker.mark_span(0, 0, 5);
1412 tracker.mark_span(0, 12, 18);
1413
1414 let patch = GridDiff::diff_dirty(&a, &b, &tracker);
1415 assert_eq!(patch.len(), 2);
1416 assert_eq!(patch.updates[0].col, 2);
1417 assert_eq!(patch.updates[1].col, 15);
1418 }
1419
1420 #[test]
1421 fn diff_dirty_full_row_vs_span_row() {
1422 let a = Grid::new(10, 3);
1423 let mut b = Grid::new(10, 3);
1424 b.cell_mut(0, 5).unwrap().set_content('A', 1);
1426 b.cell_mut(2, 3).unwrap().set_content('B', 1);
1428
1429 let mut tracker = DirtyTracker::new(10, 3);
1430 tracker.mark_row(0);
1431 tracker.mark_span(2, 0, 5);
1432
1433 let patch = GridDiff::diff_dirty(&a, &b, &tracker);
1434 assert_eq!(patch.len(), 2);
1435 assert_eq!(patch.updates[0].row, 0);
1436 assert_eq!(patch.updates[1].row, 2);
1437 }
1438
1439 #[test]
1440 fn diff_all_cells_changed() {
1441 let a = Grid::new(3, 3);
1442 let mut b = Grid::new(3, 3);
1443 for r in 0..3u16 {
1444 for c in 0..3u16 {
1445 b.cell_mut(r, c).unwrap().set_content('X', 1);
1446 }
1447 }
1448 let patch = GridDiff::diff(&a, &b);
1449 assert_eq!(patch.len(), 9);
1450 assert!((patch.density() - 1.0).abs() < f64::EPSILON);
1451 let runs = patch.runs();
1452 assert_eq!(runs.len(), 3); for run in &runs {
1454 assert_eq!(run.len(), 3);
1455 }
1456 }
1457
1458 #[test]
1459 fn diff_dirty_span_beyond_grid_cols() {
1460 let a = Grid::new(5, 1);
1461 let mut b = Grid::new(5, 1);
1462 b.cell_mut(0, 3).unwrap().set_content('Z', 1);
1463
1464 let mut tracker = DirtyTracker::new(5, 1);
1465 tracker.mark_span(0, 0, 100);
1467 let patch = GridDiff::diff_dirty(&a, &b, &tracker);
1468 assert_eq!(patch.len(), 1);
1469 assert_eq!(patch.updates[0].col, 3);
1470 }
1471
1472 #[test]
1475 fn cell_update_clone_and_eq() {
1476 let u = CellUpdate {
1477 row: 5,
1478 col: 10,
1479 cell: Cell::new('X'),
1480 };
1481 let u2 = u.clone();
1482 assert_eq!(u, u2);
1483 assert_eq!(u.row, u2.row);
1484 assert_eq!(u.col, u2.col);
1485 }
1486
1487 #[test]
1488 fn cell_update_debug_format() {
1489 let u = CellUpdate {
1490 row: 0,
1491 col: 0,
1492 cell: Cell::new('A'),
1493 };
1494 let dbg = format!("{u:?}");
1495 assert!(dbg.contains("CellUpdate"));
1496 }
1497
1498 #[test]
1501 fn tracker_merge_gap_zero_keeps_separate() {
1502 let mut t = DirtyTracker::new(80, 24);
1503 t.set_merge_gap(0);
1504 t.mark_span(0, 0, 5);
1505 t.mark_span(0, 5, 10); let spans = t.row_spans(0).unwrap();
1507 assert_eq!(spans.len(), 1);
1509 assert_eq!(spans[0].start, 0);
1510 assert_eq!(spans[0].end, 10);
1511 }
1512
1513 #[test]
1514 fn tracker_merge_gap_large_merges_everything() {
1515 let mut t = DirtyTracker::new(80, 24);
1516 t.set_merge_gap(u16::MAX);
1517 t.mark_span(0, 0, 5);
1518 t.mark_span(0, 70, 80);
1519 let spans = t.row_spans(0).unwrap();
1520 assert_eq!(spans.len(), 1);
1522 assert_eq!(spans[0].start, 0);
1523 assert_eq!(spans[0].end, 80);
1524 }
1525
1526 #[test]
1527 fn tracker_new_zero_dimensions() {
1528 let t = DirtyTracker::new(0, 0);
1529 assert_eq!(t.row_count(), 0);
1530 assert!(!t.is_dirty());
1531 let mut t = t;
1533 t.mark_cell(0, 0);
1534 assert!(!t.is_dirty());
1535 }
1536}