// src/components/common/text_editor.rs use crate::config::binds::config::{EditorConfig, EditorKeybindingMode}; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use ratatui::style::{Color, Style, Modifier}; use tui_textarea::{Input, Key, TextArea, CursorMove}; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VimMode { Normal, Insert, Visual, Operator(char), } impl VimMode { pub fn cursor_style(&self) -> Style { let color = match self { Self::Normal => Color::Reset, Self::Insert => Color::LightBlue, Self::Visual => Color::LightYellow, Self::Operator(_) => Color::LightGreen, }; Style::default().fg(color).add_modifier(Modifier::REVERSED) } } impl fmt::Display for VimMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { Self::Normal => write!(f, "NORMAL"), Self::Insert => write!(f, "INSERT"), Self::Visual => write!(f, "VISUAL"), Self::Operator(c) => write!(f, "OPERATOR({})", c), } } } #[derive(Debug, Clone, PartialEq)] enum Transition { Nop, Mode(VimMode), Pending(Input), } #[derive(Debug, Clone)] pub struct VimState { pub mode: VimMode, pub pending: Input, } impl Default for VimState { fn default() -> Self { Self { mode: VimMode::Normal, pending: Input::default(), } } } impl VimState { pub fn new(mode: VimMode) -> Self { Self { mode, pending: Input::default(), } } fn with_pending(self, pending: Input) -> Self { Self { mode: self.mode, pending, } } fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition { if input.key == Key::Null { return Transition::Nop; } match self.mode { VimMode::Normal | VimMode::Visual | VimMode::Operator(_) => { match input { Input { key: Key::Char('h'), .. } => textarea.move_cursor(CursorMove::Back), Input { key: Key::Char('j'), .. } => textarea.move_cursor(CursorMove::Down), Input { key: Key::Char('k'), .. } => textarea.move_cursor(CursorMove::Up), Input { key: Key::Char('l'), .. } => textarea.move_cursor(CursorMove::Forward), Input { key: Key::Char('w'), .. } => textarea.move_cursor(CursorMove::WordForward), Input { key: Key::Char('e'), ctrl: false, .. } => { textarea.move_cursor(CursorMove::WordEnd); if matches!(self.mode, VimMode::Operator(_)) { textarea.move_cursor(CursorMove::Forward); } } Input { key: Key::Char('b'), ctrl: false, .. } => textarea.move_cursor(CursorMove::WordBack), Input { key: Key::Char('^'), .. } => textarea.move_cursor(CursorMove::Head), Input { key: Key::Char('$'), .. } => textarea.move_cursor(CursorMove::End), Input { key: Key::Char('0'), .. } => textarea.move_cursor(CursorMove::Head), Input { key: Key::Char('D'), .. } => { textarea.delete_line_by_end(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('C'), .. } => { textarea.delete_line_by_end(); textarea.cancel_selection(); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('p'), .. } => { textarea.paste(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('u'), ctrl: false, .. } => { textarea.undo(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('r'), ctrl: true, .. } => { textarea.redo(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('x'), .. } => { textarea.delete_next_char(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('i'), .. } => { textarea.cancel_selection(); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('a'), .. } => { textarea.cancel_selection(); textarea.move_cursor(CursorMove::Forward); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('A'), .. } => { textarea.cancel_selection(); textarea.move_cursor(CursorMove::End); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('o'), .. } => { textarea.move_cursor(CursorMove::End); textarea.insert_newline(); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('O'), .. } => { textarea.move_cursor(CursorMove::Head); textarea.insert_newline(); textarea.move_cursor(CursorMove::Up); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('I'), .. } => { textarea.cancel_selection(); textarea.move_cursor(CursorMove::Head); return Transition::Mode(VimMode::Insert); } Input { key: Key::Char('v'), ctrl: false, .. } if self.mode == VimMode::Normal => { textarea.start_selection(); return Transition::Mode(VimMode::Visual); } Input { key: Key::Char('V'), ctrl: false, .. } if self.mode == VimMode::Normal => { textarea.move_cursor(CursorMove::Head); textarea.start_selection(); textarea.move_cursor(CursorMove::End); return Transition::Mode(VimMode::Visual); } Input { key: Key::Esc, .. } | Input { key: Key::Char('v'), ctrl: false, .. } if self.mode == VimMode::Visual => { textarea.cancel_selection(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('g'), ctrl: false, .. } if matches!( self.pending, Input { key: Key::Char('g'), ctrl: false, .. } ) => { textarea.move_cursor(CursorMove::Top) } Input { key: Key::Char('G'), ctrl: false, .. } => textarea.move_cursor(CursorMove::Bottom), Input { key: Key::Char(c), ctrl: false, .. } if self.mode == VimMode::Operator(c) => { textarea.move_cursor(CursorMove::Head); textarea.start_selection(); let cursor = textarea.cursor(); textarea.move_cursor(CursorMove::Down); if cursor == textarea.cursor() { textarea.move_cursor(CursorMove::End); } } Input { key: Key::Char(op @ ('y' | 'd' | 'c')), ctrl: false, .. } if self.mode == VimMode::Normal => { textarea.start_selection(); return Transition::Mode(VimMode::Operator(op)); } Input { key: Key::Char('y'), ctrl: false, .. } if self.mode == VimMode::Visual => { textarea.move_cursor(CursorMove::Forward); textarea.copy(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('d'), ctrl: false, .. } if self.mode == VimMode::Visual => { textarea.move_cursor(CursorMove::Forward); textarea.cut(); return Transition::Mode(VimMode::Normal); } Input { key: Key::Char('c'), ctrl: false, .. } if self.mode == VimMode::Visual => { textarea.move_cursor(CursorMove::Forward); textarea.cut(); return Transition::Mode(VimMode::Insert); } // Arrow keys work in normal mode Input { key: Key::Up, .. } => textarea.move_cursor(CursorMove::Up), Input { key: Key::Down, .. } => textarea.move_cursor(CursorMove::Down), Input { key: Key::Left, .. } => textarea.move_cursor(CursorMove::Back), Input { key: Key::Right, .. } => textarea.move_cursor(CursorMove::Forward), input => return Transition::Pending(input), } // Handle the pending operator match self.mode { VimMode::Operator('y') => { textarea.copy(); Transition::Mode(VimMode::Normal) } VimMode::Operator('d') => { textarea.cut(); Transition::Mode(VimMode::Normal) } VimMode::Operator('c') => { textarea.cut(); Transition::Mode(VimMode::Insert) } _ => Transition::Nop, } } VimMode::Insert => match input { Input { key: Key::Esc, .. } | Input { key: Key::Char('c'), ctrl: true, .. } => { Transition::Mode(VimMode::Normal) } input => { textarea.input(input); Transition::Mode(VimMode::Insert) } }, } } } pub struct TextEditor; impl TextEditor { pub fn new_textarea(editor_config: &EditorConfig) -> TextArea<'static> { let mut textarea = TextArea::default(); if editor_config.show_line_numbers { textarea.set_line_number_style(Style::default().fg(Color::DarkGray)); } textarea.set_tab_length(editor_config.tab_width); textarea } pub fn handle_input( textarea: &mut TextArea<'static>, key_event: KeyEvent, keybinding_mode: &EditorKeybindingMode, vim_state: &mut VimState, ) -> bool { match keybinding_mode { EditorKeybindingMode::Vim => { Self::handle_vim_input(textarea, key_event, vim_state) } _ => { let tui_input: Input = key_event.into(); textarea.input(tui_input) } } } fn handle_vim_input( textarea: &mut TextArea<'static>, key_event: KeyEvent, vim_state: &mut VimState, ) -> bool { let input = Self::convert_key_event_to_input(key_event); *vim_state = match vim_state.transition(input, textarea) { Transition::Mode(mode) if vim_state.mode != mode => { // Update cursor style based on mode textarea.set_cursor_style(mode.cursor_style()); VimState::new(mode) } Transition::Nop | Transition::Mode(_) => vim_state.clone(), Transition::Pending(input) => vim_state.clone().with_pending(input), }; true // Always consider input as handled in vim mode } fn convert_key_event_to_input(key_event: KeyEvent) -> Input { let key = match key_event.code { KeyCode::Char(c) => Key::Char(c), KeyCode::Enter => Key::Enter, KeyCode::Left => Key::Left, KeyCode::Right => Key::Right, KeyCode::Up => Key::Up, KeyCode::Down => Key::Down, KeyCode::Backspace => Key::Backspace, KeyCode::Delete => Key::Delete, KeyCode::Home => Key::Home, KeyCode::End => Key::End, KeyCode::PageUp => Key::PageUp, KeyCode::PageDown => Key::PageDown, KeyCode::Tab => Key::Tab, KeyCode::Esc => Key::Esc, _ => Key::Null, }; Input { key, ctrl: key_event.modifiers.contains(KeyModifiers::CONTROL), alt: key_event.modifiers.contains(KeyModifiers::ALT), shift: key_event.modifiers.contains(KeyModifiers::SHIFT), } } pub fn get_vim_mode_status(vim_state: &VimState) -> String { vim_state.mode.to_string() } pub fn is_vim_insert_mode(vim_state: &VimState) -> bool { matches!(vim_state.mode, VimMode::Insert) } pub fn is_vim_normal_mode(vim_state: &VimState) -> bool { matches!(vim_state.mode, VimMode::Normal) } }