use super::*;
use crate::ropetext::Text;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn key(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
fn esc() -> KeyEvent {
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)
}
fn ta() -> RopeBuffer {
RopeBuffer::new(Text::from("hello world\nsecond line"))
}
fn cmd(p: Parsed) -> Command {
match p {
Parsed::Cmd(c) => c,
_ => panic!("expected Parsed::Cmd"),
}
}
fn parse_seq(e: &mut VimEngine, keys: &str) -> Parsed {
let mut last = Parsed::Nothing;
for c in keys.chars() {
last = e.parse_normal(&key(c));
}
last
}
fn ctrl(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
}
fn visual_hello(e: &mut VimEngine, t: &mut RopeBuffer) {
e.handle_key(&key('v'), t);
for _ in 0..4 {
e.handle_key(&key('l'), t);
}
}
#[test]
fn ctrl_c_in_visual_copies_the_inclusive_selection_and_exits_to_normal() {
let mut e = VimEngine::default();
let mut t = ta();
visual_hello(&mut e, &mut t);
match e.handle_key(&ctrl('c'), &mut t) {
VimKeyOutcome::Host(VimHostAction::ClipboardCopy(s)) => assert_eq!(s, "hello"),
o => panic!("got {o:?}"),
}
assert_eq!(e.mode, EditorMode::Normal, "vim's Ctrl-C is Esc");
assert_eq!(
t.rows(),
["hello world", "second line"],
"copy must not edit"
);
}
#[test]
fn ctrl_x_in_visual_cuts_and_reports_the_removed_text() {
let mut e = VimEngine::default();
let mut t = ta();
visual_hello(&mut e, &mut t);
match e.handle_key(&ctrl('x'), &mut t) {
VimKeyOutcome::Host(VimHostAction::ClipboardCut(s)) => assert_eq!(s, "hello"),
o => panic!("got {o:?}"),
}
assert_eq!(t.rows()[0], " world");
assert_eq!(e.mode, EditorMode::Normal);
}
#[test]
fn visual_line_clipboard_copy_takes_whole_lines_with_a_trailing_newline() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('V'), &mut t);
match e.handle_key(&ctrl('c'), &mut t) {
VimKeyOutcome::Host(VimHostAction::ClipboardCopy(s)) => {
assert_eq!(s, "hello world\n", "linewise yank includes the newline");
}
o => panic!("got {o:?}"),
}
}
#[test]
fn clipboard_chords_never_touch_the_unnamed_register() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t); let before = e.registers.read().map(|r| r.text.clone());
assert!(before.is_some(), "yy must fill the register");
visual_hello(&mut e, &mut t);
e.handle_key(&ctrl('x'), &mut t);
assert_eq!(
e.registers.read().map(|r| r.text.clone()),
before,
"the OS-clipboard cut must leave the unnamed register alone"
);
}
#[test]
fn ctrl_v_in_visual_leaves_the_selection_for_the_host_to_replace() {
let mut e = VimEngine::default();
let mut t = ta();
visual_hello(&mut e, &mut t);
assert_eq!(
e.handle_key(&ctrl('v'), &mut t),
VimKeyOutcome::Host(VimHostAction::ClipboardPaste)
);
assert_eq!(
t.rows()[0],
"hello world",
"nothing may be destroyed before there is something to replace it"
);
assert_eq!(
t.selection_range(),
Some(((0, 0), (0, 5))),
"the inclusive range stays selected so the host's insert consumes it"
);
assert_eq!(e.mode, EditorMode::Normal);
}
#[test]
fn ctrl_v_with_an_unusable_clipboard_leaves_the_buffer_untouched() {
let mut e = VimEngine::default();
let mut t = ta();
visual_hello(&mut e, &mut t);
e.handle_key(&ctrl('v'), &mut t);
assert_eq!(t.rows()[0], "hello world");
}
#[test]
fn visual_line_ctrl_x_takes_the_whole_line_leaving_no_blank() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('V'), &mut t);
match e.handle_key(&ctrl('x'), &mut t) {
VimKeyOutcome::Host(VimHostAction::ClipboardCut(s)) => {
assert_eq!(s, "hello world\n", "the clipboard gets a linewise cut");
}
o => panic!("got {o:?}"),
}
assert_eq!(
t.rows(),
["second line"],
"the line's newline goes with it — no stray blank line"
);
}
#[test]
fn visual_line_ctrl_x_on_the_last_line_leaves_no_blank_either() {
let mut e = VimEngine::default();
let mut t = ta();
t.move_cursor(CursorMove::Jump(1, 0));
e.handle_key(&key('V'), &mut t);
match e.handle_key(&ctrl('x'), &mut t) {
VimKeyOutcome::Host(VimHostAction::ClipboardCut(s)) => {
assert_eq!(
s, "second line\n",
"the clipboard text is the line plus a newline, even though \
the buffer edit consumed the PRECEDING one"
);
}
o => panic!("got {o:?}"),
}
assert_eq!(t.rows(), ["hello world"]);
}
#[test]
fn ctrl_v_in_normal_reaches_the_host() {
let mut e = VimEngine::default();
let mut t = ta();
assert_eq!(
e.handle_key(&ctrl('v'), &mut t),
VimKeyOutcome::Host(VimHostAction::ClipboardPaste),
"Normal mode used to swallow this as NoOp"
);
}
#[test]
fn ctrl_c_in_normal_cancels_the_pending_sequence() {
let mut e = VimEngine::default();
let mut t = ta();
assert!(matches!(e.parse_normal(&key('2')), Parsed::Pending));
assert!(matches!(e.parse_normal(&ctrl('c')), Parsed::Cancel));
assert_eq!(e.handle_key(&key('l'), &mut t), VimKeyOutcome::CursorOnly);
assert_eq!(super::super::cursor_tuple(&t), (0, 1));
}
#[test]
fn ctrl_c_abandons_a_one_key_continuation_instead_of_being_its_target() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('r'), &mut t);
e.handle_key(&ctrl('c'), &mut t);
assert_eq!(t.rows()[0], "hello world", "r must have been abandoned");
assert!(e.awaiting.is_none());
}
#[test]
fn count_accumulates_into_motion() {
let mut e = VimEngine::default();
assert!(matches!(e.parse_normal(&key('1')), Parsed::Pending));
assert!(matches!(e.parse_normal(&key('2')), Parsed::Pending));
match cmd(e.parse_normal(&key('l'))) {
Command::Move(Motion::Right, n) => assert_eq!(n, 12),
c => panic!("got {c:?}"),
}
}
#[test]
fn operator_motion_multiplies_the_two_counts() {
let mut e = VimEngine::default();
match cmd(parse_seq(&mut e, "2d3w")) {
Command::OperateMotion(Operator::Delete, Motion::WordForward, n) => {
assert_eq!(n, 6, "2 × 3 = 6")
}
c => panic!("got {c:?}"),
}
}
#[test]
fn doubled_operator_is_linewise() {
let mut e = VimEngine::default();
match cmd(parse_seq(&mut e, "2dd")) {
Command::OperateLine(Operator::Delete, n) => assert_eq!(n, 2),
c => panic!("got {c:?}"),
}
}
#[test]
fn gg_is_file_start_and_count_makes_it_a_line() {
let mut e = VimEngine::default();
match cmd(parse_seq(&mut e, "gg")) {
Command::Move(Motion::FileStart, 1) => {}
c => panic!("gg: got {c:?}"),
}
let mut e = VimEngine::default();
match cmd(parse_seq(&mut e, "5gg")) {
Command::Move(Motion::GotoLine(5), 1) => {}
c => panic!("5gg: got {c:?}"),
}
}
#[test]
fn esc_clears_a_pending_operator() {
let mut e = VimEngine::default();
assert!(matches!(e.parse_normal(&key('d')), Parsed::Pending));
assert!(matches!(e.parse_normal(&esc()), Parsed::Cancel));
match cmd(e.parse_normal(&key('w'))) {
Command::Move(Motion::WordForward, 1) => {}
c => panic!("pending operator survived esc: {c:?}"),
}
}
#[test]
fn operator_plus_text_object_awaits_then_completes() {
let mut e = VimEngine::default();
assert!(matches!(e.parse_normal(&key('d')), Parsed::Pending));
assert!(matches!(e.parse_normal(&key('i')), Parsed::Pending));
match cmd(e.parse_normal(&key('w'))) {
Command::OperateObject(Operator::Delete, TextObject::Word { around: false }) => {}
c => panic!("diw: got {c:?}"),
}
}
#[test]
fn find_target_is_captured_with_the_operator() {
let mut e = VimEngine::default();
assert!(matches!(e.parse_normal(&key('d')), Parsed::Pending));
assert!(matches!(e.parse_normal(&key('f')), Parsed::Pending));
match cmd(e.parse_normal(&key(','))) {
Command::OperateMotion(
Operator::Delete,
Motion::FindChar {
ch: ',',
till: false,
forward: true,
},
1,
) => {}
c => panic!("df,: got {c:?}"),
}
}
#[test]
fn bare_zero_is_line_start_but_zero_extends_a_count() {
let mut e = VimEngine::default();
match cmd(e.parse_normal(&key('0'))) {
Command::Move(Motion::LineStart, 1) => {}
c => panic!("bare 0: got {c:?}"),
}
let mut e = VimEngine::default();
match cmd(parse_seq(&mut e, "10l")) {
Command::Move(Motion::Right, n) => assert_eq!(n, 10),
c => panic!("10l: got {c:?}"),
}
}
#[test]
fn i_enters_insert_mode() {
let mut e = VimEngine::default();
let mut t = ta();
let out = e.handle_key(&key('i'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(out, VimKeyOutcome::CursorOnly);
}
#[test]
fn esc_returns_to_normal_and_steps_back() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('i'), &mut t);
t.move_cursor(crate::components::text_editor::rope_buffer::CursorMove::Forward);
t.move_cursor(crate::components::text_editor::rope_buffer::CursorMove::Forward);
let col_before = super::super::cursor_tuple(&t).1;
let out = e.handle_key(&esc(), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert_eq!(out, VimKeyOutcome::CursorOnly);
assert_eq!(super::super::cursor_tuple(&t).1, col_before - 1);
}
#[test]
fn insert_mode_passes_through() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('i'), &mut t);
let out = e.handle_key(&key('x'), &mut t);
assert_eq!(out, VimKeyOutcome::PassThrough);
}
#[test]
fn l_moves_right_cursor_only() {
let mut e = VimEngine::default();
let mut t = ta();
let out = e.handle_key(&key('l'), &mut t);
assert_eq!(out, VimKeyOutcome::CursorOnly);
assert_eq!(super::super::cursor_tuple(&t), (0, 1));
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn a_enters_insert_after_cursor() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('a'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(super::super::cursor_tuple(&t), (0, 1));
}
#[test]
fn o_opens_line_below_in_insert() {
let mut e = VimEngine::default();
let mut t = ta();
let out = e.handle_key(&key('o'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(out, VimKeyOutcome::TextMutated);
assert_eq!(t.rows().len(), 3);
assert_eq!(super::super::cursor_tuple(&t).0, 1);
}
#[test]
fn reset_returns_to_normal_from_insert() {
let mut e = VimEngine::default();
let mut t = ta();
e.handle_key(&key('i'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
e.reset_to_normal();
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn unknown_normal_key_is_noop() {
let mut e = VimEngine::default();
let mut t = ta();
let out = e.handle_key(&key('z'), &mut t);
assert_eq!(out, VimKeyOutcome::NoOp);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn count_accumulates_then_moves() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('l'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 3));
e.handle_key(&key('l'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 4));
}
#[test]
fn zero_without_count_is_line_start() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('0'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 0));
}
#[test]
#[allow(non_snake_case)]
fn gg_and_G_jump_file_ends() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('G'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).0, 2);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('g'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).0, 0);
}
#[test]
fn pending_g_cancels_on_unmapped_key() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('G'), &mut t); assert_eq!(super::super::cursor_tuple(&t).0, 2);
e.handle_key(&key('g'), &mut t); e.handle_key(&key('z'), &mut t); e.handle_key(&key('g'), &mut t); assert_eq!(
super::super::cursor_tuple(&t).0,
2,
"stray g after cancelled prefix must not jump to file start"
);
}
#[test]
fn pending_g_cleared_through_insert() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('G'), &mut t);
e.handle_key(&key('g'), &mut t); e.handle_key(&key('a'), &mut t); e.handle_key(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), &mut t);
e.handle_key(&key('g'), &mut t); assert_eq!(
super::super::cursor_tuple(&t).0,
2,
"g after insert must not complete a stale gg"
);
}
#[test]
fn dw_deletes_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["world"]);
}
#[test]
fn dd_deletes_line_linewise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["two", "three"]);
let reg = e.registers.read().expect("dd must fill the register");
assert_eq!(reg.kind, RegisterKind::Linewise);
assert_eq!(reg.text, "one\n");
}
#[test]
fn yy_then_p_duplicates_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["one", "one", "two"]);
}
#[test]
fn cw_deletes_word_and_enters_insert() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(t.rows(), &[" world"]);
}
#[test]
fn charwise_p_pastes_after_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["aabc"]);
}
#[test]
fn dd_on_last_line_removes_it() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('G'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["one", "two"]);
}
#[test]
fn dd_on_only_line_leaves_empty() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("only"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &[""]);
}
#[test]
fn linewise_2p_inserts_two_copies() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t); e.handle_key(&key('2'), &mut t);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["one", "one", "one", "two"]);
}
#[test]
fn yy_last_line_then_p_duplicates() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('G'), &mut t); e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["one", "two", "two"]);
}
#[test]
fn x_deletes_char_under_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('x'), &mut t);
assert_eq!(t.rows(), &["bc"]);
}
#[test]
fn r_replaces_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('r'), &mut t);
e.handle_key(&key('Z'), &mut t);
assert_eq!(t.rows(), &["Zbc"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn u_undoes_last_edit() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('x'), &mut t);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["abc"]);
}
#[test]
fn counted_undo_takes_back_that_many_groups() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
for _ in 0..3 {
e.handle_key(&key('x'), &mut t);
}
assert_eq!(t.rows(), &["def"]);
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["abcdef"]);
}
#[test]
fn counted_undo_stops_at_the_oldest_group() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('x'), &mut t);
e.handle_key(&key('9'), &mut t);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["abcdef"]);
}
#[test]
fn tilde_toggles_case() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('~'), &mut t);
assert_eq!(t.rows(), &["Abc"]);
}
#[test]
fn f_moves_to_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello, world"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key(','), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 5));
}
#[test]
fn df_deletes_through_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello, world"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key(','), &mut t);
assert_eq!(t.rows(), &[" world"]);
}
#[test]
fn t_stops_before_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello, world"));
e.handle_key(&key('t'), &mut t);
e.handle_key(&key(','), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 4)); }
#[test]
fn semicolon_repeats_find() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a.b.c.d"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('.'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).1, 1);
e.handle_key(&key(';'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).1, 3);
}
#[test]
fn diw_deletes_inner_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar baz"));
e.handle_key(&key('w'), &mut t);
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["foo baz"]);
}
#[test]
fn ci_quote_changes_inside_quotes() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("say \"hi\" now"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('h'), &mut t);
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('"'), &mut t);
assert_eq!(t.rows(), &["say \"\" now"]);
assert_eq!(*e.mode(), EditorMode::Insert);
}
#[test]
fn di_paren_deletes_inside_parens() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo(bar)baz"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t);
assert_eq!(t.rows(), &["foo()baz"]);
}
#[test]
fn daw_deletes_word_and_trailing_space() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar baz"));
e.handle_key(&key('w'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('a'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["foo baz"]);
}
#[test]
fn percent_jumps_to_matching_paren() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo(bar)baz"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('%'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 7)); }
#[test]
fn percent_jumps_back_from_close() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo(bar)baz"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key(')'), &mut t); e.handle_key(&key('%'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 3)); }
#[test]
fn percent_handles_nested() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("(a(b)c)"));
e.handle_key(&key('%'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 6)); }
#[test]
fn v_motion_d_deletes_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('v'), &mut t); e.handle_key(&key('l'), &mut t); e.handle_key(&key('l'), &mut t); e.handle_key(&key('d'), &mut t); assert_eq!(t.rows(), &["lo"]); assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
#[allow(non_snake_case)]
fn V_then_d_deletes_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["two"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn visual_y_yanks_and_returns_to_normal() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('v'), &mut t); e.handle_key(&key('l'), &mut t); e.handle_key(&key('y'), &mut t); assert_eq!(*e.mode(), EditorMode::Normal);
let before_len: usize = t.rows().iter().map(|l| l.len()).sum();
e.handle_key(&key('p'), &mut t);
let after_len: usize = t.rows().iter().map(|l| l.len()).sum();
assert_eq!(after_len, before_len + 2);
}
#[test]
fn visual_esc_cancels_and_returns_normal() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('l'), &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
e.handle_key(&esc(), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert!(t.selection_range().is_none());
}
#[test]
fn visual_c_enters_insert_after_delete() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('v'), &mut t); e.handle_key(&key('l'), &mut t); e.handle_key(&key('c'), &mut t); assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(t.rows(), &["llo"]); }
#[test]
fn indent_line_adds_spaces() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(t.rows(), &[" x"]);
}
#[test]
fn indent_follows_the_buffers_indent_width() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
t.set_indent_width(2);
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(t.rows(), &[" x"]);
e.handle_key(&key('<'), &mut t);
e.handle_key(&key('<'), &mut t);
assert_eq!(t.rows(), &["x"]);
}
#[test]
fn outdent_removes_one_step_not_all_leading_space() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(" x"));
t.set_indent_width(3);
e.handle_key(&key('<'), &mut t);
e.handle_key(&key('<'), &mut t);
assert_eq!(t.rows(), &[" x"]);
}
#[test]
fn indent_keeps_cursor_over_same_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('l'), &mut t); e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 5)); e.handle_key(&key('2'), &mut t);
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).0, 0);
}
#[test]
fn outdent_keeps_cursor_over_same_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(" x"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('x'), &mut t); e.handle_key(&key('<'), &mut t);
e.handle_key(&key('<'), &mut t);
assert_eq!(t.rows(), &["x"]);
assert_eq!(super::super::cursor_tuple(&t).1, 0); }
#[test]
fn outdent_removes_spaces() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(" x")); e.handle_key(&key('<'), &mut t);
e.handle_key(&key('<'), &mut t);
assert_eq!(t.rows(), &[" x"]); }
#[test]
fn pending_hint_shows_operator_and_count() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('2'), &mut t);
e.handle_key(&key('d'), &mut t);
assert_eq!(e.pending_hint().as_deref(), Some("2d"));
}
#[test]
fn dot_repeats_x() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('x'), &mut t);
e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows(), &["cdef"]);
}
#[test]
fn dot_repeats_dw() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one two three four"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["three four"]);
}
#[test]
fn dot_repeats_change_with_typed_text() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('w'), &mut t); t.insert_str("X");
e.handle_key(&esc(), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["X X"]);
}
#[test]
fn dot_repeats_multiline_change() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('w'), &mut t); t.insert_str("a");
t.insert_newline();
t.insert_str("b"); e.handle_key(&esc(), &mut t); assert_eq!(t.rows(), &["a", "b bar"]);
e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert!(
t.rows().len() >= 3,
"replay of multiline insert should produce >=3 lines: {:?}",
t.rows()
);
}
#[test]
fn space_leads_only_in_clean_normal() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
assert!(e.space_leads());
e.handle_key(&key('d'), &mut t); assert!(!e.space_leads());
e.handle_key(&key('w'), &mut t); assert!(e.space_leads());
e.handle_key(&key('i'), &mut t); assert!(!e.space_leads());
}
#[test]
fn colon_emits_open_palette() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
assert_eq!(
e.handle_key(&key(':'), &mut t),
VimKeyOutcome::Host(VimHostAction::OpenPalette)
);
}
#[test]
fn slash_emits_open_search_forward() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
assert_eq!(
e.handle_key(&key('/'), &mut t),
VimKeyOutcome::Host(VimHostAction::OpenSearch { forward: true })
);
}
#[test]
#[allow(non_snake_case)]
fn n_and_N_emit_search_nav() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
assert_eq!(
e.handle_key(&key('n'), &mut t),
VimKeyOutcome::Host(VimHostAction::SearchNext)
);
assert_eq!(
e.handle_key(&key('N'), &mut t),
VimKeyOutcome::Host(VimHostAction::SearchPrev)
);
}
#[test]
fn mouse_selection_enters_and_leaves_visual() {
let mut e = VimEngine::default();
e.sync_mouse_selection(true);
assert_eq!(*e.mode(), EditorMode::Visual);
e.sync_mouse_selection(false);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn mouse_no_selection_in_normal_stays_normal() {
let mut e = VimEngine::default();
e.sync_mouse_selection(false);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn mouse_does_not_disturb_insert() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
e.handle_key(&key('i'), &mut t); e.sync_mouse_selection(true);
assert_eq!(*e.mode(), EditorMode::Insert); }
#[test]
fn di_paren_on_empty_line_does_not_panic() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("")); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t); assert_eq!(t.rows(), &[""]);
}
#[test]
fn esc_clears_pending_g_in_normal() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('G'), &mut t); assert_eq!(super::super::cursor_tuple(&t).0, 2);
e.handle_key(&key('g'), &mut t); e.handle_key(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), &mut t); e.handle_key(&key('g'), &mut t); assert_eq!(
super::super::cursor_tuple(&t).0,
2,
"Esc must cancel pending g"
);
}
#[test]
fn esc_clears_pending_operator_object_in_normal() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar baz"));
e.handle_key(&key('d'), &mut t); e.handle_key(&key('i'), &mut t); e.handle_key(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), &mut t); assert_eq!(t.rows(), &["foo bar baz"]);
e.handle_key(&key('w'), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert_eq!(
t.rows(),
&["foo bar baz"],
"w after Esc must be a motion, not diw"
);
}
#[test]
fn di_paren_nested_selects_inner_of_outer() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("((x))"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t);
assert_eq!(t.rows(), &["()"]); }
#[test]
fn di_paren_from_inside_nested() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("((x))"));
e.handle_key(&key('l'), &mut t); e.handle_key(&key('l'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t);
assert_eq!(t.rows(), &["(())"]); }
#[test]
fn di_quote_in_gap_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("\"foo\" \"bar\""));
for _ in 0..5 {
e.handle_key(&key('l'), &mut t);
}
assert_eq!(super::super::cursor_tuple(&t).1, 5);
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('"'), &mut t);
assert_eq!(t.rows(), &["\"foo\" \"bar\""]); }
#[test]
fn di_quote_inside_still_works() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("\"foo\" \"bar\""));
for _ in 0..7 {
e.handle_key(&key('l'), &mut t);
} e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('"'), &mut t);
assert_eq!(t.rows(), &["\"foo\" \"\""]); }
#[test]
fn df_last_char_does_not_join_next_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc\nxyz"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('c'), &mut t);
assert_eq!(t.rows(), &["", "xyz"]); }
#[test]
fn cc_single_line_leaves_one_empty_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('c'), &mut t);
assert_eq!(t.rows(), &[""]);
assert_eq!(*e.mode(), EditorMode::Insert);
}
#[test]
fn cc_middle_line_still_works() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('j'), &mut t); e.handle_key(&key('c'), &mut t);
e.handle_key(&key('c'), &mut t);
assert_eq!(t.rows(), &["one", "", "three"]);
assert_eq!(*e.mode(), EditorMode::Insert);
}
#[test]
fn r_on_empty_line_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(""));
e.handle_key(&key('r'), &mut t);
let out = e.handle_key(&key('Z'), &mut t);
assert_eq!(out, VimKeyOutcome::NoOp);
assert_eq!(t.rows(), &[""]);
}
#[test]
fn visual_v_then_d_deletes_char_under_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('v'), &mut t); e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["bc"]); }
#[test]
fn visual_e_then_d_inclusive() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &[" world"]); }
#[test]
fn e_lands_on_last_word_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('e'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 4)); }
#[test]
fn e_twice_reaches_second_word_end() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('e'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 10)); }
#[test]
fn de_deletes_to_word_end_inclusive() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(t.rows(), &[" world"]); }
#[test]
fn visual_y_leaves_cursor_at_selection_start() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar\nbaz"));
for _ in 0..4 {
e.handle_key(&key('l'), &mut t);
} e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('y'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 4)); }
#[test]
fn charwise_p_after_eol_word_does_not_touch_next_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar\nbaz"));
for _ in 0..4 {
e.handle_key(&key('l'), &mut t);
} e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('y'), &mut t); e.handle_key(&key('p'), &mut t); assert_eq!(t.rows()[1], "baz"); assert_eq!(t.rows().len(), 2); assert_eq!(t.rows()[0], "foo bbarar"); }
#[test]
fn charwise_p_at_line_end_appends_same_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab\ncd"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('y'), &mut t); e.handle_key(&key('$'), &mut t); e.handle_key(&key('p'), &mut t); assert_eq!(t.rows()[0], "abab");
assert_eq!(t.rows()[1], "cd"); }
#[test]
fn visual_p_replaces_charwise_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t);
e.handle_key(&key('y'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 0));
for _ in 0..4 {
e.handle_key(&key('l'), &mut t);
} e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["foo foo"]); assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn visual_p_yanks_replaced_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t);
e.handle_key(&key('y'), &mut t); for _ in 0..4 {
e.handle_key(&key('l'), &mut t);
} e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t);
e.handle_key(&key('p'), &mut t); assert_eq!(t.rows(), &["foo foo"]);
e.handle_key(&key('$'), &mut t); e.handle_key(&key('p'), &mut t); assert_eq!(t.rows(), &["foo foobar"]);
}
#[test]
fn g_underscore_jumps_to_last_non_blank() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hi there "));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('_'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 7)); }
#[test]
fn d_g_underscore_deletes_through_last_non_blank() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar "));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('_'), &mut t);
assert_eq!(t.rows(), &[" "]); }
#[test]
#[allow(non_snake_case)]
fn count_G_and_count_gg_go_to_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("1\n2\n3\n4\n5\n6"));
e.handle_key(&key('5'), &mut t);
e.handle_key(&key('G'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).0, 4); e.handle_key(&key('2'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('g'), &mut t);
assert_eq!(super::super::cursor_tuple(&t).0, 1); }
#[test]
#[allow(non_snake_case)]
fn d_count_G_deletes_lines_through_target() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('2'), &mut t);
e.handle_key(&key('G'), &mut t); assert_eq!(t.rows(), &["three"]);
}
#[test]
fn ge_jumps_to_previous_word_end() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('$'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 2)); }
#[test]
fn ge_stops_at_class_change() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar"));
e.handle_key(&key('$'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 3)); }
#[test]
#[allow(non_snake_case)]
fn gE_ignores_punctuation_boundaries() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("aa bb.cc dd"));
e.handle_key(&key('$'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('E'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 7)); }
#[test]
fn ge_at_buffer_start_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 0));
}
#[test]
fn dge_deletes_backward_inclusive_of_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc def"));
e.handle_key(&key('$'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('e'), &mut t);
assert_eq!(t.rows(), &["ab"]);
}
#[test]
#[allow(non_snake_case)]
fn W_treats_punctuated_run_as_one_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar baz"));
e.handle_key(&key('W'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 8)); }
#[test]
#[allow(non_snake_case)]
fn E_jumps_to_end_of_WORD() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar baz"));
e.handle_key(&key('E'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 6)); }
#[test]
#[allow(non_snake_case)]
fn B_jumps_to_WORD_start() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar baz"));
e.handle_key(&key('W'), &mut t); e.handle_key(&key('B'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 0));
}
#[test]
#[allow(non_snake_case)]
fn W_crosses_lines_and_stops_at_empty_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo\n\nbar"));
e.handle_key(&key('W'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (1, 0)); e.handle_key(&key('W'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (2, 0));
}
#[test]
#[allow(non_snake_case)]
fn dW_deletes_whole_WORD() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar baz"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('W'), &mut t);
assert_eq!(t.rows(), &["baz"]);
}
#[test]
#[allow(non_snake_case)]
fn cW_acts_like_cE() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo.bar baz"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('W'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(t.rows(), &[" baz"]); }
#[test]
fn hint_shows_object_scope_mid_sequence() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('i'), &mut t);
assert_eq!(e.pending_hint().as_deref(), Some("di"));
}
#[test]
fn hint_shows_actual_find_key() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo"));
e.handle_key(&key('T'), &mut t);
assert_eq!(e.pending_hint().as_deref(), Some("T")); }
#[test]
fn replace_backspace_restores_overwritten_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('R'), &mut t);
e.handle_key(&key('X'), &mut t); e.handle_key(&key('Y'), &mut t); e.handle_key(
&KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
&mut t,
);
e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["Xbc"]); }
#[test]
fn replace_backspace_removes_appended_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a"));
e.handle_key(&key('R'), &mut t);
e.handle_key(&key('X'), &mut t); e.handle_key(&key('Y'), &mut t); e.handle_key(
&KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
&mut t,
);
e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["X"]); }
fn at_after(keys: &[char], lines: &[&str], from: (usize, usize)) -> (usize, usize) {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(crate::ropetext::Text::from(lines.join("\n").as_str()));
t.jump_to(from.0, from.1);
for key_char in keys {
e.handle_key(&key(*key_char), &mut t);
}
super::super::cursor_tuple(&t)
}
#[test]
fn big_word_forward_positions() {
assert_eq!(at_after(&['W'], &["foo.bar baz"], (0, 0)), (0, 8));
assert_eq!(at_after(&['W'], &["foo", "", "bar"], (0, 0)), (1, 0));
assert_eq!(at_after(&['W'], &["foo", "", "bar"], (1, 0)), (2, 0));
}
#[test]
fn big_word_back_positions() {
assert_eq!(at_after(&['B'], &["foo.bar baz"], (0, 8)), (0, 0));
assert_eq!(
at_after(&['B'], &["foo.bar baz"], (0, 0)),
(0, 0),
"fails in place"
);
}
#[test]
fn big_word_end_positions() {
assert_eq!(at_after(&['E'], &["foo.bar baz"], (0, 0)), (0, 6));
assert_eq!(
at_after(&['E'], &["foo.bar baz"], (0, 10)),
(0, 10),
"nothing ahead leaves the cursor alone"
);
}
#[test]
fn word_end_back_positions() {
assert_eq!(at_after(&['g', 'e'], &["foo.bar"], (0, 6)), (0, 3));
assert_eq!(at_after(&['g', 'E'], &["foo.bar"], (0, 6)), (0, 6));
assert_eq!(at_after(&['g', 'e'], &["foo.bar"], (0, 0)), (0, 0));
}
#[test]
fn visual_counted_motion_extends_by_count() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["ef"]);
}
#[test]
#[allow(non_snake_case)]
fn gUu_aborts_without_running_undo() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('U'), &mut t); e.handle_key(&key('u'), &mut t); assert_eq!(t.rows(), &["b"]); }
#[test]
fn dx_and_dp_abort_with_operator_pending() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('x'), &mut t); assert_eq!(t.rows(), &["abc"]);
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('p'), &mut t); assert_eq!(t.rows(), &["abc"]);
}
#[test]
fn dge_at_buffer_start_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('e'), &mut t); assert_eq!(t.rows(), &["foo"]);
}
#[test]
fn gugu_doubled_g_form_runs_linewise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ABC def"));
for c in "gugu".chars() {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["abc def"]);
}
#[test]
#[allow(non_snake_case)]
fn visual_J_joins_selected_lines_with_space() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&key('j'), &mut t);
e.handle_key(&key('j'), &mut t); e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["a b c"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
#[allow(non_snake_case)]
fn visual_gJ_joins_selected_lines_raw() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\n b"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&key('j'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["a b"]); }
#[test]
#[allow(non_snake_case)]
fn replace_mode_arrows_move_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcd"));
e.handle_key(&key('R'), &mut t);
e.handle_key(&KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), &mut t);
e.handle_key(&KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), &mut t);
e.handle_key(&key('X'), &mut t); e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["abXd"]);
e.handle_key(&key('0'), &mut t);
e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows(), &["XbXd"]);
}
#[test]
fn esc_from_insert_clears_stray_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('i'), &mut t);
t.start_selection();
t.move_cursor(crate::components::text_editor::rope_buffer::CursorMove::Forward);
e.handle_key(&esc(), &mut t);
assert!(
t.selection_range().is_none(),
"Esc must drop the stray selection"
);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn guu_undoes_in_one_step() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("Mixed Case Line"));
for c in "guu".chars() {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["mixed case line"]);
e.handle_key(&key('u'), &mut t); e.handle_key(&key('u'), &mut t); assert_eq!(t.rows(), &["Mixed Case Line"]);
}
#[test]
fn visual_g_tilde_toggles_case_of_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("FooBar"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('~'), &mut t);
assert_eq!(t.rows(), &["fOObAR"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn visual_bare_tilde_still_passes_through_for_surround() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("FooBar"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('e'), &mut t);
let out = e.handle_key(&key('~'), &mut t);
assert_eq!(out, VimKeyOutcome::PassThrough); assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn guw_lowercases_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("HELLO world"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('u'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["hello world"]);
assert_eq!(super::super::cursor_tuple(&t), (0, 0)); }
#[test]
#[allow(non_snake_case)]
fn gU_iw_uppercases_inner_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar baz"));
e.handle_key(&key('w'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('U'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["foo BAR baz"]);
}
#[test]
fn g_tilde_toggles_case_to_word_end() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("FooBar baz"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('~'), &mut t);
e.handle_key(&key('e'), &mut t); assert_eq!(t.rows(), &["fOObAR baz"]);
}
#[test]
fn guu_lowercases_whole_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("HELLO World\nNEXT"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('u'), &mut t);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["hello world", "NEXT"]);
}
#[test]
#[allow(non_snake_case)]
fn visual_U_uppercases_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('U'), &mut t);
assert_eq!(t.rows(), &["HELlo"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn case_op_does_not_touch_register() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("keep CHANGE"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('g'), &mut t);
e.handle_key(&key('u'), &mut t);
e.handle_key(&key('w'), &mut t); assert_eq!(e.registers.read().unwrap().text, "keep"); }
#[test]
#[allow(non_snake_case)]
fn dot_repeats_gU_word() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one two"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('U'), &mut t);
e.handle_key(&key('e'), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows(), &["ONE TWO"]);
}
#[test]
#[allow(non_snake_case)]
fn R_overwrites_chars() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('R'), &mut t);
assert_eq!(*e.mode(), EditorMode::Replace);
e.handle_key(&key('X'), &mut t);
e.handle_key(&key('Y'), &mut t);
e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["XYcdef"]); assert_eq!(*e.mode(), EditorMode::Normal);
assert_eq!(super::super::cursor_tuple(&t), (0, 1)); }
#[test]
#[allow(non_snake_case)]
fn R_appends_past_line_end() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab"));
e.handle_key(&key('R'), &mut t);
for c in "XYZ".chars() {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["XYZ"]); }
#[test]
#[allow(non_snake_case)]
fn R_is_dot_repeatable() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("aaaa bbbb"));
e.handle_key(&key('R'), &mut t);
e.handle_key(&key('X'), &mut t);
e.handle_key(&key('X'), &mut t);
e.handle_key(&esc(), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["XXaa XXbb"]);
}
#[test]
#[allow(non_snake_case)]
fn aborted_R_keeps_dot_register() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('R'), &mut t);
e.handle_key(&esc(), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["c"]);
}
#[test]
#[allow(non_snake_case)]
fn R_mode_does_not_pass_through() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab"));
e.handle_key(&key('R'), &mut t);
let out = e.handle_key(&key('('), &mut t);
assert_eq!(out, VimKeyOutcome::TextMutated); assert_eq!(t.rows()[0].chars().next(), Some('(')); }
#[test]
#[allow(non_snake_case)]
fn J_joins_with_single_space_stripping_indent() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo\n bar"));
e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["foo bar"]);
assert_eq!(super::super::cursor_tuple(&t), (0, 3));
}
#[test]
#[allow(non_snake_case)]
fn J_adds_no_extra_space_when_line_ends_in_whitespace() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo \nbar"));
e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["foo bar"]);
}
#[test]
#[allow(non_snake_case)]
fn gJ_joins_without_space() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo\n bar"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["foo bar"]); }
#[test]
#[allow(non_snake_case)]
fn three_J_joins_three_lines() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('J'), &mut t);
assert_eq!(t.rows(), &["a b c"]);
}
#[test]
#[allow(non_snake_case)]
fn I_inserts_at_first_non_blank() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(" indented"));
e.handle_key(&key('$'), &mut t); e.handle_key(&key('I'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(super::super::cursor_tuple(&t), (0, 4)); }
#[test]
fn percent_matches_across_lines() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo (bar\nbaz) qux"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('%'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (1, 3)); e.handle_key(&key('%'), &mut t); assert_eq!(super::super::cursor_tuple(&t), (0, 4));
}
#[test]
fn percent_nested_across_lines() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("{a {b\nc}\nd}"));
e.handle_key(&key('%'), &mut t); assert_eq!(super::super::cursor_tuple(&t), (2, 1)); }
#[test]
fn d_percent_deletes_across_lines_inclusive() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a(b\nc)d"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('%'), &mut t); assert_eq!(t.rows(), &["ad"]);
}
#[test]
fn percent_unmatched_across_buffer_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("(a\nb"));
e.handle_key(&key('%'), &mut t); assert_eq!(super::super::cursor_tuple(&t), (0, 0));
}
#[test]
fn visual_c_dot_repeats_same_width() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcde fghij"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('c'), &mut t); t.insert_str("X");
e.handle_key(&esc(), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["Xde Xij"]);
}
#[test]
fn count_find_is_atomic() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a x b"));
e.handle_key(&key('2'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('x'), &mut t);
assert_eq!(super::super::cursor_tuple(&t), (0, 0)); let mut t2 = RopeBuffer::new(Text::from("axbx"));
e.handle_key(&key('2'), &mut t2);
e.handle_key(&key('f'), &mut t2);
e.handle_key(&key('x'), &mut t2);
assert_eq!(super::super::cursor_tuple(&t2), (0, 3));
}
#[test]
fn d2fx_with_one_x_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a x b"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('2'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('x'), &mut t); assert_eq!(t.rows(), &["a x b"]);
}
#[test]
fn reset_to_normal_clears_insert_capture() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('w'), &mut t); e.reset_to_normal(); e.handle_key(&key('x'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["ar"]); }
#[test]
fn dj_on_last_line_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("only line"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('j'), &mut t); assert_eq!(t.rows(), &["only line"]);
assert_eq!(e.registers.read().unwrap().text, "only line\n"); }
#[test]
fn dk_on_first_line_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('k'), &mut t);
assert_eq!(t.rows(), &["one", "two"]);
}
#[test]
fn failed_find_op_does_not_clobber_dot() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('z'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["cdef"]);
}
#[test]
fn noop_x_does_not_clobber_dot() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one two three\n"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('w'), &mut t); e.handle_key(&key('j'), &mut t); let out = e.handle_key(&key('x'), &mut t); assert_eq!(out, VimKeyOutcome::NoOp); e.handle_key(&key('k'), &mut t);
e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["three", ""]);
}
#[test]
fn d_percent_without_pair_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('%'), &mut t); assert_eq!(t.rows(), &["abc"]);
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('%'), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal); }
#[test]
fn visual_inner_empty_pair_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo()bar"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('v'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["foo()bar"]);
}
#[test]
fn aborted_insert_keeps_dot_register() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('i'), &mut t); e.handle_key(&esc(), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["c"]);
}
#[test]
fn o_then_esc_is_still_dot_repeatable() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
e.handle_key(&key('o'), &mut t);
e.handle_key(&esc(), &mut t);
e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows().len(), 3);
}
#[test]
fn visual_inner_object_then_delete() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo(bar)baz"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('a'), &mut t); e.handle_key(&key('v'), &mut t);
e.handle_key(&key('i'), &mut t);
e.handle_key(&key('('), &mut t); e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &["foo()baz"]);
}
#[test]
fn visual_around_quote_then_yank() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("say \"hi\" now"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('h'), &mut t); e.handle_key(&key('v'), &mut t);
e.handle_key(&key('a'), &mut t);
e.handle_key(&key('"'), &mut t); e.handle_key(&key('y'), &mut t);
let reg = e.registers.read().unwrap();
assert_eq!(reg.text, "\"hi\"");
}
#[test]
fn visual_find_extends_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello, world"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key(','), &mut t); e.handle_key(&key('d'), &mut t);
assert_eq!(t.rows(), &[" world"]);
}
#[test]
fn visual_gg_extends_to_file_start() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('j'), &mut t);
e.handle_key(&key('j'), &mut t); e.handle_key(&key('v'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('g'), &mut t); e.handle_key(&key('d'), &mut t); assert_eq!(t.rows(), &["hree"]);
}
#[test]
fn visual_o_swaps_selection_ends() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcde"));
e.handle_key(&key('l'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('v'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('o'), &mut t); assert_eq!(super::super::cursor_tuple(&t), (0, 2));
e.handle_key(&key('h'), &mut t); e.handle_key(&key('d'), &mut t); assert_eq!(t.rows(), &["ae"]);
}
#[test]
fn dot_repeats_cc_with_typed_text() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('c'), &mut t); t.insert_str("X");
e.handle_key(&esc(), &mut t); e.handle_key(&key('j'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["X", "X"]);
}
#[test]
fn dot_repeats_substitute_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab cd"));
e.handle_key(&key('s'), &mut t); t.insert_str("Z");
e.handle_key(&esc(), &mut t); e.handle_key(&key('w'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["Zb Zd"]);
}
#[test]
fn dot_repeats_plain_insert() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("world"));
e.handle_key(&key('i'), &mut t);
t.insert_str("ab");
e.handle_key(&esc(), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["aabbworld"]);
}
#[test]
fn dot_repeats_indent() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x"));
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &[" x"]);
}
#[test]
fn dot_does_not_repeat_yank() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('y'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('.'), &mut t); assert_eq!(t.rows(), &["c"]);
}
#[test]
fn counts_before_and_after_operator_multiply() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a b c d e f g"));
e.handle_key(&key('2'), &mut t);
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('w'), &mut t);
assert_eq!(t.rows(), &["g"]); }
#[test]
fn dj_deletes_two_whole_lines_linewise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('l'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('j'), &mut t);
assert_eq!(t.rows(), &["three"]);
let reg = e.registers.read().unwrap();
assert_eq!(reg.kind, RegisterKind::Linewise);
assert_eq!(reg.text, "one\ntwo\n");
}
#[test]
fn dk_deletes_two_whole_lines_upward() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('j'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('k'), &mut t);
assert_eq!(t.rows(), &["three"]);
}
#[test]
#[allow(non_snake_case)]
fn dG_deletes_to_file_end_linewise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('j'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('G'), &mut t);
assert_eq!(t.rows(), &["one"]);
}
#[test]
fn d_gg_deletes_to_file_start_linewise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('j'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('g'), &mut t);
assert_eq!(t.rows(), &["three"]);
}
#[test]
fn dt_deletes_up_to_but_not_including_target() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abx"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('t'), &mut t);
e.handle_key(&key('x'), &mut t);
assert_eq!(t.rows(), &["x"]);
}
#[test]
fn failed_find_with_operator_is_noop() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('z'), &mut t); assert_eq!(t.rows(), &["hello"]); e.handle_key(&key('c'), &mut t);
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('z'), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal); }
#[test]
fn d_semicolon_repeats_find_as_operator_range() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a.b.c"));
e.handle_key(&key('f'), &mut t);
e.handle_key(&key('.'), &mut t); e.handle_key(&key('d'), &mut t);
e.handle_key(&key(';'), &mut t); assert_eq!(t.rows(), &["ac"]);
}
#[test]
fn cj_changes_two_lines_and_enters_insert() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('c'), &mut t);
e.handle_key(&key('j'), &mut t);
assert_eq!(*e.mode(), EditorMode::Insert);
assert_eq!(t.rows(), &["", "three"]); }
#[test]
fn x_then_p_swaps_chars() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab"));
e.handle_key(&key('x'), &mut t); e.handle_key(&key('p'), &mut t); assert_eq!(t.rows(), &["ba"]);
}
#[test]
fn x_at_line_end_does_not_join_next_line() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab\ncd"));
e.handle_key(&key('l'), &mut t); e.handle_key(&key('3'), &mut t);
e.handle_key(&key('x'), &mut t); assert_eq!(t.rows(), &["a", "cd"]);
}
#[test]
fn s_fills_register_with_deleted_char() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc"));
e.handle_key(&key('s'), &mut t); assert_eq!(*e.mode(), EditorMode::Insert);
let reg = e.registers.read().expect("s must fill the register");
assert_eq!(reg.text, "a");
assert_eq!(reg.kind, RegisterKind::Charwise);
}
#[test]
#[allow(non_snake_case)]
fn S_fills_register_linewise_no_kind_desync() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('y'), &mut t); e.handle_key(&key('j'), &mut t);
e.handle_key(&key('S'), &mut t); let reg = e.registers.read().expect("S must fill the register");
assert_eq!(reg.text, "two\n");
assert_eq!(reg.kind, RegisterKind::Linewise);
}
#[test]
fn dw_fills_register_charwise() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one two"));
e.handle_key(&key('d'), &mut t);
e.handle_key(&key('w'), &mut t); let reg = e.registers.read().expect("dw must fill the register");
assert_eq!(reg.text, "one ");
assert_eq!(reg.kind, RegisterKind::Charwise);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["tone wo"]); }
#[test]
fn empty_delete_keeps_previous_register() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab\n"));
e.handle_key(&key('y'), &mut t);
e.handle_key(&key('l'), &mut t); e.handle_key(&key('j'), &mut t); e.handle_key(&key('x'), &mut t); let reg = e
.registers
.read()
.expect("register must survive a no-op delete");
assert_eq!(reg.text, "a");
}
#[test]
fn esc_in_normal_clears_stray_selection() {
let mut e = VimEngine::default(); let mut t = RopeBuffer::new(Text::from("hello world"));
t.start_selection();
t.move_cursor(crate::components::text_editor::rope_buffer::CursorMove::Forward);
t.move_cursor(crate::components::text_editor::rope_buffer::CursorMove::Forward);
assert!(t.selection_range().is_some());
let out = e.handle_key(&esc(), &mut t);
assert!(
t.selection_range().is_none(),
"Esc in Normal must cancel a stray selection"
);
assert_eq!(out, VimKeyOutcome::CursorOnly);
assert_eq!(*e.mode(), EditorMode::Normal);
}
const FAMILY: &str = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
#[test]
fn counted_x_counts_clusters_not_scalars() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(&*format!("{FAMILY}\nnext")));
e.handle_key(&key('3'), &mut t);
e.handle_key(&key('x'), &mut t);
assert_eq!(
t.rows(),
&["", "next"],
"one cluster is one `x`; the row has nothing else to give"
);
}
#[test]
fn tilde_keeps_the_rest_of_the_cluster() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("e\u{301}f"));
e.handle_key(&key('~'), &mut t);
assert_eq!(
t.rows(),
&["E\u{301}f"],
"toggling case must not drop the combining acute"
);
}