use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::byte_index;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edit {
Consumed,
Submit,
Cancel,
}
pub fn apply(buffer: &mut String, cursor: &mut usize, key: &KeyEvent) -> Edit {
let newline = (key.code == KeyCode::Enter && key.modifiers.contains(KeyModifiers::ALT))
|| (key.code == KeyCode::Char('j') && key.modifiers.contains(KeyModifiers::CONTROL));
let ctrl =
key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT);
let alt =
key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::CONTROL);
if newline {
buffer.insert(byte_index(buffer, *cursor), '\n');
*cursor += 1;
return Edit::Consumed;
}
match key.code {
KeyCode::Esc => return Edit::Cancel,
KeyCode::Enter => return Edit::Submit,
KeyCode::Char('a') if ctrl => *cursor = line_start(buffer, *cursor),
KeyCode::Char('e') if ctrl => *cursor = line_end(buffer, *cursor),
KeyCode::Char('u') if ctrl => {
let start = line_start(buffer, *cursor);
remove_chars(buffer, start, *cursor);
*cursor = start;
}
KeyCode::Char('k') if ctrl => {
let end = line_end(buffer, *cursor).max((*cursor + 1).min(buffer.chars().count()));
remove_chars(buffer, *cursor, end);
}
KeyCode::Char('w') if ctrl => {
let start = prev_word(buffer, *cursor);
remove_chars(buffer, start, *cursor);
*cursor = start;
}
KeyCode::Backspace if alt => {
let start = prev_word(buffer, *cursor);
remove_chars(buffer, start, *cursor);
*cursor = start;
}
KeyCode::Char('d') if alt => {
let end = next_word(buffer, *cursor);
remove_chars(buffer, *cursor, end);
}
KeyCode::Char('b') if alt => *cursor = prev_word(buffer, *cursor),
KeyCode::Char('f') if alt => *cursor = next_word(buffer, *cursor),
KeyCode::Char('b') if ctrl => *cursor = cursor.saturating_sub(1),
KeyCode::Char('f') if ctrl => *cursor = (*cursor + 1).min(buffer.chars().count()),
KeyCode::Char('d') if ctrl => {
if *cursor < buffer.chars().count() {
buffer.remove(byte_index(buffer, *cursor));
}
}
KeyCode::Delete => {
if *cursor < buffer.chars().count() {
buffer.remove(byte_index(buffer, *cursor));
}
}
KeyCode::Char('h') if ctrl => {
if *cursor > 0 {
*cursor -= 1;
buffer.remove(byte_index(buffer, *cursor));
}
}
KeyCode::Char(c) if !ctrl && !alt => {
buffer.insert(byte_index(buffer, *cursor), c);
*cursor += 1;
}
KeyCode::Backspace => {
if *cursor > 0 {
*cursor -= 1;
buffer.remove(byte_index(buffer, *cursor));
}
}
KeyCode::Left => *cursor = cursor.saturating_sub(1),
KeyCode::Right => *cursor = (*cursor + 1).min(buffer.chars().count()),
KeyCode::Home => *cursor = 0,
KeyCode::End => *cursor = buffer.chars().count(),
_ => {}
}
Edit::Consumed
}
pub fn line_start(buffer: &str, cursor: usize) -> usize {
buffer
.chars()
.take(cursor)
.enumerate()
.filter(|&(_, c)| c == '\n')
.last()
.map_or(0, |(i, _)| i + 1)
}
pub fn line_end(buffer: &str, cursor: usize) -> usize {
buffer
.chars()
.enumerate()
.skip(cursor)
.find(|&(_, c)| c == '\n')
.map_or_else(|| buffer.chars().count(), |(i, _)| i)
}
fn prev_word(buffer: &str, cursor: usize) -> usize {
let chars: Vec<char> = buffer.chars().take(cursor).collect();
let ws_at = |i: usize| chars.get(i).is_some_and(|c| c.is_whitespace());
let mut i = chars.len();
while i > 0 && ws_at(i - 1) {
i -= 1;
}
while i > 0 && !ws_at(i - 1) {
i -= 1;
}
i
}
fn next_word(buffer: &str, cursor: usize) -> usize {
let chars: Vec<char> = buffer.chars().collect();
let ws_at = |i: usize| chars.get(i).is_some_and(|c| c.is_whitespace());
let mut i = cursor.min(chars.len());
while i < chars.len() && ws_at(i) {
i += 1;
}
while i < chars.len() && !ws_at(i) {
i += 1;
}
i
}
fn remove_chars(buffer: &mut String, start: usize, end: usize) {
if start >= end {
return;
}
let from = byte_index(buffer, start);
let to = byte_index(buffer, end);
buffer.replace_range(from..to, "");
}
#[cfg(test)]
mod tests {
use super::*;
fn press(buffer: &mut String, cursor: &mut usize, code: KeyCode) -> Edit {
apply(buffer, cursor, &KeyEvent::new(code, KeyModifiers::NONE))
}
#[test]
fn enter_submits_and_esc_cancels_without_touching_the_buffer() {
let (mut buffer, mut cursor) = ("hi".to_owned(), 2);
assert_eq!(
press(&mut buffer, &mut cursor, KeyCode::Enter),
Edit::Submit
);
assert_eq!(press(&mut buffer, &mut cursor, KeyCode::Esc), Edit::Cancel);
assert_eq!(buffer, "hi");
}
#[test]
fn alt_enter_inserts_a_newline_at_the_cursor() {
let (mut buffer, mut cursor) = ("ab".to_owned(), 1);
let edit = apply(
&mut buffer,
&mut cursor,
&KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT),
);
assert_eq!(edit, Edit::Consumed);
assert_eq!(buffer, "a\nb");
assert_eq!(cursor, 2);
}
#[test]
fn a_multibyte_buffer_edits_by_char_not_byte() {
let (mut buffer, mut cursor) = ("héllo".to_owned(), 2);
press(&mut buffer, &mut cursor, KeyCode::Backspace);
assert_eq!(buffer, "hllo");
assert_eq!(cursor, 1);
}
}