use super::*;
#[test]
fn visual_d_deletes_selection_via_keymap() {
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('v'));
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Visual,
"must be in Visual after v"
);
for _ in 0..4 {
let consumed = app.route_chord_key(ck('l'));
assert!(consumed, "l in Visual must be consumed by keymap");
}
let consumed = app.route_chord_key(ck('d'));
assert!(
consumed,
"d in Visual must be consumed by keymap (VisualOp)"
);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec![" world"],
"vd must delete selected chars; got {lines:?}"
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Normal,
"must exit Visual mode after d"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_y_yanks_selection_via_keymap() {
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('v'));
for _ in 0..4 {
app.route_chord_key(ck('l'));
}
let consumed = app.route_chord_key(ck('y'));
assert!(
consumed,
"y in Visual must be consumed by keymap (VisualOp)"
);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["hello world"],
"vy must not modify the buffer; got {lines:?}"
);
let reg = app.active().editor.yank();
assert!(
reg.contains("hello"),
"unnamed register must contain 'hello' after vy; got {reg:?}"
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Normal,
"must exit Visual mode after y"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_line_d_deletes_line_via_keymap() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "first line\nsecond line");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
hjkl_vim::handle_key(
&mut app.active_mut().editor,
KeyEvent::new(KeyCode::Char('V'), KeyModifiers::NONE),
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::VisualLine,
"must be in VisualLine after V"
);
let consumed = app.route_chord_key(ck('d'));
assert!(
consumed,
"d in VisualLine must be consumed by keymap (VisualOp)"
);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["second line"],
"Vd must delete first line; got {lines:?}"
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Normal,
"must exit VisualLine mode after d"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_c_enters_insert_mode_via_keymap() {
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('v'));
for _ in 0..4 {
app.route_chord_key(ck('l'));
}
let consumed = app.route_chord_key(ck('c'));
assert!(
consumed,
"c in Visual must be consumed by keymap (VisualOp)"
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Insert,
"vc must enter Insert mode; got {:?}",
app.active().editor.vim_mode()
);
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec![" world"],
"vc must delete selected chars; got {lines:?}"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn gg_full_sequence_in_normal_mode_via_keymap() {
let mut app = App::new(None, false, None, None).unwrap();
let lines: Vec<String> = (0..30).map(|i| format!("line{i:02}")).collect();
seed_buffer(&mut app, &lines.join("\n"));
app.active_mut().editor.jump_cursor(20, 0);
app.sync_viewport_from_editor();
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::Normal,
"must be in Normal mode"
);
use crossterm::event::{KeyCode, KeyEvent as CtKeyEvent, KeyModifiers};
let g_key = CtKeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
let consumed = app.route_chord_key(g_key);
assert!(consumed, "first g must be consumed");
assert!(
matches!(
app.pending_state,
Some(hjkl_vim::PendingState::AfterG { .. })
),
"first g must set pending_state to AfterG; got {:?}",
app.pending_state
);
let consumed = app.route_chord_key(g_key);
assert!(consumed, "second g must be consumed");
assert!(
app.pending_state.is_none(),
"after gg the reducer must clear pending_state"
);
assert_eq!(
app.active().editor.cursor().0,
0,
"gg must move engine cursor to row 0 from row 20"
);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_d_with_named_register_writes_to_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();
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('"'));
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('a'));
assert_eq!(
app.active().editor.pending_register(),
Some('a'),
"pending_register must be Some('a') after \"a chord"
);
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('v'));
for _ in 0..4 {
app.route_chord_key(ck('l'));
}
let consumed = app.route_chord_key(ck('d'));
assert!(consumed, "d in Visual must be consumed");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec![" world"],
"\"ad must delete selection; got {lines:?}"
);
let reg_a = &app.active().editor.registers().named[0]; assert!(
reg_a.text.contains("hello"),
"register 'a' must contain 'hello' after \"ad; got {:?}",
reg_a.text
);
assert_eq!(app.active().editor.vim_mode(), hjkl_engine::VimMode::Normal);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_line_d_deletes_single_line_via_range_mutation() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "only line\nsecond line");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
hjkl_vim::handle_key(
&mut app.active_mut().editor,
KeyEvent::new(KeyCode::Char('V'), KeyModifiers::NONE),
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::VisualLine
);
let consumed = app.route_chord_key(ck('d'));
assert!(consumed, "d in VisualLine must be consumed");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["second line"],
"Vd on single line must delete it; got {lines:?}"
);
assert_eq!(app.active().editor.vim_mode(), hjkl_engine::VimMode::Normal);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_block_d_deletes_rectangle_via_range_mutation() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "abcde\nfghij\nklmno");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
hjkl_vim::handle_key(
&mut app.active_mut().editor,
KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL),
);
assert_eq!(
app.active().editor.vim_mode(),
hjkl_engine::VimMode::VisualBlock
);
for _ in 0..2 {
app.route_chord_key(ck('l'));
}
for _ in 0..2 {
app.route_chord_key(ck('j'));
}
let consumed = app.route_chord_key(ck('d'));
assert!(consumed, "d in VisualBlock must be consumed");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["de", "ij", "no"],
"VisualBlock d must remove cols 0..=2 on each row; got {lines:?}"
);
assert_eq!(app.active().editor.vim_mode(), hjkl_engine::VimMode::Normal);
assert_window_synced_to_engine(&app);
}
#[test]
fn visual_block_y_yanks_rectangle_to_register() {
let mut app = App::new(None, false, None, None).unwrap();
seed_buffer(&mut app, "abcde\nfghij\nklmno");
app.active_mut().editor.jump_cursor(0, 0);
app.sync_viewport_from_editor();
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('"'));
hjkl_vim::handle_key(&mut app.active_mut().editor, ck('a'));
hjkl_vim::handle_key(
&mut app.active_mut().editor,
KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL),
);
app.route_chord_key(ck('l'));
app.route_chord_key(ck('j'));
let consumed = app.route_chord_key(ck('y'));
assert!(consumed, "y in VisualBlock must be consumed");
let lines = app.active().editor.buffer().lines().to_vec();
assert_eq!(
lines,
vec!["abcde", "fghij", "klmno"],
"VisualBlock y must not modify buffer"
);
let reg_a = &app.active().editor.registers().named[0];
assert!(
!reg_a.text.is_empty(),
"register 'a' must be non-empty after block yank"
);
assert!(
reg_a.text.contains("ab") && reg_a.text.contains("fg"),
"register 'a' must contain block text 'ab'/'fg'; got {:?}",
reg_a.text
);
assert_eq!(app.active().editor.vim_mode(), hjkl_engine::VimMode::Normal);
assert_window_synced_to_engine(&app);
}