use crate::ui::input::InputEditor;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn press(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::empty())
}
fn ctrl(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
fn meta(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::ALT)
}
fn shift_enter() -> KeyEvent {
KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT)
}
fn meta_enter() -> KeyEvent {
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
}
fn type_str(editor: &mut InputEditor, s: &str) {
for c in s.chars() {
editor.handle_key(press(KeyCode::Char(c)));
}
}
fn len_bytes(s: &str) -> usize {
s.len()
}
#[test]
fn typing_multibyte_chars_does_not_panic() {
let mut editor = InputEditor::new();
type_str(&mut editor, "på ");
assert_eq!(editor.buffer.as_str(), "på ");
assert_eq!(editor.cursor, editor.buffer.len());
}
#[test]
fn typing_mixed_ascii_and_multibyte() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hei på deg så fin dag æøå");
assert_eq!(editor.buffer.as_str(), "hei på deg så fin dag æøå");
assert_eq!(editor.cursor, editor.buffer.len());
}
#[test]
fn backspace_after_multibyte_does_not_panic() {
let mut editor = InputEditor::new();
type_str(&mut editor, "å");
editor.handle_key(press(KeyCode::Backspace));
assert_eq!(editor.buffer.as_str(), "");
assert_eq!(editor.cursor, 0);
}
#[test]
fn left_arrow_steps_one_char_not_one_byte() {
let mut editor = InputEditor::new();
type_str(&mut editor, "aåb");
assert_eq!(editor.cursor, 4);
editor.handle_key(press(KeyCode::Left));
assert_eq!(editor.cursor, 3);
editor.handle_key(press(KeyCode::Left));
assert_eq!(editor.cursor, 1);
}
#[test]
fn right_arrow_steps_one_char_not_one_byte() {
let mut editor = InputEditor::new();
type_str(&mut editor, "aåb");
editor.cursor = 0;
editor.handle_key(press(KeyCode::Right));
assert_eq!(editor.cursor, 1);
editor.handle_key(press(KeyCode::Right));
assert_eq!(editor.cursor, 3);
}
#[test]
fn enter_returns_buffer_and_resets() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hei på");
let out = editor.handle_key(press(KeyCode::Enter)).unwrap();
assert_eq!(out.as_str(), "hei på");
assert_eq!(editor.cursor, 0);
assert_eq!(editor.buffer.as_str(), "");
}
#[test]
fn ctrl_a_moves_to_start_of_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
assert_eq!(editor.cursor, len_bytes("hello world"));
editor.handle_key(ctrl(KeyCode::Char('a')));
assert_eq!(editor.cursor, 0);
assert_eq!(editor.buffer.as_str(), "hello world");
}
#[test]
fn ctrl_e_moves_to_end_of_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('e')));
assert_eq!(editor.cursor, len_bytes("hello world"));
assert_eq!(editor.buffer.as_str(), "hello world");
}
#[test]
fn ctrl_b_moves_left_one_char() {
let mut editor = InputEditor::new();
type_str(&mut editor, "abc");
assert_eq!(editor.cursor, 3);
editor.handle_key(ctrl(KeyCode::Char('b')));
assert_eq!(editor.cursor, 2);
editor.handle_key(ctrl(KeyCode::Char('b')));
assert_eq!(editor.cursor, 1);
}
#[test]
fn ctrl_b_at_start_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "abc");
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('b')));
assert_eq!(editor.cursor, 0);
}
#[test]
fn option_left_skips_to_prev_word_start() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world foo");
let len = len_bytes("hello world foo");
assert_eq!(editor.cursor, len);
editor.handle_key(meta(KeyCode::Left));
assert_eq!(editor.cursor, len_bytes("hello world ")); editor.handle_key(meta(KeyCode::Left));
assert_eq!(editor.cursor, len_bytes("hello ")); editor.handle_key(meta(KeyCode::Left));
assert_eq!(editor.cursor, 0); }
#[test]
fn option_left_at_start_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "abc");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Left));
assert_eq!(editor.cursor, 0);
}
#[test]
fn option_left_from_middle_of_word_goes_to_its_start() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello wo"); editor.handle_key(meta(KeyCode::Left));
assert_eq!(editor.cursor, len_bytes("hello ")); }
#[test]
fn option_right_skips_to_next_word_start() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world foo");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("hello ")); editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("hello world ")); editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("hello world foo")); }
#[test]
fn option_right_at_end_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "abc");
editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, 3);
}
#[test]
fn option_right_skips_punctuation() {
let mut editor = InputEditor::new();
type_str(&mut editor, "foo.bar_baz");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("foo."));
assert_eq!(&editor.buffer[editor.cursor..], "bar_baz");
}
#[test]
fn option_right_multibyte_words() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hå bør");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("hå ")); editor.handle_key(meta(KeyCode::Right));
assert_eq!(editor.cursor, len_bytes("hå bør")); }
#[test]
fn meta_b_skips_to_prev_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
assert_eq!(editor.cursor, len_bytes("hello world"));
editor.handle_key(meta(KeyCode::Char('b')));
assert_eq!(editor.cursor, len_bytes("hello ")); editor.handle_key(meta(KeyCode::Char('b')));
assert_eq!(editor.cursor, 0); }
#[test]
fn meta_f_skips_to_next_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Char('f')));
assert_eq!(editor.cursor, len_bytes("hello ")); editor.handle_key(meta(KeyCode::Char('f')));
assert_eq!(editor.cursor, len_bytes("hello world")); }
#[test]
fn ctrl_k_kills_to_end_of_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('k')));
assert_eq!(editor.buffer.as_str(), "hello ");
assert_eq!(editor.cursor, len_bytes("hello "));
}
#[test]
fn ctrl_k_at_end_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(ctrl(KeyCode::Char('k')));
assert_eq!(editor.buffer.as_str(), "hello");
assert_eq!(editor.cursor, 5);
}
#[test]
fn consecutive_ctrl_k_accumulates_in_kill_ring() {
let mut editor = InputEditor::new();
type_str(&mut editor, "one two three");
editor.cursor = len_bytes("one ");
editor.handle_key(ctrl(KeyCode::Char('k'))); assert_eq!(editor.buffer.as_str(), "one ");
editor.handle_key(ctrl(KeyCode::Char('k')));
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "one two three");
}
#[test]
fn non_kill_action_resets_kill_accumulation() {
let mut editor = InputEditor::new();
type_str(&mut editor, "one two three");
editor.cursor = len_bytes("one ");
editor.handle_key(ctrl(KeyCode::Char('k')));
type_str(&mut editor, "X");
assert_eq!(editor.buffer.as_str(), "one X");
editor.handle_key(ctrl(KeyCode::Char('k')));
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "two threeone X");
}
#[test]
fn ctrl_u_kills_to_start_of_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('u')));
assert_eq!(editor.buffer.as_str(), "world");
assert_eq!(editor.cursor, 0);
}
#[test]
fn ctrl_u_at_start_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('u')));
assert_eq!(editor.buffer.as_str(), "hello");
assert_eq!(editor.cursor, 0);
}
#[test]
fn ctrl_u_kills_entire_line_when_at_end() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(ctrl(KeyCode::Char('u')));
assert_eq!(editor.buffer.as_str(), "");
assert_eq!(editor.cursor, 0);
}
#[test]
fn ctrl_w_kills_previous_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.handle_key(ctrl(KeyCode::Char('w')));
assert_eq!(editor.buffer.as_str(), "hello ");
assert_eq!(editor.cursor, len_bytes("hello "));
}
#[test]
fn ctrl_w_in_middle_of_word_kills_partial_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello wor");
editor.handle_key(ctrl(KeyCode::Char('w')));
assert_eq!(editor.buffer.as_str(), "hello ld");
assert_eq!(editor.cursor, len_bytes("hello "));
}
#[test]
fn ctrl_w_at_start_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('w')));
assert_eq!(editor.buffer.as_str(), "hello");
assert_eq!(editor.cursor, 0);
}
#[test]
fn ctrl_w_kills_word_with_punctuation() {
let mut editor = InputEditor::new();
type_str(&mut editor, "foo.bar baz");
editor.handle_key(ctrl(KeyCode::Char('w')));
assert_eq!(editor.buffer.as_str(), "foo.bar ");
assert_eq!(editor.cursor, len_bytes("foo.bar "));
}
#[test]
fn meta_backspace_deletes_previous_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.handle_key(meta(KeyCode::Backspace));
assert_eq!(editor.buffer.as_str(), "hello ");
assert_eq!(editor.cursor, len_bytes("hello "));
}
#[test]
fn meta_d_deletes_next_word() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world foo");
editor.cursor = 0;
editor.handle_key(meta(KeyCode::Char('d')));
assert_eq!(editor.buffer.as_str(), "world foo");
assert_eq!(editor.cursor, 0);
}
#[test]
fn meta_d_in_middle_of_word_deletes_to_word_end() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = 1; editor.handle_key(meta(KeyCode::Char('d')));
assert_eq!(editor.buffer.as_str(), "hworld");
assert_eq!(editor.cursor, 1);
}
#[test]
fn meta_d_at_end_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(meta(KeyCode::Char('d')));
assert_eq!(editor.buffer.as_str(), "hello");
assert_eq!(editor.cursor, 5);
}
#[test]
fn ctrl_y_yanks_most_recent_kill() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('k'))); assert_eq!(editor.buffer.as_str(), "hello ");
editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "worldhello ");
assert_eq!(editor.cursor, len_bytes("world"));
}
#[test]
fn ctrl_y_yanks_ctrl_w_kill() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.handle_key(ctrl(KeyCode::Char('w'))); assert_eq!(editor.buffer.as_str(), "hello ");
editor.handle_key(ctrl(KeyCode::Char('y'))); assert_eq!(editor.buffer.as_str(), "hello world");
}
#[test]
fn ctrl_y_yanks_ctrl_u_kill() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('u'))); assert_eq!(editor.buffer.as_str(), "world");
editor.cursor = len_bytes("world");
editor.handle_key(ctrl(KeyCode::Char('y'))); assert_eq!(editor.buffer.as_str(), "worldhello ");
}
#[test]
fn ctrl_y_does_nothing_with_empty_kill_ring() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "hello");
assert_eq!(editor.cursor, 5);
}
#[test]
fn meta_y_cycles_kill_ring_after_yank() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('k')));
assert_eq!(editor.buffer.as_str(), "hello ");
type_str(&mut editor, "foo bar");
editor.cursor = len_bytes("hello foo ");
editor.handle_key(ctrl(KeyCode::Char('k'))); assert_eq!(editor.buffer.as_str(), "hello foo ");
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "hello foo bar");
editor.handle_key(meta(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "hello foo world");
}
#[test]
fn meta_y_does_nothing_without_prior_yank() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.cursor = len_bytes("hello ");
editor.handle_key(ctrl(KeyCode::Char('k')));
editor.handle_key(meta(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "hello ");
}
#[test]
fn ctrl_p_navigates_history_up() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second");
editor.handle_key(press(KeyCode::Enter));
assert!(editor.buffer.is_empty());
editor.handle_key(ctrl(KeyCode::Char('p')));
assert_eq!(editor.buffer.as_str(), "second");
editor.handle_key(ctrl(KeyCode::Char('p')));
assert_eq!(editor.buffer.as_str(), "first");
}
#[test]
fn ctrl_n_navigates_history_down() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('p')));
editor.handle_key(ctrl(KeyCode::Char('p')));
assert_eq!(editor.buffer.as_str(), "first");
editor.handle_key(ctrl(KeyCode::Char('n')));
assert_eq!(editor.buffer.as_str(), "second");
}
#[test]
fn ctrl_n_at_bottom_clears_buffer() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('n')));
assert!(editor.buffer.is_empty());
assert_eq!(editor.cursor, 0);
}
#[test]
fn kill_ring_capped_at_10_entries() {
let mut editor = InputEditor::new();
for i in 0..12 {
type_str(&mut editor, &format!("word{}", i));
editor.handle_key(press(KeyCode::Enter)); editor.handle_key(ctrl(KeyCode::Char('p'))); editor.cursor = 0;
editor.handle_key(ctrl(KeyCode::Char('k'))); assert!(editor.buffer.is_empty());
}
editor.handle_key(ctrl(KeyCode::Char('y')));
assert_eq!(editor.buffer.as_str(), "word11");
}
#[test]
fn option_left_with_picker_active_handled_by_picker() {
let mut editor = InputEditor::new();
editor.cursor = 0;
editor.handle_key(press(KeyCode::Char('@')));
assert!(editor.picker.as_ref().is_some_and(|p| p.active));
let result = editor.handle_key(meta(KeyCode::Left));
assert!(result.is_none());
}
#[test]
fn shift_enter_inserts_newline() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(shift_enter());
type_str(&mut editor, "world");
assert_eq!(editor.buffer.as_str(), "hello\nworld");
}
#[test]
fn meta_enter_inserts_newline() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(meta_enter());
type_str(&mut editor, "world");
assert_eq!(editor.buffer.as_str(), "hello\nworld");
}
#[test]
fn plain_enter_submits_multiline_text() {
let mut editor = InputEditor::new();
type_str(&mut editor, "line1");
editor.handle_key(shift_enter());
type_str(&mut editor, "line2");
let submitted = editor.handle_key(press(KeyCode::Enter)).unwrap();
assert_eq!(submitted.as_str(), "line1\nline2");
assert!(editor.buffer.is_empty());
}
#[test]
fn multiline_history_saves_and_restores() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(shift_enter());
type_str(&mut editor, "second");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(press(KeyCode::Up));
assert_eq!(editor.buffer.as_str(), "first\nsecond");
}
#[test]
fn multiline_cursor_up_moves_to_previous_logical_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "line1");
editor.handle_key(shift_enter());
type_str(&mut editor, "line2");
let end_pos = editor.cursor;
editor.handle_key(press(KeyCode::Up));
assert!(editor.cursor < end_pos);
let line1_end = "line1".len();
assert!(editor.cursor <= line1_end);
}
#[test]
fn multiline_cursor_down_moves_to_next_logical_line() {
let mut editor = InputEditor::new();
type_str(&mut editor, "line1");
editor.handle_key(shift_enter());
type_str(&mut editor, "line2");
editor.cursor = 0;
editor.handle_key(press(KeyCode::Down));
let line1_len = "line1\n".len();
assert!(editor.cursor >= line1_len);
}
#[test]
fn multiline_up_at_top_goes_to_history() {
let mut editor = InputEditor::new();
type_str(&mut editor, "prev");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "line1");
editor.handle_key(shift_enter());
type_str(&mut editor, "line2");
editor.cursor = 0;
editor.handle_key(press(KeyCode::Up));
assert_eq!(editor.buffer.as_str(), "prev");
}
#[test]
fn multiline_down_at_bottom_goes_to_history() {
let mut editor = InputEditor::new();
type_str(&mut editor, "prev");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "next");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(press(KeyCode::Up));
assert_eq!(editor.buffer.as_str(), "next");
editor.handle_key(press(KeyCode::Down));
assert!(editor.buffer.is_empty());
}
#[test]
fn enter_with_picker_active_does_not_submit() {
let mut editor = InputEditor::new();
editor.cursor = 0;
editor.handle_key(press(KeyCode::Char('@')));
assert!(editor.picker.as_ref().is_some_and(|p| p.active));
let result = editor.handle_key(press(KeyCode::Enter));
assert!(result.is_none());
assert!(editor.picker.as_ref().is_some_and(|p| p.active));
}
#[test]
fn meta_enter_with_picker_active_inserts_newline() {
let mut editor = InputEditor::new();
editor.cursor = 0;
editor.handle_key(press(KeyCode::Char('@')));
let result = editor.handle_key(meta_enter());
assert!(result.is_none());
}
#[test]
fn short_paste_inserts_raw() {
let mut editor = InputEditor::new();
editor.handle_paste("hello world");
assert_eq!(editor.buffer.as_str(), "hello world");
assert_eq!(editor.cursor, "hello world".len());
}
#[test]
fn three_line_paste_still_raw() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc");
assert_eq!(editor.buffer.as_str(), "a\nb\nc");
}
#[test]
fn four_line_paste_collapses_to_placeholder() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd");
assert!(editor.buffer.contains('\u{0001}'));
let raw_line = editor.buffer.as_str();
let (display, _) = editor.render_line(raw_line, editor.cursor);
assert_eq!(display, "[4 lines pasted]");
assert_eq!(editor.expanded().as_str(), "a\nb\nc\nd");
}
#[test]
fn submit_expands_placeholders() {
let mut editor = InputEditor::new();
type_str(&mut editor, "before ");
editor.handle_paste("L1\nL2\nL3\nL4");
type_str(&mut editor, " after");
let submitted = editor.handle_key(press(KeyCode::Enter)).unwrap();
assert_eq!(submitted.as_str(), "before L1\nL2\nL3\nL4 after");
assert!(editor.buffer.is_empty());
}
#[test]
fn left_right_skip_placeholder_as_unit() {
let mut editor = InputEditor::new();
type_str(&mut editor, "x");
editor.handle_paste("a\nb\nc\nd");
type_str(&mut editor, "y");
let end = editor.cursor;
editor.handle_key(press(KeyCode::Left));
assert!(editor.cursor < end);
let after_first = editor.cursor;
editor.handle_key(press(KeyCode::Left));
assert_eq!(editor.cursor, 1); assert!(editor.cursor < after_first);
}
#[test]
fn backspace_deletes_whole_placeholder() {
let mut editor = InputEditor::new();
type_str(&mut editor, "a");
editor.handle_paste("L1\nL2\nL3\nL4");
type_str(&mut editor, "b");
let len_with_marker = editor.buffer.len();
editor.handle_key(press(KeyCode::Left));
editor.handle_key(press(KeyCode::Backspace));
assert!(editor.buffer.len() < len_with_marker);
assert_eq!(editor.buffer.as_str(), "ab");
}
#[test]
fn second_paste_of_same_content_expands_inline() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd");
assert!(editor.buffer.contains('\u{0001}'));
editor.handle_paste("a\nb\nc\nd");
assert_eq!(editor.buffer.as_str(), "a\nb\nc\nd");
assert!(!editor.buffer.contains('\u{0001}'));
}
#[test]
fn second_paste_of_different_content_creates_new_placeholder() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd");
editor.handle_paste("X\nY\nZ\nW");
let marker_count = editor.buffer.matches('\u{0001}').count();
assert_eq!(marker_count, 4); assert_eq!(editor.expanded().as_str(), "a\nb\nc\ndX\nY\nZ\nW");
}
#[test]
fn paste_mark_chars_stripped_from_input() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\n\u{0001}\nc\nd");
assert_eq!(editor.expanded().as_str(), "a\nb\n\nc\nd");
}
#[test]
fn ctrl_w_does_not_split_paste_marker() {
let mut editor = InputEditor::new();
type_str(&mut editor, "before ");
editor.handle_paste("L1\nL2\nL3\nL4");
type_str(&mut editor, " after");
editor.handle_key(ctrl(KeyCode::Char('w')));
let mark_count = editor.buffer.matches('\u{0001}').count();
assert_eq!(mark_count, 2, "marker bytes corrupted by Ctrl+W");
assert_eq!(editor.expanded().as_str(), "before L1\nL2\nL3\nL4 ");
}
#[test]
fn meta_b_skips_past_paste_marker() {
let mut editor = InputEditor::new();
editor.handle_paste("L1\nL2\nL3\nL4");
type_str(&mut editor, " trailing");
editor.handle_key(meta(KeyCode::Char('b')));
let before = editor.cursor;
editor.handle_key(meta(KeyCode::Char('b')));
assert_eq!(editor.cursor, 0);
assert!(editor.cursor < before);
assert_eq!(editor.buffer.matches('\u{0001}').count(), 2);
}
#[test]
fn paste_during_picker_is_ignored() {
let mut editor = InputEditor::new();
editor.handle_key(press(KeyCode::Char('@')));
let buffer_before = editor.buffer.to_string();
editor.handle_paste("a\nb\nc\nd");
assert_eq!(editor.buffer.as_str(), buffer_before);
}
fn ctrl_j() -> KeyEvent {
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL)
}
#[test]
fn ctrl_j_inserts_newline_as_fallback() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(ctrl_j());
type_str(&mut editor, "world");
assert_eq!(editor.buffer.as_str(), "hello\nworld");
}
#[test]
fn ctrl_j_during_picker_is_noop() {
let mut editor = InputEditor::new();
editor.handle_key(press(KeyCode::Char('@')));
let buf_before = editor.buffer.to_string();
let result = editor.handle_key(ctrl_j());
assert!(result.is_none());
assert_eq!(editor.buffer.as_str(), buf_before);
}
#[test]
fn at_picker_triggers_after_multibyte_char() {
let mut editor = InputEditor::new();
type_str(&mut editor, "é ");
editor.handle_key(press(KeyCode::Char('@')));
assert!(
editor.picker.as_ref().is_some_and(|p| p.active),
"picker should activate when `@` follows a space, even when the previous word ends in a multibyte char"
);
}
#[test]
fn at_picker_does_not_trigger_mid_word_after_multibyte() {
let mut editor = InputEditor::new();
type_str(&mut editor, "café@");
assert!(
editor.picker.is_none() || !editor.picker.as_ref().unwrap().active,
"picker must stay closed when `@` lands mid-word after a multibyte char"
);
}
#[test]
fn paste_with_crlf_line_endings_collapses() {
let mut editor = InputEditor::new();
editor.handle_paste("a\r\nb\r\nc\r\nd\r\ne");
assert!(editor.buffer.contains('\u{0001}'));
assert_eq!(editor.expanded().as_str(), "a\nb\nc\nd\ne");
}
#[test]
fn paste_with_cr_only_line_endings_collapses() {
let mut editor = InputEditor::new();
editor.handle_paste("a\rb\rc\rd\re");
assert!(editor.buffer.contains('\u{0001}'));
assert_eq!(editor.expanded().as_str(), "a\nb\nc\nd\ne");
}
#[test]
fn yank_after_submit_preserves_pasted_content() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd\ne");
editor.handle_key(ctrl(KeyCode::Char('u'))); editor.handle_key(press(KeyCode::Enter)); type_str(&mut editor, "hello");
editor.handle_key(ctrl(KeyCode::Char('y'))); let submitted = editor.handle_key(press(KeyCode::Enter)).unwrap();
assert!(
submitted.contains("a\nb\nc\nd\ne"),
"expected yank to restore paste body, got: {:?}",
submitted
);
}
#[test]
fn paste_at_start_of_buffer() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd");
type_str(&mut editor, " trailing");
assert_eq!(editor.expanded().as_str(), "a\nb\nc\nd trailing");
}
#[test]
fn paste_in_middle_of_buffer() {
let mut editor = InputEditor::new();
type_str(&mut editor, "before-after");
editor.cursor = "before-".len();
editor.handle_paste("L1\nL2\nL3\nL4");
assert_eq!(editor.expanded().as_str(), "before-L1\nL2\nL3\nL4after");
}
#[test]
fn two_distinct_pastes_keep_both_bodies() {
let mut editor = InputEditor::new();
editor.handle_paste("paste-1-a\nb\nc\nd");
type_str(&mut editor, " mid ");
editor.handle_paste("paste-2-w\nx\ny\nz");
assert_eq!(
editor.expanded().as_str(),
"paste-1-a\nb\nc\nd mid paste-2-w\nx\ny\nz"
);
assert_eq!(editor.buffer.matches('\u{0001}').count(), 4);
}
#[test]
fn home_and_end_skip_past_markers() {
let mut editor = InputEditor::new();
editor.handle_paste("a\nb\nc\nd");
type_str(&mut editor, "tail");
editor.handle_key(press(KeyCode::End));
assert_eq!(editor.cursor, editor.buffer.len());
editor.handle_key(press(KeyCode::Home));
assert_eq!(editor.cursor, 0);
assert_eq!(editor.expanded().as_str(), "a\nb\nc\ndtail");
}
#[test]
fn marker_survives_multiline_up_down_nav() {
let mut editor = InputEditor::new();
type_str(&mut editor, "row1");
editor.handle_key(shift_enter());
editor.handle_paste("p1\np2\np3\np4");
editor.handle_key(shift_enter());
type_str(&mut editor, "row3");
editor.handle_key(press(KeyCode::Up));
editor.handle_key(press(KeyCode::Up));
editor.handle_key(press(KeyCode::Down));
editor.handle_key(press(KeyCode::Down));
assert_eq!(editor.expanded().as_str(), "row1\np1\np2\np3\np4\nrow3");
}
#[test]
fn empty_paste_is_noop() {
let mut editor = InputEditor::new();
type_str(&mut editor, "existing");
let buf_before = editor.buffer.to_string();
let cur_before = editor.cursor;
editor.handle_paste("");
assert_eq!(editor.buffer.as_str(), buf_before);
assert_eq!(editor.cursor, cur_before);
}
#[test]
fn paste_containing_only_paste_mark_chars_is_noop() {
let mut editor = InputEditor::new();
editor.handle_paste("\u{0001}\u{0001}\u{0001}\u{0001}");
assert_eq!(editor.buffer.as_str(), "");
assert_eq!(editor.cursor, 0);
}
#[test]
fn delete_at_start_of_marker_removes_whole_block() {
let mut editor = InputEditor::new();
type_str(&mut editor, "before");
editor.handle_paste("p1\np2\np3\np4");
type_str(&mut editor, "after");
editor.cursor = "before".len();
editor.handle_key(press(KeyCode::Delete));
assert_eq!(editor.expanded().as_str(), "beforeafter");
assert!(!editor.buffer.contains('\u{0001}'));
}
#[test]
fn multibyte_chars_adjacent_to_marker() {
let mut editor = InputEditor::new();
type_str(&mut editor, "héllo "); editor.handle_paste("p1\np2\np3\np4");
type_str(&mut editor, " wörld");
while editor.cursor > 0 {
editor.handle_key(press(KeyCode::Left));
}
assert_eq!(editor.cursor, 0);
assert_eq!(editor.expanded().as_str(), "héllo p1\np2\np3\np4 wörld");
}
#[test]
fn ctrl_r_enters_search_mode() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "draft");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert!(editor.is_in_search());
assert_eq!(editor.search_query(), "");
assert_eq!(editor.search_match_text(), "second");
}
#[test]
fn ctrl_r_with_empty_history_is_noop() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "hello");
}
#[test]
fn search_typing_narrows_query_and_updates_match() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "different");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "different");
editor.handle_key(press(KeyCode::Char('s')));
assert!(editor.is_in_search());
assert_eq!(editor.search_query(), "s");
assert_eq!(editor.search_match_text(), "second");
}
#[test]
fn search_backspace_narrows_query() {
let mut editor = InputEditor::new();
type_str(&mut editor, "abc");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "abd");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('a')));
editor.handle_key(press(KeyCode::Char('b')));
editor.handle_key(press(KeyCode::Char('c')));
assert_eq!(editor.search_match_text(), "abc");
editor.handle_key(press(KeyCode::Backspace));
assert_eq!(editor.search_query(), "ab");
assert_eq!(editor.search_match_text(), "abd");
}
#[test]
fn search_backspace_on_empty_query_does_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "test");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Backspace));
assert!(editor.is_in_search());
assert_eq!(editor.search_query(), "");
assert_eq!(editor.search_match_text(), "test");
}
#[test]
fn ctrl_r_again_cycles_to_older_match() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first foo");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second bar");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "third foo");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "third foo");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "first foo");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "third foo");
}
#[test]
fn search_enter_accepts_match() {
let mut editor = InputEditor::new();
type_str(&mut editor, "original");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "target match");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "draft text");
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('t')));
assert_eq!(editor.search_match_text(), "target match");
editor.handle_key(press(KeyCode::Enter));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "target match");
}
#[test]
fn search_esc_cancels_and_restores_draft() {
let mut editor = InputEditor::new();
type_str(&mut editor, "old entry");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "draft text");
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('o')));
assert_eq!(editor.search_match_text(), "old entry");
editor.handle_key(press(KeyCode::Esc));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "draft text");
}
#[test]
fn search_cancels_on_ctrl_c() {
let mut editor = InputEditor::new();
type_str(&mut editor, "history");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "draft");
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(ctrl(KeyCode::Char('c')));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "draft");
}
#[test]
fn search_case_insensitive() {
let mut editor = InputEditor::new();
type_str(&mut editor, "Hello World");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('h')));
assert_eq!(editor.search_match_text(), "Hello World");
editor.handle_key(press(KeyCode::Char('e')));
editor.handle_key(press(KeyCode::Char('l')));
assert_eq!(editor.search_query(), "hel");
assert_eq!(editor.search_match_text(), "Hello World");
}
#[test]
fn search_no_match_shows_nothing() {
let mut editor = InputEditor::new();
type_str(&mut editor, "hello world");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('x')));
assert_eq!(editor.search_query(), "x");
assert!(editor.is_in_search());
assert_eq!(editor.search_match_text(), "");
}
#[test]
fn load_history_populates_for_ctrl_r() {
let mut editor = InputEditor::new();
editor.load_history_entry("first message");
editor.load_history_entry("second message");
editor.load_history_entry("third message");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert!(editor.is_in_search());
assert_eq!(editor.search_match_text(), "third message");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "second message");
editor.handle_key(press(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "first message");
}
#[test]
fn load_history_skips_dupes_and_empty() {
let mut editor = InputEditor::new();
editor.load_history_entry(""); editor.load_history_entry("msg1");
editor.load_history_entry("msg1"); editor.load_history_entry("msg2");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "msg2");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "msg1");
editor.handle_key(press(KeyCode::Char('m')));
editor.handle_key(press(KeyCode::Char('s')));
editor.handle_key(press(KeyCode::Char('g')));
editor.handle_key(press(KeyCode::Char('3')));
assert_eq!(editor.search_match_text(), "");
}
#[test]
fn search_no_match_accept_restores_draft() {
let mut editor = InputEditor::new();
type_str(&mut editor, "history");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "my draft text");
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('z'))); assert_eq!(editor.search_match_text(), "");
editor.handle_key(press(KeyCode::Enter));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "my draft text");
}
#[test]
fn search_typing_after_cycle_narrows_from_position() {
let mut editor = InputEditor::new();
type_str(&mut editor, "first foo bar");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "second foo baz");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "third foo qux");
editor.handle_key(press(KeyCode::Enter));
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('o')));
editor.handle_key(press(KeyCode::Char('o')));
assert_eq!(editor.search_match_text(), "third foo qux");
editor.handle_key(ctrl(KeyCode::Char('f')));
assert_eq!(editor.search_match_text(), "second foo baz");
editor.handle_key(press(KeyCode::Char(' ')));
editor.handle_key(press(KeyCode::Char('b')));
assert_eq!(editor.search_match_text(), "second foo baz");
}
#[test]
fn search_esc_from_input_editor_cancels() {
let mut editor = InputEditor::new();
type_str(&mut editor, "old");
editor.handle_key(press(KeyCode::Enter));
type_str(&mut editor, "draft text");
editor.handle_key(ctrl(KeyCode::Char('f')));
editor.handle_key(press(KeyCode::Char('o')));
assert!(editor.is_in_search());
editor.handle_key(press(KeyCode::Esc));
assert!(!editor.is_in_search());
assert_eq!(editor.buffer.as_str(), "draft text");
}