1use std::sync::Arc;
54
55use crate::{HunkIndex, HunkKind};
56use lattice_cells::{AnchorPosition, Cell, ProviderId, VirtualRow, VirtualRowProvider};
57use lattice_core::BufferId;
58
59use crate::subsystem::DiffSession;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum Side {
70 Baseline,
71 Current,
72 Remote,
75}
76
77impl Side {
78 fn namespace_bit(self) -> u64 {
79 match self {
80 Side::Baseline => DIFF_FILLER_BASELINE_NAMESPACE,
81 Side::Current => DIFF_FILLER_CURRENT_NAMESPACE,
82 Side::Remote => DIFF_FILLER_REMOTE_NAMESPACE,
83 }
84 }
85
86 pub fn pane_index(self) -> usize {
89 match self {
90 Side::Baseline => 0,
91 Side::Current => 1,
92 Side::Remote => 2,
93 }
94 }
95}
96
97pub fn diff_filler_provider_id(buffer_id: BufferId, side: Side) -> ProviderId {
103 side.namespace_bit() | u64::from(buffer_id.0)
104}
105
106const DIFF_FILLER_BASELINE_NAMESPACE: u64 = 0xD1FF_0001_0000_0000;
111
112const DIFF_FILLER_CURRENT_NAMESPACE: u64 = 0xD1FF_0002_0000_0000;
114
115const DIFF_FILLER_REMOTE_NAMESPACE: u64 = 0xD1FF_0003_0000_0000;
118
119#[derive(Debug)]
131pub struct FillerRowProvider {
132 session: Arc<DiffSession>,
133 side: Side,
134}
135
136impl FillerRowProvider {
137 pub fn new(session: Arc<DiffSession>, side: Side) -> Self {
138 Self { session, side }
139 }
140
141 pub fn side(&self) -> Side {
142 self.side
143 }
144}
145
146impl VirtualRowProvider for FillerRowProvider {
147 fn id(&self) -> ProviderId {
148 diff_filler_provider_id(self.session.buffer_id(), self.side)
149 }
150
151 fn version(&self) -> u64 {
152 let rev = self.session.current_hunks().revision;
158 let side_salt: u64 = match self.side {
159 Side::Baseline => 0,
160 Side::Current => 1,
161 Side::Remote => 2,
162 };
163 rev ^ side_salt
164 }
165
166 fn collect(&self) -> Vec<VirtualRow> {
167 let hunks = self.session.current_hunks();
168 compute_filler_rows(&hunks, self.side)
169 }
170}
171
172pub fn compute_filler_rows(index: &HunkIndex, side: Side) -> Vec<VirtualRow> {
192 let mut rows = Vec::new();
193 let pane_idx = side.pane_index();
194 for hunk in &index.hunks {
195 let participating = hunk.ranges.len();
196 if participating < 2 {
197 continue;
200 }
201 let is_three_way_hunk = participating >= 3;
202 if matches!(hunk.kind, HunkKind::Conflict) && !is_three_way_hunk {
203 continue;
207 }
208 let Some(this_range) = hunk.ranges.get(pane_idx).copied() else {
209 continue;
212 };
213 let this_len = this_range.end.saturating_sub(this_range.start);
214 let target_len = hunk
215 .ranges
216 .iter()
217 .map(|r| r.end.saturating_sub(r.start))
218 .max()
219 .unwrap_or(0);
220 if target_len <= this_len {
221 continue;
224 }
225 let filler_count = target_len - this_len;
226 let (anchor_line, position) = if this_len == 0 {
227 (this_range.start, AnchorPosition::Above)
233 } else {
234 (this_range.end - 1, AnchorPosition::Below)
237 };
238 let blank_cells: Arc<[Cell]> = Arc::from([Cell::BLANK]);
239 for _ in 0..filler_count {
240 rows.push(VirtualRow {
241 media: None,
242 anchor_line,
243 position,
244 cells: blank_cells.clone(),
245 height: 1,
246 kind: lattice_cells::VirtualRowKind::Filler,
250 bg: None,
251 scales: None,
252 gutter_line: None,
253 gutter_fg: None,
254 });
255 }
256 }
257 rows
258}
259
260#[cfg(test)]
261mod tests {
262 #![allow(clippy::unwrap_used)]
263 use super::*;
264 use crate::{DiffAlgorithm, Hunk, LineRange};
265 use smallvec::smallvec;
266
267 fn hunk(kind: HunkKind, baseline: LineRange, current: LineRange) -> Hunk {
268 Hunk {
269 kind,
270 ranges: smallvec![baseline, current],
271 refine: Default::default(),
272 }
273 }
274
275 fn idx(hunks: Vec<Hunk>) -> HunkIndex {
276 HunkIndex {
277 hunks,
278 algorithm: DiffAlgorithm::Histogram,
279 revision: 1,
280 }
281 }
282
283 #[test]
284 fn empty_index_emits_no_fillers_on_either_side() {
285 let i = HunkIndex::empty(DiffAlgorithm::Histogram);
286 assert!(compute_filler_rows(&i, Side::Baseline).is_empty());
287 assert!(compute_filler_rows(&i, Side::Current).is_empty());
288 }
289
290 #[test]
291 fn add_hunk_emits_fillers_on_baseline_only() {
292 let i = idx(vec![hunk(
296 HunkKind::Add,
297 LineRange::new(5, 5),
298 LineRange::new(5, 8),
299 )]);
300 let baseline_fillers = compute_filler_rows(&i, Side::Baseline);
301 let current_fillers = compute_filler_rows(&i, Side::Current);
302 assert_eq!(baseline_fillers.len(), 3);
303 assert!(current_fillers.is_empty());
304 for row in &baseline_fillers {
305 assert_eq!(row.anchor_line, 5);
306 assert_eq!(row.position, AnchorPosition::Above);
307 assert_eq!(row.height, 1);
308 }
309 }
310
311 #[test]
312 fn remove_hunk_emits_fillers_on_current_only() {
313 let i = idx(vec![hunk(
316 HunkKind::Remove,
317 LineRange::new(5, 8),
318 LineRange::new(5, 5),
319 )]);
320 let baseline_fillers = compute_filler_rows(&i, Side::Baseline);
321 let current_fillers = compute_filler_rows(&i, Side::Current);
322 assert!(baseline_fillers.is_empty());
323 assert_eq!(current_fillers.len(), 3);
324 for row in ¤t_fillers {
325 assert_eq!(row.anchor_line, 5);
326 assert_eq!(row.position, AnchorPosition::Above);
327 }
328 }
329
330 #[test]
331 fn change_hunk_baseline_longer_emits_fillers_on_current() {
332 let i = idx(vec![hunk(
336 HunkKind::Change,
337 LineRange::new(10, 15),
338 LineRange::new(10, 12),
339 )]);
340 let baseline_fillers = compute_filler_rows(&i, Side::Baseline);
341 let current_fillers = compute_filler_rows(&i, Side::Current);
342 assert!(baseline_fillers.is_empty());
343 assert_eq!(current_fillers.len(), 3);
344 for row in ¤t_fillers {
345 assert_eq!(row.anchor_line, 11);
346 assert_eq!(row.position, AnchorPosition::Below);
347 }
348 }
349
350 #[test]
351 fn change_hunk_current_longer_emits_fillers_on_baseline() {
352 let i = idx(vec![hunk(
356 HunkKind::Change,
357 LineRange::new(10, 12),
358 LineRange::new(10, 15),
359 )]);
360 let baseline_fillers = compute_filler_rows(&i, Side::Baseline);
361 let current_fillers = compute_filler_rows(&i, Side::Current);
362 assert_eq!(baseline_fillers.len(), 3);
363 assert!(current_fillers.is_empty());
364 for row in &baseline_fillers {
365 assert_eq!(row.anchor_line, 11);
366 assert_eq!(row.position, AnchorPosition::Below);
367 }
368 }
369
370 #[test]
371 fn change_hunk_equal_lengths_emits_no_fillers() {
372 let i = idx(vec![hunk(
373 HunkKind::Change,
374 LineRange::new(10, 13),
375 LineRange::new(10, 13),
376 )]);
377 assert!(compute_filler_rows(&i, Side::Baseline).is_empty());
378 assert!(compute_filler_rows(&i, Side::Current).is_empty());
379 }
380
381 #[test]
382 fn multiple_hunks_accumulate_independently() {
383 let i = idx(vec![
387 hunk(HunkKind::Add, LineRange::new(5, 5), LineRange::new(5, 7)),
388 hunk(
389 HunkKind::Remove,
390 LineRange::new(20, 23),
391 LineRange::new(22, 22),
392 ),
393 ]);
394 let baseline_fillers = compute_filler_rows(&i, Side::Baseline);
395 let current_fillers = compute_filler_rows(&i, Side::Current);
396 assert_eq!(baseline_fillers.len(), 2);
397 assert_eq!(current_fillers.len(), 3);
398 for row in &baseline_fillers {
401 assert_eq!(row.anchor_line, 5);
402 assert_eq!(row.position, AnchorPosition::Above);
403 }
404 for row in ¤t_fillers {
407 assert_eq!(row.anchor_line, 22);
408 assert_eq!(row.position, AnchorPosition::Above);
409 }
410 }
411
412 #[test]
413 fn conflict_hunks_skipped_in_two_way_provider() {
414 let i = idx(vec![hunk(
415 HunkKind::Conflict,
416 LineRange::new(5, 10),
417 LineRange::new(5, 8),
418 )]);
419 assert!(compute_filler_rows(&i, Side::Baseline).is_empty());
420 assert!(compute_filler_rows(&i, Side::Current).is_empty());
421 }
422
423 #[test]
424 fn malformed_hunk_with_fewer_than_two_ranges_is_skipped() {
425 let i = idx(vec![Hunk {
426 kind: HunkKind::Add,
427 ranges: smallvec![LineRange::new(0, 0)],
428 refine: Default::default(),
429 }]);
430 assert!(compute_filler_rows(&i, Side::Baseline).is_empty());
431 assert!(compute_filler_rows(&i, Side::Current).is_empty());
432 }
433
434 #[test]
437 fn provider_ids_distinct_per_side() {
438 let bid = BufferId(7);
439 let baseline_id = diff_filler_provider_id(bid, Side::Baseline);
440 let current_id = diff_filler_provider_id(bid, Side::Current);
441 assert_ne!(baseline_id, current_id);
442 assert_eq!(baseline_id as u32, bid.0);
444 assert_eq!(current_id as u32, bid.0);
445 }
446
447 #[test]
448 fn provider_id_does_not_collide_with_overlay_namespace() {
449 let bid = BufferId(7);
450 let overlay = crate::overlay::diff_overlay_provider_id(bid);
451 assert_ne!(diff_filler_provider_id(bid, Side::Baseline), overlay);
452 assert_ne!(diff_filler_provider_id(bid, Side::Current), overlay);
453 }
454
455 #[test]
456 fn provider_collect_reads_published_session_hunks() {
457 let session = Arc::new(DiffSession::new(BufferId(1), DiffAlgorithm::Histogram));
458 let provider = FillerRowProvider::new(session.clone(), Side::Baseline);
460 assert!(provider.collect().is_empty());
461 session.publish(Arc::new(idx(vec![hunk(
463 HunkKind::Add,
464 LineRange::new(5, 5),
465 LineRange::new(5, 8),
466 )])));
467 assert_eq!(provider.collect().len(), 3);
468 let other = FillerRowProvider::new(session, Side::Current);
470 assert!(other.collect().is_empty());
471 }
472
473 #[test]
474 fn provider_version_changes_with_session_revision() {
475 let session = Arc::new(DiffSession::new(BufferId(1), DiffAlgorithm::Histogram));
476 let p = FillerRowProvider::new(session.clone(), Side::Baseline);
477 let v0 = p.version();
478 session.publish(Arc::new(idx(vec![hunk(
479 HunkKind::Add,
480 LineRange::new(0, 0),
481 LineRange::new(0, 1),
482 )])));
483 let v1 = p.version();
484 assert_ne!(v0, v1);
485 }
486
487 #[test]
488 fn provider_version_differs_across_sides_at_rev_zero() {
489 let session = Arc::new(DiffSession::new(BufferId(1), DiffAlgorithm::Histogram));
490 let baseline = FillerRowProvider::new(session.clone(), Side::Baseline);
491 let current = FillerRowProvider::new(session, Side::Current);
492 assert_ne!(
493 baseline.version(),
494 current.version(),
495 "side salt must distinguish baseline / current at rev 0"
496 );
497 }
498
499 fn hunk3(kind: HunkKind, base: LineRange, local: LineRange, remote: LineRange) -> Hunk {
504 Hunk {
505 kind,
506 ranges: smallvec![base, local, remote],
507 refine: Default::default(),
508 }
509 }
510
511 #[test]
512 fn three_way_change_aligns_all_three_panes_to_max_length() {
513 let i = idx(vec![hunk3(
519 HunkKind::Change,
520 LineRange::new(10, 13),
521 LineRange::new(10, 14),
522 LineRange::new(10, 16),
523 )]);
524 let base_fillers = compute_filler_rows(&i, Side::Baseline);
525 let local_fillers = compute_filler_rows(&i, Side::Current);
526 let remote_fillers = compute_filler_rows(&i, Side::Remote);
527 assert_eq!(base_fillers.len(), 3, "base shorter by 3 lines");
528 assert_eq!(local_fillers.len(), 2, "local shorter by 2 lines");
529 assert!(remote_fillers.is_empty(), "remote is longest");
530 for row in &base_fillers {
532 assert_eq!(row.anchor_line, 12);
533 assert_eq!(row.position, AnchorPosition::Below);
534 }
535 for row in &local_fillers {
536 assert_eq!(row.anchor_line, 13);
537 assert_eq!(row.position, AnchorPosition::Below);
538 }
539 }
540
541 #[test]
542 fn three_way_add_on_local_emits_fillers_on_base_and_remote() {
543 let i = idx(vec![hunk3(
547 HunkKind::Add,
548 LineRange::new(10, 10),
549 LineRange::new(10, 13),
550 LineRange::new(10, 10),
551 )]);
552 let base_fillers = compute_filler_rows(&i, Side::Baseline);
553 let local_fillers = compute_filler_rows(&i, Side::Current);
554 let remote_fillers = compute_filler_rows(&i, Side::Remote);
555 assert_eq!(base_fillers.len(), 3);
556 assert!(local_fillers.is_empty(), "local is longest");
557 assert_eq!(remote_fillers.len(), 3);
558 for row in base_fillers.iter().chain(remote_fillers.iter()) {
559 assert_eq!(row.anchor_line, 10);
560 assert_eq!(row.position, AnchorPosition::Above);
561 }
562 }
563
564 #[test]
565 fn three_way_conflict_hunk_emits_fillers_for_alignment() {
566 let i = idx(vec![hunk3(
571 HunkKind::Conflict,
572 LineRange::new(10, 12), LineRange::new(10, 14), LineRange::new(10, 15), )]);
576 let base_fillers = compute_filler_rows(&i, Side::Baseline);
577 let local_fillers = compute_filler_rows(&i, Side::Current);
578 let remote_fillers = compute_filler_rows(&i, Side::Remote);
579 assert_eq!(base_fillers.len(), 3, "base shorter by 3");
580 assert_eq!(local_fillers.len(), 1, "local shorter by 1");
581 assert!(remote_fillers.is_empty(), "remote is longest");
582 }
583
584 #[test]
585 fn three_way_equal_lengths_emit_no_fillers() {
586 let i = idx(vec![hunk3(
587 HunkKind::Change,
588 LineRange::new(10, 13),
589 LineRange::new(10, 13),
590 LineRange::new(10, 13),
591 )]);
592 for side in [Side::Baseline, Side::Current, Side::Remote] {
593 assert!(
594 compute_filler_rows(&i, side).is_empty(),
595 "no fillers when all three lengths match ({side:?})"
596 );
597 }
598 }
599
600 #[test]
601 fn two_way_hunk_via_remote_side_is_skipped_not_panic() {
602 let i = idx(vec![hunk(
606 HunkKind::Add,
607 LineRange::new(5, 5),
608 LineRange::new(5, 8),
609 )]);
610 assert!(
611 compute_filler_rows(&i, Side::Remote).is_empty(),
612 "remote-side filler on a 2-way hunk should be empty, not panic"
613 );
614 }
615
616 #[test]
617 fn three_way_malformed_single_range_skipped() {
618 let i = idx(vec![Hunk {
619 kind: HunkKind::Change,
620 ranges: smallvec![LineRange::new(0, 5)],
621 refine: Default::default(),
622 }]);
623 for side in [Side::Baseline, Side::Current, Side::Remote] {
624 assert!(compute_filler_rows(&i, side).is_empty());
625 }
626 }
627
628 #[test]
631 fn remote_provider_id_distinct_from_baseline_and_current() {
632 let bid = BufferId(7);
633 let base = diff_filler_provider_id(bid, Side::Baseline);
634 let cur = diff_filler_provider_id(bid, Side::Current);
635 let rem = diff_filler_provider_id(bid, Side::Remote);
636 assert_ne!(rem, base);
637 assert_ne!(rem, cur);
638 assert_ne!(base, cur);
639 assert_eq!(rem as u32, bid.0);
641 }
642
643 #[test]
644 fn remote_provider_id_does_not_collide_with_overlay_namespace() {
645 let bid = BufferId(7);
646 let overlay = crate::overlay::diff_overlay_provider_id(bid);
647 assert_ne!(diff_filler_provider_id(bid, Side::Remote), overlay);
648 }
649
650 #[test]
654 fn filler_rows_carry_filler_kind() {
655 let i = idx(vec![hunk(
656 HunkKind::Add,
657 LineRange::new(5, 5),
658 LineRange::new(5, 8),
659 )]);
660 let rows = compute_filler_rows(&i, Side::Baseline);
661 assert!(!rows.is_empty());
662 for row in &rows {
663 assert_eq!(
664 row.kind,
665 lattice_cells::VirtualRowKind::Filler,
666 "filler rows must be tagged Filler so renderers skip the \
667 deletion-block backdrop"
668 );
669 }
670 }
671
672 #[test]
673 fn provider_version_distinct_across_all_three_sides_at_rev_zero() {
674 let session = Arc::new(DiffSession::new(BufferId(1), DiffAlgorithm::Histogram));
675 let baseline = FillerRowProvider::new(session.clone(), Side::Baseline);
676 let current = FillerRowProvider::new(session.clone(), Side::Current);
677 let remote = FillerRowProvider::new(session, Side::Remote);
678 let v = (baseline.version(), current.version(), remote.version());
680 assert_ne!(v.0, v.1);
681 assert_ne!(v.0, v.2);
682 assert_ne!(v.1, v.2);
683 }
684
685 #[test]
686 fn remote_provider_collect_reads_published_three_way_hunks() {
687 let session = Arc::new(DiffSession::new(BufferId(1), DiffAlgorithm::Histogram));
688 session.publish(Arc::new(idx(vec![hunk3(
689 HunkKind::Add,
690 LineRange::new(10, 10),
691 LineRange::new(10, 13),
692 LineRange::new(10, 10),
693 )])));
694 let remote = FillerRowProvider::new(session, Side::Remote);
695 assert_eq!(
696 remote.collect().len(),
697 3,
698 "remote pane gets 3 fillers to align with local's Add"
699 );
700 }
701
702 #[test]
703 fn side_pane_index_matches_ranges_slot() {
704 assert_eq!(Side::Baseline.pane_index(), 0);
705 assert_eq!(Side::Current.pane_index(), 1);
706 assert_eq!(Side::Remote.pane_index(), 2);
707 }
708}