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    /// Delete character at cursor (x).
263    fn delete_char(&mut self, text: &str) -> EditResult {
264        if self.cursor >= text.len() {
265            return EditResult::none();
266        }
267
268        let start = self.cursor;
269
270        // Find the end of the current character
271        let mut end = self.cursor + 1;
272        while end < text.len() && !text.is_char_boundary(end) {
273            end += 1;
274        }
275
276        let deleted = text[start..end].to_string();
277
278        // If we're deleting the last character, move cursor left
279        // (In Normal mode, cursor must always be ON a character)
280        if end >= text.len() && self.cursor > 0 {
281            self.move_left(text);
282        }
283
284        EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
285    }
286
287    /// Delete to end of line (D).
288    fn delete_to_end(&mut self, text: &str) -> EditResult {
289        let end = text[self.cursor..]
290            .find('\n')
291            .map(|i| self.cursor + i)
292            .unwrap_or(text.len());
293
294        if self.cursor >= end {
295            return EditResult::none();
296        }
297
298        let deleted = text[self.cursor..end].to_string();
299        EditResult::edit_and_yank(
300            TextEdit::Delete {
301                start: self.cursor,
302                end,
303            },
304            deleted,
305        )
306    }
307
308    /// Delete entire current line (dd).
309    fn delete_line(&mut self, text: &str) -> EditResult {
310        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
311
312        let line_end = text[self.cursor..]
313            .find('\n')
314            .map(|i| self.cursor + i + 1) // Include the newline
315            .unwrap_or(text.len());
316
317        // If this is the only line and no newline, include leading newline if any
318        let (start, end) = if line_start == 0 && line_end == text.len() {
319            (0, text.len())
320        } else if line_end == text.len() && line_start > 0 {
321            // Last line - delete the preceding newline instead
322            (line_start - 1, text.len())
323        } else {
324            (line_start, line_end)
325        };
326
327        let deleted = text[start..end].to_string();
328        self.cursor = start;
329
330        EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
331    }
332
333    /// Paste after cursor (p).
334    fn paste_after(&mut self, text: &str) -> EditResult {
335        if self.yank_buffer.is_empty() {
336            return EditResult::none();
337        }
338
339        let insert_pos = (self.cursor + 1).min(text.len());
340        let to_insert = self.yank_buffer.clone();
341        // Position cursor at end of pasted text, safely handling edge cases
342        self.cursor = (insert_pos + to_insert.len())
343            .saturating_sub(1)
344            .min(text.len() + to_insert.len());
345
346        EditResult::edit(TextEdit::Insert {
347            at: insert_pos,
348            text: to_insert,
349        })
350    }
351
352    /// Paste before cursor (P).
353    fn paste_before(&mut self, text: &str) -> EditResult {
354        if self.yank_buffer.is_empty() {
355            return EditResult::none();
356        }
357
358        let to_insert = self.yank_buffer.clone();
359        let insert_pos = self.cursor.min(text.len());
360        // Position cursor at end of pasted text
361        self.cursor = (insert_pos + to_insert.len()).min(text.len() + to_insert.len());
362
363        EditResult::edit(TextEdit::Insert {
364            at: insert_pos,
365            text: to_insert,
366        })
367    }
368
369    /// Handle key in Normal mode.
370    fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
371        match key.code {
372            // Mode switching
373            KeyCode::Char('i') => {
374                self.mode = Mode::Insert;
375                EditResult::none()
376            }
377            KeyCode::Char('a') => {
378                self.mode = Mode::Insert;
379                self.move_right(text);
380                EditResult::none()
381            }
382            KeyCode::Char('A') => {
383                self.mode = Mode::Insert;
384                self.move_line_end_insert(text);
385                EditResult::none()
386            }
387            KeyCode::Char('I') => {
388                self.mode = Mode::Insert;
389                self.move_first_non_blank(text);
390                EditResult::none()
391            }
392            KeyCode::Char('o') => {
393                self.mode = Mode::Insert;
394                self.move_line_end(text);
395                let pos = self.cursor;
396                self.cursor = pos + 1;
397                EditResult::edit(TextEdit::Insert {
398                    at: pos,
399                    text: "\n".to_string(),
400                })
401            }
402            KeyCode::Char('O') => {
403                self.mode = Mode::Insert;
404                self.move_line_start(text);
405                let pos = self.cursor;
406                EditResult::edit(TextEdit::Insert {
407                    at: pos,
408                    text: "\n".to_string(),
409                })
410            }
411
412            // Visual mode
413            KeyCode::Char('v') => {
414                self.mode = Mode::Visual;
415                self.visual_anchor = Some(self.cursor);
416                EditResult::none()
417            }
418
419            // Motions
420            KeyCode::Char('h') | KeyCode::Left => {
421                self.move_left(text);
422                EditResult::cursor_only()
423            }
424            KeyCode::Char('l') | KeyCode::Right => {
425                self.move_right(text);
426                EditResult::cursor_only()
427            }
428            KeyCode::Char('j') => {
429                self.move_down(text);
430                EditResult::cursor_only()
431            }
432            KeyCode::Char('k') => {
433                self.move_up(text);
434                EditResult::cursor_only()
435            }
436            KeyCode::Char('0') | KeyCode::Home => {
437                self.move_line_start(text);
438                EditResult::cursor_only()
439            }
440            KeyCode::Char('^') => {
441                self.move_first_non_blank(text);
442                EditResult::cursor_only()
443            }
444            KeyCode::Char('$') | KeyCode::End => {
445                self.move_line_end(text);
446                EditResult::cursor_only()
447            }
448            KeyCode::Char('w') => {
449                self.move_word_forward(text);
450                EditResult::cursor_only()
451            }
452            KeyCode::Char('b') => {
453                self.move_word_backward(text);
454                EditResult::cursor_only()
455            }
456            KeyCode::Char('e') => {
457                self.move_word_end(text);
458                EditResult::cursor_only()
459            }
460
461            // Cancel (Ctrl+C)
462            KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
463
464            // Operators (enter pending mode)
465            KeyCode::Char('d') => {
466                self.mode = Mode::OperatorPending(Operator::Delete);
467                EditResult::none()
468            }
469            KeyCode::Char('c') => {
470                self.mode = Mode::OperatorPending(Operator::Change);
471                EditResult::none()
472            }
473            KeyCode::Char('y') => {
474                self.mode = Mode::OperatorPending(Operator::Yank);
475                EditResult::none()
476            }
477
478            // Direct deletions
479            KeyCode::Char('x') => self.delete_char(text),
480            KeyCode::Char('D') => self.delete_to_end(text),
481            KeyCode::Char('C') => {
482                self.mode = Mode::Insert;
483                self.delete_to_end(text)
484            }
485
486            // Replace character (r)
487            KeyCode::Char('r') => {
488                self.mode = Mode::ReplaceChar;
489                EditResult::none()
490            }
491
492            // Paste
493            KeyCode::Char('p') => self.paste_after(text),
494            KeyCode::Char('P') => self.paste_before(text),
495
496            // History (arrows only)
497            KeyCode::Up => EditResult::action(Action::HistoryPrev),
498            KeyCode::Down => EditResult::action(Action::HistoryNext),
499
500            // Submit
501            KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
502
503            // Newline (Shift+Enter)
504            KeyCode::Enter if key.shift => {
505                self.mode = Mode::Insert;
506                let pos = self.cursor;
507                self.cursor = pos + 1;
508                EditResult::edit(TextEdit::Insert {
509                    at: pos,
510                    text: "\n".to_string(),
511                })
512            }
513
514            // Escape in Normal mode is a no-op (safe to spam like in vim)
515            // Use Ctrl+C to cancel/quit
516            KeyCode::Escape => EditResult::none(),
517
518            _ => EditResult::none(),
519        }
520    }
521
522    /// Handle key in Insert mode.
523    fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
524        match key.code {
525            KeyCode::Escape => {
526                self.mode = Mode::Normal;
527                // Move cursor left like vim does when exiting insert
528                if self.cursor > 0 {
529                    self.move_left(text);
530                }
531                EditResult::none()
532            }
533
534            // Ctrl+C exits insert mode
535            KeyCode::Char('c') if key.ctrl => {
536                self.mode = Mode::Normal;
537                EditResult::none()
538            }
539
540            KeyCode::Char(c) if !key.ctrl && !key.alt => {
541                let pos = self.cursor;
542                self.cursor = pos + c.len_utf8();
543                EditResult::edit(TextEdit::Insert {
544                    at: pos,
545                    text: c.to_string(),
546                })
547            }
548
549            KeyCode::Backspace => {
550                if self.cursor == 0 {
551                    return EditResult::none();
552                }
553                let mut start = self.cursor - 1;
554                while start > 0 && !text.is_char_boundary(start) {
555                    start -= 1;
556                }
557                let end = self.cursor; // Save original cursor before updating
558                self.cursor = start;
559                EditResult::edit(TextEdit::Delete { start, end })
560            }
561
562            KeyCode::Delete => self.delete_char(text),
563
564            KeyCode::Left => {
565                self.move_left(text);
566                EditResult::cursor_only()
567            }
568            KeyCode::Right => {
569                self.move_right(text);
570                EditResult::cursor_only()
571            }
572            KeyCode::Up => {
573                self.move_up(text);
574                EditResult::cursor_only()
575            }
576            KeyCode::Down => {
577                self.move_down(text);
578                EditResult::cursor_only()
579            }
580            KeyCode::Home => {
581                self.move_line_start(text);
582                EditResult::cursor_only()
583            }
584            KeyCode::End => {
585                // In Insert mode, cursor can go past the last character
586                self.move_line_end_insert(text);
587                EditResult::cursor_only()
588            }
589
590            // Enter inserts newline in insert mode
591            KeyCode::Enter => {
592                let pos = self.cursor;
593                self.cursor = pos + 1;
594                EditResult::edit(TextEdit::Insert {
595                    at: pos,
596                    text: "\n".to_string(),
597                })
598            }
599
600            _ => EditResult::none(),
601        }
602    }
603
604    /// Handle key in OperatorPending mode.
605    fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
606        // First, handle escape to cancel
607        if key.code == KeyCode::Escape {
608            self.mode = Mode::Normal;
609            return EditResult::none();
610        }
611
612        // Handle doubled operator (dd, cc, yy) - operates on whole line
613        let is_line_op = matches!(
614            (op, key.code),
615            (Operator::Delete, KeyCode::Char('d'))
616                | (Operator::Change, KeyCode::Char('c'))
617                | (Operator::Yank, KeyCode::Char('y'))
618        );
619
620        if is_line_op {
621            self.mode = Mode::Normal;
622            return self.apply_operator_line(op, text);
623        }
624
625        // Handle motion
626        let start = self.cursor;
627        match key.code {
628            KeyCode::Char('w') => {
629                // Special case: cw behaves like ce (change to end of word, not including space)
630                // This is a vim quirk for historical compatibility
631                if op == Operator::Change {
632                    self.move_word_end(text);
633                    // Include the character at cursor
634                    if self.cursor < text.len() {
635                        self.cursor += 1;
636                    }
637                } else {
638                    self.move_word_forward(text);
639                }
640            }
641            KeyCode::Char('b') => self.move_word_backward(text),
642            KeyCode::Char('e') => {
643                self.move_word_end(text);
644                // Include the character at cursor for delete/change
645                if self.cursor < text.len() {
646                    self.cursor += 1;
647                }
648            }
649            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
650            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
651            KeyCode::Char('^') => self.move_first_non_blank(text),
652            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
653            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
654            KeyCode::Char('j') => self.move_down(text),
655            KeyCode::Char('k') => self.move_up(text),
656            _ => {
657                // Unknown motion, cancel
658                self.mode = Mode::Normal;
659                return EditResult::none();
660            }
661        }
662
663        let end = self.cursor;
664        self.mode = Mode::Normal;
665
666        if start == end {
667            return EditResult::none();
668        }
669
670        let (range_start, range_end) = if start < end {
671            (start, end)
672        } else {
673            (end, start)
674        };
675
676        self.apply_operator(op, range_start, range_end, text)
677    }
678
679    /// Apply an operator to a range.
680    fn apply_operator(&mut self, op: Operator, start: usize, end: usize, text: &str) -> EditResult {
681        let affected = text[start..end].to_string();
682        self.yank_buffer = affected.clone();
683        self.cursor = start;
684
685        match op {
686            Operator::Delete => {
687                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
688            }
689            Operator::Change => {
690                self.mode = Mode::Insert;
691                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
692            }
693            Operator::Yank => {
694                // Just yank, no edit
695                EditResult {
696                    yanked: Some(affected),
697                    ..Default::default()
698                }
699            }
700        }
701    }
702
703    /// Apply an operator to the whole line.
704    fn apply_operator_line(&mut self, op: Operator, text: &str) -> EditResult {
705        match op {
706            Operator::Delete => self.delete_line(text),
707            Operator::Change => {
708                let result = self.delete_line(text);
709                self.mode = Mode::Insert;
710                result
711            }
712            Operator::Yank => {
713                let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
714                let line_end = text[self.cursor..]
715                    .find('\n')
716                    .map(|i| self.cursor + i + 1)
717                    .unwrap_or(text.len());
718                let line = text[line_start..line_end].to_string();
719                self.yank_buffer = line.clone();
720                EditResult {
721                    yanked: Some(line),
722                    ..Default::default()
723                }
724            }
725        }
726    }
727
728    /// Handle key in Visual mode.
729    fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
730        match key.code {
731            KeyCode::Escape => {
732                self.mode = Mode::Normal;
733                self.visual_anchor = None;
734                EditResult::none()
735            }
736
737            // Motions extend selection
738            KeyCode::Char('h') | KeyCode::Left => {
739                self.move_left(text);
740                EditResult::cursor_only()
741            }
742            KeyCode::Char('l') | KeyCode::Right => {
743                self.move_right(text);
744                EditResult::cursor_only()
745            }
746            KeyCode::Char('j') => {
747                self.move_down(text);
748                EditResult::cursor_only()
749            }
750            KeyCode::Char('k') => {
751                self.move_up(text);
752                EditResult::cursor_only()
753            }
754            KeyCode::Char('w') => {
755                self.move_word_forward(text);
756                EditResult::cursor_only()
757            }
758            KeyCode::Char('b') => {
759                self.move_word_backward(text);
760                EditResult::cursor_only()
761            }
762            KeyCode::Char('e') => {
763                self.move_word_end(text);
764                EditResult::cursor_only()
765            }
766            KeyCode::Char('0') | KeyCode::Home => {
767                self.move_line_start(text);
768                EditResult::cursor_only()
769            }
770            KeyCode::Char('$') | KeyCode::End => {
771                self.move_line_end(text);
772                EditResult::cursor_only()
773            }
774
775            // Operators on selection
776            KeyCode::Char('d') | KeyCode::Char('x') => {
777                let (start, end) = self.selection_range();
778                self.mode = Mode::Normal;
779                self.visual_anchor = None;
780                self.apply_operator(Operator::Delete, start, end, text)
781            }
782            KeyCode::Char('c') => {
783                let (start, end) = self.selection_range();
784                self.mode = Mode::Normal;
785                self.visual_anchor = None;
786                self.apply_operator(Operator::Change, start, end, text)
787            }
788            KeyCode::Char('y') => {
789                let (start, end) = self.selection_range();
790                self.mode = Mode::Normal;
791                self.visual_anchor = None;
792                self.apply_operator(Operator::Yank, start, end, text)
793            }
794
795            _ => EditResult::none(),
796        }
797    }
798
799    /// Handle key in ReplaceChar mode (waiting for character after 'r').
800    fn handle_replace_char(&mut self, key: Key, text: &str) -> EditResult {
801        self.mode = Mode::Normal;
802
803        match key.code {
804            KeyCode::Escape => EditResult::none(),
805            KeyCode::Char(c) if !key.ctrl && !key.alt => {
806                // Replace character at cursor
807                if self.cursor >= text.len() {
808                    return EditResult::none();
809                }
810
811                // Find the end of the current character
812                let mut end = self.cursor + 1;
813                while end < text.len() && !text.is_char_boundary(end) {
814                    end += 1;
815                }
816
817                // Delete current char and insert new one
818                // Note: edits are applied in reverse order, so Insert comes first in vec
819                EditResult {
820                    edits: vec![
821                        TextEdit::Insert {
822                            at: self.cursor,
823                            text: c.to_string(),
824                        },
825                        TextEdit::Delete {
826                            start: self.cursor,
827                            end,
828                        },
829                    ],
830                    ..Default::default()
831                }
832            }
833            _ => EditResult::none(),
834        }
835    }
836
837    /// Get the selection range (ordered).
838    fn selection_range(&self) -> (usize, usize) {
839        let anchor = self.visual_anchor.unwrap_or(self.cursor);
840        if self.cursor < anchor {
841            (self.cursor, anchor)
842        } else {
843            (anchor, self.cursor + 1) // Include cursor position
844        }
845    }
846}
847
848impl LineEditor for VimLineEditor {
849    fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
850        self.clamp_cursor(text);
851
852        let result = match self.mode {
853            Mode::Normal => self.handle_normal(key, text),
854            Mode::Insert => self.handle_insert(key, text),
855            Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
856            Mode::Visual => self.handle_visual(key, text),
857            Mode::ReplaceChar => self.handle_replace_char(key, text),
858        };
859
860        // Store yanked text
861        if let Some(ref yanked) = result.yanked {
862            self.yank_buffer = yanked.clone();
863        }
864
865        result
866    }
867
868    fn cursor(&self) -> usize {
869        self.cursor
870    }
871
872    fn status(&self) -> &str {
873        match self.mode {
874            Mode::Normal => "NORMAL",
875            Mode::Insert => "INSERT",
876            Mode::OperatorPending(Operator::Delete) => "d...",
877            Mode::OperatorPending(Operator::Change) => "c...",
878            Mode::OperatorPending(Operator::Yank) => "y...",
879            Mode::Visual => "VISUAL",
880            Mode::ReplaceChar => "r...",
881        }
882    }
883
884    fn selection(&self) -> Option<Range<usize>> {
885        if self.mode == Mode::Visual {
886            let (start, end) = self.selection_range();
887            Some(start..end)
888        } else {
889            None
890        }
891    }
892
893    fn reset(&mut self) {
894        self.cursor = 0;
895        self.mode = Mode::Normal;
896        self.visual_anchor = None;
897        // Keep yank buffer across resets
898    }
899
900    fn set_cursor(&mut self, pos: usize, text: &str) {
901        // Clamp to text length and ensure we're at a char boundary
902        let pos = pos.min(text.len());
903        self.cursor = if text.is_char_boundary(pos) {
904            pos
905        } else {
906            // Walk backwards to find a valid boundary
907            let mut p = pos;
908            while p > 0 && !text.is_char_boundary(p) {
909                p -= 1;
910            }
911            p
912        };
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919
920    #[test]
921    fn test_basic_motion() {
922        let mut editor = VimLineEditor::new();
923        let text = "hello world";
924
925        // Move right with 'l'
926        editor.handle_key(Key::char('l'), text);
927        assert_eq!(editor.cursor(), 1);
928
929        // Move right with 'w'
930        editor.handle_key(Key::char('w'), text);
931        assert_eq!(editor.cursor(), 6); // Start of "world"
932
933        // Move to end with '$' - cursor should be ON the last char, not past it
934        editor.handle_key(Key::char('$'), text);
935        assert_eq!(editor.cursor(), 10); // 'd' is at index 10
936
937        // Move to start with '0'
938        editor.handle_key(Key::char('0'), text);
939        assert_eq!(editor.cursor(), 0);
940    }
941
942    #[test]
943    fn test_mode_switching() {
944        let mut editor = VimLineEditor::new();
945        let text = "hello";
946
947        assert_eq!(editor.mode(), Mode::Normal);
948
949        editor.handle_key(Key::char('i'), text);
950        assert_eq!(editor.mode(), Mode::Insert);
951
952        editor.handle_key(Key::code(KeyCode::Escape), text);
953        assert_eq!(editor.mode(), Mode::Normal);
954    }
955
956    #[test]
957    fn test_delete_word() {
958        let mut editor = VimLineEditor::new();
959        let text = "hello world";
960
961        // dw should delete "hello "
962        editor.handle_key(Key::char('d'), text);
963        editor.handle_key(Key::char('w'), text);
964
965        // Check we're back in Normal mode
966        assert_eq!(editor.mode(), Mode::Normal);
967    }
968
969    #[test]
970    fn test_insert_char() {
971        let mut editor = VimLineEditor::new();
972        let text = "";
973
974        editor.handle_key(Key::char('i'), text);
975        let result = editor.handle_key(Key::char('x'), text);
976
977        assert_eq!(result.edits.len(), 1);
978        match &result.edits[0] {
979            TextEdit::Insert { at, text } => {
980                assert_eq!(*at, 0);
981                assert_eq!(text, "x");
982            }
983            _ => panic!("Expected Insert"),
984        }
985    }
986
987    #[test]
988    fn test_visual_mode() {
989        let mut editor = VimLineEditor::new();
990        let text = "hello world";
991
992        // Enter visual mode
993        editor.handle_key(Key::char('v'), text);
994        assert_eq!(editor.mode(), Mode::Visual);
995
996        // Extend selection
997        editor.handle_key(Key::char('w'), text);
998
999        // Selection should cover from 0 to cursor
1000        let sel = editor.selection().unwrap();
1001        assert_eq!(sel.start, 0);
1002        assert!(sel.end > 0);
1003    }
1004
1005    #[test]
1006    fn test_backspace_ascii() {
1007        let mut editor = VimLineEditor::new();
1008        let mut text = String::from("abc");
1009
1010        // Enter insert mode and go to end
1011        editor.handle_key(Key::char('i'), &text);
1012        editor.handle_key(Key::code(KeyCode::End), &text);
1013        assert_eq!(editor.cursor(), 3);
1014
1015        // Backspace should delete 'c'
1016        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
1017        for edit in result.edits.into_iter().rev() {
1018            edit.apply(&mut text);
1019        }
1020        assert_eq!(text, "ab");
1021        assert_eq!(editor.cursor(), 2);
1022    }
1023
1024    #[test]
1025    fn test_backspace_unicode() {
1026        let mut editor = VimLineEditor::new();
1027        let mut text = String::from("a😀b");
1028
1029        // Enter insert mode and position after emoji (byte position 5: 1 + 4)
1030        editor.handle_key(Key::char('i'), &text);
1031        editor.handle_key(Key::code(KeyCode::End), &text);
1032        editor.handle_key(Key::code(KeyCode::Left), &text); // Move before 'b'
1033        assert_eq!(editor.cursor(), 5); // After the 4-byte emoji
1034
1035        // Backspace should delete entire emoji (4 bytes), not just 1 byte
1036        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
1037        for edit in result.edits.into_iter().rev() {
1038            edit.apply(&mut text);
1039        }
1040        assert_eq!(text, "ab");
1041        assert_eq!(editor.cursor(), 1);
1042    }
1043
1044    #[test]
1045    fn test_yank_and_paste() {
1046        let mut editor = VimLineEditor::new();
1047        let mut text = String::from("hello world");
1048
1049        // Yank word with yw
1050        editor.handle_key(Key::char('y'), &text);
1051        let result = editor.handle_key(Key::char('w'), &text);
1052        assert!(result.yanked.is_some());
1053        assert_eq!(result.yanked.unwrap(), "hello ");
1054
1055        // Move to end and paste
1056        editor.handle_key(Key::char('$'), &text);
1057        let result = editor.handle_key(Key::char('p'), &text);
1058
1059        for edit in result.edits.into_iter().rev() {
1060            edit.apply(&mut text);
1061        }
1062        assert_eq!(text, "hello worldhello ");
1063    }
1064
1065    #[test]
1066    fn test_visual_mode_delete() {
1067        let mut editor = VimLineEditor::new();
1068        let mut text = String::from("hello world");
1069
1070        // Enter visual mode at position 0
1071        editor.handle_key(Key::char('v'), &text);
1072        assert_eq!(editor.mode(), Mode::Visual);
1073
1074        // Extend selection with 'e' motion to end of word (stays on 'o' of hello)
1075        editor.handle_key(Key::char('e'), &text);
1076
1077        // Delete selection with d - deletes "hello"
1078        let result = editor.handle_key(Key::char('d'), &text);
1079
1080        for edit in result.edits.into_iter().rev() {
1081            edit.apply(&mut text);
1082        }
1083        assert_eq!(text, " world");
1084        assert_eq!(editor.mode(), Mode::Normal);
1085    }
1086
1087    #[test]
1088    fn test_operator_pending_escape() {
1089        let mut editor = VimLineEditor::new();
1090        let text = "hello world";
1091
1092        // Start delete operator
1093        editor.handle_key(Key::char('d'), text);
1094        assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
1095
1096        // Cancel with Escape
1097        editor.handle_key(Key::code(KeyCode::Escape), text);
1098        assert_eq!(editor.mode(), Mode::Normal);
1099    }
1100
1101    #[test]
1102    fn test_replace_char() {
1103        let mut editor = VimLineEditor::new();
1104        let mut text = String::from("hello");
1105
1106        // Press 'r' then 'x' to replace 'h' with 'x'
1107        editor.handle_key(Key::char('r'), &text);
1108        assert_eq!(editor.mode(), Mode::ReplaceChar);
1109
1110        let result = editor.handle_key(Key::char('x'), &text);
1111        assert_eq!(editor.mode(), Mode::Normal);
1112
1113        // Apply edits
1114        for edit in result.edits.into_iter().rev() {
1115            edit.apply(&mut text);
1116        }
1117        assert_eq!(text, "xello");
1118    }
1119
1120    #[test]
1121    fn test_replace_char_escape() {
1122        let mut editor = VimLineEditor::new();
1123        let text = "hello";
1124
1125        // Press 'r' then Escape should cancel
1126        editor.handle_key(Key::char('r'), text);
1127        assert_eq!(editor.mode(), Mode::ReplaceChar);
1128
1129        editor.handle_key(Key::code(KeyCode::Escape), text);
1130        assert_eq!(editor.mode(), Mode::Normal);
1131    }
1132
1133    #[test]
1134    fn test_cw_no_trailing_space() {
1135        let mut editor = VimLineEditor::new();
1136        let mut text = String::from("hello world");
1137
1138        // cw should delete "hello" (not "hello ") and enter insert mode
1139        editor.handle_key(Key::char('c'), &text);
1140        let result = editor.handle_key(Key::char('w'), &text);
1141
1142        assert_eq!(editor.mode(), Mode::Insert);
1143
1144        // Apply edits
1145        for edit in result.edits.into_iter().rev() {
1146            edit.apply(&mut text);
1147        }
1148        // Should preserve the space before "world"
1149        assert_eq!(text, " world");
1150    }
1151
1152    #[test]
1153    fn test_dw_includes_trailing_space() {
1154        let mut editor = VimLineEditor::new();
1155        let mut text = String::from("hello world");
1156
1157        // dw should delete "hello " (including trailing space)
1158        editor.handle_key(Key::char('d'), &text);
1159        let result = editor.handle_key(Key::char('w'), &text);
1160
1161        assert_eq!(editor.mode(), Mode::Normal);
1162
1163        // Apply edits
1164        for edit in result.edits.into_iter().rev() {
1165            edit.apply(&mut text);
1166        }
1167        assert_eq!(text, "world");
1168    }
1169
1170    #[test]
1171    fn test_paste_at_empty_buffer() {
1172        let mut editor = VimLineEditor::new();
1173
1174        // First yank something from non-empty text
1175        let yank_text = String::from("test");
1176        editor.handle_key(Key::char('y'), &yank_text);
1177        editor.handle_key(Key::char('w'), &yank_text);
1178
1179        // Now paste into empty buffer
1180        let mut text = String::new();
1181        editor.set_cursor(0, &text);
1182        let result = editor.handle_key(Key::char('p'), &text);
1183
1184        for edit in result.edits.into_iter().rev() {
1185            edit.apply(&mut text);
1186        }
1187        assert_eq!(text, "test");
1188    }
1189
1190    #[test]
1191    fn test_dollar_cursor_on_last_char() {
1192        let mut editor = VimLineEditor::new();
1193        let text = "abc";
1194
1195        // $ should place cursor ON 'c' (index 2), not past it (index 3)
1196        editor.handle_key(Key::char('$'), text);
1197        assert_eq!(editor.cursor(), 2);
1198
1199        // Single character line
1200        let text = "x";
1201        editor.set_cursor(0, text);
1202        editor.handle_key(Key::char('$'), text);
1203        assert_eq!(editor.cursor(), 0); // Stay on the only char
1204    }
1205
1206    #[test]
1207    fn test_x_delete_last_char_moves_cursor_left() {
1208        let mut editor = VimLineEditor::new();
1209        let mut text = String::from("abc");
1210
1211        // Move to last char
1212        editor.handle_key(Key::char('$'), &text);
1213        assert_eq!(editor.cursor(), 2); // On 'c'
1214
1215        // Delete with x
1216        let result = editor.handle_key(Key::char('x'), &text);
1217        for edit in result.edits.into_iter().rev() {
1218            edit.apply(&mut text);
1219        }
1220
1221        assert_eq!(text, "ab");
1222        // Cursor should move left to stay on valid char
1223        assert_eq!(editor.cursor(), 1); // On 'b'
1224    }
1225
1226    #[test]
1227    fn test_x_delete_middle_char_cursor_stays() {
1228        let mut editor = VimLineEditor::new();
1229        let mut text = String::from("abc");
1230
1231        // Position on 'b' (index 1)
1232        editor.handle_key(Key::char('l'), &text);
1233        assert_eq!(editor.cursor(), 1);
1234
1235        // Delete with x
1236        let result = editor.handle_key(Key::char('x'), &text);
1237        for edit in result.edits.into_iter().rev() {
1238            edit.apply(&mut text);
1239        }
1240
1241        assert_eq!(text, "ac");
1242        // Cursor stays at same position (now on 'c')
1243        assert_eq!(editor.cursor(), 1);
1244    }
1245}