use super::*;
#[test]
fn quote_a_then_dd_deletes_into_register_a() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world\nline two");
assert_eq!(app.active().editor.cursor().0, 0);
drive_key(&mut app, key(KeyCode::Char('"')));
drive_key(&mut app, key(KeyCode::Char('a')));
drive_key(&mut app, key(KeyCode::Char('d')));
drive_key(&mut app, key(KeyCode::Char('d')));
let slot = app.active().editor.registers().read('a');
assert!(slot.is_some(), "register 'a' should be set after \"add");
let text = &slot.unwrap().text;
assert!(
text.contains("hello world"),
"register 'a' should contain 'hello world', got {text:?}"
);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(lines, vec!["line two"], "\"add must delete first line");
}
#[test]
fn quote_a_then_yy_then_quote_a_then_p_pastes_named_register() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "first line\nsecond line");
drive_key(&mut app, key(KeyCode::Char('"')));
drive_key(&mut app, key(KeyCode::Char('a')));
drive_key(&mut app, key(KeyCode::Char('y')));
drive_key(&mut app, key(KeyCode::Char('y')));
let slot = app.active().editor.registers().read('a');
assert!(slot.is_some(), "register 'a' must be set after \"ayy");
let text = slot.unwrap().text.clone();
assert!(
text.contains("first line"),
"register 'a' should contain 'first line', got {text:?}"
);
drive_key(&mut app, key(KeyCode::Char('j')));
assert_eq!(
app.active().editor.cursor().0,
1,
"cursor must be on line 1"
);
drive_key(&mut app, key(KeyCode::Char('"')));
drive_key(&mut app, key(KeyCode::Char('a')));
drive_key(&mut app, key(KeyCode::Char('p')));
let lines = app.active().editor.buffer().lines().to_vec();
assert!(lines.len() >= 3, "paste must add a line, got {lines:?}");
assert!(
lines.iter().any(|l| l.contains("first line")),
"pasted content must contain 'first line', got {lines:?}"
);
}
#[test]
fn quote_underscore_then_dd_blackhole_no_unnamed_change() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "keep me\ndelete me\nkeep too");
drive_key(&mut app, key(KeyCode::Char('y')));
drive_key(&mut app, key(KeyCode::Char('y')));
let baseline = app
.active()
.editor
.registers()
.read('"')
.map(|s| s.text.clone())
.unwrap_or_default();
drive_key(&mut app, key(KeyCode::Char('j')));
drive_key(&mut app, key(KeyCode::Char('"')));
drive_key(&mut app, key(KeyCode::Char('_')));
drive_key(&mut app, key(KeyCode::Char('d')));
drive_key(&mut app, key(KeyCode::Char('d')));
let after = app
.active()
.editor
.registers()
.read('"')
.map(|s| s.text.clone())
.unwrap_or_default();
assert_eq!(
baseline, after,
"\"_dd must not overwrite the unnamed register"
);
let lines = app.active().editor.buffer().lines().to_vec();
assert!(
!lines.iter().any(|l| l.contains("delete me")),
"\"_dd must still delete the line from the buffer, got {lines:?}"
);
}
#[test]
fn quote_then_esc_cancels() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello");
drive_key(&mut app, key(KeyCode::Char('"')));
assert!(
app.pending_state.is_some(),
"\" must set app pending_state to SelectRegister"
);
drive_key(&mut app, key(KeyCode::Esc));
assert!(
app.pending_state.is_none(),
"Esc must clear pending_state after \""
);
}
#[test]
fn quote_invalid_char_no_register_set() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world\nsecond");
drive_key(&mut app, key(KeyCode::Char('y')));
drive_key(&mut app, key(KeyCode::Char('y')));
let baseline_unnamed = app
.active()
.editor
.registers()
.read('"')
.map(|s| s.text.clone())
.unwrap_or_default();
drive_key(&mut app, key(KeyCode::Char('"')));
drive_key(&mut app, key(KeyCode::Char('!')));
assert!(
app.pending_state.is_none(),
"invalid register char must cancel pending_state"
);
let slot = app.active().editor.registers().read('!');
assert!(slot.is_none(), "register '!' must not exist");
let after = app
.active()
.editor
.registers()
.read('"')
.map(|s| s.text.clone())
.unwrap_or_default();
assert_eq!(
baseline_unnamed, after,
"unnamed register must be unchanged after \"!"
);
}
#[test]
fn m_a_then_apostrophe_a_jumps_back_to_line() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "first line\n second line\nthird line");
app.active_mut().editor.jump_cursor(2, 3);
drive_key(&mut app, key(KeyCode::Char('m')));
drive_key(&mut app, key(KeyCode::Char('a')));
assert!(
app.pending_state.is_none(),
"pending_state must clear after ma"
);
app.active_mut().editor.jump_cursor(0, 0);
drive_key(&mut app, key(KeyCode::Char('\'')));
drive_key(&mut app, key(KeyCode::Char('a')));
assert!(
app.pending_state.is_none(),
"pending_state must clear after 'a"
);
assert_eq!(
app.active().editor.cursor().0,
2,
"'a must jump back to mark row"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn m_a_then_backtick_a_jumps_back_to_pos() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "first line\nsecond line\nthird line");
app.active_mut().editor.jump_cursor(1, 4);
drive_key(&mut app, key(KeyCode::Char('m')));
drive_key(&mut app, key(KeyCode::Char('a')));
app.active_mut().editor.jump_cursor(0, 0);
drive_key(&mut app, key(KeyCode::Char('`')));
drive_key(&mut app, key(KeyCode::Char('a')));
assert!(
app.pending_state.is_none(),
"pending_state must clear after `a"
);
assert_eq!(
app.active().editor.cursor(),
(1, 4),
"`a must jump to exact mark position"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn m_then_esc_cancels() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_mut().editor.jump_cursor(0, 3);
drive_key(&mut app, key(KeyCode::Char('m')));
assert!(
app.pending_state.is_some(),
"m must enter SetMark pending state"
);
drive_key(&mut app, key(KeyCode::Esc));
assert!(
app.pending_state.is_none(),
"Esc must cancel SetMark pending state"
);
assert_eq!(
app.active().editor.cursor(),
(0, 3),
"cursor must not move after m<Esc>"
);
}
#[test]
fn apostrophe_then_esc_cancels() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_mut().editor.jump_cursor(0, 2);
drive_key(&mut app, key(KeyCode::Char('\'')));
assert!(
app.pending_state.is_some(),
"' must enter GotoMarkLine pending state"
);
drive_key(&mut app, key(KeyCode::Esc));
assert!(
app.pending_state.is_none(),
"Esc must cancel GotoMarkLine pending state"
);
assert_eq!(
app.active().editor.cursor(),
(0, 2),
"cursor must not move after '<Esc>"
);
}
#[test]
fn backtick_in_visual_jumps_pos() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "first line\nsecond line\nthird line");
app.active_mut().editor.jump_cursor(2, 2);
drive_key(&mut app, key(KeyCode::Char('m')));
drive_key(&mut app, key(KeyCode::Char('b')));
app.active_mut().editor.jump_cursor(0, 0);
drive_key(&mut app, key(KeyCode::Char('v')));
assert_eq!(app.active().editor.vim_mode(), hjkl_engine::VimMode::Visual);
app.route_chord_key(ck('`'));
app.route_chord_key(ck('b'));
assert!(
app.pending_state.is_none(),
"pending_state must clear after `b in Visual mode"
);
assert_eq!(
app.active().editor.cursor().0,
2,
"`b in Visual mode must jump to mark row"
);
}
#[test]
fn q_then_esc_cancels_no_recording_started() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
drive_key(&mut app, ck('q'));
assert_eq!(
app.pending_state,
Some(hjkl_vim::PendingState::RecordMacroTarget),
"q must set RecordMacroTarget pending state"
);
drive_key(&mut app, key(KeyCode::Esc));
assert!(
app.pending_state.is_none(),
"Esc after q must clear pending_state"
);
assert!(
!app.active().editor.is_recording_macro(),
"Esc cancel must not start recording"
);
}
#[test]
fn bare_q_during_record_stops() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
drive_key(&mut app, ck('q'));
drive_key(&mut app, ck('a'));
assert!(
app.active().editor.is_recording_macro(),
"q a must start recording"
);
assert_eq!(app.active().editor.recording_register(), Some('a'));
drive_key(&mut app, ck('q'));
assert!(
!app.active().editor.is_recording_macro(),
"bare q must stop recording"
);
assert!(
app.pending_state.is_none(),
"pending_state must be clear after stop"
);
}
#[test]
fn record_macro_a_j_motion_replay_plays() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
macro_key_seq(
&mut app,
&[
ck('q'),
ck('a'), ck('j'), ck('q'), ],
);
assert!(
!app.active().editor.is_recording_macro(),
"recording must stop after second q"
);
assert_eq!(app.active().editor.cursor().0, 1);
macro_key_seq(&mut app, &[ck('@'), ck('a')]);
assert!(
!app.active().editor.is_replaying_macro(),
"replaying_macro must be false after replay finishes"
);
assert_eq!(
app.active().editor.cursor().0,
2,
"@a must replay j motion (move to row 2)"
);
}
#[test]
fn at_at_repeats_last_macro() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
macro_key_seq(&mut app, &[ck('q'), ck('a'), ck('j'), ck('q')]);
assert_eq!(app.active().editor.cursor().0, 1);
macro_key_seq(&mut app, &[ck('@'), ck('a')]);
assert_eq!(app.active().editor.cursor().0, 2);
macro_key_seq(&mut app, &[ck('@'), ck('@')]);
assert_eq!(
app.active().editor.cursor().0,
3,
"@@ must replay the last macro"
);
}
#[test]
fn play_macro_with_count_3() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4\nline5\nline6");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
app.active_mut().editor.start_macro_record('a');
app.active_mut().editor.record_input(hjkl_engine::Input {
key: hjkl_engine::Key::Char('j'),
..Default::default()
});
app.active_mut().editor.stop_macro_record();
let inputs = app.active_mut().editor.play_macro('a', 3);
app.active_mut().editor.end_macro_replay();
assert_eq!(
inputs.len(),
3,
"play_macro with count=3 must return 3 inputs"
);
for input in inputs {
let ct_key = engine_input_to_key_event(input);
if ct_key.code != KeyCode::Null {
let consumed = app.route_chord_key(ct_key);
if !consumed {
hjkl_vim::handle_key(&mut app.active_mut().editor, ct_key);
}
app.sync_viewport_from_editor();
}
}
assert_eq!(
app.active().editor.cursor().0,
3,
"3× j motions must move cursor to row 3"
);
}
#[test]
fn record_capital_appends_to_lowercase() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3");
app.active_mut().editor.jump_cursor(1, 0);
app.sync_viewport_from_editor();
macro_key_seq(&mut app, &[ck('q'), ck('a'), ck('j'), ck('q')]);
assert_eq!(app.active().editor.cursor().0, 2);
macro_key_seq(&mut app, &[ck('q'), ck('A'), ck('k'), ck('q')]);
assert_eq!(app.active().editor.cursor().0, 1);
let start_row = app.active().editor.cursor().0; macro_key_seq(&mut app, &[ck('@'), ck('a')]);
assert_eq!(
app.active().editor.cursor().0,
start_row,
"@a with capital append must replay j+k (net zero from row {start_row})"
);
}
#[test]
fn at_colon_replays_last_ex() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
app.dispatch_ex("3");
assert_eq!(
app.active().editor.cursor().0,
2,
":3 must move cursor to row 2"
);
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
assert_eq!(app.active().editor.cursor().0, 0);
app.replay_last_ex();
assert_eq!(
app.active().editor.cursor().0,
2,
"replay_last_ex must re-run :3 and land on row 2"
);
}
#[test]
fn at_colon_via_play_macro_arm_replays() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
app.dispatch_ex("3");
assert_eq!(app.active().editor.cursor().0, 2);
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
app.dispatch_action(
crate::keymap_actions::AppAction::BeginPendingPlayMacro { count: 1 },
1,
);
assert_eq!(
app.pending_state,
Some(hjkl_vim::PendingState::PlayMacroTarget { count: 1 }),
"BeginPendingPlayMacro must set PlayMacroTarget pending state"
);
let consumed = app.route_chord_key(ck(':'));
assert!(consumed, "@: must be consumed by route_chord_key");
assert!(
app.pending_state.is_none(),
"pending_state must be cleared after @: commit"
);
assert_eq!(
app.active().editor.cursor().0,
2,
"@: chord must replay :3 and land on row 2"
);
}
#[test]
fn at_colon_with_count_3_replays_three_times() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4");
app.active_mut().editor.jump_cursor(2, 0);
app.sync_viewport_from_editor();
app.dispatch_ex("1");
assert_eq!(app.active().editor.cursor().0, 0, ":1 must go to row 0");
app.active_mut().editor.jump_cursor(4, 0);
app.sync_viewport_from_editor();
app.dispatch_action(
crate::keymap_actions::AppAction::BeginPendingPlayMacro { count: 3 },
1,
);
let consumed = app.route_chord_key(ck(':'));
assert!(consumed, "3@: must be consumed");
assert_eq!(
app.active().editor.cursor().0,
0,
"3@: of :1 must end with cursor at row 0"
);
assert_eq!(
app.last_ex_command.as_deref(),
Some("1"),
"last_ex_command must remain '1' after 3@:"
);
}
#[test]
fn at_colon_no_prior_ex_is_noop() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
assert!(
app.last_ex_command.is_none(),
"fresh app must have no last_ex_command"
);
app.replay_last_ex();
assert_eq!(
app.active().editor.cursor().0,
0,
"cursor must be unchanged"
);
assert!(
app.status_message.is_none(),
"no status message for no-op replay"
);
assert!(
!app.exit_requested,
"exit_requested must stay false on no-op"
);
}
#[test]
fn at_colon_within_macro_does_not_recurse() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "line0\nline1\nline2\nline3\nline4");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
app.dispatch_ex("1");
assert_eq!(app.active().editor.cursor().0, 0, ":1 must go to row 0");
assert_eq!(app.last_ex_command.as_deref(), Some("1"));
app.active_mut().editor.jump_cursor(2, 0);
app.sync_viewport_from_editor();
macro_key_seq(&mut app, &[ck('q'), ck('a')]);
assert!(
app.active().editor.is_recording_macro(),
"must be recording"
);
app.replay_last_ex();
assert_eq!(
app.active().editor.cursor().0,
0,
"replay_last_ex during recording must move cursor to row 0"
);
assert!(
app.active().editor.is_recording_macro(),
"replay_last_ex must not stop macro recording"
);
assert_eq!(
app.last_ex_command.as_deref(),
Some("1"),
"last_ex_command must remain '1' after replay_last_ex"
);
macro_key_seq(&mut app, &[ck('q')]);
assert!(!app.active().editor.is_recording_macro());
}
#[test]
fn count_before_op_5dd_deletes_5_lines() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 10);
app.pending_count.try_accumulate('5');
rck(&mut app, &['d', 'd']);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines.first().map(String::as_str),
Some("line6"),
"5dd must delete lines 1-5; first line must now be 'line6', got {lines:?}"
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Normal,
"must be in Normal after 5dd"
);
}
#[test]
fn register_then_count_a5dd_targets_register_a() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 10);
rck(&mut app, &['"', 'a']);
assert_eq!(
app.active().editor.pending_register(),
Some('a'),
"pending_register must be 'a' after \"a"
);
app.pending_count.try_accumulate('5');
rck(&mut app, &['d', 'd']);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines.first().map(String::as_str),
Some("line6"),
"\"a5dd must delete 5 lines; first line must now be 'line6', got {lines:?}"
);
let reg_a = &app.active().editor.registers().named[0];
assert!(
reg_a.text.contains("line1"),
"register 'a' must contain deleted text; got {:?}",
reg_a.text
);
assert_eq!(
app.active().editor.pending_register(),
None,
"pending_register must be cleared after op"
);
}
#[test]
fn count_then_register_5_quote_a_dd_targets_register_a() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 10);
app.pending_count.try_accumulate('5');
rck(&mut app, &['"', 'a']);
assert_eq!(
app.active().editor.pending_register(),
Some('a'),
"pending_register must be 'a' after \"a"
);
assert_eq!(
app.pending_count.peek(),
5,
"pending_count must survive through register selection (5\"add regression)"
);
rck(&mut app, &['d', 'd']);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines.first().map(String::as_str),
Some("line6"),
"5\"add must delete 5 lines; first line must now be 'line6', got {lines:?}"
);
let reg_a = &app.active().editor.registers().named[0];
assert!(
reg_a.text.contains("line1"),
"register 'a' must contain deleted text; got {:?}",
reg_a.text
);
}
#[test]
fn outer_count_inner_count_2_quote_a_5dd_total_10() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 30);
app.pending_count.try_accumulate('2');
rck(&mut app, &['"', 'a']);
assert_eq!(
app.pending_count.peek(),
2,
"pending_count must be 2 after \"a"
);
app.pending_count.try_accumulate('5');
assert_eq!(
app.pending_count.peek(),
25,
"pending_count digits accumulate to 25"
);
rck(&mut app, &['d', 'd']);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines.first().map(String::as_str),
Some("line26"),
"2\"a5dd with digit-accumulation semantics must delete 25 lines; got {lines:?}"
);
let reg_a = &app.active().editor.registers().named[0];
assert!(
!reg_a.text.is_empty(),
"register 'a' must be non-empty after op"
);
}
#[test]
fn register_prefix_then_x_targets_register() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
rck(&mut app, &['"', 'a']);
assert_eq!(
app.active().editor.pending_register(),
Some('a'),
"pending_register must be 'a' after \"a"
);
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('x'));
app.sync_viewport_from_editor();
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines.first().map(String::as_str),
Some("ello world"),
"\"ax must delete 'h'; got {lines:?}"
);
let reg_a = &app.active().editor.registers().named[0];
assert_eq!(
reg_a.text, "h",
"register 'a' must contain 'h' after \"ax; got {:?}",
reg_a.text
);
}
#[test]
fn register_prefix_single_use_then_next_op_unnamed() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 5);
rck(&mut app, &['"', 'a', 'd', 'd']);
let reg_a_text = app.active().editor.registers().named[0].text.clone();
assert!(
reg_a_text.contains("line1"),
"first dd must land in reg 'a'; got {:?}",
reg_a_text
);
assert_eq!(
app.active().editor.pending_register(),
None,
"pending_register must be cleared after first op"
);
let unnamed_before = app.active().editor.registers().unnamed.text.clone();
rck(&mut app, &['d', 'd']);
let unnamed_after = app.active().editor.registers().unnamed.text.clone();
assert_ne!(
unnamed_after, unnamed_before,
"second dd must update unnamed register"
);
let reg_a_text2 = app.active().editor.registers().named[0].text.clone();
assert_eq!(
reg_a_text, reg_a_text2,
"register 'a' must not be overwritten by second dd; got {:?}",
reg_a_text2
);
}
#[test]
fn count_then_play_macro_3at_a_plays_three_times() {
let mut app = App::new(None, false, None, None).unwrap();
seed_numbered_lines(&mut app, 10);
macro_key_seq(&mut app, &[ck('q'), ck('a')]);
assert!(
app.active().editor.is_recording_macro(),
"must be recording"
);
macro_key_seq(&mut app, &[ck('j')]);
macro_key_seq(&mut app, &[ck('q')]);
assert!(
!app.active().editor.is_recording_macro(),
"recording stopped"
);
let row_after_record = app.active().editor.cursor().0;
assert_eq!(row_after_record, 1, "recording 'j' moves cursor to row 1");
app.pending_count.try_accumulate('3');
rck(&mut app, &['@', 'a']);
let row_after_play = app.active().editor.cursor().0;
assert_eq!(
row_after_play, 4,
"3@a must play macro 3 times → cursor moves from row 1 to row 4; got {row_after_play}"
);
}
#[test]
fn count_then_dot_5_dot_repeats_five_times() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "hello world");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('x'));
app.sync_viewport_from_editor();
let lines_after_x = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines_after_x.first().map(String::as_str),
Some("ello world"),
"x must delete 'h'; got {lines_after_x:?}"
);
app.pending_count.try_accumulate('5');
let consumed = app.route_chord_key(ck('.'));
assert!(consumed, ". must be consumed by keymap");
app.sync_viewport_from_editor();
let lines_after_dot = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines_after_dot.first().map(String::as_str),
Some("world"),
"5. must repeat x 5 more times; got {lines_after_dot:?}"
);
}