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