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 command_line;
11mod edits;
12mod motions;
13
14/// Vim editing mode.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub(crate) enum Mode {
17    #[default]
18    Normal,
19    Insert,
20    OperatorPending(Operator),
21    Visual,
22    /// Waiting for a character to replace the one under cursor (r command)
23    ReplaceChar,
24    /// Ex-style command line entered with `:` from Normal (opt-in via the
25    /// editor's command-mode flag). Operates on a separate command buffer
26    /// owned by the editor; the host's main text is untouched.
27    CommandLine,
28}
29
30/// Operators that wait for a motion.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub(crate) enum Operator {
33    Delete,
34    Change,
35    Yank,
36}
37
38/// A vim-style line editor.
39///
40/// Implements modal editing with Normal, Insert, Visual, and OperatorPending modes.
41/// Designed for single "one-shot" inputs that may span multiple lines.
42#[derive(Debug, Clone)]
43pub struct VimLineEditor {
44    pub(in crate::vim) cursor: usize,
45    pub(in crate::vim) mode: Mode,
46    /// Anchor point for visual selection (cursor is the other end).
47    pub(in crate::vim) visual_anchor: Option<usize>,
48    /// Last yanked text (for paste).
49    pub(in crate::vim) yank_buffer: String,
50    /// Whether Normal-mode `:` enters `Mode::CommandLine`. Off by default so
51    /// non-REPL hosts keep classic vim behavior (where `:` is a no-op until
52    /// the host opts in to Ex-style command entry).
53    pub(in crate::vim) command_mode_enabled: bool,
54    /// Buffer for the Ex-style command line (only populated in
55    /// `Mode::CommandLine`). Decoupled from the host's main text.
56    pub(in crate::vim) command_buf: String,
57    /// Cursor within `command_buf` (byte offset).
58    pub(in crate::vim) command_cursor: usize,
59}
60
61impl Default for VimLineEditor {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl VimLineEditor {
68    /// Create a new editor in Normal mode.
69    pub fn new() -> Self {
70        Self {
71            cursor: 0,
72            mode: Mode::Normal,
73            visual_anchor: None,
74            yank_buffer: String::new(),
75            command_mode_enabled: false,
76            command_buf: String::new(),
77            command_cursor: 0,
78        }
79    }
80
81    /// Enable (or disable) the opt-in `:` -> `Mode::CommandLine` behavior.
82    /// Builder-style; call this on a freshly constructed editor.
83    pub fn with_command_mode(mut self, enabled: bool) -> Self {
84        self.command_mode_enabled = enabled;
85        self
86    }
87
88    /// Current command-line buffer, if the editor is in `Mode::CommandLine`.
89    /// Returns `None` in every other mode so the host can render it
90    /// unconditionally.
91    pub fn command_line_buffer(&self) -> Option<&str> {
92        if self.mode == Mode::CommandLine {
93            Some(&self.command_buf)
94        } else {
95            None
96        }
97    }
98
99    /// Cursor offset within the command-line buffer (only meaningful while
100    /// in `Mode::CommandLine`).
101    pub fn command_line_cursor(&self) -> usize {
102        self.command_cursor
103    }
104
105    /// Replace the command-line buffer and cursor. No-op outside
106    /// `Mode::CommandLine`. Used by hosts that compute completions
107    /// (`Tab`-cycling, etc.) and want to install the result.
108    pub fn set_command_line(&mut self, buf: String, cursor: usize) {
109        if self.mode != Mode::CommandLine {
110            return;
111        }
112        let cursor = cursor.min(buf.len());
113        // Snap to a char boundary if the caller handed us a mid-codepoint
114        // offset.
115        let cursor = if buf.is_char_boundary(cursor) {
116            cursor
117        } else {
118            let mut p = cursor;
119            while p > 0 && !buf.is_char_boundary(p) {
120                p -= 1;
121            }
122            p
123        };
124        self.command_buf = buf;
125        self.command_cursor = cursor;
126    }
127
128    /// Current mode — test-only accessor.
129    #[cfg(test)]
130    fn mode(&self) -> Mode {
131        self.mode
132    }
133
134    /// Clamp cursor to valid range for the given text.
135    fn clamp_cursor(&mut self, text: &str) {
136        self.cursor = self.cursor.min(text.len());
137    }
138
139    /// Enforce vim's Normal-mode invariant: the cursor is always *on* a
140    /// character, never past the final one. Forward motions (`w`/`e`/`W`/`E`
141    /// and `l`/`Right`) naturally run to `text.len()` when they exhaust the
142    /// buffer; this snaps them back onto the last character.
143    ///
144    /// Scoped to pure cursor motion — callers must NOT invoke this after an
145    /// edit-producing command (`p`/`P`/`x`/operators), because those compute
146    /// the cursor against the *post-edit* text while we only hold the
147    /// pre-edit buffer here. Mode transitions into Insert (`a`/`A`/`o`/`C`)
148    /// are likewise excluded since Insert legitimately uses `cursor == len`
149    /// to append.
150    ///
151    /// Char-boundary safe: walks back to the start of the last character
152    /// for multi-byte text. No-op on an empty buffer.
153    fn clamp_to_last_char(&mut self, text: &str) {
154        if text.is_empty() {
155            self.cursor = 0;
156        } else if self.cursor >= text.len() {
157            let mut p = text.len() - 1;
158            while p > 0 && !text.is_char_boundary(p) {
159                p -= 1;
160            }
161            self.cursor = p;
162        }
163    }
164
165    /// Move cursor left by one character.
166    fn move_left(&mut self, text: &str) {
167        self.cursor = motions::move_left(self.cursor, text);
168    }
169
170    /// Move cursor right by one character.
171    fn move_right(&mut self, text: &str) {
172        self.cursor = motions::move_right(self.cursor, text);
173    }
174
175    /// Move cursor to start of line (0).
176    fn move_line_start(&mut self, text: &str) {
177        self.cursor = motions::move_line_start(self.cursor, text);
178    }
179
180    /// Move cursor to first non-whitespace of line (^).
181    fn move_first_non_blank(&mut self, text: &str) {
182        self.cursor = motions::move_first_non_blank(self.cursor, text);
183    }
184
185    /// Move cursor to end of line (Normal mode — stays on last char).
186    fn move_line_end(&mut self, text: &str) {
187        self.cursor = motions::move_line_end(self.cursor, text);
188    }
189
190    /// Move cursor past end of line (Insert mode).
191    fn move_line_end_insert(&mut self, text: &str) {
192        self.cursor = motions::move_line_end_insert(self.cursor, text);
193    }
194
195    /// Move cursor forward by word (w).
196    fn move_word_forward(&mut self, text: &str) {
197        self.cursor = motions::move_word_forward(self.cursor, text);
198    }
199
200    /// Move cursor backward by word (b).
201    fn move_word_backward(&mut self, text: &str) {
202        self.cursor = motions::move_word_backward(self.cursor, text);
203    }
204
205    /// Move cursor to end of word (e).
206    fn move_word_end(&mut self, text: &str) {
207        self.cursor = motions::move_word_end(self.cursor, text);
208    }
209
210    /// Move cursor forward by WORD (W).
211    fn move_word_forward_word(&mut self, text: &str) {
212        self.cursor = motions::move_word_forward_word(self.cursor, text);
213    }
214
215    /// Move cursor backward by WORD (B).
216    fn move_word_backward_word(&mut self, text: &str) {
217        self.cursor = motions::move_word_backward_word(self.cursor, text);
218    }
219
220    /// Move cursor to end of WORD (E).
221    fn move_word_end_word(&mut self, text: &str) {
222        self.cursor = motions::move_word_end_word(self.cursor, text);
223    }
224
225    /// Move cursor up one line (k).
226    fn move_up(&mut self, text: &str) {
227        self.cursor = motions::move_up(self.cursor, text);
228    }
229
230    /// Move cursor down one line (j).
231    fn move_down(&mut self, text: &str) {
232        self.cursor = motions::move_down(self.cursor, text);
233    }
234
235    /// Move cursor to matching bracket (%).
236    /// Supports (), [], {}, and <>.
237    fn move_to_matching_bracket(&mut self, text: &str) {
238        self.cursor = motions::move_to_matching_bracket(self.cursor, text);
239    }
240
241    /// Dispatch a shared motion key (h/l/j/k/0/$/^/w/b/e/%/Left/Right/Home/End)
242    /// to the appropriate cursor helper. Returns `true` when the key was
243    /// recognized as a motion, `false` otherwise.
244    ///
245    /// Up/Down arrow keys are intentionally NOT handled here — Normal mode
246    /// treats them as history navigation, not motion.
247    ///
248    /// Called by Normal and Visual handlers so each can delegate motion
249    /// interpretation to one place and then wrap the result in its own way.
250    /// OperatorPending has extra `c`/`cw`/`ce` quirks and handles motion
251    /// itself.
252    fn dispatch_motion(&mut self, code: KeyCode, text: &str) -> bool {
253        match code {
254            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
255            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
256            KeyCode::Char('j') => self.move_down(text),
257            KeyCode::Char('k') => self.move_up(text),
258            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
259            KeyCode::Char('^') => self.move_first_non_blank(text),
260            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
261            KeyCode::Char('w') => self.move_word_forward(text),
262            KeyCode::Char('b') => self.move_word_backward(text),
263            KeyCode::Char('e') => self.move_word_end(text),
264            KeyCode::Char('W') => self.move_word_forward_word(text),
265            KeyCode::Char('B') => self.move_word_backward_word(text),
266            KeyCode::Char('E') => self.move_word_end_word(text),
267            KeyCode::Char('%') => self.move_to_matching_bracket(text),
268            _ => return false,
269        }
270        true
271    }
272
273    /// Handle key in Normal mode.
274    fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
275        // History at line boundaries: `k` on the first line and `j` on the
276        // last line become history navigation (matching readline vi-mode and
277        // most REPL muscle memory). Off-boundary, both stay as line motions
278        // via `dispatch_motion` below, so multi-line buffers keep working.
279        match key.code {
280            KeyCode::Char('k') if motions::is_on_first_line(self.cursor, text) => {
281                return EditResult::action(Action::HistoryPrev);
282            }
283            KeyCode::Char('j') if motions::is_on_last_line(self.cursor, text) => {
284                return EditResult::action(Action::HistoryNext);
285            }
286            _ => {}
287        }
288
289        // Shared motions (h/l/j/k/0/$/^/w/b/e/%/Left/Right/Home/End).
290        // Up/Down are NOT motions in Normal — they're history navigation below.
291        if self.dispatch_motion(key.code, text) {
292            // Pure cursor motion: enforce the vim Normal-mode invariant
293            // that the cursor sits on a character, never past the last one.
294            // (Edits and Insert transitions are excluded — see
295            // `clamp_to_last_char`.)
296            self.clamp_to_last_char(text);
297            return EditResult::cursor_only();
298        }
299
300        match key.code {
301            // Mode switching
302            KeyCode::Char('i') => {
303                self.mode = Mode::Insert;
304                EditResult::none()
305            }
306            KeyCode::Char('a') => {
307                self.mode = Mode::Insert;
308                self.move_right(text);
309                EditResult::none()
310            }
311            KeyCode::Char('A') => {
312                self.mode = Mode::Insert;
313                self.move_line_end_insert(text);
314                EditResult::none()
315            }
316            KeyCode::Char('I') => {
317                self.mode = Mode::Insert;
318                self.move_first_non_blank(text);
319                EditResult::none()
320            }
321            KeyCode::Char('o') => {
322                self.mode = Mode::Insert;
323                self.move_line_end(text);
324                let pos = self.cursor;
325                self.cursor = pos + 1;
326                EditResult::edit(TextEdit::Insert {
327                    at: pos,
328                    text: "\n".to_string(),
329                })
330            }
331            KeyCode::Char('O') => {
332                self.mode = Mode::Insert;
333                self.move_line_start(text);
334                let pos = self.cursor;
335                EditResult::edit(TextEdit::Insert {
336                    at: pos,
337                    text: "\n".to_string(),
338                })
339            }
340
341            // Visual mode
342            KeyCode::Char('v') => {
343                self.mode = Mode::Visual;
344                self.visual_anchor = Some(self.cursor);
345                EditResult::none()
346            }
347
348            // Cancel (Ctrl+C)
349            KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
350
351            // Operators (enter pending mode)
352            KeyCode::Char('d') => {
353                self.mode = Mode::OperatorPending(Operator::Delete);
354                EditResult::none()
355            }
356            KeyCode::Char('c') => {
357                self.mode = Mode::OperatorPending(Operator::Change);
358                EditResult::none()
359            }
360            KeyCode::Char('y') => {
361                self.mode = Mode::OperatorPending(Operator::Yank);
362                EditResult::none()
363            }
364
365            // Direct deletions
366            KeyCode::Char('x') => self.delete_char(text),
367            KeyCode::Char('D') => self.delete_to_end(text),
368            KeyCode::Char('C') => {
369                self.mode = Mode::Insert;
370                self.delete_to_end(text)
371            }
372
373            // Replace character (r)
374            KeyCode::Char('r') => {
375                self.mode = Mode::ReplaceChar;
376                EditResult::none()
377            }
378
379            // Ex-style command line (`:`) — opt-in. Without the flag, `:` is
380            // an unhandled key in Normal mode, matching classic vim's
381            // "press `i` first" behavior so non-REPL hosts are unaffected.
382            KeyCode::Char(':') if self.command_mode_enabled => {
383                self.mode = Mode::CommandLine;
384                self.command_buf.clear();
385                self.command_cursor = 0;
386                EditResult::none()
387            }
388
389            // Paste
390            KeyCode::Char('p') => self.paste_after(text),
391            KeyCode::Char('P') => self.paste_before(text),
392
393            // History (arrows only)
394            KeyCode::Up => EditResult::action(Action::HistoryPrev),
395            KeyCode::Down => EditResult::action(Action::HistoryNext),
396
397            // Submit
398            KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
399
400            // Newline (Shift+Enter)
401            KeyCode::Enter if key.shift => {
402                self.mode = Mode::Insert;
403                let pos = self.cursor;
404                self.cursor = pos + 1;
405                EditResult::edit(TextEdit::Insert {
406                    at: pos,
407                    text: "\n".to_string(),
408                })
409            }
410
411            // Escape in Normal mode is a no-op (safe to spam like in vim)
412            // Use Ctrl+C to cancel/quit
413            KeyCode::Escape => EditResult::none(),
414
415            _ => EditResult::none(),
416        }
417    }
418
419    /// Handle key in Insert mode.
420    fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
421        match key.code {
422            KeyCode::Escape => {
423                self.mode = Mode::Normal;
424                // Move cursor left like vim does when exiting insert
425                if self.cursor > 0 {
426                    self.move_left(text);
427                }
428                EditResult::none()
429            }
430
431            // Ctrl+C exits insert mode
432            KeyCode::Char('c') if key.ctrl => {
433                self.mode = Mode::Normal;
434                EditResult::none()
435            }
436
437            KeyCode::Char(c) if !key.ctrl && !key.alt => {
438                let pos = self.cursor;
439                self.cursor = pos + c.len_utf8();
440                EditResult::edit(TextEdit::Insert {
441                    at: pos,
442                    text: c.to_string(),
443                })
444            }
445
446            KeyCode::Backspace => {
447                if self.cursor == 0 {
448                    return EditResult::none();
449                }
450                let mut start = self.cursor - 1;
451                while start > 0 && !text.is_char_boundary(start) {
452                    start -= 1;
453                }
454                let end = self.cursor; // Save original cursor before updating
455                self.cursor = start;
456                EditResult::edit(TextEdit::Delete { start, end })
457            }
458
459            KeyCode::Delete => self.delete_char(text),
460
461            KeyCode::Left => {
462                self.move_left(text);
463                EditResult::cursor_only()
464            }
465            KeyCode::Right => {
466                self.move_right(text);
467                EditResult::cursor_only()
468            }
469            KeyCode::Up => {
470                self.move_up(text);
471                EditResult::cursor_only()
472            }
473            KeyCode::Down => {
474                self.move_down(text);
475                EditResult::cursor_only()
476            }
477            KeyCode::Home => {
478                self.move_line_start(text);
479                EditResult::cursor_only()
480            }
481            KeyCode::End => {
482                // In Insert mode, cursor can go past the last character
483                self.move_line_end_insert(text);
484                EditResult::cursor_only()
485            }
486
487            // Enter inserts newline in insert mode
488            KeyCode::Enter => {
489                let pos = self.cursor;
490                self.cursor = pos + 1;
491                EditResult::edit(TextEdit::Insert {
492                    at: pos,
493                    text: "\n".to_string(),
494                })
495            }
496
497            _ => EditResult::none(),
498        }
499    }
500
501    /// Handle key in OperatorPending mode.
502    fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
503        // First, handle escape to cancel
504        if key.code == KeyCode::Escape {
505            self.mode = Mode::Normal;
506            return EditResult::none();
507        }
508
509        // Handle doubled operator (dd, cc, yy) - operates on whole line
510        let is_line_op = matches!(
511            (op, key.code),
512            (Operator::Delete, KeyCode::Char('d'))
513                | (Operator::Change, KeyCode::Char('c'))
514                | (Operator::Yank, KeyCode::Char('y'))
515        );
516
517        if is_line_op {
518            self.mode = Mode::Normal;
519            return self.apply_operator_line(op, text);
520        }
521
522        // Handle motion
523        let start = self.cursor;
524        match key.code {
525            KeyCode::Char('w') => {
526                // Special case: cw behaves like ce (change to end of word, not including space)
527                // This is a vim quirk for historical compatibility
528                if op == Operator::Change {
529                    self.move_word_end(text);
530                    // Include the character at cursor
531                    if self.cursor < text.len() {
532                        self.cursor += 1;
533                    }
534                } else {
535                    self.move_word_forward(text);
536                }
537            }
538            KeyCode::Char('b') => self.move_word_backward(text),
539            KeyCode::Char('e') => {
540                self.move_word_end(text);
541                // Include the character at cursor for delete/change
542                if self.cursor < text.len() {
543                    self.cursor += 1;
544                }
545            }
546            KeyCode::Char('W') => {
547                // Mirror the `cw`->`ce` quirk: `cW` behaves like `cE`
548                // (change to end of WORD, not including trailing space).
549                if op == Operator::Change {
550                    self.move_word_end_word(text);
551                    if self.cursor < text.len() {
552                        self.cursor += 1;
553                    }
554                } else {
555                    self.move_word_forward_word(text);
556                }
557            }
558            KeyCode::Char('B') => self.move_word_backward_word(text),
559            KeyCode::Char('E') => {
560                self.move_word_end_word(text);
561                // Include the character at cursor for delete/change
562                if self.cursor < text.len() {
563                    self.cursor += 1;
564                }
565            }
566            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
567            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
568            KeyCode::Char('^') => self.move_first_non_blank(text),
569            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
570            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
571            KeyCode::Char('j') => self.move_down(text),
572            KeyCode::Char('k') => self.move_up(text),
573            _ => {
574                // Unknown motion, cancel
575                self.mode = Mode::Normal;
576                return EditResult::none();
577            }
578        }
579
580        let end = self.cursor;
581        self.mode = Mode::Normal;
582
583        if start == end {
584            return EditResult::none();
585        }
586
587        let (range_start, range_end) = if start < end {
588            (start, end)
589        } else {
590            (end, start)
591        };
592
593        self.apply_operator(op, range_start, range_end, text)
594    }
595
596    /// Handle key in Visual mode.
597    fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
598        match key.code {
599            KeyCode::Escape => {
600                self.mode = Mode::Normal;
601                self.visual_anchor = None;
602                EditResult::none()
603            }
604
605            // Motions extend selection (note: `^` and `%` are intentionally
606            // not wired here to preserve original behavior — tracked for a
607            // separate behavior-change PR).
608            KeyCode::Char('h') | KeyCode::Left => {
609                self.move_left(text);
610                EditResult::cursor_only()
611            }
612            KeyCode::Char('l') | KeyCode::Right => {
613                self.move_right(text);
614                EditResult::cursor_only()
615            }
616            KeyCode::Char('j') => {
617                self.move_down(text);
618                EditResult::cursor_only()
619            }
620            KeyCode::Char('k') => {
621                self.move_up(text);
622                EditResult::cursor_only()
623            }
624            KeyCode::Char('w') => {
625                self.move_word_forward(text);
626                EditResult::cursor_only()
627            }
628            KeyCode::Char('b') => {
629                self.move_word_backward(text);
630                EditResult::cursor_only()
631            }
632            KeyCode::Char('e') => {
633                self.move_word_end(text);
634                EditResult::cursor_only()
635            }
636            KeyCode::Char('W') => {
637                self.move_word_forward_word(text);
638                EditResult::cursor_only()
639            }
640            KeyCode::Char('B') => {
641                self.move_word_backward_word(text);
642                EditResult::cursor_only()
643            }
644            KeyCode::Char('E') => {
645                self.move_word_end_word(text);
646                EditResult::cursor_only()
647            }
648            KeyCode::Char('0') | KeyCode::Home => {
649                self.move_line_start(text);
650                EditResult::cursor_only()
651            }
652            KeyCode::Char('$') | KeyCode::End => {
653                self.move_line_end(text);
654                EditResult::cursor_only()
655            }
656
657            // Operators on selection
658            KeyCode::Char('d') | KeyCode::Char('x') => {
659                let (start, end) = self.selection_range();
660                self.mode = Mode::Normal;
661                self.visual_anchor = None;
662                self.apply_operator(Operator::Delete, start, end, text)
663            }
664            KeyCode::Char('c') => {
665                let (start, end) = self.selection_range();
666                self.mode = Mode::Normal;
667                self.visual_anchor = None;
668                self.apply_operator(Operator::Change, start, end, text)
669            }
670            KeyCode::Char('y') => {
671                let (start, end) = self.selection_range();
672                self.mode = Mode::Normal;
673                self.visual_anchor = None;
674                self.apply_operator(Operator::Yank, start, end, text)
675            }
676
677            _ => EditResult::none(),
678        }
679    }
680
681    /// Handle key in ReplaceChar mode (waiting for character after 'r').
682    fn handle_replace_char(&mut self, key: Key, text: &str) -> EditResult {
683        self.mode = Mode::Normal;
684
685        match key.code {
686            KeyCode::Escape => EditResult::none(),
687            KeyCode::Char(c) if !key.ctrl && !key.alt => {
688                // Replace character at cursor
689                if self.cursor >= text.len() {
690                    return EditResult::none();
691                }
692
693                // Find the end of the current character
694                let mut end = self.cursor + 1;
695                while end < text.len() && !text.is_char_boundary(end) {
696                    end += 1;
697                }
698
699                // Delete current char and insert new one
700                // Note: edits are applied in reverse order, so Insert comes first in vec
701                EditResult {
702                    edits: vec![
703                        TextEdit::Insert {
704                            at: self.cursor,
705                            text: c.to_string(),
706                        },
707                        TextEdit::Delete {
708                            start: self.cursor,
709                            end,
710                        },
711                    ],
712                    ..Default::default()
713                }
714            }
715            _ => EditResult::none(),
716        }
717    }
718
719    /// Get the selection range (ordered).
720    fn selection_range(&self) -> (usize, usize) {
721        let anchor = self.visual_anchor.unwrap_or(self.cursor);
722        if self.cursor < anchor {
723            (self.cursor, anchor)
724        } else {
725            (anchor, self.cursor + 1) // Include cursor position
726        }
727    }
728}
729
730impl LineEditor for VimLineEditor {
731    fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
732        self.clamp_cursor(text);
733
734        let result = match self.mode {
735            Mode::Normal => self.handle_normal(key, text),
736            Mode::Insert => self.handle_insert(key, text),
737            Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
738            Mode::Visual => self.handle_visual(key, text),
739            Mode::ReplaceChar => self.handle_replace_char(key, text),
740            Mode::CommandLine => self.handle_command_line(key),
741        };
742
743        // Store yanked text
744        if let Some(ref yanked) = result.yanked {
745            self.yank_buffer = yanked.clone();
746        }
747
748        result
749    }
750
751    fn cursor(&self) -> usize {
752        self.cursor
753    }
754
755    fn status(&self) -> &str {
756        match self.mode {
757            Mode::Normal => "NORMAL",
758            Mode::Insert => "INSERT",
759            Mode::OperatorPending(Operator::Delete) => "d...",
760            Mode::OperatorPending(Operator::Change) => "c...",
761            Mode::OperatorPending(Operator::Yank) => "y...",
762            Mode::Visual => "VISUAL",
763            Mode::ReplaceChar => "r...",
764            Mode::CommandLine => "COMMAND",
765        }
766    }
767
768    fn selection(&self) -> Option<Range<usize>> {
769        if self.mode == Mode::Visual {
770            let (start, end) = self.selection_range();
771            Some(start..end)
772        } else {
773            None
774        }
775    }
776
777    fn reset(&mut self) {
778        self.cursor = 0;
779        self.mode = Mode::Normal;
780        self.visual_anchor = None;
781        self.command_buf.clear();
782        self.command_cursor = 0;
783        // Keep yank buffer and command_mode_enabled flag across resets.
784    }
785
786    fn set_cursor(&mut self, pos: usize, text: &str) {
787        // Clamp to text length and ensure we're at a char boundary
788        let pos = pos.min(text.len());
789        self.cursor = if text.is_char_boundary(pos) {
790            pos
791        } else {
792            // Walk backwards to find a valid boundary
793            let mut p = pos;
794            while p > 0 && !text.is_char_boundary(p) {
795                p -= 1;
796            }
797            p
798        };
799    }
800}
801
802#[cfg(test)]
803mod tests;