vim_line/
vim.rs

1//! Vim-style line editor implementation.
2
3use crate::{Action, EditResult, Key, KeyCode, LineEditor, TextEdit};
4use std::ops::Range;
5
6/// Vim editing mode.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Mode {
9    #[default]
10    Normal,
11    Insert,
12    OperatorPending(Operator),
13    Visual,
14    /// Waiting for a character to replace the one under cursor (r command)
15    ReplaceChar,
16}
17
18/// Operators that wait for a motion.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Operator {
21    Delete,
22    Change,
23    Yank,
24}
25
26/// A vim-style line editor.
27///
28/// Implements modal editing with Normal, Insert, Visual, and OperatorPending modes.
29/// Designed for single "one-shot" inputs that may span multiple lines.
30#[derive(Debug, Clone)]
31pub struct VimLineEditor {
32    cursor: usize,
33    mode: Mode,
34    /// Anchor point for visual selection (cursor is the other end).
35    visual_anchor: Option<usize>,
36    /// Last yanked text (for paste).
37    yank_buffer: String,
38}
39
40impl Default for VimLineEditor {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl VimLineEditor {
47    /// Create a new editor in Normal mode.
48    pub fn new() -> Self {
49        Self {
50            cursor: 0,
51            mode: Mode::Normal,
52            visual_anchor: None,
53            yank_buffer: String::new(),
54        }
55    }
56
57    /// Get the current mode.
58    pub fn mode(&self) -> Mode {
59        self.mode
60    }
61
62    /// Clamp cursor to valid range for the given text.
63    fn clamp_cursor(&mut self, text: &str) {
64        self.cursor = self.cursor.min(text.len());
65    }
66
67    /// Move cursor left by one character.
68    fn move_left(&mut self, text: &str) {
69        if self.cursor > 0 {
70            // Find the previous character boundary
71            let mut new_pos = self.cursor - 1;
72            while new_pos > 0 && !text.is_char_boundary(new_pos) {
73                new_pos -= 1;
74            }
75            self.cursor = new_pos;
76        }
77    }
78
79    /// Move cursor right by one character.
80    fn move_right(&mut self, text: &str) {
81        if self.cursor < text.len() {
82            // Find the next character boundary
83            let mut new_pos = self.cursor + 1;
84            while new_pos < text.len() && !text.is_char_boundary(new_pos) {
85                new_pos += 1;
86            }
87            self.cursor = new_pos;
88        }
89    }
90
91    /// Move cursor to start of line (0).
92    fn move_line_start(&mut self, text: &str) {
93        // Find the start of the current line
94        self.cursor = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
95    }
96
97    /// Move cursor to first non-whitespace of line (^).
98    fn move_first_non_blank(&mut self, text: &str) {
99        self.move_line_start(text);
100        // Skip whitespace
101        let line_start = self.cursor;
102        for (i, c) in text[line_start..].char_indices() {
103            if c == '\n' || !c.is_whitespace() {
104                self.cursor = line_start + i;
105                return;
106            }
107        }
108    }
109
110    /// Move cursor to end of line ($).
111    /// In Normal mode, cursor should be ON the last character.
112    /// The `past_end` parameter allows Insert mode to go past the last char.
113    fn move_line_end_impl(&mut self, text: &str, past_end: bool) {
114        // Find the end of the current line
115        let line_end = text[self.cursor..]
116            .find('\n')
117            .map(|i| self.cursor + i)
118            .unwrap_or(text.len());
119
120        if past_end || line_end == 0 {
121            self.cursor = line_end;
122        } else {
123            // In Normal mode, cursor should be ON the last character
124            // Find the start of the last character (handle multi-byte)
125            let mut last_char_start = line_end.saturating_sub(1);
126            while last_char_start > 0 && !text.is_char_boundary(last_char_start) {
127                last_char_start -= 1;
128            }
129            self.cursor = last_char_start;
130        }
131    }
132
133    /// Move cursor to end of line (Normal mode - stays on last char)
134    fn move_line_end(&mut self, text: &str) {
135        self.move_line_end_impl(text, false);
136    }
137
138    /// Move cursor past end of line (Insert mode)
139    fn move_line_end_insert(&mut self, text: &str) {
140        self.move_line_end_impl(text, true);
141    }
142
143    /// Move cursor forward by word (w).
144    fn move_word_forward(&mut self, text: &str) {
145        let bytes = text.as_bytes();
146        let mut pos = self.cursor;
147
148        // Skip current word (non-whitespace)
149        while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
150            pos += 1;
151        }
152        // Skip whitespace
153        while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
154            pos += 1;
155        }
156
157        self.cursor = pos;
158    }
159
160    /// Move cursor backward by word (b).
161    fn move_word_backward(&mut self, text: &str) {
162        let bytes = text.as_bytes();
163        let mut pos = self.cursor;
164
165        // Skip whitespace before cursor
166        while pos > 0 && bytes[pos - 1].is_ascii_whitespace() {
167            pos -= 1;
168        }
169        // Skip word (non-whitespace)
170        while pos > 0 && !bytes[pos - 1].is_ascii_whitespace() {
171            pos -= 1;
172        }
173
174        self.cursor = pos;
175    }
176
177    /// Move cursor to end of word (e).
178    fn move_word_end(&mut self, text: &str) {
179        let bytes = text.as_bytes();
180        let mut pos = self.cursor;
181
182        // Move at least one character
183        if pos < bytes.len() {
184            pos += 1;
185        }
186        // Skip whitespace
187        while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
188            pos += 1;
189        }
190        // Move to end of word
191        while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
192            pos += 1;
193        }
194        // Back up one (end of word, not start of next)
195        if pos > self.cursor + 1 {
196            pos -= 1;
197        }
198
199        self.cursor = pos;
200    }
201
202    /// Move cursor up one line (k).
203    fn move_up(&mut self, text: &str) {
204        // Find current line start
205        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
206
207        if line_start == 0 {
208            // Already on first line, can't go up
209            return;
210        }
211
212        // Column offset from line start
213        let col = self.cursor - line_start;
214
215        // Find previous line start
216        let prev_line_start = text[..line_start - 1]
217            .rfind('\n')
218            .map(|i| i + 1)
219            .unwrap_or(0);
220
221        // Previous line length
222        let prev_line_end = line_start - 1; // Position of \n
223        let prev_line_len = prev_line_end - prev_line_start;
224
225        // Move to same column or end of line
226        self.cursor = prev_line_start + col.min(prev_line_len);
227    }
228
229    /// Move cursor down one line (j).
230    fn move_down(&mut self, text: &str) {
231        // Find current line start
232        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
233
234        // Column offset
235        let col = self.cursor - line_start;
236
237        // Find next line start
238        let Some(newline_pos) = text[self.cursor..].find('\n') else {
239            // Already on last line
240            return;
241        };
242        let next_line_start = self.cursor + newline_pos + 1;
243
244        if next_line_start >= text.len() {
245            // Next line is empty/doesn't exist
246            self.cursor = text.len();
247            return;
248        }
249
250        // Find next line end
251        let next_line_end = text[next_line_start..]
252            .find('\n')
253            .map(|i| next_line_start + i)
254            .unwrap_or(text.len());
255
256        let next_line_len = next_line_end - next_line_start;
257
258        // Move to same column or end of line
259        self.cursor = next_line_start + col.min(next_line_len);
260    }
261
262    /// Move cursor to matching bracket (%).
263    /// Supports (), [], {}, and <>.
264    fn move_to_matching_bracket(&mut self, text: &str) {
265        if self.cursor >= text.len() {
266            return;
267        }
268
269        // Get the character at the cursor
270        let char_at_cursor = text[self.cursor..].chars().next();
271        let c = match char_at_cursor {
272            Some(c) => c,
273            None => return,
274        };
275
276        // Define bracket pairs: (opening, closing)
277        let pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
278
279        // Check if current char is an opening bracket
280        for (open, close) in pairs.iter() {
281            if c == *open {
282                // Search forward for matching close
283                if let Some(pos) = self.find_matching_forward(text, *open, *close) {
284                    self.cursor = pos;
285                }
286                return;
287            }
288            if c == *close {
289                // Search backward for matching open
290                if let Some(pos) = self.find_matching_backward(text, *open, *close) {
291                    self.cursor = pos;
292                }
293                return;
294            }
295        }
296    }
297
298    /// Find matching closing bracket, searching forward from cursor.
299    fn find_matching_forward(&self, text: &str, open: char, close: char) -> Option<usize> {
300        let mut depth = 1;
301        let mut pos = self.cursor;
302
303        // Move past the opening bracket
304        pos += open.len_utf8();
305
306        for (i, c) in text[pos..].char_indices() {
307            if c == open {
308                depth += 1;
309            } else if c == close {
310                depth -= 1;
311                if depth == 0 {
312                    return Some(pos + i);
313                }
314            }
315        }
316        None
317    }
318
319    /// Find matching opening bracket, searching backward from cursor.
320    fn find_matching_backward(&self, text: &str, open: char, close: char) -> Option<usize> {
321        let mut depth = 1;
322
323        // Search backward from just before cursor
324        let search_text = &text[..self.cursor];
325        for (i, c) in search_text.char_indices().rev() {
326            if c == close {
327                depth += 1;
328            } else if c == open {
329                depth -= 1;
330                if depth == 0 {
331                    return Some(i);
332                }
333            }
334        }
335        None
336    }
337
338    /// Delete character at cursor (x).
339    fn delete_char(&mut self, text: &str) -> EditResult {
340        if self.cursor >= text.len() {
341            return EditResult::none();
342        }
343
344        let start = self.cursor;
345
346        // Find the end of the current character
347        let mut end = self.cursor + 1;
348        while end < text.len() && !text.is_char_boundary(end) {
349            end += 1;
350        }
351
352        let deleted = text[start..end].to_string();
353
354        // If we're deleting the last character, move cursor left
355        // (In Normal mode, cursor must always be ON a character)
356        if end >= text.len() && self.cursor > 0 {
357            self.move_left(text);
358        }
359
360        EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
361    }
362
363    /// Delete to end of line (D).
364    fn delete_to_end(&mut self, text: &str) -> EditResult {
365        let end = text[self.cursor..]
366            .find('\n')
367            .map(|i| self.cursor + i)
368            .unwrap_or(text.len());
369
370        if self.cursor >= end {
371            return EditResult::none();
372        }
373
374        let deleted = text[self.cursor..end].to_string();
375        EditResult::edit_and_yank(
376            TextEdit::Delete {
377                start: self.cursor,
378                end,
379            },
380            deleted,
381        )
382    }
383
384    /// Delete entire current line (dd).
385    fn delete_line(&mut self, text: &str) -> EditResult {
386        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
387
388        let line_end = text[self.cursor..]
389            .find('\n')
390            .map(|i| self.cursor + i + 1) // Include the newline
391            .unwrap_or(text.len());
392
393        // If this is the only line and no newline, include leading newline if any
394        let (start, end) = if line_start == 0 && line_end == text.len() {
395            (0, text.len())
396        } else if line_end == text.len() && line_start > 0 {
397            // Last line - delete the preceding newline instead
398            (line_start - 1, text.len())
399        } else {
400            (line_start, line_end)
401        };
402
403        let deleted = text[start..end].to_string();
404        self.cursor = start;
405
406        EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
407    }
408
409    /// Paste after cursor (p).
410    fn paste_after(&mut self, text: &str) -> EditResult {
411        if self.yank_buffer.is_empty() {
412            return EditResult::none();
413        }
414
415        let insert_pos = (self.cursor + 1).min(text.len());
416        let to_insert = self.yank_buffer.clone();
417        // Position cursor at end of pasted text, safely handling edge cases
418        self.cursor = (insert_pos + to_insert.len())
419            .saturating_sub(1)
420            .min(text.len() + to_insert.len());
421
422        EditResult::edit(TextEdit::Insert {
423            at: insert_pos,
424            text: to_insert,
425        })
426    }
427
428    /// Paste before cursor (P).
429    fn paste_before(&mut self, text: &str) -> EditResult {
430        if self.yank_buffer.is_empty() {
431            return EditResult::none();
432        }
433
434        let to_insert = self.yank_buffer.clone();
435        let insert_pos = self.cursor.min(text.len());
436        // Position cursor at end of pasted text
437        self.cursor = (insert_pos + to_insert.len()).min(text.len() + to_insert.len());
438
439        EditResult::edit(TextEdit::Insert {
440            at: insert_pos,
441            text: to_insert,
442        })
443    }
444
445    /// Handle key in Normal mode.
446    fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
447        match key.code {
448            // Mode switching
449            KeyCode::Char('i') => {
450                self.mode = Mode::Insert;
451                EditResult::none()
452            }
453            KeyCode::Char('a') => {
454                self.mode = Mode::Insert;
455                self.move_right(text);
456                EditResult::none()
457            }
458            KeyCode::Char('A') => {
459                self.mode = Mode::Insert;
460                self.move_line_end_insert(text);
461                EditResult::none()
462            }
463            KeyCode::Char('I') => {
464                self.mode = Mode::Insert;
465                self.move_first_non_blank(text);
466                EditResult::none()
467            }
468            KeyCode::Char('o') => {
469                self.mode = Mode::Insert;
470                self.move_line_end(text);
471                let pos = self.cursor;
472                self.cursor = pos + 1;
473                EditResult::edit(TextEdit::Insert {
474                    at: pos,
475                    text: "\n".to_string(),
476                })
477            }
478            KeyCode::Char('O') => {
479                self.mode = Mode::Insert;
480                self.move_line_start(text);
481                let pos = self.cursor;
482                EditResult::edit(TextEdit::Insert {
483                    at: pos,
484                    text: "\n".to_string(),
485                })
486            }
487
488            // Visual mode
489            KeyCode::Char('v') => {
490                self.mode = Mode::Visual;
491                self.visual_anchor = Some(self.cursor);
492                EditResult::none()
493            }
494
495            // Motions
496            KeyCode::Char('h') | KeyCode::Left => {
497                self.move_left(text);
498                EditResult::cursor_only()
499            }
500            KeyCode::Char('l') | KeyCode::Right => {
501                self.move_right(text);
502                EditResult::cursor_only()
503            }
504            KeyCode::Char('j') => {
505                self.move_down(text);
506                EditResult::cursor_only()
507            }
508            KeyCode::Char('k') => {
509                self.move_up(text);
510                EditResult::cursor_only()
511            }
512            KeyCode::Char('0') | KeyCode::Home => {
513                self.move_line_start(text);
514                EditResult::cursor_only()
515            }
516            KeyCode::Char('^') => {
517                self.move_first_non_blank(text);
518                EditResult::cursor_only()
519            }
520            KeyCode::Char('$') | KeyCode::End => {
521                self.move_line_end(text);
522                EditResult::cursor_only()
523            }
524            KeyCode::Char('w') => {
525                self.move_word_forward(text);
526                EditResult::cursor_only()
527            }
528            KeyCode::Char('b') => {
529                self.move_word_backward(text);
530                EditResult::cursor_only()
531            }
532            KeyCode::Char('e') => {
533                self.move_word_end(text);
534                EditResult::cursor_only()
535            }
536            KeyCode::Char('%') => {
537                self.move_to_matching_bracket(text);
538                EditResult::cursor_only()
539            }
540
541            // Cancel (Ctrl+C)
542            KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
543
544            // Operators (enter pending mode)
545            KeyCode::Char('d') => {
546                self.mode = Mode::OperatorPending(Operator::Delete);
547                EditResult::none()
548            }
549            KeyCode::Char('c') => {
550                self.mode = Mode::OperatorPending(Operator::Change);
551                EditResult::none()
552            }
553            KeyCode::Char('y') => {
554                self.mode = Mode::OperatorPending(Operator::Yank);
555                EditResult::none()
556            }
557
558            // Direct deletions
559            KeyCode::Char('x') => self.delete_char(text),
560            KeyCode::Char('D') => self.delete_to_end(text),
561            KeyCode::Char('C') => {
562                self.mode = Mode::Insert;
563                self.delete_to_end(text)
564            }
565
566            // Replace character (r)
567            KeyCode::Char('r') => {
568                self.mode = Mode::ReplaceChar;
569                EditResult::none()
570            }
571
572            // Paste
573            KeyCode::Char('p') => self.paste_after(text),
574            KeyCode::Char('P') => self.paste_before(text),
575
576            // History (arrows only)
577            KeyCode::Up => EditResult::action(Action::HistoryPrev),
578            KeyCode::Down => EditResult::action(Action::HistoryNext),
579
580            // Submit
581            KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
582
583            // Newline (Shift+Enter)
584            KeyCode::Enter if key.shift => {
585                self.mode = Mode::Insert;
586                let pos = self.cursor;
587                self.cursor = pos + 1;
588                EditResult::edit(TextEdit::Insert {
589                    at: pos,
590                    text: "\n".to_string(),
591                })
592            }
593
594            // Escape in Normal mode is a no-op (safe to spam like in vim)
595            // Use Ctrl+C to cancel/quit
596            KeyCode::Escape => EditResult::none(),
597
598            _ => EditResult::none(),
599        }
600    }
601
602    /// Handle key in Insert mode.
603    fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
604        match key.code {
605            KeyCode::Escape => {
606                self.mode = Mode::Normal;
607                // Move cursor left like vim does when exiting insert
608                if self.cursor > 0 {
609                    self.move_left(text);
610                }
611                EditResult::none()
612            }
613
614            // Ctrl+C exits insert mode
615            KeyCode::Char('c') if key.ctrl => {
616                self.mode = Mode::Normal;
617                EditResult::none()
618            }
619
620            KeyCode::Char(c) if !key.ctrl && !key.alt => {
621                let pos = self.cursor;
622                self.cursor = pos + c.len_utf8();
623                EditResult::edit(TextEdit::Insert {
624                    at: pos,
625                    text: c.to_string(),
626                })
627            }
628
629            KeyCode::Backspace => {
630                if self.cursor == 0 {
631                    return EditResult::none();
632                }
633                let mut start = self.cursor - 1;
634                while start > 0 && !text.is_char_boundary(start) {
635                    start -= 1;
636                }
637                let end = self.cursor; // Save original cursor before updating
638                self.cursor = start;
639                EditResult::edit(TextEdit::Delete { start, end })
640            }
641
642            KeyCode::Delete => self.delete_char(text),
643
644            KeyCode::Left => {
645                self.move_left(text);
646                EditResult::cursor_only()
647            }
648            KeyCode::Right => {
649                self.move_right(text);
650                EditResult::cursor_only()
651            }
652            KeyCode::Up => {
653                self.move_up(text);
654                EditResult::cursor_only()
655            }
656            KeyCode::Down => {
657                self.move_down(text);
658                EditResult::cursor_only()
659            }
660            KeyCode::Home => {
661                self.move_line_start(text);
662                EditResult::cursor_only()
663            }
664            KeyCode::End => {
665                // In Insert mode, cursor can go past the last character
666                self.move_line_end_insert(text);
667                EditResult::cursor_only()
668            }
669
670            // Enter inserts newline in insert mode
671            KeyCode::Enter => {
672                let pos = self.cursor;
673                self.cursor = pos + 1;
674                EditResult::edit(TextEdit::Insert {
675                    at: pos,
676                    text: "\n".to_string(),
677                })
678            }
679
680            _ => EditResult::none(),
681        }
682    }
683
684    /// Handle key in OperatorPending mode.
685    fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
686        // First, handle escape to cancel
687        if key.code == KeyCode::Escape {
688            self.mode = Mode::Normal;
689            return EditResult::none();
690        }
691
692        // Handle doubled operator (dd, cc, yy) - operates on whole line
693        let is_line_op = matches!(
694            (op, key.code),
695            (Operator::Delete, KeyCode::Char('d'))
696                | (Operator::Change, KeyCode::Char('c'))
697                | (Operator::Yank, KeyCode::Char('y'))
698        );
699
700        if is_line_op {
701            self.mode = Mode::Normal;
702            return self.apply_operator_line(op, text);
703        }
704
705        // Handle motion
706        let start = self.cursor;
707        match key.code {
708            KeyCode::Char('w') => {
709                // Special case: cw behaves like ce (change to end of word, not including space)
710                // This is a vim quirk for historical compatibility
711                if op == Operator::Change {
712                    self.move_word_end(text);
713                    // Include the character at cursor
714                    if self.cursor < text.len() {
715                        self.cursor += 1;
716                    }
717                } else {
718                    self.move_word_forward(text);
719                }
720            }
721            KeyCode::Char('b') => self.move_word_backward(text),
722            KeyCode::Char('e') => {
723                self.move_word_end(text);
724                // Include the character at cursor for delete/change
725                if self.cursor < text.len() {
726                    self.cursor += 1;
727                }
728            }
729            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
730            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
731            KeyCode::Char('^') => self.move_first_non_blank(text),
732            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
733            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
734            KeyCode::Char('j') => self.move_down(text),
735            KeyCode::Char('k') => self.move_up(text),
736            _ => {
737                // Unknown motion, cancel
738                self.mode = Mode::Normal;
739                return EditResult::none();
740            }
741        }
742
743        let end = self.cursor;
744        self.mode = Mode::Normal;
745
746        if start == end {
747            return EditResult::none();
748        }
749
750        let (range_start, range_end) = if start < end {
751            (start, end)
752        } else {
753            (end, start)
754        };
755
756        self.apply_operator(op, range_start, range_end, text)
757    }
758
759    /// Apply an operator to a range.
760    fn apply_operator(&mut self, op: Operator, start: usize, end: usize, text: &str) -> EditResult {
761        let affected = text[start..end].to_string();
762        self.yank_buffer = affected.clone();
763        self.cursor = start;
764
765        match op {
766            Operator::Delete => {
767                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
768            }
769            Operator::Change => {
770                self.mode = Mode::Insert;
771                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
772            }
773            Operator::Yank => {
774                // Just yank, no edit
775                EditResult {
776                    yanked: Some(affected),
777                    ..Default::default()
778                }
779            }
780        }
781    }
782
783    /// Apply an operator to the whole line.
784    fn apply_operator_line(&mut self, op: Operator, text: &str) -> EditResult {
785        match op {
786            Operator::Delete => self.delete_line(text),
787            Operator::Change => {
788                let result = self.delete_line(text);
789                self.mode = Mode::Insert;
790                result
791            }
792            Operator::Yank => {
793                let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
794                let line_end = text[self.cursor..]
795                    .find('\n')
796                    .map(|i| self.cursor + i + 1)
797                    .unwrap_or(text.len());
798                let line = text[line_start..line_end].to_string();
799                self.yank_buffer = line.clone();
800                EditResult {
801                    yanked: Some(line),
802                    ..Default::default()
803                }
804            }
805        }
806    }
807
808    /// Handle key in Visual mode.
809    fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
810        match key.code {
811            KeyCode::Escape => {
812                self.mode = Mode::Normal;
813                self.visual_anchor = None;
814                EditResult::none()
815            }
816
817            // Motions extend selection
818            KeyCode::Char('h') | KeyCode::Left => {
819                self.move_left(text);
820                EditResult::cursor_only()
821            }
822            KeyCode::Char('l') | KeyCode::Right => {
823                self.move_right(text);
824                EditResult::cursor_only()
825            }
826            KeyCode::Char('j') => {
827                self.move_down(text);
828                EditResult::cursor_only()
829            }
830            KeyCode::Char('k') => {
831                self.move_up(text);
832                EditResult::cursor_only()
833            }
834            KeyCode::Char('w') => {
835                self.move_word_forward(text);
836                EditResult::cursor_only()
837            }
838            KeyCode::Char('b') => {
839                self.move_word_backward(text);
840                EditResult::cursor_only()
841            }
842            KeyCode::Char('e') => {
843                self.move_word_end(text);
844                EditResult::cursor_only()
845            }
846            KeyCode::Char('0') | KeyCode::Home => {
847                self.move_line_start(text);
848                EditResult::cursor_only()
849            }
850            KeyCode::Char('$') | KeyCode::End => {
851                self.move_line_end(text);
852                EditResult::cursor_only()
853            }
854
855            // Operators on selection
856            KeyCode::Char('d') | KeyCode::Char('x') => {
857                let (start, end) = self.selection_range();
858                self.mode = Mode::Normal;
859                self.visual_anchor = None;
860                self.apply_operator(Operator::Delete, start, end, text)
861            }
862            KeyCode::Char('c') => {
863                let (start, end) = self.selection_range();
864                self.mode = Mode::Normal;
865                self.visual_anchor = None;
866                self.apply_operator(Operator::Change, start, end, text)
867            }
868            KeyCode::Char('y') => {
869                let (start, end) = self.selection_range();
870                self.mode = Mode::Normal;
871                self.visual_anchor = None;
872                self.apply_operator(Operator::Yank, start, end, text)
873            }
874
875            _ => EditResult::none(),
876        }
877    }
878
879    /// Handle key in ReplaceChar mode (waiting for character after 'r').
880    fn handle_replace_char(&mut self, key: Key, text: &str) -> EditResult {
881        self.mode = Mode::Normal;
882
883        match key.code {
884            KeyCode::Escape => EditResult::none(),
885            KeyCode::Char(c) if !key.ctrl && !key.alt => {
886                // Replace character at cursor
887                if self.cursor >= text.len() {
888                    return EditResult::none();
889                }
890
891                // Find the end of the current character
892                let mut end = self.cursor + 1;
893                while end < text.len() && !text.is_char_boundary(end) {
894                    end += 1;
895                }
896
897                // Delete current char and insert new one
898                // Note: edits are applied in reverse order, so Insert comes first in vec
899                EditResult {
900                    edits: vec![
901                        TextEdit::Insert {
902                            at: self.cursor,
903                            text: c.to_string(),
904                        },
905                        TextEdit::Delete {
906                            start: self.cursor,
907                            end,
908                        },
909                    ],
910                    ..Default::default()
911                }
912            }
913            _ => EditResult::none(),
914        }
915    }
916
917    /// Get the selection range (ordered).
918    fn selection_range(&self) -> (usize, usize) {
919        let anchor = self.visual_anchor.unwrap_or(self.cursor);
920        if self.cursor < anchor {
921            (self.cursor, anchor)
922        } else {
923            (anchor, self.cursor + 1) // Include cursor position
924        }
925    }
926}
927
928impl LineEditor for VimLineEditor {
929    fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
930        self.clamp_cursor(text);
931
932        let result = match self.mode {
933            Mode::Normal => self.handle_normal(key, text),
934            Mode::Insert => self.handle_insert(key, text),
935            Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
936            Mode::Visual => self.handle_visual(key, text),
937            Mode::ReplaceChar => self.handle_replace_char(key, text),
938        };
939
940        // Store yanked text
941        if let Some(ref yanked) = result.yanked {
942            self.yank_buffer = yanked.clone();
943        }
944
945        result
946    }
947
948    fn cursor(&self) -> usize {
949        self.cursor
950    }
951
952    fn status(&self) -> &str {
953        match self.mode {
954            Mode::Normal => "NORMAL",
955            Mode::Insert => "INSERT",
956            Mode::OperatorPending(Operator::Delete) => "d...",
957            Mode::OperatorPending(Operator::Change) => "c...",
958            Mode::OperatorPending(Operator::Yank) => "y...",
959            Mode::Visual => "VISUAL",
960            Mode::ReplaceChar => "r...",
961        }
962    }
963
964    fn selection(&self) -> Option<Range<usize>> {
965        if self.mode == Mode::Visual {
966            let (start, end) = self.selection_range();
967            Some(start..end)
968        } else {
969            None
970        }
971    }
972
973    fn reset(&mut self) {
974        self.cursor = 0;
975        self.mode = Mode::Normal;
976        self.visual_anchor = None;
977        // Keep yank buffer across resets
978    }
979
980    fn set_cursor(&mut self, pos: usize, text: &str) {
981        // Clamp to text length and ensure we're at a char boundary
982        let pos = pos.min(text.len());
983        self.cursor = if text.is_char_boundary(pos) {
984            pos
985        } else {
986            // Walk backwards to find a valid boundary
987            let mut p = pos;
988            while p > 0 && !text.is_char_boundary(p) {
989                p -= 1;
990            }
991            p
992        };
993    }
994}
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999
1000    #[test]
1001    fn test_basic_motion() {
1002        let mut editor = VimLineEditor::new();
1003        let text = "hello world";
1004
1005        // Move right with 'l'
1006        editor.handle_key(Key::char('l'), text);
1007        assert_eq!(editor.cursor(), 1);
1008
1009        // Move right with 'w'
1010        editor.handle_key(Key::char('w'), text);
1011        assert_eq!(editor.cursor(), 6); // Start of "world"
1012
1013        // Move to end with '$' - cursor should be ON the last char, not past it
1014        editor.handle_key(Key::char('$'), text);
1015        assert_eq!(editor.cursor(), 10); // 'd' is at index 10
1016
1017        // Move to start with '0'
1018        editor.handle_key(Key::char('0'), text);
1019        assert_eq!(editor.cursor(), 0);
1020    }
1021
1022    #[test]
1023    fn test_mode_switching() {
1024        let mut editor = VimLineEditor::new();
1025        let text = "hello";
1026
1027        assert_eq!(editor.mode(), Mode::Normal);
1028
1029        editor.handle_key(Key::char('i'), text);
1030        assert_eq!(editor.mode(), Mode::Insert);
1031
1032        editor.handle_key(Key::code(KeyCode::Escape), text);
1033        assert_eq!(editor.mode(), Mode::Normal);
1034    }
1035
1036    #[test]
1037    fn test_delete_word() {
1038        let mut editor = VimLineEditor::new();
1039        let text = "hello world";
1040
1041        // dw should delete "hello "
1042        editor.handle_key(Key::char('d'), text);
1043        editor.handle_key(Key::char('w'), text);
1044
1045        // Check we're back in Normal mode
1046        assert_eq!(editor.mode(), Mode::Normal);
1047    }
1048
1049    #[test]
1050    fn test_insert_char() {
1051        let mut editor = VimLineEditor::new();
1052        let text = "";
1053
1054        editor.handle_key(Key::char('i'), text);
1055        let result = editor.handle_key(Key::char('x'), text);
1056
1057        assert_eq!(result.edits.len(), 1);
1058        match &result.edits[0] {
1059            TextEdit::Insert { at, text } => {
1060                assert_eq!(*at, 0);
1061                assert_eq!(text, "x");
1062            }
1063            _ => panic!("Expected Insert"),
1064        }
1065    }
1066
1067    #[test]
1068    fn test_visual_mode() {
1069        let mut editor = VimLineEditor::new();
1070        let text = "hello world";
1071
1072        // Enter visual mode
1073        editor.handle_key(Key::char('v'), text);
1074        assert_eq!(editor.mode(), Mode::Visual);
1075
1076        // Extend selection
1077        editor.handle_key(Key::char('w'), text);
1078
1079        // Selection should cover from 0 to cursor
1080        let sel = editor.selection().unwrap();
1081        assert_eq!(sel.start, 0);
1082        assert!(sel.end > 0);
1083    }
1084
1085    #[test]
1086    fn test_backspace_ascii() {
1087        let mut editor = VimLineEditor::new();
1088        let mut text = String::from("abc");
1089
1090        // Enter insert mode and go to end
1091        editor.handle_key(Key::char('i'), &text);
1092        editor.handle_key(Key::code(KeyCode::End), &text);
1093        assert_eq!(editor.cursor(), 3);
1094
1095        // Backspace should delete 'c'
1096        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
1097        for edit in result.edits.into_iter().rev() {
1098            edit.apply(&mut text);
1099        }
1100        assert_eq!(text, "ab");
1101        assert_eq!(editor.cursor(), 2);
1102    }
1103
1104    #[test]
1105    fn test_backspace_unicode() {
1106        let mut editor = VimLineEditor::new();
1107        let mut text = String::from("a😀b");
1108
1109        // Enter insert mode and position after emoji (byte position 5: 1 + 4)
1110        editor.handle_key(Key::char('i'), &text);
1111        editor.handle_key(Key::code(KeyCode::End), &text);
1112        editor.handle_key(Key::code(KeyCode::Left), &text); // Move before 'b'
1113        assert_eq!(editor.cursor(), 5); // After the 4-byte emoji
1114
1115        // Backspace should delete entire emoji (4 bytes), not just 1 byte
1116        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
1117        for edit in result.edits.into_iter().rev() {
1118            edit.apply(&mut text);
1119        }
1120        assert_eq!(text, "ab");
1121        assert_eq!(editor.cursor(), 1);
1122    }
1123
1124    #[test]
1125    fn test_yank_and_paste() {
1126        let mut editor = VimLineEditor::new();
1127        let mut text = String::from("hello world");
1128
1129        // Yank word with yw
1130        editor.handle_key(Key::char('y'), &text);
1131        let result = editor.handle_key(Key::char('w'), &text);
1132        assert!(result.yanked.is_some());
1133        assert_eq!(result.yanked.unwrap(), "hello ");
1134
1135        // Move to end and paste
1136        editor.handle_key(Key::char('$'), &text);
1137        let result = editor.handle_key(Key::char('p'), &text);
1138
1139        for edit in result.edits.into_iter().rev() {
1140            edit.apply(&mut text);
1141        }
1142        assert_eq!(text, "hello worldhello ");
1143    }
1144
1145    #[test]
1146    fn test_visual_mode_delete() {
1147        let mut editor = VimLineEditor::new();
1148        let mut text = String::from("hello world");
1149
1150        // Enter visual mode at position 0
1151        editor.handle_key(Key::char('v'), &text);
1152        assert_eq!(editor.mode(), Mode::Visual);
1153
1154        // Extend selection with 'e' motion to end of word (stays on 'o' of hello)
1155        editor.handle_key(Key::char('e'), &text);
1156
1157        // Delete selection with d - deletes "hello"
1158        let result = editor.handle_key(Key::char('d'), &text);
1159
1160        for edit in result.edits.into_iter().rev() {
1161            edit.apply(&mut text);
1162        }
1163        assert_eq!(text, " world");
1164        assert_eq!(editor.mode(), Mode::Normal);
1165    }
1166
1167    #[test]
1168    fn test_operator_pending_escape() {
1169        let mut editor = VimLineEditor::new();
1170        let text = "hello world";
1171
1172        // Start delete operator
1173        editor.handle_key(Key::char('d'), text);
1174        assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
1175
1176        // Cancel with Escape
1177        editor.handle_key(Key::code(KeyCode::Escape), text);
1178        assert_eq!(editor.mode(), Mode::Normal);
1179    }
1180
1181    #[test]
1182    fn test_replace_char() {
1183        let mut editor = VimLineEditor::new();
1184        let mut text = String::from("hello");
1185
1186        // Press 'r' then 'x' to replace 'h' with 'x'
1187        editor.handle_key(Key::char('r'), &text);
1188        assert_eq!(editor.mode(), Mode::ReplaceChar);
1189
1190        let result = editor.handle_key(Key::char('x'), &text);
1191        assert_eq!(editor.mode(), Mode::Normal);
1192
1193        // Apply edits
1194        for edit in result.edits.into_iter().rev() {
1195            edit.apply(&mut text);
1196        }
1197        assert_eq!(text, "xello");
1198    }
1199
1200    #[test]
1201    fn test_replace_char_escape() {
1202        let mut editor = VimLineEditor::new();
1203        let text = "hello";
1204
1205        // Press 'r' then Escape should cancel
1206        editor.handle_key(Key::char('r'), text);
1207        assert_eq!(editor.mode(), Mode::ReplaceChar);
1208
1209        editor.handle_key(Key::code(KeyCode::Escape), text);
1210        assert_eq!(editor.mode(), Mode::Normal);
1211    }
1212
1213    #[test]
1214    fn test_cw_no_trailing_space() {
1215        let mut editor = VimLineEditor::new();
1216        let mut text = String::from("hello world");
1217
1218        // cw should delete "hello" (not "hello ") and enter insert mode
1219        editor.handle_key(Key::char('c'), &text);
1220        let result = editor.handle_key(Key::char('w'), &text);
1221
1222        assert_eq!(editor.mode(), Mode::Insert);
1223
1224        // Apply edits
1225        for edit in result.edits.into_iter().rev() {
1226            edit.apply(&mut text);
1227        }
1228        // Should preserve the space before "world"
1229        assert_eq!(text, " world");
1230    }
1231
1232    #[test]
1233    fn test_dw_includes_trailing_space() {
1234        let mut editor = VimLineEditor::new();
1235        let mut text = String::from("hello world");
1236
1237        // dw should delete "hello " (including trailing space)
1238        editor.handle_key(Key::char('d'), &text);
1239        let result = editor.handle_key(Key::char('w'), &text);
1240
1241        assert_eq!(editor.mode(), Mode::Normal);
1242
1243        // Apply edits
1244        for edit in result.edits.into_iter().rev() {
1245            edit.apply(&mut text);
1246        }
1247        assert_eq!(text, "world");
1248    }
1249
1250    #[test]
1251    fn test_paste_at_empty_buffer() {
1252        let mut editor = VimLineEditor::new();
1253
1254        // First yank something from non-empty text
1255        let yank_text = String::from("test");
1256        editor.handle_key(Key::char('y'), &yank_text);
1257        editor.handle_key(Key::char('w'), &yank_text);
1258
1259        // Now paste into empty buffer
1260        let mut text = String::new();
1261        editor.set_cursor(0, &text);
1262        let result = editor.handle_key(Key::char('p'), &text);
1263
1264        for edit in result.edits.into_iter().rev() {
1265            edit.apply(&mut text);
1266        }
1267        assert_eq!(text, "test");
1268    }
1269
1270    #[test]
1271    fn test_dollar_cursor_on_last_char() {
1272        let mut editor = VimLineEditor::new();
1273        let text = "abc";
1274
1275        // $ should place cursor ON 'c' (index 2), not past it (index 3)
1276        editor.handle_key(Key::char('$'), text);
1277        assert_eq!(editor.cursor(), 2);
1278
1279        // Single character line
1280        let text = "x";
1281        editor.set_cursor(0, text);
1282        editor.handle_key(Key::char('$'), text);
1283        assert_eq!(editor.cursor(), 0); // Stay on the only char
1284    }
1285
1286    #[test]
1287    fn test_x_delete_last_char_moves_cursor_left() {
1288        let mut editor = VimLineEditor::new();
1289        let mut text = String::from("abc");
1290
1291        // Move to last char
1292        editor.handle_key(Key::char('$'), &text);
1293        assert_eq!(editor.cursor(), 2); // On 'c'
1294
1295        // Delete with x
1296        let result = editor.handle_key(Key::char('x'), &text);
1297        for edit in result.edits.into_iter().rev() {
1298            edit.apply(&mut text);
1299        }
1300
1301        assert_eq!(text, "ab");
1302        // Cursor should move left to stay on valid char
1303        assert_eq!(editor.cursor(), 1); // On 'b'
1304    }
1305
1306    #[test]
1307    fn test_x_delete_middle_char_cursor_stays() {
1308        let mut editor = VimLineEditor::new();
1309        let mut text = String::from("abc");
1310
1311        // Position on 'b' (index 1)
1312        editor.handle_key(Key::char('l'), &text);
1313        assert_eq!(editor.cursor(), 1);
1314
1315        // Delete with x
1316        let result = editor.handle_key(Key::char('x'), &text);
1317        for edit in result.edits.into_iter().rev() {
1318            edit.apply(&mut text);
1319        }
1320
1321        assert_eq!(text, "ac");
1322        // Cursor stays at same position (now on 'c')
1323        assert_eq!(editor.cursor(), 1);
1324    }
1325
1326    #[test]
1327    fn test_percent_bracket_matching() {
1328        let mut editor = VimLineEditor::new();
1329        let text = "(hello world)";
1330
1331        // Cursor starts at position 0, on '('
1332        assert_eq!(editor.cursor(), 0);
1333
1334        // Press '%' to jump to matching ')'
1335        editor.handle_key(Key::char('%'), text);
1336        assert_eq!(editor.cursor(), 12); // Position of ')'
1337
1338        // Press '%' again to jump back to '('
1339        editor.handle_key(Key::char('%'), text);
1340        assert_eq!(editor.cursor(), 0);
1341    }
1342
1343    #[test]
1344    fn test_percent_nested_brackets() {
1345        let mut editor = VimLineEditor::new();
1346        let text = "([{<>}])";
1347
1348        // Start on '('
1349        editor.handle_key(Key::char('%'), text);
1350        assert_eq!(editor.cursor(), 7); // Matching ')'
1351
1352        // Move to '[' at position 1
1353        editor.set_cursor(1, text);
1354        editor.handle_key(Key::char('%'), text);
1355        assert_eq!(editor.cursor(), 6); // Matching ']'
1356
1357        // Move to '{' at position 2
1358        editor.set_cursor(2, text);
1359        editor.handle_key(Key::char('%'), text);
1360        assert_eq!(editor.cursor(), 5); // Matching '}'
1361
1362        // Move to '<' at position 3
1363        editor.set_cursor(3, text);
1364        editor.handle_key(Key::char('%'), text);
1365        assert_eq!(editor.cursor(), 4); // Matching '>'
1366    }
1367
1368    #[test]
1369    fn test_percent_on_non_bracket() {
1370        let mut editor = VimLineEditor::new();
1371        let text = "hello";
1372
1373        // Start at position 0 on 'h'
1374        let orig_cursor = editor.cursor();
1375        editor.handle_key(Key::char('%'), text);
1376        // Cursor should not move when not on a bracket
1377        assert_eq!(editor.cursor(), orig_cursor);
1378    }
1379}