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}
15
16/// Operators that wait for a motion.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Operator {
19    Delete,
20    Change,
21    Yank,
22}
23
24/// A vim-style line editor.
25///
26/// Implements modal editing with Normal, Insert, Visual, and OperatorPending modes.
27/// Designed for single "one-shot" inputs that may span multiple lines.
28#[derive(Debug, Clone)]
29pub struct VimLineEditor {
30    cursor: usize,
31    mode: Mode,
32    /// Anchor point for visual selection (cursor is the other end).
33    visual_anchor: Option<usize>,
34    /// Last yanked text (for paste).
35    yank_buffer: String,
36}
37
38impl Default for VimLineEditor {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl VimLineEditor {
45    /// Create a new editor in Normal mode.
46    pub fn new() -> Self {
47        Self {
48            cursor: 0,
49            mode: Mode::Normal,
50            visual_anchor: None,
51            yank_buffer: String::new(),
52        }
53    }
54
55    /// Get the current mode.
56    pub fn mode(&self) -> Mode {
57        self.mode
58    }
59
60    /// Clamp cursor to valid range for the given text.
61    fn clamp_cursor(&mut self, text: &str) {
62        self.cursor = self.cursor.min(text.len());
63    }
64
65    /// Move cursor left by one character.
66    fn move_left(&mut self, text: &str) {
67        if self.cursor > 0 {
68            // Find the previous character boundary
69            let mut new_pos = self.cursor - 1;
70            while new_pos > 0 && !text.is_char_boundary(new_pos) {
71                new_pos -= 1;
72            }
73            self.cursor = new_pos;
74        }
75    }
76
77    /// Move cursor right by one character.
78    fn move_right(&mut self, text: &str) {
79        if self.cursor < text.len() {
80            // Find the next character boundary
81            let mut new_pos = self.cursor + 1;
82            while new_pos < text.len() && !text.is_char_boundary(new_pos) {
83                new_pos += 1;
84            }
85            self.cursor = new_pos;
86        }
87    }
88
89    /// Move cursor to start of line (0).
90    fn move_line_start(&mut self, text: &str) {
91        // Find the start of the current line
92        self.cursor = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
93    }
94
95    /// Move cursor to first non-whitespace of line (^).
96    fn move_first_non_blank(&mut self, text: &str) {
97        self.move_line_start(text);
98        // Skip whitespace
99        let line_start = self.cursor;
100        for (i, c) in text[line_start..].char_indices() {
101            if c == '\n' || !c.is_whitespace() {
102                self.cursor = line_start + i;
103                return;
104            }
105        }
106    }
107
108    /// Move cursor to end of line ($).
109    fn move_line_end(&mut self, text: &str) {
110        // Find the end of the current line
111        self.cursor = text[self.cursor..]
112            .find('\n')
113            .map(|i| self.cursor + i)
114            .unwrap_or(text.len());
115    }
116
117    /// Move cursor forward by word (w).
118    fn move_word_forward(&mut self, text: &str) {
119        let bytes = text.as_bytes();
120        let mut pos = self.cursor;
121
122        // Skip current word (non-whitespace)
123        while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
124            pos += 1;
125        }
126        // Skip whitespace
127        while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
128            pos += 1;
129        }
130
131        self.cursor = pos;
132    }
133
134    /// Move cursor backward by word (b).
135    fn move_word_backward(&mut self, text: &str) {
136        let bytes = text.as_bytes();
137        let mut pos = self.cursor;
138
139        // Skip whitespace before cursor
140        while pos > 0 && bytes[pos - 1].is_ascii_whitespace() {
141            pos -= 1;
142        }
143        // Skip word (non-whitespace)
144        while pos > 0 && !bytes[pos - 1].is_ascii_whitespace() {
145            pos -= 1;
146        }
147
148        self.cursor = pos;
149    }
150
151    /// Move cursor to end of word (e).
152    fn move_word_end(&mut self, text: &str) {
153        let bytes = text.as_bytes();
154        let mut pos = self.cursor;
155
156        // Move at least one character
157        if pos < bytes.len() {
158            pos += 1;
159        }
160        // Skip whitespace
161        while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
162            pos += 1;
163        }
164        // Move to end of word
165        while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
166            pos += 1;
167        }
168        // Back up one (end of word, not start of next)
169        if pos > self.cursor + 1 {
170            pos -= 1;
171        }
172
173        self.cursor = pos;
174    }
175
176    /// Move cursor up one line (k).
177    fn move_up(&mut self, text: &str) {
178        // Find current line start
179        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
180
181        if line_start == 0 {
182            // Already on first line, can't go up
183            return;
184        }
185
186        // Column offset from line start
187        let col = self.cursor - line_start;
188
189        // Find previous line start
190        let prev_line_start = text[..line_start - 1]
191            .rfind('\n')
192            .map(|i| i + 1)
193            .unwrap_or(0);
194
195        // Previous line length
196        let prev_line_end = line_start - 1; // Position of \n
197        let prev_line_len = prev_line_end - prev_line_start;
198
199        // Move to same column or end of line
200        self.cursor = prev_line_start + col.min(prev_line_len);
201    }
202
203    /// Move cursor down one line (j).
204    fn move_down(&mut self, text: &str) {
205        // Find current line start
206        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
207
208        // Column offset
209        let col = self.cursor - line_start;
210
211        // Find next line start
212        let Some(newline_pos) = text[self.cursor..].find('\n') else {
213            // Already on last line
214            return;
215        };
216        let next_line_start = self.cursor + newline_pos + 1;
217
218        if next_line_start >= text.len() {
219            // Next line is empty/doesn't exist
220            self.cursor = text.len();
221            return;
222        }
223
224        // Find next line end
225        let next_line_end = text[next_line_start..]
226            .find('\n')
227            .map(|i| next_line_start + i)
228            .unwrap_or(text.len());
229
230        let next_line_len = next_line_end - next_line_start;
231
232        // Move to same column or end of line
233        self.cursor = next_line_start + col.min(next_line_len);
234    }
235
236    /// Delete character at cursor (x).
237    fn delete_char(&mut self, text: &str) -> EditResult {
238        if self.cursor >= text.len() {
239            return EditResult::none();
240        }
241
242        // Find the end of the current character
243        let mut end = self.cursor + 1;
244        while end < text.len() && !text.is_char_boundary(end) {
245            end += 1;
246        }
247
248        let deleted = text[self.cursor..end].to_string();
249        EditResult::edit_and_yank(
250            TextEdit::Delete {
251                start: self.cursor,
252                end,
253            },
254            deleted,
255        )
256    }
257
258    /// Delete to end of line (D).
259    fn delete_to_end(&mut self, text: &str) -> EditResult {
260        let end = text[self.cursor..]
261            .find('\n')
262            .map(|i| self.cursor + i)
263            .unwrap_or(text.len());
264
265        if self.cursor >= end {
266            return EditResult::none();
267        }
268
269        let deleted = text[self.cursor..end].to_string();
270        EditResult::edit_and_yank(
271            TextEdit::Delete {
272                start: self.cursor,
273                end,
274            },
275            deleted,
276        )
277    }
278
279    /// Delete entire current line (dd).
280    fn delete_line(&mut self, text: &str) -> EditResult {
281        let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
282
283        let line_end = text[self.cursor..]
284            .find('\n')
285            .map(|i| self.cursor + i + 1) // Include the newline
286            .unwrap_or(text.len());
287
288        // If this is the only line and no newline, include leading newline if any
289        let (start, end) = if line_start == 0 && line_end == text.len() {
290            (0, text.len())
291        } else if line_end == text.len() && line_start > 0 {
292            // Last line - delete the preceding newline instead
293            (line_start - 1, text.len())
294        } else {
295            (line_start, line_end)
296        };
297
298        let deleted = text[start..end].to_string();
299        self.cursor = start;
300
301        EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
302    }
303
304    /// Paste after cursor (p).
305    fn paste_after(&mut self, text: &str) -> EditResult {
306        if self.yank_buffer.is_empty() {
307            return EditResult::none();
308        }
309
310        let insert_pos = (self.cursor + 1).min(text.len());
311        let to_insert = self.yank_buffer.clone();
312        // Position cursor at end of pasted text, safely handling edge cases
313        self.cursor = (insert_pos + to_insert.len())
314            .saturating_sub(1)
315            .min(text.len() + to_insert.len());
316
317        EditResult::edit(TextEdit::Insert {
318            at: insert_pos,
319            text: to_insert,
320        })
321    }
322
323    /// Paste before cursor (P).
324    fn paste_before(&mut self, text: &str) -> EditResult {
325        if self.yank_buffer.is_empty() {
326            return EditResult::none();
327        }
328
329        let to_insert = self.yank_buffer.clone();
330        let insert_pos = self.cursor.min(text.len());
331        // Position cursor at end of pasted text
332        self.cursor = (insert_pos + to_insert.len()).min(text.len() + to_insert.len());
333
334        EditResult::edit(TextEdit::Insert {
335            at: insert_pos,
336            text: to_insert,
337        })
338    }
339
340    /// Handle key in Normal mode.
341    fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
342        match key.code {
343            // Mode switching
344            KeyCode::Char('i') => {
345                self.mode = Mode::Insert;
346                EditResult::none()
347            }
348            KeyCode::Char('a') => {
349                self.mode = Mode::Insert;
350                self.move_right(text);
351                EditResult::none()
352            }
353            KeyCode::Char('A') => {
354                self.mode = Mode::Insert;
355                self.move_line_end(text);
356                EditResult::none()
357            }
358            KeyCode::Char('I') => {
359                self.mode = Mode::Insert;
360                self.move_first_non_blank(text);
361                EditResult::none()
362            }
363            KeyCode::Char('o') => {
364                self.mode = Mode::Insert;
365                self.move_line_end(text);
366                let pos = self.cursor;
367                self.cursor = pos + 1;
368                EditResult::edit(TextEdit::Insert {
369                    at: pos,
370                    text: "\n".to_string(),
371                })
372            }
373            KeyCode::Char('O') => {
374                self.mode = Mode::Insert;
375                self.move_line_start(text);
376                let pos = self.cursor;
377                EditResult::edit(TextEdit::Insert {
378                    at: pos,
379                    text: "\n".to_string(),
380                })
381            }
382
383            // Visual mode
384            KeyCode::Char('v') => {
385                self.mode = Mode::Visual;
386                self.visual_anchor = Some(self.cursor);
387                EditResult::none()
388            }
389
390            // Motions
391            KeyCode::Char('h') | KeyCode::Left => {
392                self.move_left(text);
393                EditResult::cursor_only()
394            }
395            KeyCode::Char('l') | KeyCode::Right => {
396                self.move_right(text);
397                EditResult::cursor_only()
398            }
399            KeyCode::Char('j') => {
400                self.move_down(text);
401                EditResult::cursor_only()
402            }
403            KeyCode::Char('k') => {
404                self.move_up(text);
405                EditResult::cursor_only()
406            }
407            KeyCode::Char('0') | KeyCode::Home => {
408                self.move_line_start(text);
409                EditResult::cursor_only()
410            }
411            KeyCode::Char('^') => {
412                self.move_first_non_blank(text);
413                EditResult::cursor_only()
414            }
415            KeyCode::Char('$') | KeyCode::End => {
416                self.move_line_end(text);
417                EditResult::cursor_only()
418            }
419            KeyCode::Char('w') => {
420                self.move_word_forward(text);
421                EditResult::cursor_only()
422            }
423            KeyCode::Char('b') => {
424                self.move_word_backward(text);
425                EditResult::cursor_only()
426            }
427            KeyCode::Char('e') => {
428                self.move_word_end(text);
429                EditResult::cursor_only()
430            }
431
432            // Cancel (Ctrl+C)
433            KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
434
435            // Operators (enter pending mode)
436            KeyCode::Char('d') => {
437                self.mode = Mode::OperatorPending(Operator::Delete);
438                EditResult::none()
439            }
440            KeyCode::Char('c') => {
441                self.mode = Mode::OperatorPending(Operator::Change);
442                EditResult::none()
443            }
444            KeyCode::Char('y') => {
445                self.mode = Mode::OperatorPending(Operator::Yank);
446                EditResult::none()
447            }
448
449            // Direct deletions
450            KeyCode::Char('x') => self.delete_char(text),
451            KeyCode::Char('D') => self.delete_to_end(text),
452            KeyCode::Char('C') => {
453                self.mode = Mode::Insert;
454                self.delete_to_end(text)
455            }
456
457            // Paste
458            KeyCode::Char('p') => self.paste_after(text),
459            KeyCode::Char('P') => self.paste_before(text),
460
461            // History (arrows only)
462            KeyCode::Up => EditResult::action(Action::HistoryPrev),
463            KeyCode::Down => EditResult::action(Action::HistoryNext),
464
465            // Submit
466            KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
467
468            // Newline (Shift+Enter)
469            KeyCode::Enter if key.shift => {
470                self.mode = Mode::Insert;
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
479            // Escape in Normal mode is a no-op (safe to spam like in vim)
480            // Use Ctrl+C to cancel/quit
481            KeyCode::Escape => EditResult::none(),
482
483            _ => EditResult::none(),
484        }
485    }
486
487    /// Handle key in Insert mode.
488    fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
489        match key.code {
490            KeyCode::Escape => {
491                self.mode = Mode::Normal;
492                // Move cursor left like vim does when exiting insert
493                if self.cursor > 0 {
494                    self.move_left(text);
495                }
496                EditResult::none()
497            }
498
499            // Ctrl+C exits insert mode
500            KeyCode::Char('c') if key.ctrl => {
501                self.mode = Mode::Normal;
502                EditResult::none()
503            }
504
505            KeyCode::Char(c) if !key.ctrl && !key.alt => {
506                let pos = self.cursor;
507                self.cursor = pos + c.len_utf8();
508                EditResult::edit(TextEdit::Insert {
509                    at: pos,
510                    text: c.to_string(),
511                })
512            }
513
514            KeyCode::Backspace => {
515                if self.cursor == 0 {
516                    return EditResult::none();
517                }
518                let mut start = self.cursor - 1;
519                while start > 0 && !text.is_char_boundary(start) {
520                    start -= 1;
521                }
522                let end = self.cursor; // Save original cursor before updating
523                self.cursor = start;
524                EditResult::edit(TextEdit::Delete { start, end })
525            }
526
527            KeyCode::Delete => self.delete_char(text),
528
529            KeyCode::Left => {
530                self.move_left(text);
531                EditResult::cursor_only()
532            }
533            KeyCode::Right => {
534                self.move_right(text);
535                EditResult::cursor_only()
536            }
537            KeyCode::Up => {
538                self.move_up(text);
539                EditResult::cursor_only()
540            }
541            KeyCode::Down => {
542                self.move_down(text);
543                EditResult::cursor_only()
544            }
545            KeyCode::Home => {
546                self.move_line_start(text);
547                EditResult::cursor_only()
548            }
549            KeyCode::End => {
550                self.move_line_end(text);
551                EditResult::cursor_only()
552            }
553
554            // Enter inserts newline in insert mode
555            KeyCode::Enter => {
556                let pos = self.cursor;
557                self.cursor = pos + 1;
558                EditResult::edit(TextEdit::Insert {
559                    at: pos,
560                    text: "\n".to_string(),
561                })
562            }
563
564            _ => EditResult::none(),
565        }
566    }
567
568    /// Handle key in OperatorPending mode.
569    fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
570        // First, handle escape to cancel
571        if key.code == KeyCode::Escape {
572            self.mode = Mode::Normal;
573            return EditResult::none();
574        }
575
576        // Handle doubled operator (dd, cc, yy) - operates on whole line
577        let is_line_op = matches!(
578            (op, key.code),
579            (Operator::Delete, KeyCode::Char('d'))
580                | (Operator::Change, KeyCode::Char('c'))
581                | (Operator::Yank, KeyCode::Char('y'))
582        );
583
584        if is_line_op {
585            self.mode = Mode::Normal;
586            return self.apply_operator_line(op, text);
587        }
588
589        // Handle motion
590        let start = self.cursor;
591        match key.code {
592            KeyCode::Char('w') => self.move_word_forward(text),
593            KeyCode::Char('b') => self.move_word_backward(text),
594            KeyCode::Char('e') => {
595                self.move_word_end(text);
596                // Include the character at cursor for delete/change
597                if self.cursor < text.len() {
598                    self.cursor += 1;
599                }
600            }
601            KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
602            KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
603            KeyCode::Char('^') => self.move_first_non_blank(text),
604            KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
605            KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
606            KeyCode::Char('j') => self.move_down(text),
607            KeyCode::Char('k') => self.move_up(text),
608            _ => {
609                // Unknown motion, cancel
610                self.mode = Mode::Normal;
611                return EditResult::none();
612            }
613        }
614
615        let end = self.cursor;
616        self.mode = Mode::Normal;
617
618        if start == end {
619            return EditResult::none();
620        }
621
622        let (range_start, range_end) = if start < end {
623            (start, end)
624        } else {
625            (end, start)
626        };
627
628        self.apply_operator(op, range_start, range_end, text)
629    }
630
631    /// Apply an operator to a range.
632    fn apply_operator(&mut self, op: Operator, start: usize, end: usize, text: &str) -> EditResult {
633        let affected = text[start..end].to_string();
634        self.yank_buffer = affected.clone();
635        self.cursor = start;
636
637        match op {
638            Operator::Delete => {
639                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
640            }
641            Operator::Change => {
642                self.mode = Mode::Insert;
643                EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
644            }
645            Operator::Yank => {
646                // Just yank, no edit
647                EditResult {
648                    yanked: Some(affected),
649                    ..Default::default()
650                }
651            }
652        }
653    }
654
655    /// Apply an operator to the whole line.
656    fn apply_operator_line(&mut self, op: Operator, text: &str) -> EditResult {
657        match op {
658            Operator::Delete => self.delete_line(text),
659            Operator::Change => {
660                let result = self.delete_line(text);
661                self.mode = Mode::Insert;
662                result
663            }
664            Operator::Yank => {
665                let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
666                let line_end = text[self.cursor..]
667                    .find('\n')
668                    .map(|i| self.cursor + i + 1)
669                    .unwrap_or(text.len());
670                let line = text[line_start..line_end].to_string();
671                self.yank_buffer = line.clone();
672                EditResult {
673                    yanked: Some(line),
674                    ..Default::default()
675                }
676            }
677        }
678    }
679
680    /// Handle key in Visual mode.
681    fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
682        match key.code {
683            KeyCode::Escape => {
684                self.mode = Mode::Normal;
685                self.visual_anchor = None;
686                EditResult::none()
687            }
688
689            // Motions extend selection
690            KeyCode::Char('h') | KeyCode::Left => {
691                self.move_left(text);
692                EditResult::cursor_only()
693            }
694            KeyCode::Char('l') | KeyCode::Right => {
695                self.move_right(text);
696                EditResult::cursor_only()
697            }
698            KeyCode::Char('j') => {
699                self.move_down(text);
700                EditResult::cursor_only()
701            }
702            KeyCode::Char('k') => {
703                self.move_up(text);
704                EditResult::cursor_only()
705            }
706            KeyCode::Char('w') => {
707                self.move_word_forward(text);
708                EditResult::cursor_only()
709            }
710            KeyCode::Char('b') => {
711                self.move_word_backward(text);
712                EditResult::cursor_only()
713            }
714            KeyCode::Char('e') => {
715                self.move_word_end(text);
716                EditResult::cursor_only()
717            }
718            KeyCode::Char('0') | KeyCode::Home => {
719                self.move_line_start(text);
720                EditResult::cursor_only()
721            }
722            KeyCode::Char('$') | KeyCode::End => {
723                self.move_line_end(text);
724                EditResult::cursor_only()
725            }
726
727            // Operators on selection
728            KeyCode::Char('d') | KeyCode::Char('x') => {
729                let (start, end) = self.selection_range();
730                self.mode = Mode::Normal;
731                self.visual_anchor = None;
732                self.apply_operator(Operator::Delete, start, end, text)
733            }
734            KeyCode::Char('c') => {
735                let (start, end) = self.selection_range();
736                self.mode = Mode::Normal;
737                self.visual_anchor = None;
738                self.apply_operator(Operator::Change, start, end, text)
739            }
740            KeyCode::Char('y') => {
741                let (start, end) = self.selection_range();
742                self.mode = Mode::Normal;
743                self.visual_anchor = None;
744                self.apply_operator(Operator::Yank, start, end, text)
745            }
746
747            _ => EditResult::none(),
748        }
749    }
750
751    /// Get the selection range (ordered).
752    fn selection_range(&self) -> (usize, usize) {
753        let anchor = self.visual_anchor.unwrap_or(self.cursor);
754        if self.cursor < anchor {
755            (self.cursor, anchor)
756        } else {
757            (anchor, self.cursor + 1) // Include cursor position
758        }
759    }
760}
761
762impl LineEditor for VimLineEditor {
763    fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
764        self.clamp_cursor(text);
765
766        let result = match self.mode {
767            Mode::Normal => self.handle_normal(key, text),
768            Mode::Insert => self.handle_insert(key, text),
769            Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
770            Mode::Visual => self.handle_visual(key, text),
771        };
772
773        // Store yanked text
774        if let Some(ref yanked) = result.yanked {
775            self.yank_buffer = yanked.clone();
776        }
777
778        result
779    }
780
781    fn cursor(&self) -> usize {
782        self.cursor
783    }
784
785    fn status(&self) -> &str {
786        match self.mode {
787            Mode::Normal => "NORMAL",
788            Mode::Insert => "INSERT",
789            Mode::OperatorPending(Operator::Delete) => "d...",
790            Mode::OperatorPending(Operator::Change) => "c...",
791            Mode::OperatorPending(Operator::Yank) => "y...",
792            Mode::Visual => "VISUAL",
793        }
794    }
795
796    fn selection(&self) -> Option<Range<usize>> {
797        if self.mode == Mode::Visual {
798            let (start, end) = self.selection_range();
799            Some(start..end)
800        } else {
801            None
802        }
803    }
804
805    fn reset(&mut self) {
806        self.cursor = 0;
807        self.mode = Mode::Normal;
808        self.visual_anchor = None;
809        // Keep yank buffer across resets
810    }
811
812    fn set_cursor(&mut self, pos: usize, text: &str) {
813        // Clamp to text length and ensure we're at a char boundary
814        let pos = pos.min(text.len());
815        self.cursor = if text.is_char_boundary(pos) {
816            pos
817        } else {
818            // Walk backwards to find a valid boundary
819            let mut p = pos;
820            while p > 0 && !text.is_char_boundary(p) {
821                p -= 1;
822            }
823            p
824        };
825    }
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831
832    #[test]
833    fn test_basic_motion() {
834        let mut editor = VimLineEditor::new();
835        let text = "hello world";
836
837        // Move right with 'l'
838        editor.handle_key(Key::char('l'), text);
839        assert_eq!(editor.cursor(), 1);
840
841        // Move right with 'w'
842        editor.handle_key(Key::char('w'), text);
843        assert_eq!(editor.cursor(), 6); // Start of "world"
844
845        // Move to end with '$'
846        editor.handle_key(Key::char('$'), text);
847        assert_eq!(editor.cursor(), 11);
848
849        // Move to start with '0'
850        editor.handle_key(Key::char('0'), text);
851        assert_eq!(editor.cursor(), 0);
852    }
853
854    #[test]
855    fn test_mode_switching() {
856        let mut editor = VimLineEditor::new();
857        let text = "hello";
858
859        assert_eq!(editor.mode(), Mode::Normal);
860
861        editor.handle_key(Key::char('i'), text);
862        assert_eq!(editor.mode(), Mode::Insert);
863
864        editor.handle_key(Key::code(KeyCode::Escape), text);
865        assert_eq!(editor.mode(), Mode::Normal);
866    }
867
868    #[test]
869    fn test_delete_word() {
870        let mut editor = VimLineEditor::new();
871        let text = "hello world";
872
873        // dw should delete "hello "
874        editor.handle_key(Key::char('d'), text);
875        editor.handle_key(Key::char('w'), text);
876
877        // Check we're back in Normal mode
878        assert_eq!(editor.mode(), Mode::Normal);
879    }
880
881    #[test]
882    fn test_insert_char() {
883        let mut editor = VimLineEditor::new();
884        let text = "";
885
886        editor.handle_key(Key::char('i'), text);
887        let result = editor.handle_key(Key::char('x'), text);
888
889        assert_eq!(result.edits.len(), 1);
890        match &result.edits[0] {
891            TextEdit::Insert { at, text } => {
892                assert_eq!(*at, 0);
893                assert_eq!(text, "x");
894            }
895            _ => panic!("Expected Insert"),
896        }
897    }
898
899    #[test]
900    fn test_visual_mode() {
901        let mut editor = VimLineEditor::new();
902        let text = "hello world";
903
904        // Enter visual mode
905        editor.handle_key(Key::char('v'), text);
906        assert_eq!(editor.mode(), Mode::Visual);
907
908        // Extend selection
909        editor.handle_key(Key::char('w'), text);
910
911        // Selection should cover from 0 to cursor
912        let sel = editor.selection().unwrap();
913        assert_eq!(sel.start, 0);
914        assert!(sel.end > 0);
915    }
916
917    #[test]
918    fn test_backspace_ascii() {
919        let mut editor = VimLineEditor::new();
920        let mut text = String::from("abc");
921
922        // Enter insert mode and go to end
923        editor.handle_key(Key::char('i'), &text);
924        editor.handle_key(Key::code(KeyCode::End), &text);
925        assert_eq!(editor.cursor(), 3);
926
927        // Backspace should delete 'c'
928        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
929        for edit in result.edits.into_iter().rev() {
930            edit.apply(&mut text);
931        }
932        assert_eq!(text, "ab");
933        assert_eq!(editor.cursor(), 2);
934    }
935
936    #[test]
937    fn test_backspace_unicode() {
938        let mut editor = VimLineEditor::new();
939        let mut text = String::from("a😀b");
940
941        // Enter insert mode and position after emoji (byte position 5: 1 + 4)
942        editor.handle_key(Key::char('i'), &text);
943        editor.handle_key(Key::code(KeyCode::End), &text);
944        editor.handle_key(Key::code(KeyCode::Left), &text); // Move before 'b'
945        assert_eq!(editor.cursor(), 5); // After the 4-byte emoji
946
947        // Backspace should delete entire emoji (4 bytes), not just 1 byte
948        let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
949        for edit in result.edits.into_iter().rev() {
950            edit.apply(&mut text);
951        }
952        assert_eq!(text, "ab");
953        assert_eq!(editor.cursor(), 1);
954    }
955
956    #[test]
957    fn test_yank_and_paste() {
958        let mut editor = VimLineEditor::new();
959        let mut text = String::from("hello world");
960
961        // Yank word with yw
962        editor.handle_key(Key::char('y'), &text);
963        let result = editor.handle_key(Key::char('w'), &text);
964        assert!(result.yanked.is_some());
965        assert_eq!(result.yanked.unwrap(), "hello ");
966
967        // Move to end and paste
968        editor.handle_key(Key::char('$'), &text);
969        let result = editor.handle_key(Key::char('p'), &text);
970
971        for edit in result.edits.into_iter().rev() {
972            edit.apply(&mut text);
973        }
974        assert_eq!(text, "hello worldhello ");
975    }
976
977    #[test]
978    fn test_visual_mode_delete() {
979        let mut editor = VimLineEditor::new();
980        let mut text = String::from("hello world");
981
982        // Enter visual mode at position 0
983        editor.handle_key(Key::char('v'), &text);
984        assert_eq!(editor.mode(), Mode::Visual);
985
986        // Extend selection with 'e' motion to end of word (stays on 'o' of hello)
987        editor.handle_key(Key::char('e'), &text);
988
989        // Delete selection with d - deletes "hello"
990        let result = editor.handle_key(Key::char('d'), &text);
991
992        for edit in result.edits.into_iter().rev() {
993            edit.apply(&mut text);
994        }
995        assert_eq!(text, " world");
996        assert_eq!(editor.mode(), Mode::Normal);
997    }
998
999    #[test]
1000    fn test_operator_pending_escape() {
1001        let mut editor = VimLineEditor::new();
1002        let text = "hello world";
1003
1004        // Start delete operator
1005        editor.handle_key(Key::char('d'), text);
1006        assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
1007
1008        // Cancel with Escape
1009        editor.handle_key(Key::code(KeyCode::Escape), text);
1010        assert_eq!(editor.mode(), Mode::Normal);
1011    }
1012
1013    #[test]
1014    fn test_paste_at_empty_buffer() {
1015        let mut editor = VimLineEditor::new();
1016
1017        // First yank something from non-empty text
1018        let yank_text = String::from("test");
1019        editor.handle_key(Key::char('y'), &yank_text);
1020        editor.handle_key(Key::char('w'), &yank_text);
1021
1022        // Now paste into empty buffer
1023        let mut text = String::new();
1024        editor.set_cursor(0, &text);
1025        let result = editor.handle_key(Key::char('p'), &text);
1026
1027        for edit in result.edits.into_iter().rev() {
1028            edit.apply(&mut text);
1029        }
1030        assert_eq!(text, "test");
1031    }
1032}