excel_cli/app/
vim.rs

1use ratatui::style::{Color, Modifier, Style};
2use ratatui::widgets::Block;
3use std::fmt;
4use tui_textarea::{CursorMove, Input, Key, TextArea};
5
6use crate::app::word::move_cursor_to_word_end;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VimMode {
10    Normal,
11    Insert,
12    Visual,
13    Operator(char),
14}
15
16impl VimMode {
17    pub fn block<'a>(&self) -> Block<'a> {
18        let help = match self {
19            Self::Normal => "Esc=exit, i=insert, v=visual, y/d/c=operator",
20            Self::Insert => "Esc=normal mode",
21            Self::Visual => "Esc=normal, y=yank, d=delete, c=change",
22            Self::Operator(_) => "Move cursor to apply operator",
23        };
24        let title = format!(" {} MODE ({}) ", self, help);
25        Block::default().title(title)
26    }
27
28    pub fn cursor_style(&self) -> Style {
29        let color = match self {
30            Self::Normal => Color::Reset,
31            Self::Insert => Color::LightBlue,
32            Self::Visual => Color::LightYellow,
33            Self::Operator(_) => Color::LightGreen,
34        };
35        Style::default().fg(color).add_modifier(Modifier::REVERSED)
36    }
37}
38
39impl fmt::Display for VimMode {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
41        match self {
42            Self::Normal => write!(f, "NORMAL"),
43            Self::Insert => write!(f, "INSERT"),
44            Self::Visual => write!(f, "VISUAL"),
45            Self::Operator(c) => write!(f, "OPERATOR({})", c),
46        }
47    }
48}
49
50// How the Vim emulation state transitions
51pub enum Transition {
52    Nop,
53    Mode(VimMode),
54    Pending(Input),
55    Exit,
56}
57
58// State of Vim emulation
59#[derive(Clone)]
60pub struct VimState {
61    pub mode: VimMode,
62    pub pending: Input, // Pending input to handle a sequence with two keys like gg
63}
64
65impl VimState {
66    pub fn new(mode: VimMode) -> Self {
67        Self {
68            mode,
69            pending: Input::default(),
70        }
71    }
72
73    pub fn with_pending(self, pending: Input) -> Self {
74        Self {
75            mode: self.mode,
76            pending,
77        }
78    }
79
80    pub fn transition(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
81        if input.key == Key::Null {
82            return Transition::Nop;
83        }
84
85        match self.mode {
86            VimMode::Normal | VimMode::Visual | VimMode::Operator(_) => {
87                match input {
88                    // Navigation
89                    Input {
90                        key: Key::Char('h'),
91                        ..
92                    } => textarea.move_cursor(CursorMove::Back),
93                    Input {
94                        key: Key::Char('j'),
95                        ..
96                    } => textarea.move_cursor(CursorMove::Down),
97                    Input {
98                        key: Key::Char('k'),
99                        ..
100                    } => textarea.move_cursor(CursorMove::Up),
101                    Input {
102                        key: Key::Char('l'),
103                        ..
104                    } => textarea.move_cursor(CursorMove::Forward),
105                    Input {
106                        key: Key::Char('w'),
107                        ..
108                    } => textarea.move_cursor(CursorMove::WordForward),
109                    Input {
110                        key: Key::Char('e'),
111                        ctrl: false,
112                        ..
113                    } => {
114                        // Use custom WordEnd implementation
115                        let lines = textarea.lines();
116                        let (row, col) = textarea.cursor();
117                        let (new_row, new_col) = move_cursor_to_word_end(lines, row, col);
118
119                        // Set the cursor to the new position
120                        if row != new_row {
121                            // If need to move to a different row
122                            while textarea.cursor().0 < new_row {
123                                textarea.move_cursor(CursorMove::Down);
124                            }
125                            textarea.move_cursor(CursorMove::Head);
126                            while textarea.cursor().1 < new_col {
127                                textarea.move_cursor(CursorMove::Forward);
128                            }
129                        } else {
130                            // If staying on the same row
131                            if col < new_col {
132                                // Move forward
133                                while textarea.cursor().1 < new_col {
134                                    textarea.move_cursor(CursorMove::Forward);
135                                }
136                            } else {
137                                // Move backward
138                                while textarea.cursor().1 > new_col {
139                                    textarea.move_cursor(CursorMove::Back);
140                                }
141                            }
142                        }
143
144                        // For operator mode, include the character under the cursor
145                        if matches!(self.mode, VimMode::Operator(_)) {
146                            textarea.move_cursor(CursorMove::Forward);
147                        }
148                    }
149                    Input {
150                        key: Key::Char('b'),
151                        ctrl: false,
152                        ..
153                    } => textarea.move_cursor(CursorMove::WordBack),
154                    Input {
155                        key: Key::Char('^'),
156                        ..
157                    } => textarea.move_cursor(CursorMove::Head),
158                    Input {
159                        key: Key::Char('$'),
160                        ..
161                    } => textarea.move_cursor(CursorMove::End),
162
163                    // Editing operations
164                    Input {
165                        key: Key::Char('D'),
166                        ..
167                    } => {
168                        textarea.delete_line_by_end();
169                        return Transition::Mode(VimMode::Normal);
170                    }
171                    Input {
172                        key: Key::Char('C'),
173                        ..
174                    } => {
175                        textarea.delete_line_by_end();
176                        textarea.cancel_selection();
177                        return Transition::Mode(VimMode::Insert);
178                    }
179                    Input {
180                        key: Key::Char('p'),
181                        ..
182                    } => {
183                        textarea.paste();
184                        return Transition::Mode(VimMode::Normal);
185                    }
186                    Input {
187                        key: Key::Char('u'),
188                        ctrl: false,
189                        ..
190                    } => {
191                        textarea.undo();
192                        return Transition::Mode(VimMode::Normal);
193                    }
194                    Input {
195                        key: Key::Char('r'),
196                        ctrl: true,
197                        ..
198                    } => {
199                        textarea.redo();
200                        return Transition::Mode(VimMode::Normal);
201                    }
202                    Input {
203                        key: Key::Char('x'),
204                        ..
205                    } => {
206                        textarea.delete_next_char();
207                        return Transition::Mode(VimMode::Normal);
208                    }
209
210                    // Mode changes
211                    Input {
212                        key: Key::Char('i'),
213                        ..
214                    } => {
215                        textarea.cancel_selection();
216                        return Transition::Mode(VimMode::Insert);
217                    }
218                    Input {
219                        key: Key::Char('a'),
220                        ..
221                    } => {
222                        textarea.cancel_selection();
223                        textarea.move_cursor(CursorMove::Forward);
224                        return Transition::Mode(VimMode::Insert);
225                    }
226                    Input {
227                        key: Key::Char('A'),
228                        ..
229                    } => {
230                        textarea.cancel_selection();
231                        textarea.move_cursor(CursorMove::End);
232                        return Transition::Mode(VimMode::Insert);
233                    }
234                    Input {
235                        key: Key::Char('I'),
236                        ..
237                    } => {
238                        textarea.cancel_selection();
239                        textarea.move_cursor(CursorMove::Head);
240                        return Transition::Mode(VimMode::Insert);
241                    }
242                    Input {
243                        key: Key::Char('o'),
244                        ..
245                    } => {
246                        textarea.move_cursor(CursorMove::End);
247                        textarea.insert_newline();
248                        return Transition::Mode(VimMode::Insert);
249                    }
250                    Input {
251                        key: Key::Char('O'),
252                        ..
253                    } => {
254                        textarea.move_cursor(CursorMove::Head);
255                        textarea.insert_newline();
256                        textarea.move_cursor(CursorMove::Up);
257                        return Transition::Mode(VimMode::Insert);
258                    }
259
260                    // Exit
261                    Input { key: Key::Esc, .. } => {
262                        if self.mode == VimMode::Visual {
263                            textarea.cancel_selection();
264                            return Transition::Mode(VimMode::Normal);
265                        }
266                        return Transition::Exit;
267                    }
268
269                    // Scrolling
270                    Input {
271                        key: Key::Char('e'),
272                        ctrl: true,
273                        ..
274                    } => textarea.scroll((1, 0)),
275                    Input {
276                        key: Key::Char('y'),
277                        ctrl: true,
278                        ..
279                    } => textarea.scroll((-1, 0)),
280
281                    // Visual mode
282                    Input {
283                        key: Key::Char('v'),
284                        ctrl: false,
285                        ..
286                    } if self.mode == VimMode::Normal => {
287                        textarea.start_selection();
288                        return Transition::Mode(VimMode::Visual);
289                    }
290                    Input {
291                        key: Key::Char('V'),
292                        ctrl: false,
293                        ..
294                    } if self.mode == VimMode::Normal => {
295                        textarea.move_cursor(CursorMove::Head);
296                        textarea.start_selection();
297                        textarea.move_cursor(CursorMove::End);
298                        return Transition::Mode(VimMode::Visual);
299                    }
300
301                    // Operators
302                    Input {
303                        key: Key::Char('g'),
304                        ctrl: false,
305                        ..
306                    } if matches!(
307                        self.pending,
308                        Input {
309                            key: Key::Char('g'),
310                            ctrl: false,
311                            ..
312                        }
313                    ) =>
314                    {
315                        textarea.move_cursor(CursorMove::Top)
316                    }
317                    Input {
318                        key: Key::Char('G'),
319                        ctrl: false,
320                        ..
321                    } => textarea.move_cursor(CursorMove::Bottom),
322                    Input {
323                        key: Key::Char('y'),
324                        ctrl: false,
325                        ..
326                    } if self.mode == VimMode::Visual => {
327                        textarea.move_cursor(CursorMove::Forward); // Vim's text selection is inclusive
328                        textarea.copy();
329                        return Transition::Mode(VimMode::Normal);
330                    }
331                    Input {
332                        key: Key::Char('d'),
333                        ctrl: false,
334                        ..
335                    } if self.mode == VimMode::Visual => {
336                        textarea.move_cursor(CursorMove::Forward); // Vim's text selection is inclusive
337                        textarea.cut();
338                        return Transition::Mode(VimMode::Normal);
339                    }
340                    Input {
341                        key: Key::Char('c'),
342                        ctrl: false,
343                        ..
344                    } if self.mode == VimMode::Visual => {
345                        textarea.move_cursor(CursorMove::Forward); // Vim's text selection is inclusive
346                        textarea.cut();
347                        return Transition::Mode(VimMode::Insert);
348                    }
349                    Input {
350                        key: Key::Char(op @ ('y' | 'd' | 'c')),
351                        ctrl: false,
352                        ..
353                    } if self.mode == VimMode::Normal => {
354                        textarea.start_selection();
355                        return Transition::Mode(VimMode::Operator(op));
356                    }
357
358                    input => return Transition::Pending(input),
359                }
360
361                // Handle the pending operator
362                match self.mode {
363                    VimMode::Operator('y') => {
364                        textarea.copy();
365                        Transition::Mode(VimMode::Normal)
366                    }
367                    VimMode::Operator('d') => {
368                        textarea.cut();
369                        Transition::Mode(VimMode::Normal)
370                    }
371                    VimMode::Operator('c') => {
372                        textarea.cut();
373                        Transition::Mode(VimMode::Insert)
374                    }
375                    _ => Transition::Nop,
376                }
377            }
378            VimMode::Insert => match input {
379                Input { key: Key::Esc, .. }
380                | Input {
381                    key: Key::Char('c'),
382                    ctrl: true,
383                    ..
384                } => Transition::Mode(VimMode::Normal),
385                input => {
386                    textarea.input(input); // Use default key mappings in insert mode
387                    Transition::Mode(VimMode::Insert)
388                }
389            },
390        }
391    }
392}