aven 0.1.13

Local-first task manager CLI and sync server
Documentation
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::tui::text::{
    char_boundary_at_or_before, next_char_boundary, next_char_is_whitespace,
    normalize_pasted_newlines, previous_char_boundary, previous_word_start,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LineEdit {
    pub(crate) text: String,
    pub(crate) cursor: usize,
}

impl LineEdit {
    pub(crate) fn new(text: String) -> Self {
        let cursor = text.len();
        Self { text, cursor }
    }

    pub(crate) fn blank() -> Self {
        Self::new(String::new())
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.text
    }

    pub(crate) fn insert_paste(&mut self, text: &str) {
        let text = normalize_pasted_newlines(text).replace('\n', " ");
        let cursor = char_boundary_at_or_before(&self.text, self.cursor);
        self.text.insert_str(cursor, &text);
        self.cursor = cursor + text.len();
    }

    pub(crate) fn handle_key(&mut self, key: KeyEvent) {
        let cursor = char_boundary_at_or_before(&self.text, self.cursor);
        match key.code {
            KeyCode::Left => self.cursor = previous_char_boundary(&self.text, cursor),
            KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.cursor = previous_char_boundary(&self.text, cursor);
            }
            KeyCode::Right => self.cursor = next_char_boundary(&self.text, cursor),
            KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.cursor = next_char_boundary(&self.text, cursor);
            }
            KeyCode::Home => self.cursor = 0,
            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.cursor = 0;
            }
            KeyCode::End => self.cursor = self.text.len(),
            KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.cursor = self.text.len();
            }
            KeyCode::Backspace if cursor > 0 => {
                let previous = previous_char_boundary(&self.text, cursor);
                self.text.drain(previous..cursor);
                self.cursor = previous;
            }
            KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) && cursor > 0 => {
                let previous = previous_char_boundary(&self.text, cursor);
                self.text.drain(previous..cursor);
                self.cursor = previous;
            }
            KeyCode::Delete if cursor < self.text.len() => {
                let next = next_char_boundary(&self.text, cursor);
                self.text.drain(cursor..next);
                self.cursor = cursor;
            }
            KeyCode::Char('d')
                if key.modifiers.contains(KeyModifiers::CONTROL) && cursor < self.text.len() =>
            {
                let next = next_char_boundary(&self.text, cursor);
                self.text.drain(cursor..next);
                self.cursor = cursor;
            }
            KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.text.truncate(cursor);
                self.cursor = cursor;
            }
            KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.text.drain(..cursor);
                self.cursor = 0;
            }
            KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                let previous = previous_word_start(&self.text, cursor);
                self.text.drain(previous..cursor);
                if previous > 0 && next_char_is_whitespace(&self.text, previous) {
                    let before = previous_char_boundary(&self.text, previous);
                    if self.text[before..previous].chars().all(char::is_whitespace) {
                        self.text.drain(before..previous);
                        self.cursor = before;
                    } else {
                        self.cursor = previous;
                    }
                } else {
                    self.cursor = previous;
                }
            }
            KeyCode::Char(ch)
                if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
            {
                self.text.insert(cursor, ch);
                self.cursor = cursor + ch.len_utf8();
            }
            _ => self.cursor = cursor,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    fn ctrl(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::CONTROL)
    }

    fn line_edit(input: &str, cursor: usize) -> LineEdit {
        LineEdit {
            text: input.to_string(),
            cursor,
        }
    }

    #[test]
    fn text_input_edits_at_cursor() {
        let mut state = line_edit("ab", 1);
        state.handle_key(key(KeyCode::Char('x')));
        assert_eq!(state.text, "axb");
        assert_eq!(state.cursor, 2);
        state.handle_key(key(KeyCode::Backspace));
        assert_eq!(state.text, "ab");
        assert_eq!(state.cursor, 1);
    }

    #[test]
    fn text_input_supports_emacs_navigation() {
        let mut state = line_edit("abc", 1);
        state.handle_key(ctrl(KeyCode::Char('a')));
        assert_eq!(state.cursor, 0);
        state.handle_key(ctrl(KeyCode::Char('e')));
        assert_eq!(state.cursor, 3);
        state.handle_key(ctrl(KeyCode::Char('b')));
        assert_eq!(state.cursor, 2);
        state.handle_key(ctrl(KeyCode::Char('f')));
        assert_eq!(state.cursor, 3);
    }

    #[test]
    fn text_input_supports_emacs_deletion() {
        let mut state = line_edit("one two three", 7);
        state.handle_key(ctrl(KeyCode::Char('w')));
        assert_eq!(state.text, "one three");
        assert_eq!(state.cursor, 3);
        state.handle_key(ctrl(KeyCode::Char('k')));
        assert_eq!(state.text, "one");
        assert_eq!(state.cursor, 3);
        state.handle_key(ctrl(KeyCode::Char('u')));
        assert_eq!(state.text, "");
        assert_eq!(state.cursor, 0);
    }

    #[test]
    fn text_input_ignores_control_chars_that_are_not_editing_keys() {
        let mut state = line_edit("ab", 1);
        state.handle_key(ctrl(KeyCode::Char('x')));
        assert_eq!(state.text, "ab");
        assert_eq!(state.cursor, 1);
    }
}