clankerdiff-ratatui 0.1.0

Embeddable Ratatui diff review widget
Documentation
#![allow(missing_docs)]

mod support;

use clankerdiff_core::{DiffDocument, testing::DocumentBuilder};
use crossterm::event::{KeyCode, MouseEventKind};
use std::sync::Arc;
use support::{ReviewHarness, key, mouse};

fn large_document(rows: usize) -> Arc<DiffDocument> {
    DocumentBuilder::new()
        .generated("src/large.rs", rows)
        .build()
}

/// A single large hunk where every row pairs a removed and an added line, so a
/// split layout exercises one highlight sequence per side.
fn modified_document(rows: usize) -> Arc<DiffDocument> {
    DocumentBuilder::new()
        .changed(
            "src/large.rs",
            &source_lines(rows, ""),
            &source_lines(rows, " + 1"),
        )
        .build()
}

fn source_lines(rows: usize, suffix: &str) -> String {
    use std::fmt::Write;
    (1..=rows).fold(String::new(), |mut source, i| {
        let _ = writeln!(source, "let value_{i} = {i}{suffix};");
        source
    })
}

#[test]
fn settled_frame_emits_no_terminal_cells_and_reuses_highlights() {
    let mut harness = ReviewHarness::new(large_document(1_000), 100, 24);
    let cold = harness.draw();
    assert!(cold.backend.cells_drawn > 0);
    assert!(cold.highlight_misses > 0);

    let settled = harness.draw();
    assert_eq!(settled.backend.cells_drawn, 0);
    assert_eq!(settled.highlight_misses, 0);
    assert_eq!(settled.highlight_calls, settled.highlight_hits);
}

#[test]
fn an_equal_content_snapshot_swap_reuses_every_highlight() {
    let old = source_lines(1_000, "");
    let new = source_lines(1_000, " + 1");
    let fixture = || {
        DocumentBuilder::new()
            .changed("src/large.rs", &old, &new)
            .build()
    };
    let mut harness = ReviewHarness::new(fixture(), 100, 24);
    harness.draw();
    assert_eq!(harness.draw().highlight_misses, 0);

    harness.state_mut().set_document(fixture());

    let swapped = harness.draw();
    assert_eq!(
        swapped.highlight_misses, 0,
        "content-addressed highlights must survive a snapshot swap"
    );
    assert_eq!(swapped.highlight_calls, swapped.highlight_hits);
}

#[test]
fn highlighting_work_is_bounded_by_the_viewport_not_document_size() {
    let mut small = ReviewHarness::new(large_document(1_000), 80, 20);
    let mut large = ReviewHarness::new(large_document(100_000), 80, 20);

    let small_frame = small.draw();
    let large_frame = large.draw();
    assert_eq!(small_frame.highlight_calls, large_frame.highlight_calls);
    assert_eq!(small_frame.highlight_misses, large_frame.highlight_misses);
    assert!(large_frame.highlight_calls <= 18);
    assert_eq!(small_frame.highlighted_bytes, large_frame.highlighted_bytes);
    assert!(
        large_frame.highlighted_bytes < 100_000,
        "highlighted {} bytes from a 100,000-line document",
        large_frame.highlighted_bytes
    );
}

#[test]
fn deep_full_file_scroll_parses_complete_sources_once() {
    let old = source_lines(100_000, "");
    let new = old.replacen(
        "let value_50000 = 50000;",
        "let value_50000 = 50000 + 1;",
        1,
    );
    let fixture = DocumentBuilder::new()
        .changed_with_hunk_window("src/large.rs", &old, &new, 49_997..=50_003)
        .build();
    let mut harness = ReviewHarness::new(fixture, 100, 24);
    harness.input(key(KeyCode::Tab));
    harness.input(key(KeyCode::Char('f')));
    let first_parse = harness.draw();
    assert!(
        first_parse.highlighted_bytes >= old.len(),
        "first visible complete side parsed {} bytes",
        first_parse.highlighted_bytes
    );
    assert!(first_parse.highlighted_bytes <= old.len() + new.len());
    let jumped = harness.input_and_draw(key(KeyCode::End));
    assert!(
        jumped.highlighted_bytes < 100_000,
        "deep full-file navigation parsed {} bytes",
        jumped.highlighted_bytes
    );
    let settled = harness.draw();
    assert_eq!(settled.highlight_misses, 0);
}

#[test]
fn deep_patch_only_split_scroll_highlights_visible_lines_and_then_settles() {
    let mut harness = ReviewHarness::new(modified_document(5_000), 100, 24);
    harness.draw();
    harness.input(key(KeyCode::Enter));
    for _ in 0..3 {
        if harness.state().layout().is_split() {
            break;
        }
        harness.input(key(KeyCode::Char('v')));
    }
    assert!(harness.state().layout().is_split());
    harness.draw();

    for _ in 0..60 {
        harness.input(key(KeyCode::PageDown));
    }
    let jumped = harness.draw();
    assert!(
        jumped.highlighted_bytes < 150_000,
        "a deep jump parses bounded context per side, parsed {} bytes",
        jumped.highlighted_bytes
    );

    let settled = harness.draw();
    assert_eq!(
        settled.highlight_misses, 0,
        "the current patch-only viewport stays cached"
    );
    let paged = harness.input_and_draw(key(KeyCode::PageDown));
    assert!(
        paged.highlight_misses <= 48,
        "patch-only fallback highlights at most the newly visible split cells"
    );
}

#[test]
fn moving_one_row_changes_only_a_bounded_screen_region() {
    const WIDTH: u16 = 100;
    let mut harness = ReviewHarness::new(large_document(10_000), WIDTH, 24);
    harness.draw();
    harness.input(key(KeyCode::Enter));
    harness.draw();

    let moved = harness.input_and_draw(key(KeyCode::Down));
    assert!(
        moved.backend.cells_drawn <= u64::from(WIDTH) * 3,
        "one-row navigation changed {} cells",
        moved.backend.cells_drawn
    );
    assert_eq!(moved.highlight_misses, 0);
}

#[test]
fn one_wheel_notch_changes_only_a_bounded_screen_region() {
    const WIDTH: u16 = 100;
    const PATCH_COLUMN: u16 = 60;
    let mut harness = ReviewHarness::new(large_document(10_000), WIDTH, 24);
    harness.draw();
    harness.input(key(KeyCode::Enter));
    harness.draw();

    let moved = harness.input_and_draw(mouse(MouseEventKind::ScrollDown, PATCH_COLUMN, 5));
    assert!(
        moved.backend.cells_drawn <= u64::from(WIDTH) * 3,
        "one wheel notch changed {} cells",
        moved.backend.cells_drawn
    );
    assert_eq!(moved.highlight_misses, 0);
}

#[test]
fn moving_back_to_a_cached_row_does_not_rehighlight_it() {
    let mut harness = ReviewHarness::new(large_document(1_000), 80, 20);
    harness.draw();
    harness.input(key(KeyCode::Enter));
    harness.draw();
    harness.input_and_draw(key(KeyCode::Down));

    let back = harness.input_and_draw(key(KeyCode::Up));
    assert_eq!(back.highlight_misses, 0);
    assert_eq!(back.highlight_calls, back.highlight_hits);
    assert!(harness.state().selected_row().is_some());
    assert!(!harness.buffer().content().is_empty());
}