332 lines
13 KiB
Rust
332 lines
13 KiB
Rust
// 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)
|
|
}
|
|
}
|