use crossterm::event::{self, KeyCode, KeyEvent};
use crate::domain::input::InputState;
pub(crate) fn is_control_key(key: KeyEvent) -> bool {
key.modifiers == event::KeyModifiers::CONTROL
}
pub(crate) fn is_alt_key(key: KeyEvent) -> bool {
key.modifiers.contains(event::KeyModifiers::ALT)
}
pub(crate) fn is_word_delete_backspace(key: KeyEvent) -> bool {
key.modifiers
.intersects(event::KeyModifiers::ALT | event::KeyModifiers::SHIFT)
}
pub(crate) fn is_line_delete_backspace(key: KeyEvent) -> bool {
key.modifiers.contains(event::KeyModifiers::SUPER)
}
pub(crate) fn is_insertable_char_key(key: KeyEvent) -> bool {
matches!(
key.modifiers,
event::KeyModifiers::NONE | event::KeyModifiers::SHIFT
)
}
pub(crate) fn should_insert_newline(key: KeyEvent) -> bool {
is_enter_key(key.code)
&& key
.modifiers
.intersects(event::KeyModifiers::ALT | event::KeyModifiers::SHIFT)
}
pub(crate) fn is_enter_key(key_code: KeyCode) -> bool {
matches!(key_code, KeyCode::Enter | KeyCode::Char('\r' | '\n'))
}
pub(crate) fn is_control_newline_key(key: KeyEvent, character: char) -> bool {
key.modifiers == event::KeyModifiers::CONTROL && matches!(character, 'j' | 'm' | '\n' | '\r')
}
pub(crate) fn is_cursor_on_first_line(input: &InputState) -> bool {
input.text().chars().take(input.cursor).all(|ch| ch != '\n')
}
pub(crate) fn is_cursor_on_last_line(input: &InputState) -> bool {
input.text().chars().skip(input.cursor).all(|ch| ch != '\n')
}
pub(crate) fn move_cursor_word_left(input: &mut InputState) {
if input.cursor == 0 {
return;
}
let characters: Vec<char> = input.text().chars().collect();
let mut cursor = input.cursor;
while cursor > 0 && characters[cursor - 1].is_whitespace() {
cursor -= 1;
}
while cursor > 0 && !characters[cursor - 1].is_whitespace() {
cursor -= 1;
}
input.cursor = cursor;
}
pub(crate) fn move_cursor_word_right(input: &mut InputState) {
let characters: Vec<char> = input.text().chars().collect();
let mut cursor = input.cursor;
while cursor < characters.len() && !characters[cursor].is_whitespace() {
cursor += 1;
}
while cursor < characters.len() && characters[cursor].is_whitespace() {
cursor += 1;
}
input.cursor = cursor;
}
pub(crate) fn word_delete_range(text: &str, cursor: usize) -> Option<(usize, usize)> {
if cursor == 0 {
return None;
}
let characters: Vec<char> = text.chars().collect();
let mut start = cursor;
while start > 0 && characters[start - 1].is_whitespace() {
start -= 1;
}
while start > 0 && !characters[start - 1].is_whitespace() {
start -= 1;
}
while start > 0 && characters[start - 1].is_whitespace() {
start -= 1;
}
Some((start, cursor))
}
pub(crate) fn delete_word_backward(input: &mut InputState) {
if let Some((start, end)) = word_delete_range(input.text(), input.cursor) {
input.replace_range(start, end, "");
}
}
pub(crate) fn normalize_pasted_text(pasted_text: &str) -> String {
let mut normalized_text = String::with_capacity(pasted_text.len());
let mut characters = pasted_text.chars().peekable();
while let Some(character) = characters.next() {
if character == '\r' {
if matches!(characters.peek(), Some(&'\n')) {
let _ = characters.next();
}
normalized_text.push('\n');
continue;
}
normalized_text.push(character);
}
normalized_text
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_insert_newline_for_alt_enter() {
let key = KeyEvent::new(KeyCode::Enter, event::KeyModifiers::ALT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_insert_newline_for_alt_shift_enter() {
let key = KeyEvent::new(
KeyCode::Enter,
event::KeyModifiers::ALT | event::KeyModifiers::SHIFT,
);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_insert_newline_for_alt_carriage_return() {
let key = KeyEvent::new(KeyCode::Char('\r'), event::KeyModifiers::ALT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_insert_newline_for_alt_line_feed() {
let key = KeyEvent::new(KeyCode::Char('\n'), event::KeyModifiers::ALT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_not_insert_newline_for_plain_enter() {
let key = KeyEvent::new(KeyCode::Enter, event::KeyModifiers::NONE);
let result = should_insert_newline(key);
assert!(!result);
}
#[test]
fn test_should_insert_newline_for_shift_enter() {
let key = KeyEvent::new(KeyCode::Enter, event::KeyModifiers::SHIFT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_insert_newline_for_shift_carriage_return() {
let key = KeyEvent::new(KeyCode::Char('\r'), event::KeyModifiers::SHIFT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_insert_newline_for_shift_line_feed() {
let key = KeyEvent::new(KeyCode::Char('\n'), event::KeyModifiers::SHIFT);
let result = should_insert_newline(key);
assert!(result);
}
#[test]
fn test_should_not_insert_newline_for_control_enter() {
let key = KeyEvent::new(KeyCode::Enter, event::KeyModifiers::CONTROL);
let result = should_insert_newline(key);
assert!(!result);
}
#[test]
fn test_should_not_insert_newline_for_non_enter_key() {
let key = KeyEvent::new(KeyCode::Char('x'), event::KeyModifiers::SHIFT);
let result = should_insert_newline(key);
assert!(!result);
}
#[test]
fn test_is_enter_key_for_enter() {
let result = is_enter_key(KeyCode::Enter);
assert!(result);
}
#[test]
fn test_is_enter_key_for_carriage_return() {
let result = is_enter_key(KeyCode::Char('\r'));
assert!(result);
}
#[test]
fn test_is_enter_key_for_line_feed() {
let result = is_enter_key(KeyCode::Char('\n'));
assert!(result);
}
#[test]
fn test_is_enter_key_for_other_key() {
let result = is_enter_key(KeyCode::Char('x'));
assert!(!result);
}
#[test]
fn test_is_control_newline_key_accepts_ctrl_j() {
let key = KeyEvent::new(KeyCode::Char('j'), event::KeyModifiers::CONTROL);
let result = is_control_newline_key(key, 'j');
assert!(result);
}
#[test]
fn test_is_control_newline_key_accepts_ctrl_m() {
let key = KeyEvent::new(KeyCode::Char('m'), event::KeyModifiers::CONTROL);
let result = is_control_newline_key(key, 'm');
assert!(result);
}
#[test]
fn test_is_control_newline_key_rejects_plain_j() {
let key = KeyEvent::new(KeyCode::Char('j'), event::KeyModifiers::NONE);
let result = is_control_newline_key(key, 'j');
assert!(!result);
}
#[test]
fn test_is_word_delete_backspace_accepts_alt_modifier() {
let key = KeyEvent::new(KeyCode::Backspace, event::KeyModifiers::ALT);
let result = is_word_delete_backspace(key);
assert!(result);
}
#[test]
fn test_is_word_delete_backspace_accepts_shift_modifier() {
let key = KeyEvent::new(KeyCode::Backspace, event::KeyModifiers::SHIFT);
let result = is_word_delete_backspace(key);
assert!(result);
}
#[test]
fn test_is_word_delete_backspace_rejects_plain_backspace() {
let key = KeyEvent::new(KeyCode::Backspace, event::KeyModifiers::NONE);
let result = is_word_delete_backspace(key);
assert!(!result);
}
#[test]
fn test_is_line_delete_backspace_accepts_super_modifier() {
let key = KeyEvent::new(KeyCode::Backspace, event::KeyModifiers::SUPER);
let result = is_line_delete_backspace(key);
assert!(result);
}
#[test]
fn test_is_line_delete_backspace_rejects_plain_backspace() {
let key = KeyEvent::new(KeyCode::Backspace, event::KeyModifiers::NONE);
let result = is_line_delete_backspace(key);
assert!(!result);
}
#[test]
fn test_is_control_key_accepts_ctrl() {
let key = KeyEvent::new(KeyCode::Char('u'), event::KeyModifiers::CONTROL);
assert!(is_control_key(key));
}
#[test]
fn test_is_control_key_rejects_plain() {
let key = KeyEvent::new(KeyCode::Char('u'), event::KeyModifiers::NONE);
assert!(!is_control_key(key));
}
#[test]
fn test_is_insertable_char_key_accepts_none() {
let key = KeyEvent::new(KeyCode::Char('a'), event::KeyModifiers::NONE);
assert!(is_insertable_char_key(key));
}
#[test]
fn test_is_insertable_char_key_accepts_shift() {
let key = KeyEvent::new(KeyCode::Char('A'), event::KeyModifiers::SHIFT);
assert!(is_insertable_char_key(key));
}
#[test]
fn test_is_insertable_char_key_rejects_control() {
let key = KeyEvent::new(KeyCode::Char('a'), event::KeyModifiers::CONTROL);
assert!(!is_insertable_char_key(key));
}
#[test]
fn test_is_cursor_on_first_line_at_start() {
let mut input = InputState::with_text("hello\nworld".to_string());
input.cursor = 0;
assert!(is_cursor_on_first_line(&input));
}
#[test]
fn test_is_cursor_on_first_line_after_newline() {
let mut input = InputState::with_text("hello\nworld".to_string());
input.cursor = "hello\nw".chars().count();
assert!(!is_cursor_on_first_line(&input));
}
#[test]
fn test_is_cursor_on_last_line_at_end() {
let mut input = InputState::with_text("hello\nworld".to_string());
input.cursor = "hello\nworld".chars().count();
assert!(is_cursor_on_last_line(&input));
}
#[test]
fn test_is_cursor_on_last_line_before_newline() {
let mut input = InputState::with_text("hello\nworld".to_string());
input.cursor = 3;
assert!(!is_cursor_on_last_line(&input));
}
#[test]
fn test_move_cursor_word_left_skips_whitespace_and_word() {
let mut input = InputState::with_text("hello world".to_string());
input.cursor = "hello world".chars().count();
move_cursor_word_left(&mut input);
assert_eq!(input.cursor, "hello ".chars().count());
}
#[test]
fn test_move_cursor_word_left_at_zero_is_noop() {
let mut input = InputState::with_text("hello".to_string());
input.cursor = 0;
move_cursor_word_left(&mut input);
assert_eq!(input.cursor, 0);
}
#[test]
fn test_move_cursor_word_right_skips_word_and_whitespace() {
let mut input = InputState::with_text("hello world".to_string());
input.cursor = 0;
move_cursor_word_right(&mut input);
assert_eq!(input.cursor, "hello ".chars().count());
}
#[test]
fn test_move_cursor_word_right_at_end_is_noop() {
let mut input = InputState::with_text("hello".to_string());
input.cursor = "hello".chars().count();
move_cursor_word_right(&mut input);
assert_eq!(input.cursor, "hello".chars().count());
}
#[test]
fn test_word_delete_range_returns_none_at_zero() {
let result = word_delete_range("hello world", 0);
assert!(result.is_none());
}
#[test]
fn test_word_delete_range_deletes_last_word_and_separator() {
let result = word_delete_range("hello brave world", "hello brave world".chars().count());
assert_eq!(result, Some((11, "hello brave world".chars().count())));
}
#[test]
fn test_delete_word_backward_removes_last_word_and_separator() {
let mut input = InputState::with_text("hello brave world".to_string());
input.cursor = "hello brave world".chars().count();
delete_word_backward(&mut input);
assert_eq!(input.text(), "hello brave");
}
#[test]
fn test_delete_word_backward_noop_at_zero() {
let mut input = InputState::with_text("hello".to_string());
input.cursor = 0;
delete_word_backward(&mut input);
assert_eq!(input.text(), "hello");
}
#[test]
fn test_normalize_pasted_text_replaces_carriage_returns() {
let pasted_text = "line 1\r\nline 2\rline 3\nline 4";
let normalized = normalize_pasted_text(pasted_text);
assert_eq!(normalized, "line 1\nline 2\nline 3\nline 4");
}
#[test]
fn test_normalize_pasted_text_preserves_plain_newlines() {
let pasted_text = "line 1\nline 2\nline 3";
let normalized = normalize_pasted_text(pasted_text);
assert_eq!(normalized, pasted_text);
}
}