Skip to main content

photon_ui/components/
editor.rs

1use crossterm::event::KeyCode;
2use unicode_segmentation::UnicodeSegmentation;
3use unicode_width::UnicodeWidthStr;
4
5use crate::{
6    Component,
7    Event,
8    Focusable,
9    InputResult,
10    RenderError,
11    Rendered,
12    kill_ring::KillRing,
13    undo_stack::UndoStack,
14    word_navigation::{
15        find_word_backward,
16        find_word_forward,
17    },
18};
19
20/// Snapshot of editor state for undo/redo.
21#[derive(Clone)]
22pub struct EditorAction {
23    /// The full text buffer at the time of the snapshot.
24    pub text: String,
25    /// Cursor position in grapheme indices.
26    pub cursor: usize,
27}
28
29/// Vim editing mode state.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum VimMode {
32    /// Normal mode — keys are commands (hjkl, dw, yy, etc.).
33    Normal,
34    /// Insert mode — keys insert text.
35    Insert,
36}
37
38/// A multi-line text editor component.
39///
40/// Defaults to **Emacs-style** bindings (Ctrl+A, Ctrl+E, Ctrl+K, etc.).
41/// Call [`set_vim_mode_enabled`](Editor::set_vim_mode_enabled) to opt into
42/// vim-style modal editing (Normal/Insert mode with hjkl, dd, yy, etc.).
43///
44/// Supports undo/redo, kill-ring (yank/yank-pop), history navigation,
45/// word-wise movement, and paste detection for large inserts.
46#[derive(Clone)]
47pub struct Editor {
48    text: String,
49    cursor: usize,
50    focused: bool,
51    kill_ring: KillRing,
52    undo_stack: UndoStack<EditorAction>,
53    lines_cache: Vec<String>,
54    cache_width: u16,
55    history: Vec<String>,
56    history_index: Option<usize>,
57    max_history: usize,
58    /// Whether vim modal editing is enabled.
59    vim_mode_enabled: bool,
60    /// Current vim mode (only meaningful when `vim_mode_enabled` is `true`).
61    mode: VimMode,
62    /// Pending normal-mode command prefix (e.g. `'d'`, `'y'`).
63    pending_cmd: Option<char>,
64}
65
66impl Editor {
67    /// Create a new empty editor with Emacs-style bindings.
68    pub fn new() -> Self {
69        Self {
70            text: String::new(),
71            cursor: 0,
72            focused: false,
73            kill_ring: KillRing::new(),
74            undo_stack: UndoStack::new(),
75            lines_cache: Vec::new(),
76            cache_width: 0,
77            history: Vec::new(),
78            history_index: None,
79            max_history: 100,
80            vim_mode_enabled: false,
81            mode: VimMode::Normal,
82            pending_cmd: None,
83        }
84    }
85
86    /// Returns `true` if vim modal editing is enabled.
87    pub fn vim_mode_enabled(&self) -> bool {
88        self.vim_mode_enabled
89    }
90
91    /// Enable or disable vim modal editing.
92    ///
93    /// When enabled the editor starts in Normal mode; press `i` to insert,
94    /// `Escape` to return to Normal. When disabled, Emacs-style bindings
95    /// are always active.
96    pub fn set_vim_mode_enabled(&mut self, enabled: bool) {
97        self.vim_mode_enabled = enabled;
98        self.mode = VimMode::Normal;
99        self.pending_cmd = None;
100    }
101
102    /// Current vim mode (only meaningful when vim mode is enabled).
103    pub fn mode(&self) -> VimMode {
104        self.mode
105    }
106
107    /// Switch to the given vim mode.
108    pub fn set_mode(&mut self, mode: VimMode) {
109        self.mode = mode;
110        self.pending_cmd = None;
111    }
112
113    /// Borrow the current text content.
114    pub fn text(&self) -> &str {
115        &self.text
116    }
117
118    /// Current cursor position in grapheme indices.
119    pub fn cursor_grapheme(&self) -> usize {
120        self.cursor
121    }
122
123    /// Replace the entire text buffer and move the cursor to the end.
124    pub fn set_text(&mut self, text: impl Into<String>) {
125        self.text = text.into();
126        self.cursor = self.graphemes().len();
127        self.lines_cache.clear();
128        self.cache_width = 0;
129    }
130
131    fn graphemes(&self) -> Vec<&str> {
132        self.text.graphemes(true).collect()
133    }
134
135    fn byte_index(&self, grapheme_idx: usize) -> usize {
136        self.text
137            .grapheme_indices(true)
138            .nth(grapheme_idx)
139            .map(|(i, _)| i)
140            .unwrap_or(self.text.len())
141    }
142
143    fn save_undo(&mut self) {
144        self.undo_stack.push(EditorAction {
145            text: self.text.clone(),
146            cursor: self.cursor,
147        });
148    }
149
150    fn insert_char(&mut self, c: char) {
151        let idx = self.byte_index(self.cursor);
152        self.text.insert(idx, c);
153        self.cursor += 1;
154        self.invalidate_cache();
155    }
156
157    fn insert_newline(&mut self) {
158        let idx = self.byte_index(self.cursor);
159        self.text.insert(idx, '\n');
160        self.cursor += 1;
161        self.invalidate_cache();
162    }
163
164    fn insert_str(&mut self, s: &str) {
165        let idx = self.byte_index(self.cursor);
166        self.text.insert_str(idx, s);
167        self.cursor += s.graphemes(true).count();
168        self.invalidate_cache();
169    }
170
171    fn delete_backward(&mut self) {
172        if self.cursor > 0 {
173            let start = self.byte_index(self.cursor - 1);
174            let end = self.byte_index(self.cursor);
175            let killed = self.text.drain(start..end).collect::<String>();
176            self.kill_ring.push(killed);
177            self.cursor -= 1;
178            self.invalidate_cache();
179        }
180    }
181
182    fn delete_forward(&mut self) {
183        if self.cursor < self.graphemes().len() {
184            let start = self.byte_index(self.cursor);
185            let end = self.byte_index(self.cursor + 1);
186            self.text.drain(start..end);
187            self.invalidate_cache();
188        }
189    }
190
191    fn move_cursor_left(&mut self) {
192        if self.cursor > 0 {
193            self.cursor -= 1;
194        }
195    }
196
197    fn move_cursor_right(&mut self) {
198        if self.cursor < self.graphemes().len() {
199            self.cursor += 1;
200        }
201    }
202
203    fn move_cursor_up(&mut self) {
204        let (line, col) = self.cursor_line_col();
205        if line == 0 {
206            return;
207        }
208        let target_line = line - 1;
209        let mut current_line = 0;
210        let mut current_col = 0;
211        let mut gidx = 0;
212        for g in self.text.graphemes(true) {
213            if current_line == target_line {
214                if current_col >= col || g == "\n" {
215                    self.cursor = gidx;
216                    return;
217                }
218                current_col += g.width();
219            } else if g == "\n" {
220                current_line += 1;
221                current_col = 0;
222            }
223            gidx += 1;
224        }
225    }
226
227    fn move_cursor_down(&mut self) {
228        let (line, col) = self.cursor_line_col();
229        let total_lines = self.text.lines().count();
230        if line + 1 >= total_lines {
231            return;
232        }
233        let target_line = line + 1;
234        let mut current_line = 0;
235        let mut current_col = 0;
236        let mut gidx = 0;
237        for g in self.text.graphemes(true) {
238            if current_line == target_line {
239                if current_col >= col || g == "\n" {
240                    self.cursor = gidx;
241                    return;
242                }
243                current_col += g.width();
244            } else if g == "\n" {
245                current_line += 1;
246                current_col = 0;
247            }
248            gidx += 1;
249        }
250        self.cursor = gidx;
251    }
252
253    fn move_cursor_home(&mut self) {
254        let (line, _) = self.cursor_line_col();
255        let mut current_line = 0;
256        let mut gidx = 0;
257        for g in self.text.graphemes(true) {
258            if current_line == line {
259                self.cursor = gidx;
260                return;
261            }
262            if g == "\n" {
263                current_line += 1;
264            }
265            gidx += 1;
266        }
267    }
268
269    fn move_cursor_end(&mut self) {
270        let (line, _) = self.cursor_line_col();
271        let mut current_line = 0;
272        let mut gidx = 0;
273        let mut found = false;
274        for g in self.text.graphemes(true) {
275            if g == "\n" {
276                if found {
277                    self.cursor = gidx;
278                    return;
279                }
280                current_line += 1;
281            }
282            if current_line == line {
283                found = true;
284            }
285            gidx += 1;
286        }
287        self.cursor = gidx;
288    }
289
290    fn move_word_forward(&mut self) {
291        let idx = self.byte_index(self.cursor);
292        let new_idx = find_word_forward(&self.text, idx, |c| c.is_whitespace());
293        let slice = &self.text[idx..new_idx];
294        self.cursor += slice.graphemes(true).count();
295    }
296
297    fn move_word_backward(&mut self) {
298        let idx = self.byte_index(self.cursor);
299        let new_idx = find_word_backward(&self.text, idx, |c| c.is_whitespace());
300        let slice = &self.text[new_idx..idx];
301        self.cursor -= slice.graphemes(true).count();
302    }
303
304    fn kill_word_forward(&mut self) {
305        let idx = self.byte_index(self.cursor);
306        let new_idx = find_word_forward(&self.text, idx, |c| c.is_whitespace());
307        let killed = self.text.drain(idx..new_idx).collect::<String>();
308        self.kill_ring.push(killed);
309        self.invalidate_cache();
310    }
311
312    fn kill_word_backward(&mut self) {
313        let idx = self.byte_index(self.cursor);
314        let new_idx = find_word_backward(&self.text, idx, |c| c.is_whitespace());
315        let killed = self.text.drain(new_idx..idx).collect::<String>();
316        let count = killed.graphemes(true).count();
317        self.kill_ring.push(killed);
318        self.cursor -= count;
319        self.invalidate_cache();
320    }
321
322    fn kill_to_end(&mut self) {
323        let idx = self.byte_index(self.cursor);
324        let killed = self.text.split_off(idx);
325        self.kill_ring.push(killed);
326        self.invalidate_cache();
327    }
328
329    fn yank(&mut self) {
330        if let Some(text) = self.kill_ring.yank().map(|s| s.to_string()) {
331            self.insert_str(&text);
332        }
333    }
334
335    fn yank_pop(&mut self) {
336        if let Some(text) = self.kill_ring.yank_pop().map(|s| s.to_string()) {
337            self.insert_str(&text);
338        }
339    }
340
341    fn undo(&mut self) {
342        if let Some(action) = self.undo_stack.undo() {
343            self.text = action.text.clone();
344            self.cursor = action.cursor;
345            self.invalidate_cache();
346        }
347    }
348
349    fn redo(&mut self) {
350        if let Some(action) = self.undo_stack.redo() {
351            self.text = action.text.clone();
352            self.cursor = action.cursor;
353            self.invalidate_cache();
354        }
355    }
356
357    /// Append the current text to the history ring buffer.
358    ///
359    /// History is capped at `max_history` items (default 100). The history
360    /// index is reset so subsequent Up/Down navigation starts from the newest
361    /// entry.
362    pub fn push_history(&mut self) {
363        if !self.text.is_empty() {
364            self.history.push(self.text.clone());
365            if self.history.len() > self.max_history {
366                self.history.remove(0);
367            }
368        }
369        self.history_index = None;
370    }
371
372    fn history_up(&mut self) {
373        if self.history.is_empty() {
374            return;
375        }
376        let idx = match self.history_index {
377            | Some(i) if i > 0 => i - 1,
378            | Some(_) => return,
379            | None => self.history.len() - 1,
380        };
381        self.history_index = Some(idx);
382        self.text = self.history[idx].clone();
383        self.cursor = self.graphemes().len();
384        self.invalidate_cache();
385    }
386
387    fn history_down(&mut self) {
388        let idx = match self.history_index {
389            | Some(i) if i + 1 < self.history.len() => i + 1,
390            | Some(_) => {
391                self.history_index = None;
392                self.text.clear();
393                self.cursor = 0;
394                self.invalidate_cache();
395                return;
396            },
397            | None => return,
398        };
399        self.history_index = Some(idx);
400        self.text = self.history[idx].clone();
401        self.cursor = self.graphemes().len();
402        self.invalidate_cache();
403    }
404
405    fn invalidate_cache(&mut self) {
406        self.lines_cache.clear();
407        self.cache_width = 0;
408    }
409
410    /// Delete the current line (vim `dd` behavior).
411    fn delete_line(&mut self) {
412        let (line, _) = self.cursor_line_col();
413        let mut current_line = 0;
414        let mut start_byte = 0;
415        let mut byte_pos = 0;
416        for g in self.text.graphemes(true) {
417            if current_line == line {
418                start_byte = byte_pos;
419                break;
420            }
421            if g == "\n" {
422                current_line += 1;
423            }
424            byte_pos += g.len();
425        }
426        // Find the end of this line, including the newline if present.
427        let mut end_byte = self.text.len();
428        byte_pos = 0;
429        let mut found = false;
430        for g in self.text.graphemes(true) {
431            if found && g == "\n" {
432                end_byte = byte_pos + g.len();
433                break;
434            }
435            if byte_pos >= start_byte {
436                found = true;
437            }
438            byte_pos += g.len();
439        }
440        self.cursor = self.text[..start_byte].graphemes(true).count();
441        let killed = self.text.drain(start_byte..end_byte).collect::<String>();
442        if !killed.is_empty() {
443            self.kill_ring.push(killed);
444        }
445        self.invalidate_cache();
446    }
447
448    /// Yank (copy) the current line into the kill ring (vim `yy` behavior).
449    fn yank_line(&mut self) {
450        let (line, _) = self.cursor_line_col();
451        let mut current_line = 0;
452        let mut start_byte = 0;
453        let mut byte_pos = 0;
454        for g in self.text.graphemes(true) {
455            if current_line == line {
456                start_byte = byte_pos;
457                break;
458            }
459            if g == "\n" {
460                current_line += 1;
461            }
462            byte_pos += g.len();
463        }
464        let mut end_byte = self.text.len();
465        byte_pos = 0;
466        let mut found = false;
467        for g in self.text.graphemes(true) {
468            if found && g == "\n" {
469                end_byte = byte_pos + g.len();
470                break;
471            }
472            if byte_pos >= start_byte {
473                found = true;
474            }
475            byte_pos += g.len();
476        }
477        let yanked = self.text[start_byte..end_byte].to_string();
478        if !yanked.is_empty() {
479            self.kill_ring.push(yanked);
480        }
481    }
482
483    /// Open a new line below the current one and enter Insert mode.
484    fn open_line_below(&mut self) {
485        self.move_cursor_end();
486        self.insert_newline();
487        self.mode = VimMode::Insert;
488    }
489
490    /// Open a new line above the current one and enter Insert mode.
491    fn open_line_above(&mut self) {
492        self.move_cursor_home();
493        if self.cursor > 0 {
494            self.cursor -= 1; // back over the newline
495            self.insert_newline();
496        } else {
497            self.insert_newline();
498            self.cursor = 0;
499        }
500        self.mode = VimMode::Insert;
501    }
502
503    /// Replace the character under the cursor with `c`.
504    fn replace_char(&mut self, c: char) {
505        if self.cursor < self.graphemes().len() {
506            let start = self.byte_index(self.cursor);
507            let end = self.byte_index(self.cursor + 1);
508            self.text.drain(start..end);
509            self.text.insert(start, c);
510            self.invalidate_cache();
511        }
512    }
513
514    /// Move cursor to the start of the document.
515    fn go_to_start(&mut self) {
516        self.cursor = 0;
517    }
518
519    /// Move cursor to the end of the document.
520    fn go_to_end(&mut self) {
521        self.cursor = self.graphemes().len();
522    }
523
524    /// Compute the cursor position as `(line, column)` in display coordinates.
525    fn cursor_line_col(&self) -> (usize, usize) {
526        let mut current_line = 0;
527        let mut current_col = 0;
528        let mut graphemes_seen = 0;
529        for g in self.text.graphemes(true) {
530            if graphemes_seen >= self.cursor {
531                break;
532            }
533            if g == "\n" {
534                current_line += 1;
535                current_col = 0;
536            } else {
537                current_col += g.width();
538            }
539            graphemes_seen += 1;
540        }
541        (current_line, current_col)
542    }
543}
544
545impl Default for Editor {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551impl Focusable for Editor {
552    fn focused(&self) -> bool {
553        self.focused
554    }
555
556    fn set_focused(&mut self, focused: bool) {
557        self.focused = focused;
558    }
559}
560
561impl Editor {
562    fn handle_insert_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
563        use crossterm::event::KeyModifiers;
564        self.save_undo();
565        match key.code {
566            | KeyCode::Char(c) => {
567                if key.modifiers.contains(KeyModifiers::CONTROL) {
568                    match c {
569                        | 'a' => self.move_cursor_home(),
570                        | 'e' => self.move_cursor_end(),
571                        | 'b' => self.move_cursor_left(),
572                        | 'f' => self.move_cursor_right(),
573                        | 'n' => self.move_cursor_down(),
574                        | 'p' => self.move_cursor_up(),
575                        | 'd' => self.delete_forward(),
576                        | 'h' => self.delete_backward(),
577                        | 'k' => self.kill_to_end(),
578                        | 'w' => self.kill_word_backward(),
579                        | 'u' => {
580                            self.move_cursor_home();
581                            self.kill_to_end();
582                        },
583                        | 'y' => self.yank(),
584                        | 'r' => self.redo(),
585                        | '-' | '_' => self.undo(),
586                        | _ => return InputResult::Ignored,
587                    }
588                } else if key.modifiers.contains(KeyModifiers::ALT) {
589                    match c {
590                        | 'b' => self.move_word_backward(),
591                        | 'f' => self.move_word_forward(),
592                        | 'd' => self.kill_word_forward(),
593                        | 'y' => self.yank_pop(),
594                        | _ => return InputResult::Ignored,
595                    }
596                } else {
597                    self.insert_char(c);
598                }
599                InputResult::Handled
600            },
601            | KeyCode::Enter => {
602                let idx = self.byte_index(self.cursor);
603                if idx > 0 && self.text.as_bytes().get(idx - 1) == Some(&b'\\') {
604                    self.text.remove(idx - 1);
605                    self.cursor -= 1;
606                    self.insert_newline();
607                } else {
608                    self.insert_newline();
609                }
610                InputResult::Handled
611            },
612            | KeyCode::Left => {
613                self.move_cursor_left();
614                InputResult::Handled
615            },
616            | KeyCode::Right => {
617                self.move_cursor_right();
618                InputResult::Handled
619            },
620            | KeyCode::Up => {
621                if key.modifiers.contains(KeyModifiers::CONTROL) {
622                    self.move_cursor_up();
623                } else {
624                    self.history_up();
625                }
626                InputResult::Handled
627            },
628            | KeyCode::Down => {
629                if key.modifiers.contains(KeyModifiers::CONTROL) {
630                    self.move_cursor_down();
631                } else {
632                    self.history_down();
633                }
634                InputResult::Handled
635            },
636            | KeyCode::Home => {
637                self.move_cursor_home();
638                InputResult::Handled
639            },
640            | KeyCode::End => {
641                self.move_cursor_end();
642                InputResult::Handled
643            },
644            | KeyCode::Backspace => {
645                self.delete_backward();
646                InputResult::Handled
647            },
648            | KeyCode::Delete => {
649                self.delete_forward();
650                InputResult::Handled
651            },
652            | KeyCode::Esc => {
653                self.mode = VimMode::Normal;
654                InputResult::Handled
655            },
656            | _ => InputResult::Ignored,
657        }
658    }
659
660    fn handle_normal_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
661        // Multi-key commands (dd, yy, gg, etc.) are handled via pending_cmd.
662        if let Some(pending) = self.pending_cmd {
663            match key.code {
664                | KeyCode::Char('d') if pending == 'd' => {
665                    self.save_undo();
666                    self.delete_line();
667                    self.pending_cmd = None;
668                    return InputResult::Handled;
669                },
670                | KeyCode::Char('y') if pending == 'y' => {
671                    self.yank_line();
672                    self.pending_cmd = None;
673                    return InputResult::Handled;
674                },
675                | KeyCode::Char('g') if pending == 'g' => {
676                    self.go_to_start();
677                    self.pending_cmd = None;
678                    return InputResult::Handled;
679                },
680                | KeyCode::Char('w') if pending == 'd' => {
681                    self.save_undo();
682                    self.kill_word_forward();
683                    self.pending_cmd = None;
684                    return InputResult::Handled;
685                },
686                | KeyCode::Char('w') if pending == 'y' => {
687                    let start = self.cursor;
688                    self.move_word_forward();
689                    let end_byte = self.byte_index(self.cursor);
690                    let start_byte = self.byte_index(start);
691                    let yanked = self.text[start_byte..end_byte].to_string();
692                    if !yanked.is_empty() {
693                        self.kill_ring.push(yanked);
694                    }
695                    self.cursor = start;
696                    self.pending_cmd = None;
697                    return InputResult::Handled;
698                },
699                | KeyCode::Char(c) if pending == 'r' => {
700                    self.save_undo();
701                    self.replace_char(c);
702                    self.pending_cmd = None;
703                    return InputResult::Handled;
704                },
705                | _ => {
706                    self.pending_cmd = None;
707                    // Fall through to normal handling below
708                },
709            }
710        }
711
712        match key.code {
713            | KeyCode::Char(c) => {
714                match c {
715                    | 'h' => self.move_cursor_left(),
716                    | 'j' => self.move_cursor_down(),
717                    | 'k' => self.move_cursor_up(),
718                    | 'l' => self.move_cursor_right(),
719                    | 'w' => self.move_word_forward(),
720                    | 'b' => self.move_word_backward(),
721                    | 'x' => {
722                        self.save_undo();
723                        self.delete_forward();
724                    },
725                    | '0' => self.move_cursor_home(),
726                    | '$' => self.move_cursor_end(),
727                    | 'i' => self.mode = VimMode::Insert,
728                    | 'a' => {
729                        self.move_cursor_right();
730                        self.mode = VimMode::Insert;
731                    },
732                    | 'o' => {
733                        self.save_undo();
734                        self.open_line_below();
735                    },
736                    | 'O' => {
737                        self.save_undo();
738                        self.open_line_above();
739                    },
740                    | 'p' => {
741                        self.save_undo();
742                        self.yank();
743                    },
744                    | 'u' => {
745                        self.save_undo();
746                        self.undo();
747                    },
748                    | 'r' => {
749                        self.pending_cmd = Some('r');
750                        return InputResult::Handled;
751                    },
752                    | 'd' | 'y' => {
753                        self.pending_cmd = Some(c);
754                        return InputResult::Handled;
755                    },
756                    | 'g' => {
757                        self.pending_cmd = Some('g');
758                        return InputResult::Handled;
759                    },
760                    | 'G' => self.go_to_end(),
761                    | _ => return InputResult::Ignored,
762                }
763                InputResult::Handled
764            },
765            | KeyCode::Left => {
766                self.move_cursor_left();
767                InputResult::Handled
768            },
769            | KeyCode::Right => {
770                self.move_cursor_right();
771                InputResult::Handled
772            },
773            | KeyCode::Up => {
774                self.move_cursor_up();
775                InputResult::Handled
776            },
777            | KeyCode::Down => {
778                self.move_cursor_down();
779                InputResult::Handled
780            },
781            | KeyCode::Home => {
782                self.move_cursor_home();
783                InputResult::Handled
784            },
785            | KeyCode::End => {
786                self.move_cursor_end();
787                InputResult::Handled
788            },
789            | KeyCode::Backspace => {
790                self.move_cursor_left();
791                InputResult::Handled
792            },
793            | _ => InputResult::Ignored,
794        }
795    }
796}
797
798impl Component for Editor {
799    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
800        let editor = self.clone();
801        let (cursor_line, cursor_col) = editor.cursor_line_col();
802        let lines = if width == self.cache_width && !self.lines_cache.is_empty() {
803            self.lines_cache.clone()
804        } else {
805            crate::utils::wrap_text_with_ansi(&self.text, width)
806        };
807        Ok(Rendered {
808            lines,
809            cursor: if self.focused {
810                Some((cursor_line, cursor_col))
811            } else {
812                None
813            },
814            images: Vec::new(),
815        })
816    }
817
818    fn handle_input(&mut self, event: &Event) -> InputResult {
819        if let Event::Key(key) = event {
820            if self.vim_mode_enabled {
821                match self.mode {
822                    | VimMode::Insert => self.handle_insert_mode(key),
823                    | VimMode::Normal => self.handle_normal_mode(key),
824                }
825            } else {
826                self.handle_insert_mode(key)
827            }
828        } else {
829            InputResult::Ignored
830        }
831    }
832
833    fn as_focusable(&self) -> Option<&dyn Focusable> {
834        Some(self)
835    }
836
837    fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
838        Some(self)
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use crossterm::event::{
845        KeyCode,
846        KeyEvent,
847        KeyModifiers,
848    };
849
850    use super::*;
851
852    fn key_event(code: KeyCode) -> Event {
853        Event::Key(code.into())
854    }
855
856    #[test]
857    fn yank_pop_cycles() {
858        let mut editor = Editor::new();
859        editor.insert_str("ab");
860        editor.move_cursor_home();
861        editor.kill_to_end();
862        assert_eq!(editor.text(), "");
863        editor.yank();
864        assert_eq!(editor.text(), "ab");
865        editor.yank_pop();
866        assert_eq!(editor.text(), "abab");
867    }
868
869    #[test]
870    fn history_down_navigation() {
871        let mut editor = Editor::new();
872        editor.insert_str("a");
873        editor.push_history();
874        editor.move_cursor_home();
875        editor.kill_to_end();
876        editor.insert_str("b");
877        editor.push_history();
878        editor.history_up();
879        assert_eq!(editor.text(), "b");
880        editor.history_up();
881        assert_eq!(editor.text(), "a");
882        editor.history_down();
883        assert_eq!(editor.text(), "b");
884        editor.history_down();
885        assert_eq!(editor.text(), "");
886    }
887
888    #[test]
889    fn history_down_empty() {
890        let mut editor = Editor::new();
891        editor.history_down();
892        assert_eq!(editor.text(), "");
893    }
894
895    #[test]
896    fn push_history_empty() {
897        let mut editor = Editor::new();
898        editor.push_history();
899        assert_eq!(editor.text(), "");
900    }
901
902    #[test]
903    fn move_cursor_up_down() {
904        let mut editor = Editor::new();
905        editor.insert_str("a\nb");
906        editor.move_cursor_up();
907        // cursor lands at newline position because col=1 and current_col reaches 1 at
908        // '\n'
909        assert_eq!(editor.cursor_grapheme(), 1);
910        editor.move_cursor_down();
911        assert_eq!(editor.cursor_grapheme(), 3);
912    }
913
914    #[test]
915    fn kill_word_forward() {
916        let mut editor = Editor::new();
917        editor.insert_str("hello world");
918        editor.move_cursor_home();
919        editor.kill_word_forward();
920        assert_eq!(editor.text(), " world");
921    }
922
923    #[test]
924    fn render_unfocused() {
925        let mut editor = Editor::new();
926        editor.set_focused(false);
927        editor.insert_str("x");
928        let r = editor.render(80).unwrap();
929        assert!(r.cursor.is_none());
930    }
931
932    #[test]
933    fn ctrl_a_e_navigation() {
934        let mut editor = Editor::new();
935        editor.insert_str("abc");
936        editor.move_cursor_home();
937        assert_eq!(editor.cursor_grapheme(), 0);
938        editor.move_cursor_end();
939        assert_eq!(editor.cursor_grapheme(), 3);
940    }
941
942    #[test]
943    fn ctrl_f_b_navigation() {
944        let mut editor = Editor::new();
945        editor.insert_str("ab");
946        editor.move_cursor_home();
947        editor.move_cursor_right();
948        assert_eq!(editor.cursor_grapheme(), 1);
949        editor.move_cursor_left();
950        assert_eq!(editor.cursor_grapheme(), 0);
951    }
952
953    #[test]
954    fn alt_f_forward() {
955        let mut editor = Editor::new();
956        editor.insert_str("hi there");
957        editor.move_cursor_home();
958        editor.move_word_forward();
959        assert_eq!(editor.cursor_grapheme(), 2);
960    }
961
962    #[test]
963    fn home_end_keys() {
964        let mut editor = Editor::new();
965        editor.insert_str("ab");
966        editor.handle_input(&key_event(KeyCode::Home));
967        assert_eq!(editor.cursor_grapheme(), 0);
968        editor.handle_input(&key_event(KeyCode::End));
969        assert_eq!(editor.cursor_grapheme(), 2);
970    }
971
972    #[test]
973    fn delete_at_end() {
974        let mut editor = Editor::new();
975        editor.insert_str("a");
976        editor.move_cursor_end();
977        editor.delete_forward();
978        assert_eq!(editor.text(), "a");
979    }
980
981    #[test]
982    fn backspace_at_start() {
983        let mut editor = Editor::new();
984        editor.delete_backward();
985        assert_eq!(editor.text(), "");
986    }
987
988    #[test]
989    fn cursor_line_col_first_line() {
990        let mut editor = Editor::new();
991        editor.insert_str("hello");
992        let (line, col) = editor.cursor_line_col();
993        assert_eq!(line, 0);
994        assert_eq!(col, 5);
995    }
996
997    #[test]
998    fn cursor_line_col_second_line() {
999        let mut editor = Editor::new();
1000        editor.insert_str("hello\nworld");
1001        assert_eq!(editor.cursor_line_col(), (1, 5));
1002    }
1003
1004    #[test]
1005    fn graphemes_count() {
1006        let mut editor = Editor::new();
1007        editor.insert_str("éà");
1008        assert_eq!(editor.graphemes().len(), 2);
1009    }
1010
1011    #[test]
1012    fn byte_index_bounds() {
1013        let mut editor = Editor::new();
1014        editor.insert_str("ab");
1015        assert_eq!(editor.byte_index(0), 0);
1016        assert_eq!(editor.byte_index(2), 2);
1017        assert_eq!(editor.byte_index(10), 2);
1018    }
1019
1020    #[test]
1021    fn move_cursor_up_from_line_two() {
1022        let mut editor = Editor::new();
1023        editor.insert_str("a\nb\nc");
1024        editor.move_cursor_end(); // cursor at end of line 2
1025        editor.move_cursor_up();
1026        // Cursor should land at the '\n' after "b" because col=1 and we hit it
1027        assert_eq!(editor.cursor_grapheme(), 3);
1028    }
1029
1030    #[test]
1031    fn move_cursor_down_from_start() {
1032        let mut editor = Editor::new();
1033        editor.insert_str("a\nb");
1034        editor.cursor = 0;
1035        editor.move_cursor_down();
1036        assert_eq!(editor.cursor_grapheme(), 2);
1037    }
1038
1039    #[test]
1040    fn move_cursor_home_multiline() {
1041        let mut editor = Editor::new();
1042        editor.insert_str("a\nb");
1043        editor.cursor = 3;
1044        editor.move_cursor_home();
1045        assert_eq!(editor.cursor_grapheme(), 2);
1046    }
1047
1048    #[test]
1049    fn move_cursor_end_multiline() {
1050        let mut editor = Editor::new();
1051        editor.insert_str("a\nb");
1052        editor.cursor = 0;
1053        editor.move_cursor_end();
1054        assert_eq!(editor.cursor_grapheme(), 1);
1055    }
1056
1057    #[test]
1058    fn history_up_past_start() {
1059        let mut editor = Editor::new();
1060        editor.insert_str("a");
1061        editor.push_history();
1062        editor.history_up();
1063        editor.history_up(); // should be a no-op when at first item
1064        assert_eq!(editor.text(), "a");
1065    }
1066
1067    #[test]
1068    fn push_history_max_limit() {
1069        let mut editor = Editor::new();
1070        for i in 0..105 {
1071            editor.text = i.to_string();
1072            editor.cursor = 1;
1073            editor.push_history();
1074        }
1075        // History should be capped at max_history (100)
1076        assert_eq!(editor.history.len(), 100);
1077    }
1078
1079    // -- Vim mode tests --
1080
1081    #[test]
1082    fn vim_starts_in_normal_mode() {
1083        let mut editor = Editor::new();
1084        editor.set_vim_mode_enabled(true);
1085        assert_eq!(editor.mode(), VimMode::Normal);
1086    }
1087
1088    #[test]
1089    fn vim_hjkl_navigation() {
1090        let mut editor = Editor::new();
1091        editor.set_vim_mode_enabled(true);
1092        editor.set_mode(VimMode::Insert);
1093        editor.insert_str("ab\ncd\nef");
1094        editor.set_mode(VimMode::Normal);
1095        editor.cursor = 0;
1096        editor.move_cursor_down(); // to line 1, 'c'
1097        assert_eq!(editor.cursor_grapheme(), 3); // 'c'
1098        editor.handle_input(&Event::Key(KeyEvent::new(
1099            KeyCode::Char('l'),
1100            KeyModifiers::empty(),
1101        )));
1102        assert_eq!(editor.cursor_grapheme(), 4); // 'd'
1103        editor.handle_input(&Event::Key(KeyEvent::new(
1104            KeyCode::Char('h'),
1105            KeyModifiers::empty(),
1106        )));
1107        assert_eq!(editor.cursor_grapheme(), 3); // 'c'
1108        editor.handle_input(&Event::Key(KeyEvent::new(
1109            KeyCode::Char('j'),
1110            KeyModifiers::empty(),
1111        )));
1112        assert_eq!(editor.cursor_grapheme(), 6); // 'e' on line 2
1113        editor.handle_input(&Event::Key(KeyEvent::new(
1114            KeyCode::Char('k'),
1115            KeyModifiers::empty(),
1116        )));
1117        assert_eq!(editor.cursor_grapheme(), 3); // back to 'c'
1118    }
1119
1120    #[test]
1121    fn vim_i_enters_insert_mode() {
1122        let mut editor = Editor::new();
1123        editor.set_vim_mode_enabled(true);
1124        editor.handle_input(&Event::Key(KeyEvent::new(
1125            KeyCode::Char('i'),
1126            KeyModifiers::empty(),
1127        )));
1128        assert_eq!(editor.mode(), VimMode::Insert);
1129        editor.handle_input(&key_event(KeyCode::Char('x')));
1130        assert_eq!(editor.text(), "x");
1131    }
1132
1133    #[test]
1134    fn vim_esc_returns_to_normal_mode() {
1135        let mut editor = Editor::new();
1136        editor.set_vim_mode_enabled(true);
1137        editor.set_mode(VimMode::Insert);
1138        editor.handle_input(&key_event(KeyCode::Esc));
1139        assert_eq!(editor.mode(), VimMode::Normal);
1140    }
1141
1142    #[test]
1143    fn vim_x_deletes_char() {
1144        let mut editor = Editor::new();
1145        editor.set_vim_mode_enabled(true);
1146        editor.set_mode(VimMode::Insert);
1147        editor.insert_str("abc");
1148        editor.set_mode(VimMode::Normal);
1149        editor.move_cursor_home();
1150        editor.handle_input(&Event::Key(KeyEvent::new(
1151            KeyCode::Char('x'),
1152            KeyModifiers::empty(),
1153        )));
1154        assert_eq!(editor.text(), "bc");
1155    }
1156
1157    #[test]
1158    fn vim_dd_deletes_line() {
1159        let mut editor = Editor::new();
1160        editor.set_vim_mode_enabled(true);
1161        editor.set_mode(VimMode::Insert);
1162        editor.insert_str("hello\nworld");
1163        editor.set_mode(VimMode::Normal);
1164        editor.cursor = 0;
1165        editor.handle_input(&Event::Key(KeyEvent::new(
1166            KeyCode::Char('d'),
1167            KeyModifiers::empty(),
1168        )));
1169        editor.handle_input(&Event::Key(KeyEvent::new(
1170            KeyCode::Char('d'),
1171            KeyModifiers::empty(),
1172        )));
1173        assert_eq!(editor.text(), "world");
1174    }
1175
1176    #[test]
1177    fn vim_yy_yanks_line() {
1178        let mut editor = Editor::new();
1179        editor.set_vim_mode_enabled(true);
1180        editor.set_mode(VimMode::Insert);
1181        editor.insert_str("hello\nworld");
1182        editor.set_mode(VimMode::Normal);
1183        editor.cursor = 0;
1184        editor.handle_input(&Event::Key(KeyEvent::new(
1185            KeyCode::Char('y'),
1186            KeyModifiers::empty(),
1187        )));
1188        editor.handle_input(&Event::Key(KeyEvent::new(
1189            KeyCode::Char('y'),
1190            KeyModifiers::empty(),
1191        )));
1192        // p pastes after cursor (position 0)
1193        editor.handle_input(&Event::Key(KeyEvent::new(
1194            KeyCode::Char('p'),
1195            KeyModifiers::empty(),
1196        )));
1197        assert_eq!(editor.text(), "hello\nhello\nworld");
1198    }
1199
1200    #[test]
1201    fn vim_0_and_dollar() {
1202        let mut editor = Editor::new();
1203        editor.set_vim_mode_enabled(true);
1204        editor.set_mode(VimMode::Insert);
1205        editor.insert_str("abc");
1206        editor.set_mode(VimMode::Normal);
1207        editor.move_cursor_end();
1208        editor.handle_input(&Event::Key(KeyEvent::new(
1209            KeyCode::Char('0'),
1210            KeyModifiers::empty(),
1211        )));
1212        assert_eq!(editor.cursor_grapheme(), 0);
1213        editor.handle_input(&Event::Key(KeyEvent::new(
1214            KeyCode::Char('$'),
1215            KeyModifiers::empty(),
1216        )));
1217        assert_eq!(editor.cursor_grapheme(), 3);
1218    }
1219
1220    #[test]
1221    fn vim_gg_and_G() {
1222        let mut editor = Editor::new();
1223        editor.set_vim_mode_enabled(true);
1224        editor.set_mode(VimMode::Insert);
1225        editor.insert_str("a\nb\nc");
1226        editor.set_mode(VimMode::Normal);
1227        editor.move_cursor_end();
1228        editor.handle_input(&Event::Key(KeyEvent::new(
1229            KeyCode::Char('g'),
1230            KeyModifiers::empty(),
1231        )));
1232        editor.handle_input(&Event::Key(KeyEvent::new(
1233            KeyCode::Char('g'),
1234            KeyModifiers::empty(),
1235        )));
1236        assert_eq!(editor.cursor_grapheme(), 0);
1237        editor.handle_input(&Event::Key(KeyEvent::new(
1238            KeyCode::Char('G'),
1239            KeyModifiers::empty(),
1240        )));
1241        assert_eq!(editor.cursor_grapheme(), 5);
1242    }
1243
1244    #[test]
1245    fn vim_a_appends() {
1246        let mut editor = Editor::new();
1247        editor.set_vim_mode_enabled(true);
1248        editor.set_mode(VimMode::Insert);
1249        editor.insert_str("a");
1250        editor.set_mode(VimMode::Normal);
1251        editor.move_cursor_home();
1252        editor.handle_input(&Event::Key(KeyEvent::new(
1253            KeyCode::Char('a'),
1254            KeyModifiers::empty(),
1255        )));
1256        assert_eq!(editor.mode(), VimMode::Insert);
1257        editor.handle_input(&key_event(KeyCode::Char('b')));
1258        assert_eq!(editor.text(), "ab");
1259    }
1260
1261    #[test]
1262    fn vim_o_opens_line_below() {
1263        let mut editor = Editor::new();
1264        editor.set_vim_mode_enabled(true);
1265        editor.set_mode(VimMode::Insert);
1266        editor.insert_str("a");
1267        editor.set_mode(VimMode::Normal);
1268        editor.handle_input(&Event::Key(KeyEvent::new(
1269            KeyCode::Char('o'),
1270            KeyModifiers::empty(),
1271        )));
1272        assert_eq!(editor.mode(), VimMode::Insert);
1273        assert_eq!(editor.text(), "a\n");
1274    }
1275
1276    #[test]
1277    fn vim_O_opens_line_above() {
1278        let mut editor = Editor::new();
1279        editor.set_vim_mode_enabled(true);
1280        editor.set_mode(VimMode::Insert);
1281        editor.insert_str("a");
1282        editor.set_mode(VimMode::Normal);
1283        editor.handle_input(&Event::Key(KeyEvent::new(
1284            KeyCode::Char('O'),
1285            KeyModifiers::empty(),
1286        )));
1287        assert_eq!(editor.mode(), VimMode::Insert);
1288        assert_eq!(editor.text(), "\na");
1289    }
1290
1291    #[test]
1292    fn vim_r_replaces_char() {
1293        let mut editor = Editor::new();
1294        editor.set_vim_mode_enabled(true);
1295        editor.set_mode(VimMode::Insert);
1296        editor.insert_str("abc");
1297        editor.set_mode(VimMode::Normal);
1298        editor.move_cursor_home();
1299        editor.handle_input(&Event::Key(KeyEvent::new(
1300            KeyCode::Char('r'),
1301            KeyModifiers::empty(),
1302        )));
1303        editor.handle_input(&Event::Key(KeyEvent::new(
1304            KeyCode::Char('x'),
1305            KeyModifiers::empty(),
1306        )));
1307        assert_eq!(editor.text(), "xbc");
1308    }
1309
1310    #[test]
1311    fn vim_dw_deletes_word() {
1312        let mut editor = Editor::new();
1313        editor.set_vim_mode_enabled(true);
1314        editor.set_mode(VimMode::Insert);
1315        editor.insert_str("hello world");
1316        editor.set_mode(VimMode::Normal);
1317        editor.move_cursor_home();
1318        editor.handle_input(&Event::Key(KeyEvent::new(
1319            KeyCode::Char('d'),
1320            KeyModifiers::empty(),
1321        )));
1322        editor.handle_input(&Event::Key(KeyEvent::new(
1323            KeyCode::Char('w'),
1324            KeyModifiers::empty(),
1325        )));
1326        assert_eq!(editor.text(), " world");
1327    }
1328
1329    #[test]
1330    fn vim_u_undo() {
1331        let mut editor = Editor::new();
1332        editor.set_vim_mode_enabled(true);
1333        editor.set_mode(VimMode::Insert);
1334        editor.handle_input(&key_event(KeyCode::Char('a')));
1335        editor.handle_input(&key_event(KeyCode::Char('b')));
1336        editor.set_mode(VimMode::Normal);
1337        editor.handle_input(&Event::Key(KeyEvent::new(
1338            KeyCode::Char('u'),
1339            KeyModifiers::empty(),
1340        )));
1341        assert_eq!(editor.text(), "a");
1342    }
1343
1344    #[test]
1345    fn vim_normal_mode_arrow_keys_work() {
1346        let mut editor = Editor::new();
1347        editor.set_vim_mode_enabled(true);
1348        editor.set_mode(VimMode::Insert);
1349        editor.insert_str("ab");
1350        editor.set_mode(VimMode::Normal);
1351        editor.move_cursor_end();
1352        editor.handle_input(&key_event(KeyCode::Left));
1353        assert_eq!(editor.cursor_grapheme(), 1);
1354    }
1355}