quinjet 0.0.74

A fast, live, keyboard-first Git source-control interface for the terminal
use std::time::Instant;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::Terminal;
use ratatui::backend::TestBackend;

use super::*;

fn wrap_app(layout: DiffLayout) -> App {
    let mut app = App::new("/example/project", "example");
    app.diff_layout = layout;
    app.focus = Focus::Content;
    let mut code = test_line(
        DiffLineKind::Added,
        &format!("{}END_OF_LINE", "界0123456789".repeat(20)),
    );
    code.new_line = Some(42);
    app.set_document(DiffDocument {
        lines: vec![
            test_file_header("example.rs", 1, 0),
            code,
            test_line(DiffLineKind::FileFooter, ""),
        ],
        ..DiffDocument::default()
    });
    app
}

#[test]
fn wrap_shortcut_reveals_the_tail_and_preserves_line_numbers_in_both_layouts() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = wrap_app(layout);
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert!(!rendered_text(terminal.backend().buffer()).contains("END_OF_LINE"));
        app.horizontal_scroll = 20;
        app.handle_key(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
            Instant::now(),
        );
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let text = rendered_text(terminal.backend().buffer());
        assert!(app.wrap_code);
        let code = visible_wrapped_code(terminal.backend().buffer(), &app, layout);
        assert!(code.ends_with("END_OF_LINE"), "{layout:?}: {code}");
        assert_eq!(text.matches("  42 ").count(), 1);
        assert_eq!(text.matches('界').count(), 20);
        assert_eq!(app.horizontal_scroll, 0);
        assert_eq!(app.horizontal_scroll_regions.len(), 0);
        let pointer = app.diff_wrap_offsets.as_ptr();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert_eq!(app.diff_wrap_offsets.as_ptr(), pointer);
        app.handle_key(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
            Instant::now(),
        );
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert!(!app.wrap_code);
        assert!(!rendered_text(terminal.backend().buffer()).contains("END_OF_LINE"));
        assert!(app.horizontal_scroll_limit > 0);
    }
}

#[test]
fn wrapping_uses_each_side_width_and_reflows_when_the_divider_moves() {
    let mut app = wrap_app(DiffLayout::SideBySide);
    app.wrap_code = true;
    app.diff_split_percent = 30;
    app.document
        .lines
        .insert(1, test_line(DiffLineKind::Removed, &"old text ".repeat(30)));
    app.invalidate_diff_rows();
    let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
    terminal
        .draw(|frame| draw(frame, &mut app, &Theme::default()))
        .unwrap();
    let narrow_old_rows = app
        .side_by_side_diff_rows
        .iter()
        .filter(|row| matches!(row, SideBySideRow::Split(Some(1), _)))
        .count();
    assert!(
        app.side_by_side_diff_rows
            .iter()
            .any(|row| matches!(row, SideBySideRow::Split(Some(1), None)))
    );
    app.diff_split_percent = 70;
    terminal
        .draw(|frame| draw(frame, &mut app, &Theme::default()))
        .unwrap();
    let wide_old_rows = app
        .side_by_side_diff_rows
        .iter()
        .filter(|row| matches!(row, SideBySideRow::Split(Some(1), _)))
        .count();
    assert!(wide_old_rows < narrow_old_rows);
    assert!(rendered_text(terminal.backend().buffer()).contains("END_OF_LINE"));
}

#[test]
fn wrap_toggling_keeps_the_same_source_line_at_the_top() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = wrap_app(layout);
        app.document
            .lines
            .extend((0..100).map(|row| test_line(DiffLineKind::Context, &format!("line {row}"))));
        app.invalidate_diff_rows();
        app.content_scroll = 15;
        let mut terminal = Terminal::new(TestBackend::new(180, 30)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let index = app.diff_scroll_anchor.unwrap().0;
        app.handle_key(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
            Instant::now(),
        );
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert_eq!(app.diff_scroll_anchor.unwrap().0, index);
        assert!(app.content_scroll > 15);
        app.handle_key(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
            Instant::now(),
        );
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert_eq!(app.diff_scroll_anchor.unwrap().0, index);
        assert_eq!(app.content_scroll, 15);
    }
}

#[test]
fn clipping_a_wide_character_does_not_pull_later_highlight_spans_forward() {
    let spans = vec![
        HighlightSpan {
            text: "abc界".to_owned(),
            foreground: None,
            bold: false,
            italic: false,
        },
        HighlightSpan {
            text: "tail".to_owned(),
            foreground: None,
            bold: true,
            italic: false,
        },
    ];
    let first = highlight_spans(&spans, 0, 4, DiffLineKind::Added, None, &Theme::default());
    assert_eq!(
        first
            .iter()
            .map(|span| span.content.as_ref())
            .collect::<String>(),
        "abc"
    );
    let second = highlight_spans(&spans, 3, 6, DiffLineKind::Added, None, &Theme::default());
    assert_eq!(
        second
            .iter()
            .map(|span| span.content.as_ref())
            .collect::<String>(),
        "界tail"
    );
    assert!(
        second
            .last()
            .unwrap()
            .style
            .add_modifier
            .contains(Modifier::BOLD)
    );
}

#[expect(clippy::integer_division, reason = "the test uses equal panes")]
fn visible_wrapped_code(buffer: &Buffer, app: &App, layout: DiffLayout) -> String {
    let area = app.geometry.content;
    let width = area.width.saturating_sub(1);
    let (start, end) = if layout == DiffLayout::SideBySide {
        (
            area.x + width.saturating_sub(1) / 2 + 1 + 7,
            area.right() - 1,
        )
    } else {
        (area.x + 12, area.right() - 1)
    };
    let mut result = String::new();
    let rows = if layout == DiffLayout::SideBySide {
        &app.side_by_side_diff_rows
            .iter()
            .enumerate()
            .filter_map(|(row, value)| {
                matches!(value, SideBySideRow::Split(_, Some(1))).then_some(row)
            })
            .collect::<Vec<_>>()
    } else {
        &app.unified_diff_rows
            .iter()
            .enumerate()
            .filter_map(|(row, index)| (*index == 1).then_some(row))
            .collect::<Vec<_>>()
    };
    for row in rows {
        let mut column = start;
        let mut text = String::new();
        while column < end {
            let symbol = buffer[(column, area.y + 1 + cells(*row))].symbol();
            text.push_str(symbol);
            column += cells(symbol.width().max(1));
        }
        result.push_str(text.trim_end());
    }
    result
}