use super::*;
use crate::app::event_loop::KeyOutcome;
use crate::keymap_actions::AppAction;
use hjkl_app::diff::DiffRowKind;
#[test]
fn diffsplit_forms_pair_and_caches_alignment() {
let a = std::env::temp_dir().join("hjkl_diffsplit_a.txt");
let b = std::env::temp_dir().join("hjkl_diffsplit_b.txt");
std::fs::write(&a, "one\ntwo\nthree\n").unwrap();
std::fs::write(&b, "one\nTWO\nthree\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
assert_eq!(
app.diff_windows.len(),
2,
"diffsplit forms a 2-window group"
);
assert!(app.diff_pair().is_some(), "a diff pair must be active");
let cache = app.diff_cache.as_ref().expect("alignment must be cached");
let changes = cache
.diff
.rows
.iter()
.filter(|r| r.kind == DiffRowKind::Change)
.count();
assert_eq!(changes, 1, "one changed line between the buffers");
assert!(!cache.diff.is_empty_diff());
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn diffoff_disbands_group() {
let a = std::env::temp_dir().join("hjkl_diffoff_a.txt");
let b = std::env::temp_dir().join("hjkl_diffoff_b.txt");
std::fs::write(&a, "x\n").unwrap();
std::fs::write(&b, "y\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
assert!(app.diff_pair().is_some());
app.dispatch_ex("diffoff");
assert!(app.diff_windows.is_empty(), "diffoff clears the group");
assert!(app.diff_pair().is_none());
assert!(app.diff_cache.is_none(), "cache dropped on diffoff");
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn diffthis_single_window_is_not_a_pair() {
let a = std::env::temp_dir().join("hjkl_diffthis_solo.txt");
std::fs::write(&a, "x\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex("diffthis");
assert_eq!(app.diff_windows.len(), 1);
assert!(
app.diff_pair().is_none(),
"one window cannot form a diff pair"
);
assert!(app.diff_cache.is_none());
let _ = std::fs::remove_file(&a);
}
#[test]
fn diff_change_navigation_jumps_between_hunks() {
let a = std::env::temp_dir().join("hjkl_diffnav_a.txt");
let b = std::env::temp_dir().join("hjkl_diffnav_b.txt");
std::fs::write(&a, "l0\nl1\nl2\nl3\nl4\n").unwrap();
std::fs::write(&b, "l0\nX1\nl2\nl3\nX4\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
app.active_editor_mut().jump_cursor(0, 0);
app.dispatch_action(AppAction::DiffNextChange, 1);
assert_eq!(app.active_editor().cursor().0, 1);
app.dispatch_action(AppAction::DiffNextChange, 1);
assert_eq!(app.active_editor().cursor().0, 4);
app.dispatch_action(AppAction::DiffNextChange, 1);
assert_eq!(app.active_editor().cursor().0, 4);
app.dispatch_action(AppAction::DiffPrevChange, 1);
assert_eq!(app.active_editor().cursor().0, 1);
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn diff_scroll_binds_partner_top_row() {
let a = std::env::temp_dir().join("hjkl_diffscroll_a.txt");
let b = std::env::temp_dir().join("hjkl_diffscroll_b.txt");
std::fs::write(&a, "l0\nl1\nl2\nl3\nl4\nl5\nzebra\n").unwrap();
std::fs::write(&b, "l0\nl1\nins\nl2\nl3\nl4\nl5\nzebra\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
let (a_win, b_win) = app.diff_pair().unwrap();
assert_eq!(app.focused_window(), b_win);
app.window_editors
.get_mut(&b_win)
.unwrap()
.host_mut()
.viewport_mut()
.top_row = 3;
app.sync_diff_scroll();
assert_eq!(
app.window_scroll(a_win).0,
2,
"partner window must scroll-bind to the aligned line"
);
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn fallthrough_key_edit_refreshes_diff_alignment_and_avoids_stale_row_panic() {
let a = tmp_path("hjkl_diff_dg_a.txt");
let b = tmp_path("hjkl_diff_dg_b.txt");
std::fs::write(&a, "one\ntwo\nthree\nfour\nfive\n").unwrap();
std::fs::write(&b, "one\ntwo\nTHREE\nfour\nfive\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
let (_, b_win) = app.diff_pair().expect("diffsplit must form a pair");
assert_eq!(app.diff_cache.as_ref().unwrap().diff.rows.len(), 5);
app.active_editor_mut().jump_cursor(1, 0);
app.sync_viewport_from_editor();
app.active_editor_mut().enter_insert_i(1);
app.sync_after_engine_mutation();
let delete_key =
crossterm::event::KeyEvent::new(crossterm::event::KeyCode::Delete, KeyModifiers::NONE);
for _ in 0..40 {
if let KeyOutcome::FallThrough = app.handle_keypress(delete_key) {
app.dispatch_fallthrough_key(delete_key);
}
}
assert_eq!(
app.active_editor().buffer().rope().to_string(),
"one\n",
"buffer b must have shrunk to just the first line"
);
let cache = app.diff_cache.as_ref().expect("cache must still exist");
for row in &cache.diff.rows {
if let Some(bi) = row.b {
assert!(
bi < 1,
"stale diff row references deleted b-line {bi} — diff_cache was not refreshed"
);
}
}
let _ = app.diff_line_classes(b_win);
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}
#[test]
fn editing_refreshes_alignment_cache() {
let a = std::env::temp_dir().join("hjkl_diffedit_a.txt");
let b = std::env::temp_dir().join("hjkl_diffedit_b.txt");
std::fs::write(&a, "same\nsame\n").unwrap();
std::fs::write(&b, "same\nsame\n").unwrap();
let mut app = App::new(Some(a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("diffsplit {}", b.display()));
assert!(app.diff_cache.as_ref().unwrap().diff.is_empty_diff());
seed_buffer(&mut app, "same\nCHANGED\n");
app.sync_after_engine_mutation();
let cache = app.diff_cache.as_ref().unwrap();
assert!(
!cache.diff.is_empty_diff(),
"edit must invalidate + recompute the alignment"
);
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}