quinjet 0.0.72

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

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

use super::*;

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

fn swipe(app: &mut App, kind: MouseEventKind, column: u16, row: u16, modifiers: KeyModifiers) {
    app.handle_mouse(
        MouseEvent {
            kind,
            column,
            row,
            modifiers,
        },
        Instant::now(),
    );
}

#[test]
fn horizontal_wheel_events_outside_code_never_move_the_preview() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        for (column, row) in [(4, 1), (4, 10), (90, 3), (90, 4), (90, 46), (90, 30)] {
            swipe(
                &mut app,
                MouseEventKind::ScrollRight,
                column,
                row,
                KeyModifiers::NONE,
            );
            swipe(
                &mut app,
                MouseEventKind::ScrollDown,
                column,
                row,
                KeyModifiers::SHIFT,
            );
            assert_eq!(app.horizontal_scroll, 0);
        }
        let region = app.horizontal_scroll_regions[0];
        swipe(
            &mut app,
            MouseEventKind::ScrollRight,
            region.x - 1,
            region.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 0);
        if layout == DiffLayout::SideBySide {
            let column = app.geometry.content.x + 3;
            swipe(
                &mut app,
                MouseEventKind::ScrollRight,
                column,
                region.y,
                KeyModifiers::NONE,
            );
            assert_eq!(app.horizontal_scroll, 0);
        }
        swipe(
            &mut app,
            MouseEventKind::ScrollRight,
            region.x,
            region.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 3);
    }
}

#[test]
fn horizontal_swipes_over_short_code_rows_pan_the_overflowing_diff() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        app.document
            .lines
            .insert(2, test_line(DiffLineKind::Added, "short code"));
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let region = app.horizontal_scroll_regions[0];
        swipe(
            &mut app,
            MouseEventKind::ScrollRight,
            region.x,
            region.y + 1,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 3);
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        swipe(
            &mut app,
            MouseEventKind::ScrollLeft,
            region.x,
            region.y + 1,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 0);
    }
}

#[test]
fn repeated_swipes_stop_at_the_visible_text_extent_and_reset_is_reachable() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let region = app.horizontal_scroll_regions[0];
        for _ in 0..1_000 {
            swipe(
                &mut app,
                MouseEventKind::ScrollRight,
                region.x,
                region.y,
                KeyModifiers::NONE,
            );
        }
        assert_eq!(app.horizontal_scroll, app.horizontal_scroll_limit);
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert!(app.geometry.horizontal_scrollbar.is_some());
        assert!(!rendered_text(terminal.backend().buffer()).contains("[0 Reset]"));
        app.handle_key(
            KeyEvent::new(KeyCode::Char('0'), KeyModifiers::NONE),
            Instant::now(),
        );
        assert_eq!(app.horizontal_scroll, 0);
        for _ in 0..1_000 {
            swipe(
                &mut app,
                MouseEventKind::ScrollLeft,
                region.x,
                region.y,
                KeyModifiers::NONE,
            );
        }
        assert_eq!(app.horizontal_scroll, 0);
    }
}

#[test]
fn shorter_content_and_wider_panes_clamp_an_old_pan_offset() {
    let mut app = long_code_app(DiffLayout::Unified);
    let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
    app.horizontal_scroll = usize::MAX;
    terminal
        .draw(|frame| draw(frame, &mut app, &Theme::default()))
        .unwrap();
    assert_eq!(app.horizontal_scroll, app.horizontal_scroll_limit);
    app.set_document(DiffDocument::empty("short", "visible code"));
    terminal
        .draw(|frame| draw(frame, &mut app, &Theme::default()))
        .unwrap();
    assert_eq!(app.horizontal_scroll, 0);
    assert_eq!(app.horizontal_scroll_regions.len(), 0);
    assert!(app.geometry.horizontal_scrollbar.is_none());
    assert!(rendered_text(terminal.backend().buffer()).contains("visible code"));
}

