fmtview 0.2.1

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use super::*;
use std::{hint::black_box, time::Instant};

use ratatui::text::Line;

use crate::diff::{DiffLayout, DiffModel};

use super::super::palette::{diff_removed_inline_bg, diff_removed_line_bg};
use super::super::render::char_count;
use super::{input::*, render::*};

fn sample_model() -> DiffModel {
    DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1,4 +1,4 @@
 {
-  \"a\": 1,
+  \"a\": 2,
   \"b\": true
 }
",
    )
}

fn line_text(line: &Line<'static>) -> String {
    line.spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>()
}

fn line_width(line: &Line<'static>) -> usize {
    line.spans
        .iter()
        .map(|span| char_count(span.content.as_ref()))
        .sum()
}

#[test]
fn renders_unified_diff_rows_with_line_numbers() {
    let lines = render_rows(&sample_model(), DiffLayout::Unified, 0, 3, 80, 0);
    let text = lines
        .iter()
        .map(|line| {
            line.spans
                .iter()
                .map(|span| span.content.as_ref())
                .collect::<String>()
                .trim_end()
                .to_owned()
        })
        .collect::<Vec<_>>();

    assert_eq!(text[0], "1 1   {");
    assert_eq!(text[1], "2   -   \"a\": 1,");
    assert_eq!(text[2], "  2 +   \"a\": 2,");
}

#[test]
fn renders_side_by_side_change_pairs() {
    let lines = render_rows(&sample_model(), DiffLayout::SideBySide, 1, 1, 80, 0);
    let text = lines[0]
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(text.contains("-   \"a\": 1,"));
    assert!(text.contains("+   \"a\": 2,"));
}

#[test]
fn interactive_diff_hides_patch_control_rows() {
    let lines = render_rows(&sample_model(), DiffLayout::Unified, 0, 10, 80, 0);
    let text = lines
        .iter()
        .flat_map(|line| line.spans.iter())
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(!text.contains("@@"));
    assert!(!text.contains("---"));
    assert!(!text.contains("+++"));
}

#[test]
fn changed_rows_use_line_and_inline_backgrounds() {
    let lines = render_rows(&sample_model(), DiffLayout::Unified, 1, 1, 80, 0);
    let removed = &lines[0];

    assert!(removed.spans.iter().any(|span| {
        span.style.bg == Some(diff_removed_line_bg(crate::diff::DiffIntensity::Low))
    }));
    assert!(removed.spans.iter().any(|span| {
        span.content.as_ref().contains('1')
            && span.style.bg == Some(diff_removed_inline_bg(crate::diff::DiffIntensity::Low))
    }));
}

#[test]
fn change_jump_skips_adjacent_rows_in_same_block() {
    let model = DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1,4 +1,4 @@
 a
-old
+new
 b
 c
@@ -20,3 +20,3 @@
 x
-old2
+new2
 y
",
    );
    let targets = change_block_starts(model.changed_rows(DiffLayout::Unified));
    let mut state = DiffViewState::new(DiffLayout::Unified);

    assert_eq!(targets, vec![1, 6]);
    jump_change(&model, &mut state, DiffJump::Next, 9);
    assert_eq!(state.change_cursor, Some(1));

    assert!(jump_change(&model, &mut state, DiffJump::Next, 9));
    assert_eq!(state.change_cursor, Some(6));
    assert_eq!(state.top, 3);
}

#[test]
fn diff_scroll_clamps_to_last_full_page() {
    let model = sample_model();
    let mut state = DiffViewState::new(DiffLayout::Unified);

    assert!(scroll_by(&mut state, &model, 3, 80, 99));

    assert_eq!(state.top, model.row_count(DiffLayout::Unified) - 3);
}

#[test]
fn side_by_side_scroll_uses_longer_display_side() {
    let model = DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1,3 +1,5 @@
 a
-old
+new1
+new2
+new3
 z
",
    );
    let mut state = DiffViewState::new(DiffLayout::SideBySide);

    assert!(scroll_by(&mut state, &model, 3, 80, 99));
    assert_eq!(state.top, 2);

    let text = render_rows(&model, DiffLayout::SideBySide, state.top, 3, 80, 0)
        .iter()
        .flat_map(|line| line.spans.iter())
        .map(|span| span.content.as_ref())
        .collect::<String>();
    assert!(text.contains("new3"));
}

