Skip to main content

vim_line/
vim.rs

1//! Vim-style line editor implementation.
2//!
3//! Owns the `VimLineEditor` struct, its `Mode` / `Operator` state, cursor-
4//! motion and edit helpers, the five mode-specific key handlers, and the
5//! `LineEditor` trait implementation.
6
7use crate::{Action, EditResult, Key, KeyCode, LineEditor, TextEdit};
8use std::ops::Range;
9
10mod edits;
11mod motions;
12
13/// Vim editing mode.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub(crate) enum Mode {
16    #[default]
17    Normal,
18    Insert,
19    OperatorPending(Operator),
20    Visual,
21    /// Waiting for a character to replace the one under cursor (r command)
22    ReplaceChar,
23}
24
25/// Operators that wait for a motion.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum Operator {
28    Delete,
29    Change,
30    Yank,
31}
32
33/// A vim-style line editor.
34///
35/// Implements modal editing with Normal, Insert, Visual, and OperatorPending modes.
36/// Designed for single "one-shot" inputs that may span multiple lines.
37#[derive(Debug, Clone)]
38pub struct VimLineEditor {
39    pub(in crate::vim) cursor: usize,
40    pub(in crate::vim) mode: Mode,
41    /// Anchor point for visual selection (cursor is the other end).
42    pub(in crate::vim) visual_anchor: Option<usize>,
43    /// Last yanked text (for paste).
44    pub(in crate::vim) yank_buffer: String,
45}
46
47impl Default for VimLineEditor {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl VimLineEditor {
54    /// Create a new editor in Normal mode.
55    pub fn new() -> Self {
56        Self {
57            cursor: 0,
58            mode: Mode::Normal,
59            visual_anchor: None,
60            yank_buffer: String::new(),
61        }
62    }
63
64    /// Current mode — test-only accessor.
65    #[cfg(test)]
66    fn mode(&self) -> Mode {
67        self.mode
68    }
69
70    /// Clamp cursor to valid range for the given text.
71    fn clamp_cursor(&mut self, text: &str) {
72        self.cursor = self.cursor.min(text.len());
73    }
74
75    /// Move cursor left by one character.
76    fn move_left(&mut self, text: &str) {
77        self.cursor = motions::move_left(self.cursor, text);
78    }
79
80    /// Move cursor right by one character.
81    fn move_right(&mut self, text: &str) {
82        self.cursor = motions::move_right(self.cursor, text);
83    }
84
85    /// Move cursor to start of line (0).
86    fn move_line_start(&mut self, text: &str) {
87        self.cursor = motions::move_line_start(self.cursor, text);
88    }
89
90    /// Move cursor to first non-whitespace of line (^).
91    fn move_first_non_blank(&mut self, text: &str) {
92        self.cursor = motions::move_first_non_blank(self.cursor, text);
93    }
94
95    /// Move cursor to end of line (Normal mode — stays on last char).
96    fn move_line_end(&mut self, text: &str) {
97        self.cursor = motions::move_line_end(self.cursor, text);
98    }
99
100    /// Move cursor past end of line (Insert mode).
101    fn move_line_end_insert(&mut self, text: &str) {
102        self.cursor = motions::move_line_end_insert(self.cursor, text);
103    }
104
105    /// Move cursor forward by word (w).
106    fn move_word_forward(&mut self, text: &str) {
107        self.cursor = motions::move_word_forward(self.cursor, text);
108    }
109
110    /// Move cursor backward by word (b).
111    fn move_word_backward(&mut self, text: &str) {
112        self.cursor = motions::move_word_backward(self.cursor, text);
113    }
114
115    /// Move cursor to end of word (e).
116    fn move_word_end(&mut self, text: &str) {
117        self.cursor = motions::move_word_end(self.cursor, text);
118    }
119
120    /// Move cursor up one line (k).
121    fn move_up(&mut self, text: &str) {
122        self.cursor = motions::move_up(self.cursor, text);
123    }
124
125    /// Move cursor down one line (j).
126    fn move_down(&mut self, text: &str) {
127        self.cursor = motions::move_down(self.cursor, text);
128    }
129
130    /// Move cursor to matching bracket (%).
131    /// Supports (), [], {}, and <>.
132    fn move_to_matching_bracket(&mut self, text: &str) {
133        self.cursor = motions::move_to_matching_bracket(self.cursor, text);
134    }
135
136    /// Dispatch a shared motion key (h/l/j/k/0/$/^/w/b/e/%/Left/Right/Home/End)
137    /// to the appropriate cursor helper. Returns `true` when the key was
138    /// recognized as a motion, `false` otherwise.
139    ///
140    /// Up/Down arrow keys are intentionally NOT handled here — Normal mode
141    /// treats them as history navigation, not motion.
142    ///
143    /// Called by Normal and Visual handlers so each can delegate motion
144    /// interpretation to one place and then wrap the result in its own way.
145    /// OperatorPending has extra `c`/`cw`/`ce` quirks and handles motion
146    /// itself.
147    fn dispatch_motion(&mut self, code: KeyCode, text: &str) -> bool {
148        match code {
149            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
150            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
151            KeyCode::Char('j') => self.move_down(text),
152            KeyCode::Char('k') => self.move_up(text),
153            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
154            KeyCode::Char('^') => self.move_first_non_blank(text),
155            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
156            KeyCode::Char('w') => self.move_word_forward(text),
157            KeyCode::Char('b') => self.move_word_backward(text),
158            KeyCode::Char('e') => self.move_word_end(text),
159            KeyCode::Char('%') => self.move_to_matching_bracket(text),
160            _ => return false,
161        }
162        true
163    }
164
165    /// Handle key in Normal mode.
166    fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
167        // Shared motions (h/l/j/k/0/$/^/w/b/e/%/Left/Right/Home/End).
168        // Up/Down are NOT motions in Normal — they're history navigation below.
169        if self.dispatch_motion(key.code, text) {
170            return EditResult::cursor_only();
171        }
172
173        match key.code {
174            // Mode switching
175            KeyCode::Char('i') => {
176                self.mode = Mode::Insert;
177                EditResult::none()
178            }
179            KeyCode::Char('a') => {
180                self.mode = Mode::Insert;
181                self.move_right(text);
182                EditResult::none()
183            }
184            KeyCode::Char('A') => {
185                self.mode = Mode::Insert;
186                self.move_line_end_insert(text);
187                EditResult::none()
188            }
189            KeyCode::Char('I') => {
190                self.mode = Mode::Insert;
191                self.move_first_non_blank(text);
192                EditResult::none()
193            }
194            KeyCode::Char('o') => {
195                self.mode = Mode::Insert;
196                self.move_line_end(text);
197                let pos = self.cursor;
198                self.cursor = pos + 1;
199                EditResult::edit(TextEdit::Insert {
200                    at: pos,
201                    text: "\n".to_string(),
202                })
203            }
204            KeyCode::Char('O') => {
205                self.mode = Mode::Insert;
206                self.move_line_start(text);
207                let pos = self.cursor;
208                EditResult::edit(TextEdit::Insert {
209                    at: pos,
210                    text: "\n".to_string(),
211                })
212            }
213
214            // Visual mode
215            KeyCode::Char('v') => {
216                self.mode = Mode::Visual;
217                self.visual_anchor = Some(self.cursor);
218                EditResult::none()
219            }
220
221            // Cancel (Ctrl+C)
222            KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
223
224            // Operators (enter pending mode)
225            KeyCode::Char('d') => {
226                self.mode = Mode::OperatorPending(Operator::Delete);
227                EditResult::none()
228            }
229            KeyCode::Char('c') => {
230                self.mode = Mode::OperatorPending(Operator::Change);
231                EditResult::none()
232            }
233            KeyCode::Char('y') => {
234                self.mode = Mode::OperatorPending(Operator::Yank);
235                EditResult::none()
236            }
237
238            // Direct deletions
239            KeyCode::Char('x') => self.delete_char(text),
240            KeyCode::Char('D') => self.delete_to_end(text),
241            KeyCode::Char('C') => {
242                self.mode = Mode::Insert;
243                self.delete_to_end(text)
244            }
245
246            // Replace character (r)
247            KeyCode::Char('r') => {
248                self.mode = Mode::ReplaceChar;
249                EditResult::none()
250            }
251
252            // Paste
253            KeyCode::Char('p') => self.paste_after(text),
254            KeyCode::Char('P') => self.paste_before(text),
255
256            // History (arrows only)
257            KeyCode::Up => EditResult::action(Action::HistoryPrev),
258            KeyCode::Down => EditResult::action(Action::HistoryNext),
259
260            // Submit
261            KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
262
263            // Newline (Shift+Enter)
264            KeyCode::Enter if key.shift => {
265                self.mode = Mode::Insert;
266                let pos = self.cursor;
267                self.cursor = pos + 1;
268                EditResult::edit(TextEdit::Insert {
269                    at: pos,
270                    text: "\n".to_string(),
271                })
272            }
273
274            // Escape in Normal mode is a no-op (safe to spam like in vim)
275            // Use Ctrl+C to cancel/quit
276            KeyCode::Escape => EditResult::none(),
277
278            _ => EditResult::none(),
279        }
280    }
281
282    /// Handle key in Insert mode.
283    fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
284        match key.code {
285            KeyCode::Escape => {
286                self.mode = Mode::Normal;
287                // Move cursor left like vim does when exiting insert
288                if self.cursor > 0 {
289                    self.move_left(text);
290                }
291                EditResult::none()
292            }
293
294            // Ctrl+C exits insert mode
295            KeyCode::Char('c') if key.ctrl => {
296                self.mode = Mode::Normal;
297                EditResult::none()
298            }
299
300            KeyCode::Char(c) if !key.ctrl && !key.alt => {
301                let pos = self.cursor;
302                self.cursor = pos + c.len_utf8();
303                EditResult::edit(TextEdit::Insert {
304                    at: pos,
305                    text: c.to_string(),
306                })
307            }
308
309            KeyCode::Backspace => {
310                if self.cursor == 0 {
311                    return EditResult::none();
312                }
313                let mut start = self.cursor - 1;
314                while start > 0 && !text.is_char_boundary(start) {
315                    start -= 1;
316                }
317                let end = self.cursor; // Save original cursor before updating
318                self.cursor = start;
319                EditResult::edit(TextEdit::Delete { start, end })
320            }
321
322            KeyCode::Delete => self.delete_char(text),
323
324            KeyCode::Left => {
325                self.move_left(text);
326                EditResult::cursor_only()
327            }
328            KeyCode::Right => {
329                self.move_right(text);
330                EditResult::cursor_only()
331            }
332            KeyCode::Up => {
333                self.move_up(text);
334                EditResult::cursor_only()
335            }
336            KeyCode::Down => {
337                self.move_down(text);
338                EditResult::cursor_only()
339            }
340            KeyCode::Home => {
341                self.move_line_start(text);
342                EditResult::cursor_only()
343            }
344            KeyCode::End => {
345                // In Insert mode, cursor can go past the last character
346                self.move_line_end_insert(text);
347                EditResult::cursor_only()
348            }
349
350            // Enter inserts newline in insert mode
351            KeyCode::Enter => {
352                let pos = self.cursor;
353                self.cursor = pos + 1;
354                EditResult::edit(TextEdit::Insert {
355                    at: pos,
356                    text: "\n".to_string(),
357                })
358            }
359
360            _ => EditResult::none(),
361        }
362    }
363
364    /// Handle key in OperatorPending mode.
365    fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
366        // First, handle escape to cancel
367        if key.code == KeyCode::Escape {
368            self.mode = Mode::Normal;
369            return EditResult::none();
370        }
371
372        // Handle doubled operator (dd, cc, yy) - operates on whole line
373        let is_line_op = matches!(
374            (op, key.code),
375            (Operator::Delete, KeyCode::Char('d'))
376                | (Operator::Change, KeyCode::Char('c'))
377                | (Operator::Yank, KeyCode::Char('y'))
378        );
379
380        if is_line_op {
381            self.mode = Mode::Normal;
382            return self.apply_operator_line(op, text);
383        }
384
385        // Handle motion
386        let start = self.cursor;
387        match key.code {
388            KeyCode::Char('w') => {
389                // Special case: cw behaves like ce (change to end of word, not including space)
390                // This is a vim quirk for historical compatibility
391                if op == Operator::Change {
392                    self.move_word_end(text);
393                    // Include the character at cursor
394                    if self.cursor < text.len() {
395                        self.cursor += 1;
396                    }
397                } else {
398                    self.move_word_forward(text);
399                }
400            }
401            KeyCode::Char('b') => self.move_word_backward(text),
402            KeyCode::Char('e') => {
403                self.move_word_end(text);
404                // Include the character at cursor for delete/change
405                if self.cursor < text.len() {
406                    self.cursor += 1;
407                }
408            }
409            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
410            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
411            KeyCode::Char('^') => self.move_first_non_blank(text),
412            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
413            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
414            KeyCode::Char('j') => self.move_down(text),
415            KeyCode::Char('k') => self.move_up(text),
416            _ => {
417                // Unknown motion, cancel
418                self.mode = Mode::Normal;
419                return EditResult::none();
420            }
421        }
422
423        let end = self.cursor;
424        self.mode = Mode::Normal;
425
426        if start == end {
427            return EditResult::none();
428        }
429
430        let (range_start, range_end) = if start < end {
431            (start, end)
432        } else {
433            (end, start)
434        };
435
436        self.apply_operator(op, range_start, range_end, text)
437    }
438
439    /// Handle key in Visual mode.
440    fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
441        match key.code {
442            KeyCode::Escape => {
443                self.mode = Mode::Normal;
444                self.visual_anchor = None;
445                EditResult::none()
446            }
447
448            // Motions extend selection (note: `^` and `%` are intentionally
449            // not wired here to preserve original behavior — tracked for a
450            // separate behavior-change PR).
451            KeyCode::Char('h') | KeyCode::Left => {
452                self.move_left(text);
453                EditResult::cursor_only()
454            }
455            KeyCode::Char('l') | KeyCode::Right => {
456                self.move_right(text);
457                EditResult::cursor_only()
458            }
459            KeyCode::Char('j') => {
460                self.move_down(text);
461                EditResult::cursor_only()
462            }
463            KeyCode::Char('k') => {
464                self.move_up(text);
465                EditResult::cursor_only()
466            }
467            KeyCode::Char('w') => {
468                self.move_word_forward(text);
469                EditResult::cursor_only()
470            }
471            KeyCode::Char('b') => {
472                self.move_word_backward(text);
473                EditResult::cursor_only()
474            }
475            KeyCode::Char('e') => {
476                self.move_word_end(text);
477                EditResult::cursor_only()
478            }
479            KeyCode::Char('0') | KeyCode::Home => {
480                self.move_line_start(text);
481                EditResult::cursor_only()
482            }
483            KeyCode::Char('$') | KeyCode::End => {
484                self.move_line_end(text);
485                EditResult::cursor_only()
486            }
487
488            // Operators on selection
489            KeyCode::Char('d') | KeyCode::Char('x') => {
490                let (start, end) = self.selection_range();
491                self.mode = Mode::Normal;
492                self.visual_anchor = None;
493                self.apply_operator(Operator::Delete, start, end, text)
494            }
495            KeyCode::Char('c') => {
496                let (start, end) = self.selection_range();
497                self.mode = Mode::Normal;
498                self.visual_anchor = None;
499                self.apply_operator(Operator::Change, start, end, text)
500            }
501            KeyCode::Char('y') => {
502                let (start, end) = self.selection_range();
503                self.mode = Mode::Normal;
504                self.visual_anchor = None;
505                self.apply_operator(Operator::Yank, start, end, text)
506            }
507
508            _ => EditResult::none(),
509        }
510    }
511
512    /// Handle key in ReplaceChar mode (waiting for character after 'r').
513    fn handle_replace_char(&mut self, key: Key, text: &str) -> EditResult {
514        self.mode = Mode::Normal;
515
516        match key.code {
517            KeyCode::Escape => EditResult::none(),
518            KeyCode::Char(c) if !key.ctrl && !key.alt => {
519                // Replace character at cursor
520                if self.cursor >= text.len() {
521                    return EditResult::none();
522                }
523
524                // Find the end of the current character
525                let mut end = self.cursor + 1;
526                while end < text.len() && !text.is_char_boundary(end) {
527                    end += 1;
528                }
529
530                // Delete current char and insert new one
531                // Note: edits are applied in reverse order, so Insert comes first in vec
532                EditResult {
533                    edits: vec![
534                        TextEdit::Insert {
535                            at: self.cursor,
536                            text: c.to_string(),
537                        },
538                        TextEdit::Delete {
539                            start: self.cursor,
540                            end,
541                        },
542                    ],
543                    ..Default::default()
544                }
545            }
546            _ => EditResult::none(),
547        }
548    }
549
550    /// Get the selection range (ordered).
551    fn selection_range(&self) -> (usize, usize) {
552        let anchor = self.visual_anchor.unwrap_or(self.cursor);
553        if self.cursor < anchor {
554            (self.cursor, anchor)
555        } else {
556            (anchor, self.cursor + 1) // Include cursor position
557        }
558    }
559}
560
561impl LineEditor for VimLineEditor {
562    fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
563        self.clamp_cursor(text);
564
565        let result = match self.mode {
566            Mode::Normal => self.handle_normal(key, text),
567            Mode::Insert => self.handle_insert(key, text),
568            Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
569            Mode::Visual => self.handle_visual(key, text),
570            Mode::ReplaceChar => self.handle_replace_char(key, text),
571        };
572
573        // Store yanked text
574        if let Some(ref yanked) = result.yanked {
575            self.yank_buffer = yanked.clone();
576        }
577
578        result
579    }
580
581    fn cursor(&self) -> usize {
582        self.cursor
583    }
584
585    fn status(&self) -> &str {
586        match self.mode {
587            Mode::Normal => "NORMAL",
588            Mode::Insert => "INSERT",
589            Mode::OperatorPending(Operator::Delete) => "d...",
590            Mode::OperatorPending(Operator::Change) => "c...",
591            Mode::OperatorPending(Operator::Yank) => "y...",
592            Mode::Visual => "VISUAL",
593            Mode::ReplaceChar => "r...",
594        }
595    }
596
597    fn selection(&self) -> Option<Range<usize>> {
598        if self.mode == Mode::Visual {
599            let (start, end) = self.selection_range();
600            Some(start..end)
601        } else {
602            None
603        }
604    }
605
606    fn reset(&mut self) {
607        self.cursor = 0;
608        self.mode = Mode::Normal;
609        self.visual_anchor = None;
610        // Keep yank buffer across resets
611    }
612
613    fn set_cursor(&mut self, pos: usize, text: &str) {
614        // Clamp to text length and ensure we're at a char boundary
615        let pos = pos.min(text.len());
616        self.cursor = if text.is_char_boundary(pos) {
617            pos
618        } else {
619            // Walk backwards to find a valid boundary
620            let mut p = pos;
621            while p > 0 && !text.is_char_boundary(p) {
622                p -= 1;
623            }
624            p
625        };
626    }
627}
628
629#[cfg(test)]
630mod tests;