#[test]
fn horizontal_scrollbar_thumb_tracks_the_pan_and_keeps_bottom_navigation_clear() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        app.document.lines.extend(std::iter::repeat_n(
            test_line(DiffLineKind::Context, "short code"),
            100,
        ));
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let bar = app.geometry.horizontal_scrollbar.unwrap();
        assert_eq!(bar.thumb.x, bar.area.x + 1);
        assert_eq!(bar.area.y, app.geometry.content.bottom() - 1);
        let bottom = app
            .geometry
            .scm_action_hits
            .iter()
            .find(|hit| hit.action == ScmAction::JumpToBottom)
            .unwrap();
        assert!(bar.area.right() <= bottom.area.x);
        let text = rendered_text(terminal.backend().buffer());
        assert!(!text.contains("←/→"));
        assert!(!text.contains("[0 Reset]"));
        assert_eq!(
            terminal.backend().buffer()[(bar.area.x, bar.area.y)].symbol(),
            "◀"
        );
        assert_eq!(
            terminal.backend().buffer()[(bar.thumb.x, bar.thumb.y)].symbol(),
            "━"
        );
        app.horizontal_scroll = app.horizontal_scroll_limit;
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let end = app.geometry.horizontal_scrollbar.unwrap();
        assert_eq!(end.thumb.right(), end.area.right() - 1);
    }
}

#[test]
fn horizontal_scrollbar_accepts_track_clicks_arrows_and_bounded_thumb_dragging() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let bar = app.geometry.horizontal_scrollbar.unwrap();
        swipe(
            &mut app,
            MouseEventKind::Down(MouseButton::Left),
            bar.area.right() - 1,
            bar.area.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 3);
        swipe(
            &mut app,
            MouseEventKind::Down(MouseButton::Left),
            bar.area.x,
            bar.area.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 0);
        swipe(
            &mut app,
            MouseEventKind::Down(MouseButton::Left),
            bar.area.right() - 2,
            bar.area.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, app.horizontal_scroll_limit);
        assert!(app.text_selection.is_none());
        swipe(
            &mut app,
            MouseEventKind::Up(MouseButton::Left),
            bar.area.right() - 2,
            bar.area.y,
            KeyModifiers::NONE,
        );
        app.horizontal_scroll /= 2;
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let middle = app.geometry.horizontal_scrollbar.unwrap();
        let offset = app.horizontal_scroll;
        swipe(
            &mut app,
            MouseEventKind::Down(MouseButton::Left),
            middle.thumb.x + 1,
            middle.thumb.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, offset);
        swipe(
            &mut app,
            MouseEventKind::Drag(MouseButton::Left),
            middle.thumb.x + 1,
            middle.thumb.y,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, offset);
        swipe(
            &mut app,
            MouseEventKind::Drag(MouseButton::Left),
            middle.area.right() + 100,
            middle.area.y + 2,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, app.horizontal_scroll_limit);
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        swipe(
            &mut app,
            MouseEventKind::Drag(MouseButton::Left),
            0,
            middle.area.y + 2,
            KeyModifiers::NONE,
        );
        assert_eq!(app.horizontal_scroll, 0);
        let effects = app.handle_mouse(
            MouseEvent {
                kind: MouseEventKind::Up(MouseButton::Left),
                column: 0,
                row: middle.area.y + 2,
                modifiers: KeyModifiers::NONE,
            },
            Instant::now(),
        );
        assert!(effects.is_empty());
        assert!(app.horizontal_scroll_drag.is_none());
        assert!(app.text_selection.is_none());
    }
}

#[test]
fn horizontal_scrollbar_reflows_on_resize_and_disappears_when_code_wraps() {
    for layout in [DiffLayout::Unified, DiffLayout::SideBySide] {
        let mut app = long_code_app(layout);
        let mut terminal = Terminal::new(TestBackend::new(180, 48)).unwrap();
        terminal
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let wide = app.geometry.horizontal_scrollbar.unwrap();
        let mut narrow = Terminal::new(TestBackend::new(120, 24)).unwrap();
        narrow
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        let bar = app.geometry.horizontal_scrollbar.unwrap();
        assert!(bar.area.width < wide.area.width);
        swipe(
            &mut app,
            MouseEventKind::Down(MouseButton::Left),
            bar.thumb.x,
            bar.thumb.y,
            KeyModifiers::NONE,
        );
        assert!(app.horizontal_scroll_drag.is_some());
        app.handle_key(
            KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
            Instant::now(),
        );
        narrow
            .draw(|frame| draw(frame, &mut app, &Theme::default()))
            .unwrap();
        assert_eq!(app.horizontal_scroll, 0);
        assert!(app.geometry.horizontal_scrollbar.is_none());
        assert!(app.horizontal_scroll_drag.is_none());
    }
}