#[test]
fn unified_diff_wraps_and_scrolls_inside_long_rows() {
    let model = DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1,1 +1,1 @@
-abcdefghijklmnopqrstuvwxyz
+abcxyzdefghijklmnopqrstuvwxyz
",
    );
    let mut state = DiffViewState::new(DiffLayout::Unified);
    state.top = 0;

    let lines = render_rows_with_status(&model, DiffLayout::Unified, 0, 0, 3, 18, 0, true)
        .rows
        .iter()
        .map(line_text)
        .collect::<Vec<_>>();

    assert_eq!(lines.len(), 3);
    assert!(lines[0].contains("- abcdefghij"));
    assert!(lines[1].contains("mnopqrst"));
    assert!(!lines[1].contains("1  "));

    assert!(scroll_by(&mut state, &model, 3, 18, 1));
    assert_eq!(state.top, 0);
    assert_eq!(state.top_row_offset, 1);
}

#[test]
fn side_by_side_wrap_uses_longer_cell() {
    let model = DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1,1 +1,1 @@
-short
+abcdefghijklmnopqrstuvwxyz0123456789
",
    );

    let lines = render_rows_with_status(&model, DiffLayout::SideBySide, 0, 0, 4, 32, 0, true).rows;
    let text = lines.iter().map(line_text).collect::<Vec<_>>();

    assert!(text.len() > 1);
    assert!(text.iter().any(|line| line.contains("short")));
    assert!(text.iter().any(|line| line.contains("lmnopqrst")));
    assert!(text.iter().any(|line| line.contains("uvwxyz")));
}

#[test]
fn side_by_side_wrapped_rows_fit_content_width() {
    let model = DiffModel::from_unified_patch(
        "left".to_owned(),
        "right".to_owned(),
        "\
--- left
+++ right
@@ -1000,1 +1000,1 @@
-abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
+01234567890123456789012345678901234567890123456789
",
    );

    for width in [24, 32, 80, 118] {
        let rows =
            render_rows_with_status(&model, DiffLayout::SideBySide, 0, 0, 16, width, 0, true).rows;

        assert!(!rows.is_empty());
        assert!(
            rows.iter().all(|line| line_width(line) <= width),
            "wrapped side-by-side rows exceeded width {width}: {:?}",
            rows.iter().map(line_text).collect::<Vec<_>>()
        );
    }
}

#[test]
#[ignore = "performance smoke; run benches/diff-performance.sh"]
fn perf_diff_view_render() {
    let patch = generated_patch(2_048, 3);
    let model = DiffModel::from_unified_patch("left".to_owned(), "right".to_owned(), &patch);
    let mut rendered_rows = 0_usize;
    let started = Instant::now();
    for index in 0..2_000 {
        let top = index
            % model
                .row_count(DiffLayout::Unified)
                .saturating_sub(32)
                .max(1);
        let unified = render_rows(&model, DiffLayout::Unified, top, 28, 120, index % 8);
        rendered_rows = rendered_rows.saturating_add(unified.len());
        black_box(unified);

        let side_top = index
            % model
                .row_count(DiffLayout::SideBySide)
                .saturating_sub(32)
                .max(1);
        let side = render_rows(&model, DiffLayout::SideBySide, side_top, 28, 160, index % 8);
        rendered_rows = rendered_rows.saturating_add(side.len());
        black_box(side);
    }
    let elapsed = started.elapsed();
    eprintln!(
        "diff view render: {elapsed:?}, rows={} changes={} rendered_rows={} patch_bytes={}",
        model.row_count(DiffLayout::Unified),
        model.changed_rows(DiffLayout::Unified).len(),
        rendered_rows,
        patch.len()
    );
}

fn generated_patch(hunks: usize, changes_per_hunk: usize) -> String {
    let mut patch = String::from("--- left\n+++ right\n");
    for hunk in 0..hunks {
        let start = hunk.saturating_mul(16).saturating_add(1);
        patch.push_str(&format!("@@ -{start},10 +{start},10 @@\n"));
        patch.push_str(" {\n");
        patch.push_str(&format!("   \"id\": {hunk},\n"));
        for change in 0..changes_per_hunk {
            patch.push_str(&format!("-  \"old_{change}\": \"{}\",\n", "x".repeat(48)));
        }
        for change in 0..changes_per_hunk {
            patch.push_str(&format!("+  \"new_{change}\": \"{}\",\n", "y".repeat(48)));
        }
        patch.push_str("   \"ok\": true\n");
        patch.push_str(" }\n");
    }
    patch
}