use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use tempfile::TempDir;
fn split_horizontal(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("split horiz").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn split_vertical(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("split vert").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn prev_split(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn next_split(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn close_split(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("close split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
fn test_horizontal_split_shares_same_buffer() {
let mut harness = EditorTestHarness::new(100, 40).unwrap();
harness.type_text("Original content").unwrap();
harness.render().unwrap();
let original_content = harness.get_buffer_content().unwrap();
assert_eq!(original_content, "Original content");
split_horizontal(&mut harness);
let new_split_content = harness.get_buffer_content().unwrap();
assert_eq!(
new_split_content, "Original content",
"New split should show the same buffer, got '{}'",
new_split_content
);
harness.assert_screen_contains("Split pane horiz");
let screen = harness.screen_to_string();
assert!(
screen.contains('─'),
"Horizontal split should show ─ separator"
);
}
#[test]
fn test_vertical_split_shares_same_buffer() {
let mut harness = EditorTestHarness::new(100, 40).unwrap();
harness.type_text("Buffer content").unwrap();
harness.render().unwrap();
split_vertical(&mut harness);
let new_split_content = harness.get_buffer_content().unwrap();
assert_eq!(
new_split_content, "Buffer content",
"New split should show the same buffer"
);
let screen = harness.screen_to_string();
assert!(
screen.contains('│'),
"Vertical split should show │ separator"
);
}
#[test]
fn test_typing_modifies_shared_buffer() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("Hello").unwrap();
split_vertical(&mut harness);
assert_eq!(harness.get_buffer_content().unwrap(), "Hello");
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(" World").unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Hello World");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Hello World");
}
#[test]
fn test_independent_cursor_positions_same_buffer() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("ABCDEFGHIJ").unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("split vert").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "ABCDEFGHIJ");
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
let cursor_in_second_split = harness.cursor_position();
assert_eq!(cursor_in_second_split, 10);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let cursor_in_first_split = harness.cursor_position();
assert_eq!(
cursor_in_first_split, 0,
"First split cursor should remain at 0, not {}",
cursor_in_first_split
);
}
#[test]
fn test_independent_scroll_positions_same_buffer() {
let mut harness = EditorTestHarness::new(120, 30).unwrap();
let long_text = (1..=50)
.map(|i| format!("Line {}", i))
.collect::<Vec<_>>()
.join("\n");
harness.type_text(&long_text).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let top_byte_first_split = harness.top_byte();
split_vertical(&mut harness);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let top_byte_second_split = harness.top_byte();
assert_ne!(
top_byte_second_split, top_byte_first_split,
"Second split should have different scroll position"
);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(
harness.top_byte(),
top_byte_first_split,
"First split scroll position should be preserved"
);
}
#[test]
fn test_split_navigation_circular() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("Shared buffer").unwrap();
split_vertical(&mut harness);
split_vertical(&mut harness);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
let cursor_third = harness.cursor_position();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Shared buffer");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.cursor_position(), cursor_third);
}
#[test]
fn test_close_split_expands_remaining() {
let mut harness = EditorTestHarness::new(100, 40).unwrap();
harness.type_text("Buffer content").unwrap();
split_vertical(&mut harness);
let screen_before = harness.screen_to_string();
assert!(
screen_before.contains('│'),
"Should have vertical separator"
);
close_split(&mut harness);
harness.assert_screen_contains("Closed split");
assert_eq!(harness.get_buffer_content().unwrap(), "Buffer content");
}
#[test]
fn test_split_with_different_files() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
std::fs::write(&file1, "Content of file 1").unwrap();
std::fs::write(&file2, "Content of file 2").unwrap();
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.open_file(&file1).unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Content of file 1");
split_vertical(&mut harness);
assert_eq!(harness.get_buffer_content().unwrap(), "Content of file 1");
harness.open_file(&file2).unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Content of file 2");
harness.assert_screen_contains("file1.txt");
harness.assert_screen_contains("file2.txt");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Content of file 1");
}
#[test]
fn test_nested_splits_maintain_hierarchy() {
let mut harness = EditorTestHarness::new(160, 50).unwrap();
harness.type_text("Base content").unwrap();
split_vertical(&mut harness);
split_horizontal(&mut harness);
split_vertical(&mut harness);
assert_eq!(harness.get_buffer_content().unwrap(), "Base content");
for _ in 0..4 {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Base content");
}
}
#[test]
fn test_undo_redo_affects_shared_buffer() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("Hello").unwrap();
split_vertical(&mut harness);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.type_text(" World").unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "Hello World");
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let after_undo = harness.get_buffer_content().unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(
harness.get_buffer_content().unwrap(),
after_undo,
"Both splits should show same buffer state after undo"
);
}
#[test]
fn test_status_bar_reflects_active_split() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("alpha.txt");
let file2 = temp_dir.path().join("beta.txt");
std::fs::write(&file1, "Alpha content").unwrap();
std::fs::write(&file2, "Beta content").unwrap();
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.open_file(&file1).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("alpha.txt");
split_vertical(&mut harness);
harness.open_file(&file2).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("beta.txt");
}
#[test]
fn test_delete_visible_in_all_splits() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("ABCDE").unwrap();
split_vertical(&mut harness);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "ABC");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "ABC");
}
#[test]
fn test_cursor_movement_isolated_to_active_split() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
let first_cursor_before = harness.cursor_position();
assert_eq!(first_cursor_before, 0);
split_vertical(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
let second_cursor = harness.cursor_position();
assert_ne!(second_cursor, first_cursor_before);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.cursor_position(), first_cursor_before);
}
#[test]
fn test_split_with_minimal_height() {
let mut harness = EditorTestHarness::new(100, 12).unwrap();
harness.type_text("Small terminal").unwrap();
split_horizontal(&mut harness);
harness.assert_screen_contains("Split pane horiz");
harness.type_text(" - modified").unwrap();
harness.render().unwrap();
assert!(harness.get_buffer_content().unwrap().contains("modified"));
}
#[test]
fn test_clipboard_shared_across_splits() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("CopyThis").unwrap();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
split_vertical(&mut harness);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "CopyThisCopyThis");
}
#[test]
fn test_cursor_adjustment_on_shared_buffer_edit() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
harness.type_text("ABCDEFGHIJ").unwrap();
split_vertical(&mut harness);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
assert_eq!(harness.cursor_position(), 5);
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("prev split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.type_text("XXX").unwrap(); harness.render().unwrap();
assert_eq!(harness.get_buffer_content().unwrap(), "XXXABCDEFGHIJ");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("next split").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let adjusted_cursor = harness.cursor_position();
assert_eq!(
adjusted_cursor, 8,
"Cursor in second split should adjust for insertion, got {}",
adjusted_cursor
);
}
#[test]
fn test_cursors_visible_in_all_splits() {
let mut harness = EditorTestHarness::new(120, 30).unwrap();
harness.type_text("ABCDEFGHIJ").unwrap();
harness.render().unwrap();
split_vertical(&mut harness);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let second_split_cursor_pos = harness.cursor_position();
assert_eq!(second_split_cursor_pos, 5);
let cursors = harness.find_all_cursors();
assert!(
cursors.len() >= 2,
"Should render cursors for both splits, found {} cursors: {:?}",
cursors.len(),
cursors
);
let primary_cursors: Vec<_> = cursors
.iter()
.filter(|(_, _, _, is_primary)| *is_primary)
.collect();
let secondary_cursors: Vec<_> = cursors
.iter()
.filter(|(_, _, _, is_primary)| !*is_primary)
.collect();
assert_eq!(
primary_cursors.len(),
1,
"Should have exactly one primary (hardware) cursor"
);
assert!(
!secondary_cursors.is_empty(),
"Should have at least one secondary cursor for inactive split"
);
}
#[test]
fn test_cursor_movement_after_split_switch() {
let mut harness = EditorTestHarness::new(120, 30).unwrap();
harness.type_text("ABCDEFGHIJ").unwrap();
harness.render().unwrap();
let initial_pos = harness.cursor_position();
assert_eq!(initial_pos, 10);
split_vertical(&mut harness);
let second_split_pos = harness.cursor_position();
assert_eq!(second_split_pos, 0);
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let after_right = harness.cursor_position();
assert_eq!(after_right, 1);
prev_split(&mut harness);
let first_split_pos = harness.cursor_position();
assert_eq!(first_split_pos, 10);
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let after_left = harness.cursor_position();
assert_eq!(after_left, 9, "Cursor should move left from 10 to 9");
let (screen_x1, _screen_y1) = harness.screen_cursor_position();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let after_right2 = harness.cursor_position();
assert_eq!(after_right2, 10);
let (screen_x2, _screen_y2) = harness.screen_cursor_position();
assert_ne!(screen_x1, screen_x2, "Screen cursor X should have moved");
}
#[test]
fn test_scroll_wheel_targets_viewport_under_pointer() {
let mut harness = EditorTestHarness::new(120, 40).unwrap();
let long_text = (1..=100)
.map(|i| format!("Line {}", i))
.collect::<Vec<_>>()
.join("\n");
let _fixture = harness.load_buffer_from_text(&long_text).unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
split_horizontal(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
prev_split(&mut harness);
let top_byte_first_split = harness.top_byte();
for _ in 0..5 {
harness.mouse_scroll_down(60, 30).unwrap();
}
let top_byte_first_after = harness.top_byte();
assert_eq!(
top_byte_first_after, top_byte_first_split,
"Focused (top) split should NOT have scrolled when mouse was over bottom split"
);
next_split(&mut harness);
let top_byte_second_after = harness.top_byte();
assert_ne!(
top_byte_second_after, top_byte_first_split,
"Bottom split (under mouse pointer) should have scrolled"
);
}