use super::*;
#[test]
fn test_basic_motion() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('l'), text);
assert_eq!(editor.cursor(), 1);
editor.handle_key(Key::char('w'), text);
assert_eq!(editor.cursor(), 6);
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 10);
editor.handle_key(Key::char('0'), text);
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_mode_switching() {
let mut editor = VimLineEditor::new();
let text = "hello";
assert_eq!(editor.mode(), Mode::Normal);
editor.handle_key(Key::char('i'), text);
assert_eq!(editor.mode(), Mode::Insert);
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_delete_word() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('d'), text);
editor.handle_key(Key::char('w'), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_insert_char() {
let mut editor = VimLineEditor::new();
let text = "";
editor.handle_key(Key::char('i'), text);
let result = editor.handle_key(Key::char('x'), text);
assert_eq!(result.edits.len(), 1);
match &result.edits[0] {
TextEdit::Insert { at, text } => {
assert_eq!(*at, 0);
assert_eq!(text, "x");
}
_ => panic!("Expected Insert"),
}
}
#[test]
fn test_visual_mode() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('v'), text);
assert_eq!(editor.mode(), Mode::Visual);
editor.handle_key(Key::char('w'), text);
let sel = editor.selection().unwrap();
assert_eq!(sel.start, 0);
assert!(sel.end > 0);
}
#[test]
fn test_backspace_ascii() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('i'), &text);
editor.handle_key(Key::code(KeyCode::End), &text);
assert_eq!(editor.cursor(), 3);
let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 2);
}
#[test]
fn test_backspace_unicode() {
let mut editor = VimLineEditor::new();
let mut text = String::from("a😀b");
editor.handle_key(Key::char('i'), &text);
editor.handle_key(Key::code(KeyCode::End), &text);
editor.handle_key(Key::code(KeyCode::Left), &text); assert_eq!(editor.cursor(), 5);
let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 1);
}
#[test]
fn test_yank_and_paste() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('y'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert!(result.yanked.is_some());
assert_eq!(result.yanked.unwrap(), "hello ");
editor.handle_key(Key::char('$'), &text);
let result = editor.handle_key(Key::char('p'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "hello worldhello ");
}
#[test]
fn test_visual_mode_delete() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('v'), &text);
assert_eq!(editor.mode(), Mode::Visual);
editor.handle_key(Key::char('e'), &text);
let result = editor.handle_key(Key::char('d'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, " world");
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_operator_pending_escape() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('d'), text);
assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_replace_char() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello");
editor.handle_key(Key::char('r'), &text);
assert_eq!(editor.mode(), Mode::ReplaceChar);
let result = editor.handle_key(Key::char('x'), &text);
assert_eq!(editor.mode(), Mode::Normal);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "xello");
}
#[test]
fn test_replace_char_escape() {
let mut editor = VimLineEditor::new();
let text = "hello";
editor.handle_key(Key::char('r'), text);
assert_eq!(editor.mode(), Mode::ReplaceChar);
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_cw_no_trailing_space() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('c'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert_eq!(editor.mode(), Mode::Insert);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, " world");
}
#[test]
fn test_dw_includes_trailing_space() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('d'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert_eq!(editor.mode(), Mode::Normal);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "world");
}
#[test]
fn test_paste_at_empty_buffer() {
let mut editor = VimLineEditor::new();
let yank_text = String::from("test");
editor.handle_key(Key::char('y'), &yank_text);
editor.handle_key(Key::char('w'), &yank_text);
let mut text = String::new();
editor.set_cursor(0, &text);
let result = editor.handle_key(Key::char('p'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "test");
}
#[test]
fn test_dollar_cursor_on_last_char() {
let mut editor = VimLineEditor::new();
let text = "abc";
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 2);
let text = "x";
editor.set_cursor(0, text);
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 0); }
#[test]
fn test_x_delete_last_char_moves_cursor_left() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('$'), &text);
assert_eq!(editor.cursor(), 2);
let result = editor.handle_key(Key::char('x'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 1); }
#[test]
fn test_x_delete_middle_char_cursor_stays() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('l'), &text);
assert_eq!(editor.cursor(), 1);
let result = editor.handle_key(Key::char('x'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ac");
assert_eq!(editor.cursor(), 1);
}
#[test]
fn test_percent_bracket_matching() {
let mut editor = VimLineEditor::new();
let text = "(hello world)";
assert_eq!(editor.cursor(), 0);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 12);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_percent_nested_brackets() {
let mut editor = VimLineEditor::new();
let text = "([{<>}])";
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 7);
editor.set_cursor(1, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 6);
editor.set_cursor(2, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 5);
editor.set_cursor(3, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 4); }
#[test]
fn test_percent_on_non_bracket() {
let mut editor = VimLineEditor::new();
let text = "hello";
let orig_cursor = editor.cursor();
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), orig_cursor);
}
#[test]
fn test_command_line_enter_clean() {
let text = String::from("preserved");
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.set_cursor(0, &text);
let result = editor.handle_key(Key::char(':'), &text);
assert_eq!(editor.mode(), Mode::CommandLine);
assert_eq!(editor.command_line_buffer(), Some(""));
assert_eq!(editor.command_line_cursor(), 0);
assert!(result.edits.is_empty(), "no edits should touch main text");
assert!(result.action.is_none());
assert_eq!(editor.status(), "COMMAND");
assert_eq!(text, "preserved");
}
#[test]
fn test_command_line_typing_submit_escape() {
let text = String::from("main");
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.handle_key(Key::char(':'), &text);
for c in "ir".chars() {
let r = editor.handle_key(Key::char(c), &text);
assert!(r.edits.is_empty(), "char '{}' must not edit main text", c);
assert!(r.action.is_none(), "char '{}' must not emit an action", c);
}
assert_eq!(editor.command_line_buffer(), Some("ir"));
assert_eq!(editor.command_line_cursor(), 2);
let submit = editor.handle_key(Key::code(KeyCode::Enter), &text);
match submit.action {
Some(Action::SubmitCommand(s)) => assert_eq!(s, "ir"),
other => panic!("expected SubmitCommand(\"ir\"), got {:?}", other),
}
assert!(submit.edits.is_empty(), "submit must not edit main text");
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.command_line_buffer(), None);
editor.handle_key(Key::char(':'), &text);
editor.handle_key(Key::char('q'), &text);
let esc = editor.handle_key(Key::code(KeyCode::Escape), &text);
assert!(esc.action.is_none());
assert!(esc.edits.is_empty());
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.command_line_buffer(), None);
assert_eq!(text, "main");
}
#[test]
fn test_command_line_disabled_by_default() {
let text = String::from("hello");
let mut editor = VimLineEditor::new();
let result = editor.handle_key(Key::char(':'), &text);
assert_eq!(editor.mode(), Mode::Normal);
assert!(result.edits.is_empty());
assert!(result.action.is_none());
assert_eq!(editor.command_line_buffer(), None);
}
#[test]
fn test_command_line_does_not_steal_r_replace() {
let mut text = String::from("abc");
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.handle_key(Key::char('r'), &text);
assert_eq!(editor.mode(), Mode::ReplaceChar);
let result = editor.handle_key(Key::char(':'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, ":bc");
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_command_line_does_not_steal_d_operator() {
let text = String::from("hello");
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.handle_key(Key::char('d'), &text);
assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
let result = editor.handle_key(Key::char(':'), &text);
assert_eq!(editor.mode(), Mode::Normal);
assert!(result.edits.is_empty());
assert!(result.action.is_none());
assert_eq!(editor.command_line_buffer(), None);
}
#[test]
fn test_command_line_insert_mode_literal_colon_unchanged() {
let mut text = String::new();
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.handle_key(Key::char('i'), &text);
let result = editor.handle_key(Key::char(':'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, ":");
assert_eq!(editor.mode(), Mode::Insert);
}
#[test]
fn test_command_line_backspace_and_arrows() {
let text = String::from("ignored");
let mut editor = VimLineEditor::new().with_command_mode(true);
editor.handle_key(Key::char(':'), &text);
for c in "abd".chars() {
editor.handle_key(Key::char(c), &text);
}
assert_eq!(editor.command_line_buffer(), Some("abd"));
editor.handle_key(Key::code(KeyCode::Backspace), &text);
assert_eq!(editor.command_line_buffer(), Some("ab"));
assert_eq!(editor.command_line_cursor(), 2);
editor.handle_key(Key::code(KeyCode::Left), &text);
assert_eq!(editor.command_line_cursor(), 1);
editor.handle_key(Key::char('X'), &text);
assert_eq!(editor.command_line_buffer(), Some("aXb"));
editor.handle_key(Key::code(KeyCode::Home), &text);
assert_eq!(editor.command_line_cursor(), 0);
editor.handle_key(Key::code(KeyCode::End), &text);
assert_eq!(editor.command_line_cursor(), 3);
assert_eq!(text, "ignored");
}
#[test]
fn normal_k_on_single_line_emits_history_prev() {
let mut editor = VimLineEditor::new();
let text = "hello";
let result = editor.handle_key(Key::char('k'), text);
assert_eq!(result.action, Some(Action::HistoryPrev));
}
#[test]
fn normal_j_on_single_line_emits_history_next() {
let mut editor = VimLineEditor::new();
let text = "hello";
let result = editor.handle_key(Key::char('j'), text);
assert_eq!(result.action, Some(Action::HistoryNext));
}
#[test]
fn normal_kj_in_middle_of_multiline_moves_between_lines() {
let mut editor = VimLineEditor::new();
let text = "alpha\nbeta\ngamma";
editor.set_cursor(6, text);
let result = editor.handle_key(Key::char('k'), text);
assert!(
result.action.is_none(),
"k off-boundary is a motion, not history"
);
assert_eq!(editor.cursor(), 0, "moved up to 'a' of \"alpha\"");
let result = editor.handle_key(Key::char('k'), text);
assert_eq!(result.action, Some(Action::HistoryPrev));
editor.set_cursor(6, text);
let result = editor.handle_key(Key::char('j'), text);
assert!(
result.action.is_none(),
"j off-boundary is a motion, not history"
);
assert_eq!(editor.cursor(), 11, "moved down to 'g' of \"gamma\"");
let result = editor.handle_key(Key::char('j'), text);
assert_eq!(result.action, Some(Action::HistoryNext));
}
#[test]
fn normal_arrows_still_emit_history_unchanged() {
let mut editor = VimLineEditor::new();
let text = "alpha\nbeta\ngamma";
editor.set_cursor(6, text);
let up = editor.handle_key(Key::code(KeyCode::Up), text);
assert_eq!(up.action, Some(Action::HistoryPrev));
let down = editor.handle_key(Key::code(KeyCode::Down), text);
assert_eq!(down.action, Some(Action::HistoryNext));
}
#[test]
fn visual_kj_remains_motion_only() {
let mut editor = VimLineEditor::new();
let text = "one\ntwo";
editor.handle_key(Key::char('v'), text);
let result = editor.handle_key(Key::char('j'), text);
assert!(result.action.is_none());
assert_eq!(editor.cursor(), 4);
}