declutter-diff 0.1.3

Review AI-generated diffs with comments, tests, imports, logging and formatting toggled in and out
Documentation
use declutter::review::{ChangeStatus, FileChange, FileReview, Layers};
use declutter::tui::{App, draw};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::crossterm::event::{KeyCode, KeyEvent};

fn app() -> App {
    let file = FileReview::new(FileChange {
        path: "rate.ts".to_string(),
        old_path: None,
        status: ChangeStatus::Modified,
        old: Some("// Percent.\nconst rate = 5;\n".to_string()),
        new: Some("// Fraction.\nconst rate = 0.05;\n".to_string()),
        binary: false,
    });
    App::new(vec![file], Layers::default())
}

fn screen(app: &mut App) -> String {
    let mut terminal = Terminal::new(TestBackend::new(110, 16)).expect("test terminal");
    terminal.draw(|frame| draw(frame, app)).expect("draw");
    let buffer = terminal.backend().buffer();
    let width = buffer.area.width as usize;
    buffer
        .content()
        .chunks(width)
        .map(|row| {
            row.iter()
                .map(|cell| cell.symbol())
                .collect::<String>()
                .trim_end()
                .to_string()
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn c_hides_and_shows_comments_and_shift_c_shows_them_alone() {
    let mut app = app();
    let press = |app: &mut App, c| app.handle_key(KeyEvent::from(KeyCode::Char(c)));

    let hidden = screen(&mut app);
    assert!(hidden.contains("comments: hidden"), "{hidden}");
    assert!(hidden.contains("+ const rate = 0.05;"), "{hidden}");
    assert!(!hidden.contains("Fraction"), "{hidden}");

    press(&mut app, 'c');
    let shown = screen(&mut app);
    assert!(shown.contains("all layers shown"), "{shown}");
    assert!(
        shown.contains("+ // Fraction.") && shown.contains("+ const rate = 0.05;"),
        "{shown}"
    );

    press(&mut app, 'c');
    assert!(screen(&mut app).contains("comments: hidden"));

    press(&mut app, 'C');
    let only = screen(&mut app);
    assert!(only.contains("comments: only"), "{only}");
    assert!(only.contains("+ // Fraction."), "{only}");
    assert!(!only.contains("0.05"), "{only}");

    press(&mut app, 'C');
    assert!(
        screen(&mut app).contains("comments: hidden"),
        "pressing C again goes back to how comments were"
    );
}

fn change(path: &str, old: &str, new: &str) -> FileReview {
    FileReview::new(FileChange {
        path: path.to_string(),
        old_path: None,
        status: ChangeStatus::Modified,
        old: Some(old.to_string()),
        new: Some(new.to_string()),
        binary: false,
    })
}

#[test]
fn arrow_keys_move_through_the_file_list_until_the_diff_has_focus() {
    let long_old: String = (0..60).map(|i| format!("let v{i} = {i}\n")).collect();
    let long_new = long_old
        .replace("= 0\n", "= 100\n")
        .replace("= 59\n", "= 159\n");
    let mut app = App::new(
        vec![
            change("A.swift", "let a = 1\n", "let a = 2\n"),
            change("B.swift", &long_old, &long_new),
        ],
        Layers::default(),
    );
    screen(&mut app);

    app.handle_key(KeyEvent::from(KeyCode::Down));
    assert_eq!(app.selected, 1);
    assert!(screen(&mut app).contains(" B.swift [Swift] "));

    app.handle_key(KeyEvent::from(KeyCode::Right));
    let first_change = app.cursor;
    app.handle_key(KeyEvent::from(KeyCode::Down));
    assert_eq!(app.selected, 1);
    assert_eq!(app.cursor, first_change + 1);

    app.handle_key(KeyEvent::from(KeyCode::Left));
    app.handle_key(KeyEvent::from(KeyCode::Up));
    assert_eq!(app.selected, 0);
}