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 visual_line_does_not_move_the_cursor() {
let mut e = VimEngine::default();
let mut t = ta();
t.move_cursor(CursorMove::Jump(0, 3));
e.handle_key(&key('V'), &mut t);
assert_eq!(*e.mode(), EditorMode::VisualLine);
assert_eq!(t.cursor(), (0, 3), "V must not move the cursor");
assert_eq!(
t.selection_range(),
Some(((0, 3), (0, 3))),
"the live selection anchors at the cursor; the full-line highlight \
is derived from the row range, not from this column"
);
}
#[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!(t.cursor(), (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 = t.cursor().1;
let out = e.handle_key(&esc(), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert_eq!(out, VimKeyOutcome::CursorOnly);
assert_eq!(t.cursor().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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor().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!(t.cursor(), (0, 3));
e.handle_key(&key('l'), &mut t);
assert_eq!(t.cursor(), (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!(t.cursor(), (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!(t.cursor().0, 2);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('g'), &mut t);
assert_eq!(t.cursor().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!(t.cursor().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!(
t.cursor().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!(
t.cursor().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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor().1, 1);
e.handle_key(&key(';'), &mut t);
assert_eq!(t.cursor().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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (0, 5)); e.handle_key(&key('2'), &mut t);
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(t.cursor().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!(t.cursor().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 outdent_removes_a_leading_tab_as_one_step() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("\tx"));
e.handle_key(&key('<'), &mut t);
e.handle_key(&key('<'), &mut t);
assert_eq!(t.rows(), &["x"]);
}
#[test]
fn indent_always_inserts_spaces() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("\tx"));
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(t.rows(), &[" \tx"]);
}
#[test]
fn a_counted_indent_stops_at_the_last_row() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb"));
e.handle_key(&key('5'), &mut t);
e.handle_key(&key('>'), &mut t);
e.handle_key(&key('>'), &mut t);
assert_eq!(t.rows(), &[" a", " b"]);
assert_eq!(t.cursor(), (0, 4));
}
#[test]
fn an_outdent_with_nothing_to_remove_reports_no_op() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo"));
e.handle_key(&key('<'), &mut t);
let outcome = e.handle_key(&key('<'), &mut t);
assert_eq!(outcome, VimKeyOutcome::NoOp);
assert_eq!(t.rows(), &["foo"]);
assert!(!t.undo(), "nothing was recorded");
}
#[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_visual_indent_over_the_same_rows() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
for c in ['V', 'j', '>', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" one", " two", "three"]);
}
#[test]
fn gv_reselects_the_last_visual_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
for c in ['j', 'V', 'k', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(*e.mode(), EditorMode::Normal);
e.handle_key(&key('G'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::VisualLine);
assert_eq!(t.cursor().0, 0, "cursor back on the end it was on");
e.handle_key(&key('>'), &mut t);
assert_eq!(t.rows(), &[" one", " two", "three"]);
}
#[test]
fn gv_keeps_charwise_mode_and_endpoints() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
for c in ['w', 'v', 'l', 'l'] {
e.handle_key(&key(c), &mut t);
}
let selected = t.selection_range();
e.handle_key(&esc(), &mut t);
e.handle_key(&key('0'), &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.selection_range(), selected);
assert_eq!(t.cursor(), (0, 8));
}
#[test]
fn gv_without_a_previous_selection_does_nothing() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one"));
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert!(t.selection_range().is_none());
}
#[test]
fn gv_clamps_to_a_shrunken_buffer() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
for c in ['j', 'j', '$', 'v', 'h'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['d', 'd', 'd', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["one"]);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.cursor().0, 0);
}
#[test]
fn gv_in_visual_swaps_with_the_previous_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&esc(), &mut t);
for c in ['G', 'v', 'l'] {
e.handle_key(&key(c), &mut t);
}
let second = t.selection_range();
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::VisualLine);
assert_eq!(t.cursor().0, 0);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.selection_range(), second);
}
#[test]
fn dot_repeats_visual_outdent_over_the_same_rows() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from(" one\n two\nthree"));
for c in ['j', 'V', 'k', '<', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["one", "two", "three"]);
}
#[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();
let mut t = ta();
e.sync_mouse_selection(true, &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
e.sync_mouse_selection(false, &mut t);
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, &mut ta());
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, &mut t);
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!(t.cursor().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!(t.cursor().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!(t.cursor().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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor().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!(t.cursor().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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (1, 0)); e.handle_key(&key('W'), &mut t);
assert_eq!(t.cursor(), (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);
}
t.cursor()
}
#[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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (1, 3)); e.handle_key(&key('%'), &mut t); assert_eq!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t.cursor(), (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!(t2.cursor(), (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!(t.cursor(), (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"
);
}
#[test]
fn charwise_visual_indent_shifts_the_touched_rows() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
for c in ['l', 'v', 'j', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" one", " two", "three"]);
assert_eq!(*e.mode(), EditorMode::Normal);
}
#[test]
fn charwise_visual_indent_within_one_row() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one two"));
for c in ['w', 'v', 'l', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" one two"]);
}
#[test]
fn dot_repeats_visual_line_delete_not_an_older_change() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd\ne\nf"));
for c in ['V', '>'] {
e.handle_key(&key(c), &mut t); }
for c in ['j', 'V', 'j', 'd', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" a", "f"]);
}
#[test]
fn dot_repeats_charwise_visual_delete_same_width() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef ghijkl"));
for c in ['v', 'l', 'l', 'd', 'w', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["def jkl"]);
}
#[test]
fn dot_repeats_multirow_charwise_delete_same_shape() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcd\nefgh\nijkl\nmnop"));
for c in ['l', 'v', 'j', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["agh", "ijkl", "mnop"]);
for c in ['j', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["agh", "iop"]);
}
#[test]
fn dot_repeats_visual_case_ops() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abc def"));
for c in ['v', 'l', 'U', 'w', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["ABc DEf"]);
}
#[test]
fn dot_repeats_visual_join() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd\ne\nf"));
for c in ['V', 'j', 'j', 'J', 'j', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a b c", "d e f"]);
}
#[test]
fn visual_yank_leaves_the_dot_register_alone() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
for c in ['x', 'v', 'l', 'y', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["cdef"]);
}
#[test]
fn visual_indent_takes_a_count() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
t.set_indent_width(2);
for c in ['V', 'j', '3', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" a", " b", "c"]);
e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows(), &[" a", " b", "c"]);
e.handle_key(&key('u'), &mut t);
assert_eq!(
t.rows(),
&[" a", " b", "c"],
"a counted shift is one undo"
);
}
#[test]
fn dot_repeats_visual_put() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("foo bar baz"));
for c in ['y', 'i', 'w', 'w', 'v', 'e', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["foo foo baz"]);
for c in ['w', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["foo foo bar"]);
}
#[test]
fn visual_g_then_unmapped_key_does_not_linger() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo\nthree"));
for c in ['v', 'g', 'o', 'j'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor().0, 1, "`j` after the aborted `go` must move");
assert_eq!(e.pending_hint(), None);
}
#[test]
fn visual_arrow_uses_the_count() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd\ne\nf"));
e.handle_key(&key('v'), &mut t);
e.handle_key(&key('2'), &mut t);
e.handle_key(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), &mut t);
assert_eq!(t.cursor().0, 2);
e.handle_key(&key('j'), &mut t);
assert_eq!(t.cursor().0, 3, "no count left over for `j`");
}
#[test]
fn visual_o_clears_a_pending_count() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd\ne\nf"));
for c in ['v', 'j', '3', 'o', 'j'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor().0, 1, "`o` swallows the count; `j` moves one row");
}
#[test]
fn operator_then_gv_aborts() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&esc(), &mut t);
for c in ['d', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(*e.mode(), EditorMode::Normal);
assert!(t.selection_range().is_none());
e.handle_key(&key('j'), &mut t);
assert_eq!(t.rows(), &["one", "two"], "the `d` is gone, not pending");
}
#[test]
fn operator_then_gj_join_aborts() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
for c in ['d', 'g', 'J'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["one", "two"]);
}
#[test]
fn reset_forgets_the_last_visual_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
e.handle_key(&key('V'), &mut t);
e.handle_key(&esc(), &mut t);
e.reset_to_normal();
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
assert!(t.selection_range().is_none());
}
#[test]
fn gv_reselects_a_mouse_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
t.jump_to(0, 1);
t.start_selection();
t.jump_to(0, 4);
e.sync_mouse_selection(true, &mut t);
assert_eq!(*e.mode(), EditorMode::Visual);
let dragged = t.selection_range();
t.cancel_selection();
t.jump_to(0, 8);
e.sync_mouse_selection(false, &mut t);
assert_eq!(*e.mode(), EditorMode::Normal);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(t.selection_range(), dragged);
}
#[test]
fn gv_reselects_a_keyboard_selection_a_click_cleared() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
for c in ['v', 'l', 'l'] {
e.handle_key(&key(c), &mut t);
}
let selected = t.selection_range();
t.cancel_selection();
t.jump_to(0, 8);
e.sync_mouse_selection(false, &mut t);
e.handle_key(&key('g'), &mut t);
e.handle_key(&key('v'), &mut t);
assert_eq!(t.selection_range(), selected);
}
#[test]
fn gv_follows_rows_deleted_above() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['j', 'j', 'V', 'j'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['g', 'g', 'd', 'd', 'g', 'v', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["b", " c", " d"]);
}
#[test]
fn gv_follows_rows_inserted_above() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
for c in ['j', 'V'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['g', 'g', 'O', 'x'] {
e.handle_key(&key(c), &mut t);
}
t.insert_str("x");
e.handle_key(&esc(), &mut t);
for c in ['g', 'v', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["x", "a", " b", "c"]);
}
#[test]
fn gv_follows_text_typed_before_it_on_the_row() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
for c in ['w', 'v', 'e'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['0', 'i'] {
e.handle_key(&key(c), &mut t);
}
t.insert_str("XX");
e.handle_key(&esc(), &mut t);
for c in ['g', 'v', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["XXhello "]);
}
#[test]
fn gv_follows_an_undo() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['j', 'j', 'V', 'j'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['g', 'g', 'd', 'd', 'u', 'g', 'v', '>'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a", "b", " c", " d"]);
}
#[test]
fn gv_after_its_text_is_deleted_stays_in_bounds() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
for c in ['j', 'V', 'j'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['k', 'd', 'j', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a"]);
assert_eq!(*e.mode(), EditorMode::VisualLine);
assert_eq!(t.cursor().0, 0);
}
#[test]
fn gv_after_a_visual_delete_uses_that_selection() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['v', 'l'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t); for c in ['G', 'V', 'k', 'd', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a", "b"]);
assert_eq!(*e.mode(), EditorMode::VisualLine);
assert_eq!(t.cursor().0, 1, "where the deleted rows were, not row 0");
}
#[test]
fn gv_after_a_charwise_delete_lands_where_the_text_was() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("abcdef"));
for c in ['l', 'l', 'v', 'l', 'd', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["abef"]);
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.cursor(), (0, 2));
}
#[test]
fn visual_put_with_an_empty_register_keeps_the_dot_change() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb"));
for c in ['>', '>', 'j', 'v', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(*e.mode(), EditorMode::Normal);
assert_eq!(t.rows(), &[" a", "b"]);
e.handle_key(&key('.'), &mut t);
assert_eq!(t.rows(), &[" a", " b"]);
}
#[test]
fn visual_put_undoes_in_one_step() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("bye hello"));
for c in ['y', 'i', 'w', 'w', 'v', 'i', 'w', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["bye bye"]);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["bye hello"]);
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one\ntwo"));
for c in ['y', 'y', 'j', 'V', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["one", "one"]);
e.handle_key(&key('u'), &mut t);
assert_eq!(t.rows(), &["one", "two"]);
}
#[test]
fn visual_delete_takes_the_whole_emoji_under_the_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a❤️b"));
for c in ['l', 'v', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["ab"]);
e.handle_key(&key('p'), &mut t);
assert_eq!(t.rows(), &["ab❤️"], "the register holds the whole emoji");
}
#[test]
fn visual_selection_ending_on_a_modified_emoji_takes_all_of_it() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x👍🏽y"));
for c in ['v', 'l', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["y"]);
}
#[test]
fn visual_delete_takes_a_decomposed_accent_with_its_letter() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("e\u{301}x"));
for c in ['v', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["x"]);
}
#[test]
fn visual_case_op_keeps_the_combining_mark() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("e\u{301}x"));
for c in ['v', 'U'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["E\u{301}x"]);
}
#[test]
fn dot_replays_a_visual_delete_by_characters_not_codepoints() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("❤️x abc"));
for c in ['v', 'l', 'd', 'w', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &[" c"]);
}
#[test]
fn dot_replay_ending_inside_an_emoji_takes_all_of_it() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab\ncd\nef\n❤️z"));
for c in ['v', 'j', 'l', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["", "ef", "❤️z"]);
for c in ['j', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["", "z"]);
}
#[test]
fn visual_range_covers_whole_characters() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a❤️b\nx👍🏽"));
for c in ['l', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(e.visual_range(&t), Some(((0, 1), (0, 3))));
for c in ['j', 'l'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(e.visual_range(&t), Some(((0, 1), (1, 3))));
}
#[test]
fn gv_survives_a_combining_mark_typed_at_its_mark() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ex"));
for c in ['l', 'v'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
e.handle_key(&key('i'), &mut t);
t.insert_str("\u{301}");
e.handle_key(&esc(), &mut t);
assert_eq!(t.rows(), &["e\u{301}x"]);
for c in ['g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.selection_range(), Some(((0, 2), (0, 2))));
}
#[test]
fn gv_survives_a_join_that_fuses_characters_at_its_mark() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x\n\u{301}y"));
for c in ['j', 'v'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['k', 'g', 'J'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["x\u{301}y"]);
for c in ['g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(*e.mode(), EditorMode::Visual);
assert_eq!(t.selection_range(), Some(((0, 2), (0, 2))));
}
#[test]
fn paste_after_an_emoji_goes_after_all_of_it() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a❤️"));
for c in ['x', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["❤️a"]);
}
#[test]
fn till_stops_on_the_emoji_before_the_target() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a👍🏽x"));
for c in ['t', 'x'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor(), (0, 1));
}
#[test]
fn till_backward_stops_on_the_emoji_after_the_target() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("x❤️b"));
for c in ['$', 'T', 'x'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor(), (0, 1));
}
#[test]
fn find_forward_starts_past_the_emoji_under_the_cursor() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("❤️\u{fe0f}"));
for c in ['f', '\u{fe0f}'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor(), (0, 0));
}
#[test]
fn replace_char_swaps_a_whole_emoji() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a❤️b"));
for c in ['l', 'r', 'z'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["azb"]);
}
#[test]
fn visual_line_put_over_the_last_row_stays_in_place() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
for c in ['y', 'y', 'G', 'V', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a", "b", "a"]);
assert_eq!(t.cursor().0, 2);
}
#[test]
fn visual_line_put_over_the_whole_buffer_leaves_no_extra_row() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("one"));
for c in ['y', 'y', 'V', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["one"]);
}
#[test]
fn visual_line_put_of_several_lines_over_the_last_rows() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['y', 'j', 'G', 'V', 'k', 'p'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a", "b", "a", "b"]);
assert_eq!(t.cursor().0, 2, "on the first pasted row");
}
#[test]
fn copy_visual_takes_the_inclusive_range_and_leaves_visual() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("hello world"));
for c in ['v', 'e'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(e.copy_visual(&mut t).as_deref(), Some("hello"));
assert_eq!(*e.mode(), EditorMode::Normal);
assert!(t.selection_range().is_none());
}
#[test]
fn copy_visual_of_an_empty_line_is_a_newline() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\n\nb"));
for c in ['j', 'V'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(e.copy_visual(&mut t).as_deref(), Some("\n"));
}
#[test]
fn visual_operators_on_a_very_long_row() {
let mut e = VimEngine::default();
let row: String = "ab❤️".repeat(5000);
let mut t = RopeBuffer::new(Text::from(format!("{row}\n{row}").as_str()));
for c in ['v', '$', 'd'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows()[0], "");
for c in ['j', '.'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows()[1], "");
}
#[test]
fn gv_after_undoing_a_visual_delete_reselects_its_rows() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['j', 'V', 'j', 'd', 'u', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["a", "b", "c", "d"]);
assert_eq!(*e.mode(), EditorMode::VisualLine);
let ((sr, _), (er, _)) = t.selection_range().expect("gv selected");
assert_eq!((sr, er), (1, 2));
}
#[test]
fn gv_after_redo_follows_the_redone_delete() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("a\nb\nc\nd"));
for c in ['j', 'V', 'j', 'd', 'u'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(
&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
&mut t,
);
assert_eq!(t.rows(), &["a", "d"]);
for c in ['g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.cursor().0, 1, "where the deleted rows were");
}
#[test]
fn undo_keeps_a_selection_made_after_the_change() {
let mut e = VimEngine::default();
let mut t = RopeBuffer::new(Text::from("ab\ncd\nef"));
e.handle_key(&key('x'), &mut t);
for c in ['j', 'V', 'j'] {
e.handle_key(&key(c), &mut t);
}
e.handle_key(&esc(), &mut t);
for c in ['u', 'g', 'v'] {
e.handle_key(&key(c), &mut t);
}
assert_eq!(t.rows(), &["ab", "cd", "ef"]);
let ((sr, _), (er, _)) = t.selection_range().expect("gv selected");
assert_eq!((sr, er), (1, 2));
}