use crate::common::harness::EditorTestHarness;
#[test]
fn test_add_cursor_next_match_with_backward_selection() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("foo bar foo").unwrap();
harness.assert_buffer_content("foo bar foo");
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
let primary = harness.editor().active_cursors().primary();
assert_eq!(
primary.position, 3,
"Cursor should be at position 3 after first 'foo'"
);
harness
.send_key(KeyCode::Left, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Left, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Left, KeyModifiers::SHIFT)
.unwrap();
let primary = harness.editor().active_cursors().primary();
assert_eq!(
primary.position, 0,
"After Shift+Left, cursor should be at start of selection"
);
assert_eq!(
primary.anchor,
Some(3),
"After Shift+Left, anchor should be at end of selection"
);
harness.editor_mut().add_cursor_at_next_match();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().iter().count(), 2);
for (id, cursor) in harness.editor().active_cursors().iter() {
let selection = cursor
.selection_range()
.expect("Cursor should have selection");
let is_at_start = cursor.position == selection.start;
let is_at_end = cursor.position == selection.end;
assert!(
is_at_start,
"Cursor {:?} should be at start of selection. Position: {}, Selection: {:?}",
id, cursor.position, selection
);
assert!(
!is_at_end || selection.start == selection.end,
"Cursor {:?} should NOT be at end of selection (unless collapsed). Position: {}, Selection: {:?}",
id, cursor.position, selection
);
}
harness.type_text("X").unwrap();
harness.render().unwrap();
harness.assert_buffer_content("X bar X");
}
#[test]
fn test_add_cursor_next_match_with_forward_selection() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("foo bar foo").unwrap();
harness.assert_buffer_content("foo bar foo");
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
let primary = harness.editor().active_cursors().primary();
assert_eq!(
primary.position, 3,
"After Shift+Right, cursor should be at end of selection"
);
assert_eq!(
primary.anchor,
Some(0),
"After Shift+Right, anchor should be at start of selection"
);
harness.editor_mut().add_cursor_at_next_match();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().iter().count(), 2);
harness.type_text("X").unwrap();
harness.render().unwrap();
harness.assert_buffer_content("X bar X");
}
#[test]
fn test_add_cursor_next_match() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("foo bar foo baz foo").unwrap();
harness.assert_buffer_content("foo bar foo baz foo");
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
let primary = harness.editor().active_cursors().primary();
assert_eq!(primary.position, 3);
assert_eq!(primary.anchor, Some(0));
harness.editor_mut().add_cursor_at_next_match();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 2);
harness.editor_mut().add_cursor_at_next_match();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 3);
}
#[test]
fn test_add_cursor_above() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness.assert_buffer_content("Line 1\nLine 2\nLine 3");
harness.editor_mut().add_cursor_above();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 2);
harness.editor_mut().add_cursor_above();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 3);
}
#[test]
fn test_add_cursor_below() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 2);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
let cursors = &harness.editor().active_cursors();
assert_eq!(cursors.iter().count(), 3);
}
#[test]
fn test_multi_cursor_typing() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("aaa\nbbb\nccc\nddd").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below();
let cursor_count = harness.editor().active_cursors().iter().count();
assert_eq!(cursor_count, 3, "Should have 3 cursors");
harness.type_text("xyz").unwrap();
harness.assert_buffer_content("xyzaaa\nxyzbbb\nxyzccc\nddd");
}
#[test]
fn test_remove_secondary_cursors() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness.editor_mut().add_cursor_above();
harness.editor_mut().add_cursor_above();
assert_eq!(harness.editor().active_cursors().iter().count(), 3);
harness.editor_mut().active_cursors_mut().remove_secondary();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().iter().count(), 1);
}
#[test]
fn test_multi_cursor_undo_atomic() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("aaa\nbbb\nccc\nddd").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below();
let cursor_count = harness.editor().active_cursors().iter().count();
assert_eq!(cursor_count, 3, "Should have 3 cursors");
harness.type_text("xyz").unwrap();
harness.assert_buffer_content("xyzaaa\nxyzbbb\nxyzccc\nddd");
for _ in 0..3 {
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
harness.assert_buffer_content("aaa\nbbb\nccc\nddd");
for _ in 0..3 {
harness
.send_key(KeyCode::Char('y'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
harness.assert_buffer_content("xyzaaa\nxyzbbb\nxyzccc\nddd");
}
#[test]
fn test_multi_cursor_delete_undo_atomic() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("aaa\nbbb\nccc").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
assert_eq!(harness.editor().active_cursors().iter().count(), 3);
harness
.send_key(KeyCode::Delete, KeyModifiers::NONE)
.unwrap();
harness.assert_buffer_content("aa\nbb\ncc");
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.assert_buffer_content("aaa\nbbb\nccc");
}
#[test]
fn test_add_cursor_undo() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 2);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 3);
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 2);
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
harness
.send_key(KeyCode::Char('y'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 2);
}
#[test]
#[ignore]
fn test_remove_cursor_undo() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
assert_eq!(harness.editor().active_cursors().count(), 3);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 3);
harness
.send_key(KeyCode::Char('y'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
}
#[test]
fn test_undo_beyond_cursor_add() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("aaa\nbbb\nccc").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 2);
harness.type_text("xyz").unwrap();
harness.assert_buffer_content("xyzaaa\nxyzbbb\nccc");
for _ in 0..3 {
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
harness.assert_buffer_content("aaa\nbbb\nccc");
assert_eq!(harness.editor().active_cursors().count(), 2);
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 1);
harness
.send_key(KeyCode::Char('y'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().count(), 2);
for _ in 0..3 {
harness
.send_key(KeyCode::Char('y'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
harness.assert_buffer_content("xyzaaa\nxyzbbb\nccc");
}
#[test]
fn test_multi_cursor_status_bar_indicator() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains(" cursors"),
"Should not show cursor count with single cursor"
);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("2 cursors"),
"Status bar should show '2 cursors'. Screen:\n{screen}"
);
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("3 cursors"),
"Status bar should show '3 cursors'. Screen:\n{screen}"
);
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
!screen.contains(" cursors"),
"Should not show cursor count after removing cursors"
);
}
#[test]
fn test_all_cursors_visible_in_viewport() {
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Modifier;
let mut harness = EditorTestHarness::new_no_wrap(80, 24).unwrap();
harness.type_text("Line 1\nLine 2\nLine 3").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(harness.cursor_count(), 3);
let line_y_positions = vec![1, 2, 3];
let mut cursor_indicators_found = 0;
for y in line_y_positions {
for x in 0..10 {
if let Some(style) = harness.get_cell_style(x, y) {
if style.add_modifier.contains(Modifier::REVERSED) {
cursor_indicators_found += 1;
break; }
}
}
}
assert!(
cursor_indicators_found >= 2,
"Expected at least 2 visible cursors (secondary cursors), found {cursor_indicators_found}"
);
}
#[test]
fn test_multi_cursor_comprehensive_abc_editing() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("abc1\nabc2\nabc3\nabc4").unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below();
let cursor_count = harness.editor().active_cursors().iter().count();
assert_eq!(cursor_count, 4, "Should have 4 cursors");
harness.type_text("xyz").unwrap();
harness.assert_buffer_content("xyzabc1\nxyzabc2\nxyzabc3\nxyzabc4");
for _ in 0..3 {
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
harness.assert_buffer_content("abc1\nabc2\nabc3\nabc4");
assert_eq!(harness.editor().active_cursors().iter().count(), 4);
}
#[test]
fn test_single_cursor_visible() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new_no_wrap(80, 24).unwrap();
harness
.type_text("Hello World\nSecond Line Here\nThird Line\nFourth")
.unwrap();
let expected_content = "Hello World\nSecond Line Here\nThird Line\nFourth";
harness.assert_buffer_content(expected_content);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let expected_chars = vec![
'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\n', ];
println!("\nStarting comprehensive cursor visibility test...");
println!("Testing first line: 'Hello World'");
for (step, expected_char) in expected_chars.iter().enumerate() {
harness.render().unwrap();
let cursor_pos = harness.cursor_position();
println!("\nStep {step}: cursor at buffer position {cursor_pos}");
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Step {step}: Cursor not visible at buffer position {cursor_pos}! Expected char: '{expected_char}'"
);
let (x, y, char_at_cursor, _is_primary) = &cursors[0];
println!(" Screen position: ({x}, {y}), char: '{char_at_cursor}'");
if *expected_char == '\n' {
println!(" At newline - expecting space or newline indicator");
} else {
let expected_str = expected_char.to_string();
assert_eq!(
*char_at_cursor, expected_str,
"Step {step}: Cursor at wrong character. Expected '{expected_str}', got '{char_at_cursor}'"
);
}
if step < expected_chars.len() - 1 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
}
println!("\nTesting navigation to second line...");
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
let after_down = harness.cursor_position();
println!("After Down: cursor at buffer position {after_down}");
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
let after_home = harness.cursor_position();
println!("After Home: cursor at buffer position {after_home}");
harness.render().unwrap();
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor should be visible at start of second line"
);
let (x, y, char_at_cursor, _is_primary) = &cursors[0];
println!(
"At start of line 2: screen ({x}, {y}), char: '{char_at_cursor}', buffer pos: {after_home}"
);
if after_home == 12 {
assert_eq!(*char_at_cursor, "S", "Should be at 'S' of 'Second'");
} else {
println!("WARNING: Cursor not at expected position 12, it's at {after_home}");
}
let second_chars = ['S', 'e', 'c', 'o', 'n', 'd'];
for (i, expected_char) in second_chars.iter().enumerate() {
harness.render().unwrap();
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor not visible at char {i} of 'Second'"
);
let (_, _, char_at_cursor, _is_primary) = &cursors[0];
let expected_str = expected_char.to_string();
assert_eq!(
*char_at_cursor, expected_str,
"At position {i} of 'Second': expected '{expected_str}', got '{char_at_cursor}'"
);
if i < second_chars.len() - 1 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
}
println!("\nTesting vertical navigation...");
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor should be visible after moving down"
);
println!("After Down: cursor at {:?}", cursors[0]);
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor should be visible after moving up"
);
println!("After Up: cursor at {:?}", cursors[0]);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor should be visible at end of document"
);
println!("At end of document: cursor at {:?}", cursors[0]);
println!("\nCursor visibility test completed successfully!");
}
#[test]
fn test_cursor_visible_on_empty_line() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.render().unwrap();
assert_eq!(harness.cursor_count(), 1);
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor should be visible on empty line"
);
assert_eq!(cursors.len(), 1, "Should have exactly 1 visible cursor");
harness.type_text("Test").unwrap();
for _ in 0..4 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let cursors_after_delete = harness.find_all_cursors();
assert!(
!cursors_after_delete.is_empty(),
"Cursor should be visible on empty line after deleting text"
);
harness.type_text("\n\n\n").unwrap();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let cursors_on_middle_empty = harness.find_all_cursors();
assert!(
!cursors_on_middle_empty.is_empty(),
"Cursor should be visible on middle empty line"
);
}
#[test]
fn test_cursor_visible_on_initial_empty_buffer() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.render().unwrap();
println!("Testing initial empty buffer cursor visibility...");
println!(
"Buffer length: {}",
harness.editor().active_state().buffer.len()
);
println!(
"Cursor position: {}",
harness.editor().active_cursors().primary().position
);
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor must be visible when editor opens with empty buffer"
);
let (x, y, char_at_cursor, is_primary) = &cursors[0];
println!(
"Found cursor at screen position ({x}, {y}): '{char_at_cursor}' (primary: {is_primary})"
);
assert!(*is_primary, "The only cursor should be the primary cursor");
let gutter_width = harness.editor().active_state().margins.left_total_width();
println!("Gutter width: {}", gutter_width);
println!("Cursor screen position: ({}, {})", x, y);
println!(
"Margins enabled: {}",
harness.editor().active_state().margins.left_config.enabled
);
assert!(
*x >= gutter_width as u16,
"Cursor x position ({}) must be >= gutter width ({}) - cursor should not be in gutter area",
x,
gutter_width
);
}
#[test]
fn test_ctrl_end_cursor_position() {
use crossterm::event::{KeyCode, KeyModifiers};
{
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.render().unwrap();
let gutter_width = harness.editor().active_state().margins.left_total_width();
println!("Test 1: Empty buffer");
println!(" Gutter width: {}", gutter_width);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let (cursor_x, _cursor_y) = harness.screen_cursor_position();
println!(" Cursor x after Ctrl+End: {}", cursor_x);
assert!(
cursor_x >= gutter_width as u16,
"Empty buffer: Cursor x ({}) should be >= gutter width ({})",
cursor_x,
gutter_width
);
}
{
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("hello\n").unwrap();
harness.render().unwrap();
let gutter_width = harness.editor().active_state().margins.left_total_width();
println!("Test 2: Buffer with trailing newline 'hello\\n'");
println!(" Gutter width: {}", gutter_width);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let (cursor_x, cursor_y) = harness.screen_cursor_position();
println!(
" Cursor position after Ctrl+End: ({}, {})",
cursor_x, cursor_y
);
assert!(
cursor_x >= gutter_width as u16,
"Trailing newline: Cursor x ({}) should be >= gutter width ({})",
cursor_x,
gutter_width
);
let row_text = harness.get_row_text(cursor_y);
println!(" Row text at cursor y={}: '{}'", cursor_y, row_text);
let gutter_text: String = row_text.chars().take(gutter_width).collect();
println!(" Gutter text: '{}'", gutter_text);
assert!(
gutter_text.contains("2"),
"Trailing newline: Gutter should show line number 2 for the implicit line, got: '{}'",
gutter_text
);
}
{
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("hello\n\n").unwrap();
harness.render().unwrap();
let gutter_width = harness.editor().active_state().margins.left_total_width();
println!("Test 3: Buffer with empty line 'hello\\n\\n'");
println!(" Gutter width: {}", gutter_width);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let (cursor_x, _cursor_y) = harness.screen_cursor_position();
println!(" Cursor x after Ctrl+End: {}", cursor_x);
assert!(
cursor_x >= gutter_width as u16,
"Empty line: Cursor x ({}) should be >= gutter width ({})",
cursor_x,
gutter_width
);
}
{
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("hello").unwrap();
harness.render().unwrap();
let gutter_width = harness.editor().active_state().margins.left_total_width();
println!("Test 4: Buffer without trailing newline 'hello'");
println!(" Gutter width: {}", gutter_width);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let (cursor_x, _cursor_y) = harness.screen_cursor_position();
println!(" Cursor x after Ctrl+End: {}", cursor_x);
let expected_x = gutter_width as u16 + 5;
assert!(
cursor_x >= gutter_width as u16,
"No trailing newline: Cursor x ({}) should be >= gutter width ({})",
cursor_x,
gutter_width
);
assert_eq!(
cursor_x, expected_x,
"No trailing newline: Cursor x ({}) should be at end of 'hello' ({})",
cursor_x, expected_x
);
}
}
#[test]
fn test_cursor_visible_when_opening_file() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "Hello World\nSecond Line").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
println!("Testing cursor visibility when opening file...");
println!(
"Buffer content: {}",
harness.editor().active_state().buffer.to_string().unwrap()
);
println!(
"Buffer length: {}",
harness.editor().active_state().buffer.len()
);
println!(
"Cursor position: {}",
harness.editor().active_cursors().primary().position
);
let cursors = harness.find_all_cursors();
assert!(
!cursors.is_empty(),
"Cursor must be visible when opening a file"
);
let (x, y, char_at_cursor, is_primary) = &cursors[0];
println!(
"Found cursor at screen position ({x}, {y}): '{char_at_cursor}' (primary: {is_primary})"
);
assert!(*is_primary, "The only cursor should be the primary cursor");
}
#[test]
fn test_identical_lines_cursor_positions() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("abc\nabc\nabc\nabc").unwrap();
harness.assert_buffer_content("abc\nabc\nabc\nabc");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
let initial_pos = harness.cursor_position();
println!("Initial cursor position: {initial_pos}");
harness.editor_mut().add_cursor_below();
println!("After adding 1st cursor below:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(
" Cursor {:?}: position={}, anchor={:?}",
id, cursor.position, cursor.anchor
);
}
harness.editor_mut().add_cursor_below();
println!("After adding 2nd cursor below:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(
" Cursor {:?}: position={}, anchor={:?}",
id, cursor.position, cursor.anchor
);
}
harness.editor_mut().add_cursor_below();
println!("After adding 3rd cursor below:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(
" Cursor {:?}: position={}, anchor={:?}",
id, cursor.position, cursor.anchor
);
}
let cursor_count = harness.editor().active_cursors().iter().count();
println!("Total cursors: {cursor_count}");
assert_eq!(cursor_count, 4, "Should have 4 cursors");
harness.type_text("xyz").unwrap();
harness.assert_buffer_content("xyzabc\nxyzabc\nxyzabc\nxyzabc");
}
#[test]
fn test_multi_cursor_end_key_positioning() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new_no_wrap(80, 24).unwrap();
harness.type_text("Hello\nWorld\nTest").unwrap();
harness.assert_buffer_content("Hello\nWorld\nTest");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below(); harness.render().unwrap();
assert_eq!(harness.cursor_count(), 3);
println!("Before End key:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(" Cursor {:?}: position={}", id, cursor.position);
}
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
println!("\nAfter End key:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(" Cursor {:?}: position={}", id, cursor.position);
}
let positions: Vec<usize> = harness
.editor()
.active_cursors()
.iter()
.map(|(_, c)| c.position)
.collect();
assert!(
positions.contains(&5),
"Should have cursor at position 5 (end of 'Hello')"
);
assert!(
positions.contains(&11),
"Should have cursor at position 11 (end of 'World')"
);
assert!(
positions.contains(&16),
"Should have cursor at position 16 (end of 'Test')"
);
let visible_cursors = harness.find_all_cursors();
println!("\nVisible cursors on screen:");
for (x, y, char_at, is_primary) in &visible_cursors {
println!(
" Screen ({}, {}): char='{}', primary={}",
x, y, char_at, is_primary
);
}
assert!(
visible_cursors.len() >= 3,
"Should have 3 visible cursors, found {}. Screen:\n{}",
visible_cursors.len(),
harness.screen_to_string()
);
let y_positions: Vec<u16> = visible_cursors.iter().map(|(_, y, _, _)| *y).collect();
let unique_y_positions: std::collections::HashSet<_> = y_positions.iter().collect();
assert_eq!(
unique_y_positions.len(),
3,
"Cursors should be on 3 different lines. Y positions: {:?}",
y_positions
);
let gutter_width = harness.editor().active_state().margins.left_total_width() as u16;
for (x, y, char_at, _) in &visible_cursors {
println!(
"Cursor at ({}, {}): char='{}', gutter_width={}",
x, y, char_at, gutter_width
);
assert!(
*x > gutter_width,
"Cursor at y={} should be past column 0 (x={} should be > gutter_width={}). This cursor is at the START of line instead of END!",
y, x, gutter_width
);
}
}
#[test]
fn test_esc_returns_to_original_cursor_position() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness
.type_text("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
.unwrap();
harness.assert_buffer_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
let original_position = harness.cursor_position();
println!("Original cursor position: {original_position} (should be at start of Line 1)");
assert_eq!(original_position, 0, "Should start at position 0");
harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below(); harness.editor_mut().add_cursor_below();
assert_eq!(harness.editor().active_cursors().iter().count(), 4);
println!("After adding cursors:");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(" Cursor {:?}: position={}", id, cursor.position);
}
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
assert_eq!(harness.editor().active_cursors().iter().count(), 1);
let final_position = harness.cursor_position();
println!("Final cursor position: {final_position} (should be back at original position 0)");
assert_eq!(
final_position, original_position,
"After pressing Esc, cursor should return to original position {original_position} but is at {final_position}"
);
}
#[test]
fn test_auto_close_parens_multiple_cursors() {
use crate::common::harness::HarnessOptions;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
use std::fs;
use tempfile::TempDir;
let mut config = Config::default();
config.editor.auto_indent = true;
let mut harness = EditorTestHarness::create(
80,
24,
HarnessOptions::new()
.with_config(config)
.without_empty_plugins_dir(),
)
.unwrap();
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.py");
fs::write(&file_path, "").unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
let language = harness.editor().active_state().language.clone();
println!("Detected language: {}", language);
assert_eq!(language, "python", "File should be detected as Python");
harness.type_text("test(").unwrap();
harness.render().unwrap();
let single_cursor_content = harness.get_buffer_content().unwrap();
assert_eq!(
single_cursor_content, "test()",
"BASELINE: Auto-close should work with single cursor. Got: {:?}",
single_cursor_content
);
println!(
"✓ Single cursor auto-close works: {:?}",
single_cursor_content
);
harness
.send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
.unwrap(); harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap(); harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_buffer_content("\n\n\n");
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(
harness.editor().active_cursors().iter().count(),
3,
"Should have 3 cursors"
);
println!("\nBefore typing 'foo()':");
for (id, cursor) in harness.editor().active_cursors().iter() {
println!(" Cursor {:?}: position={}", id, cursor.position);
}
harness.type_text("foo").unwrap();
harness.render().unwrap();
println!("\nAfter typing 'foo':");
println!("Buffer: {:?}", harness.get_buffer_content().unwrap());
harness.type_text("(").unwrap();
harness.render().unwrap();
println!("\nAfter typing '(':");
println!("Buffer: {:?}", harness.get_buffer_content().unwrap());
let buffer_content = harness.get_buffer_content().unwrap();
assert_eq!(
buffer_content, "foo()\nfoo()\nfoo()\n",
"Auto-close should have added closing paren on ALL lines with multiple cursors. Buffer: {:?}",
buffer_content
);
println!("✓ Auto-close works with multiple cursors");
harness.type_text(")").unwrap();
harness.render().unwrap();
println!("\nAfter typing ')':");
println!("Buffer: {:?}", harness.get_buffer_content().unwrap());
let final_buffer = harness.get_buffer_content().unwrap();
assert_eq!(
final_buffer, "foo()\nfoo()\nfoo()\n",
"Skip-over should work with multiple cursors - typing ')' should skip existing ')' on ALL lines. Got: {:?}",
final_buffer
);
}
#[test]
fn test_multicursor_cut() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness
.type_text("hello world\nhello world\nhello world\n")
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(
harness.editor().active_cursors().iter().count(),
3,
"Should have 3 cursors"
);
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
for (id, cursor) in harness.editor().active_cursors().iter() {
assert!(
cursor.selection_range().is_some(),
"Cursor {:?} should have a selection",
id
);
let range = cursor.selection_range().unwrap();
assert_eq!(
range.end - range.start,
5,
"Selection should be 5 characters (hello), got {}",
range.end - range.start
);
}
harness
.send_key(KeyCode::Char('x'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let buffer_content = harness.get_buffer_content().unwrap();
assert_eq!(
buffer_content, " world\n world\n world\n",
"Cut should remove 'hello' from all lines. Got: {:?}",
buffer_content
);
}
#[test]
fn test_multicursor_cut_same_line() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("foo bar foo baz").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
for _ in 0..3 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness.editor_mut().add_cursor_at_next_match();
harness.render().unwrap();
assert_eq!(
harness.editor().active_cursors().iter().count(),
2,
"Should have 2 cursors"
);
harness
.send_key(KeyCode::Char('x'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let buffer_content = harness.get_buffer_content().unwrap();
assert_eq!(
buffer_content, " bar baz",
"Cut should remove both 'foo' instances. Got: {:?}",
buffer_content
);
}
#[test]
fn test_multicursor_cut_undo_batched() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness
.type_text("hello world\nhello world\nhello world\n")
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.editor_mut().add_cursor_below();
harness.editor_mut().add_cursor_below();
harness.render().unwrap();
assert_eq!(
harness.editor().active_cursors().iter().count(),
3,
"Should have 3 cursors"
);
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Char('x'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let buffer_after_cut = harness.get_buffer_content().unwrap();
assert_eq!(
buffer_after_cut, " world\n world\n world\n",
"Cut should remove 'hello' from all lines"
);
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let buffer_after_undo = harness.get_buffer_content().unwrap();
assert_eq!(
buffer_after_undo, "hello world\nhello world\nhello world\n",
"Single undo should restore all 'hello' instances (undo should be batched)"
);
}