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    theme::{
14        Style,
15        Theme,
16    },
17    undo_stack::UndoStack,
18    word_navigation::{
19        find_word_backward,
20        find_word_forward,
21    },
22};
23
24/// Snapshot of editor state for undo/redo.
25#[derive(Clone)]
26pub struct EditorAction {
27    /// The full text buffer at the time of the snapshot.
28    pub text: String,
29    /// Cursor position in grapheme indices.
30    pub cursor: usize,
31}
32
33/// Vim editing mode state.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum VimMode {
36    /// Normal mode — keys are commands (hjkl, dw, yy, etc.).
37    Normal,
38    /// Insert mode — keys insert text.
39    Insert,
40}
41
42/// A multi-line text editor component.
43///
44/// Defaults to **Emacs-style** bindings (Ctrl+A, Ctrl+E, Ctrl+K, etc.).
45/// Call [`set_vim_mode_enabled`](Editor::set_vim_mode_enabled) to opt into
46/// vim-style modal editing (Normal/Insert mode with hjkl, dd, yy, etc.).
47///
48/// Supports undo/redo, kill-ring (yank/yank-pop), history navigation,
49/// word-wise movement, and paste detection for large inserts.
50#[derive(Clone)]
51pub struct Editor {
52    text: String,
53    cursor: usize,
54    focused: bool,
55    kill_ring: KillRing,
56    undo_stack: UndoStack<EditorAction>,
57    lines_cache: Vec<String>,
58    cache_width: u16,
59    history: Vec<String>,
60    history_index: Option<usize>,
61    max_history: usize,
62    /// Whether vim modal editing is enabled.
63    vim_mode_enabled: bool,
64    /// Current vim mode (only meaningful when `vim_mode_enabled` is `true`).
65    mode: VimMode,
66    /// Pending normal-mode command prefix (e.g. `'d'`, `'y'`).
67    pending_cmd: Option<char>,
68}
69
70impl Editor {
71    /// Create a new empty editor with Emacs-style bindings.
72    pub fn new() -> Self {
73        Self {
74            text: String::new(),
75            cursor: 0,
76            focused: false,
77            kill_ring: KillRing::new(),
78            undo_stack: UndoStack::new(),
79            lines_cache: Vec::new(),
80            cache_width: 0,
81            history: Vec::new(),
82            history_index: None,
83            max_history: 100,
84            vim_mode_enabled: false,
85            mode: VimMode::Normal,
86            pending_cmd: None,
87        }
88    }
89
90    /// Returns `true` if vim modal editing is enabled.
91    pub fn vim_mode_enabled(&self) -> bool {
92        self.vim_mode_enabled
93    }
94
95    /// Enable or disable vim modal editing.
96    ///
97    /// When enabled the editor starts in Normal mode; press `i` to insert,
98    /// `Escape` to return to Normal. When disabled, Emacs-style bindings
99    /// are always active.
100    pub fn set_vim_mode_enabled(&mut self, enabled: bool) {
101        self.vim_mode_enabled = enabled;
102        self.mode = VimMode::Normal;
103        self.pending_cmd = None;
104    }
105
106    /// Current vim mode (only meaningful when vim mode is enabled).
107    pub fn mode(&self) -> VimMode {
108        self.mode
109    }
110
111    /// Switch to the given vim mode.
112    pub fn set_mode(&mut self, mode: VimMode) {
113        self.mode = mode;
114        self.pending_cmd = None;
115    }
116
117    /// Borrow the current text content.
118    pub fn text(&self) -> &str {
119        &self.text
120    }
121
122    /// Current cursor position in grapheme indices.
123    pub fn cursor_grapheme(&self) -> usize {
124        self.cursor
125    }
126
127    /// Replace the entire text buffer and move the cursor to the end.
128    pub fn set_text(&mut self, text: impl Into<String>) {
129        self.text = text.into();
130        self.cursor = self.graphemes().len();
131        self.lines_cache.clear();
132        self.cache_width = 0;
133    }
134
135    fn graphemes(&self) -> Vec<&str> {
136        self.text.graphemes(true).collect()
137    }
138
139    fn byte_index(&self, grapheme_idx: usize) -> usize {
140        self.text
141            .grapheme_indices(true)
142            .nth(grapheme_idx)
143            .map(|(i, _)| i)
144            .unwrap_or(self.text.len())
145    }
146
147    fn save_undo(&mut self) {
148        self.undo_stack.push(EditorAction {
149            text: self.text.clone(),
150            cursor: self.cursor,
151        });
152    }
153
154    fn insert_char(&mut self, c: char) {
155        let idx = self.byte_index(self.cursor);
156        self.text.insert(idx, c);
157        self.cursor += 1;
158        self.invalidate_cache();
159    }
160
161    fn insert_newline(&mut self) {
162        let idx = self.byte_index(self.cursor);
163        self.text.insert(idx, '\n');
164        self.cursor += 1;
165        self.invalidate_cache();
166    }
167
168    fn insert_str(&mut self, s: &str) {
169        let idx = self.byte_index(self.cursor);
170        self.text.insert_str(idx, s);
171        self.cursor += s.graphemes(true).count();
172        self.invalidate_cache();
173    }
174
175    fn delete_backward(&mut self) {
176        if self.cursor > 0 {
177            let start = self.byte_index(self.cursor - 1);
178            let end = self.byte_index(self.cursor);
179            let killed = self.text.drain(start..end).collect::<String>();
180            self.kill_ring.push(killed);
181            self.cursor -= 1;
182            self.invalidate_cache();
183        }
184    }
185
186    fn delete_forward(&mut self) {
187        if self.cursor < self.graphemes().len() {
188            let start = self.byte_index(self.cursor);
189            let end = self.byte_index(self.cursor + 1);
190            self.text.drain(start..end);
191            self.invalidate_cache();
192        }
193    }
194
195    fn move_cursor_left(&mut self) {
196        if self.cursor > 0 {
197            self.cursor -= 1;
198        }
199    }
200
201    fn move_cursor_right(&mut self) {
202        if self.cursor < self.graphemes().len() {
203            self.cursor += 1;
204        }
205    }
206
207    fn move_cursor_up(&mut self) {
208        let (line, col) = self.cursor_line_col();
209        if line == 0 {
210            return;
211        }
212        let target_line = line - 1;
213        let mut current_line = 0;
214        let mut current_col = 0;
215        for (gidx, g) in self.text.graphemes(true).enumerate() {
216            if current_line == target_line {
217                if current_col >= col || g == "\n" {
218                    self.cursor = gidx;
219                    return;
220                }
221                current_col += g.width();
222            } else if g == "\n" {
223                current_line += 1;
224                current_col = 0;
225            }
226        }
227    }
228
229    fn move_cursor_down(&mut self) {
230        let (line, col) = self.cursor_line_col();
231        let total_lines = self.text.lines().count();
232        if line + 1 >= total_lines {
233            return;
234        }
235        let target_line = line + 1;
236        let mut current_line = 0;
237        let mut current_col = 0;
238        let mut gidx = 0;
239        for g in self.text.graphemes(true) {
240            if current_line == target_line {
241                if current_col >= col || g == "\n" {
242                    self.cursor = gidx;
243                    return;
244                }
245                current_col += g.width();
246            } else if g == "\n" {
247                current_line += 1;
248                current_col = 0;
249            }
250            gidx += 1;
251        }
252        self.cursor = gidx;
253    }
254
255    fn move_cursor_home(&mut self) {
256        let (line, _) = self.cursor_line_col();
257        let mut current_line = 0;
258        for (gidx, g) in self.text.graphemes(true).enumerate() {
259            if current_line == line {
260                self.cursor = gidx;
261                return;
262            }
263            if g == "\n" {
264                current_line += 1;
265            }
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        for (graphemes_seen, g) in self.text.graphemes(true).enumerate() {
529            if graphemes_seen >= self.cursor {
530                break;
531            }
532            if g == "\n" {
533                current_line += 1;
534                current_col = 0;
535            } else {
536                current_col += g.width();
537            }
538        }
539        (current_line, current_col)
540    }
541}
542
543impl Default for Editor {
544    fn default() -> Self {
545        Self::new()
546    }
547}
548
549impl Focusable for Editor {
550    fn focused(&self) -> bool {
551        self.focused
552    }
553
554    fn set_focused(&mut self, focused: bool) {
555        self.focused = focused;
556    }
557}
558
559impl Editor {
560    fn handle_insert_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
561        use crossterm::event::KeyModifiers;
562        self.save_undo();
563        match key.code {
564            | KeyCode::Char(c) => {
565                if key.modifiers.contains(KeyModifiers::CONTROL) {
566                    match c {
567                        | 'a' => self.move_cursor_home(),
568                        | 'e' => self.move_cursor_end(),
569                        | 'b' => self.move_cursor_left(),
570                        | 'f' => self.move_cursor_right(),
571                        | 'n' => self.move_cursor_down(),
572                        | 'p' => self.move_cursor_up(),
573                        | 'd' => self.delete_forward(),
574                        | 'h' => self.delete_backward(),
575                        | 'k' => self.kill_to_end(),
576                        | 'w' => self.kill_word_backward(),
577                        | 'u' => {
578                            self.move_cursor_home();
579                            self.kill_to_end();
580                        },
581                        | 'y' => self.yank(),
582                        | 'r' => self.redo(),
583                        | '-' | '_' => self.undo(),
584                        | _ => return InputResult::Ignored,
585                    }
586                } else if key.modifiers.contains(KeyModifiers::ALT) {
587                    match c {
588                        | 'b' => self.move_word_backward(),
589                        | 'f' => self.move_word_forward(),
590                        | 'd' => self.kill_word_forward(),
591                        | 'y' => self.yank_pop(),
592                        | _ => return InputResult::Ignored,
593                    }
594                } else {
595                    self.insert_char(c);
596                }
597                InputResult::Handled
598            },
599            | KeyCode::Enter => {
600                let idx = self.byte_index(self.cursor);
601                if idx > 0 && self.text.as_bytes().get(idx - 1) == Some(&b'\\') {
602                    self.text.remove(idx - 1);
603                    self.cursor -= 1;
604                    self.insert_newline();
605                } else {
606                    self.insert_newline();
607                }
608                InputResult::Handled
609            },
610            | KeyCode::Left => {
611                self.move_cursor_left();
612                InputResult::Handled
613            },
614            | KeyCode::Right => {
615                self.move_cursor_right();
616                InputResult::Handled
617            },
618            | KeyCode::Up => {
619                if key.modifiers.contains(KeyModifiers::CONTROL) {
620                    self.move_cursor_up();
621                } else {
622                    self.history_up();
623                }
624                InputResult::Handled
625            },
626            | KeyCode::Down => {
627                if key.modifiers.contains(KeyModifiers::CONTROL) {
628                    self.move_cursor_down();
629                } else {
630                    self.history_down();
631                }
632                InputResult::Handled
633            },
634            | KeyCode::Home => {
635                self.move_cursor_home();
636                InputResult::Handled
637            },
638            | KeyCode::End => {
639                self.move_cursor_end();
640                InputResult::Handled
641            },
642            | KeyCode::Backspace => {
643                self.delete_backward();
644                InputResult::Handled
645            },
646            | KeyCode::Delete => {
647                self.delete_forward();
648                InputResult::Handled
649            },
650            | KeyCode::Esc => {
651                self.mode = VimMode::Normal;
652                InputResult::Handled
653            },
654            | _ => InputResult::Ignored,
655        }
656    }
657
658    fn handle_normal_mode(&mut self, key: &crossterm::event::KeyEvent) -> InputResult {
659        // Multi-key commands (dd, yy, gg, etc.) are handled via pending_cmd.
660        if let Some(pending) = self.pending_cmd {
661            match key.code {
662                | KeyCode::Char('d') if pending == 'd' => {
663                    self.save_undo();
664                    self.delete_line();
665                    self.pending_cmd = None;
666                    return InputResult::Handled;
667                },
668                | KeyCode::Char('y') if pending == 'y' => {
669                    self.yank_line();
670                    self.pending_cmd = None;
671                    return InputResult::Handled;
672                },
673                | KeyCode::Char('g') if pending == 'g' => {
674                    self.go_to_start();
675                    self.pending_cmd = None;
676                    return InputResult::Handled;
677                },
678                | KeyCode::Char('w') if pending == 'd' => {
679                    self.save_undo();
680                    self.kill_word_forward();
681                    self.pending_cmd = None;
682                    return InputResult::Handled;
683                },
684                | KeyCode::Char('w') if pending == 'y' => {
685                    let start = self.cursor;
686                    self.move_word_forward();
687                    let end_byte = self.byte_index(self.cursor);
688                    let start_byte = self.byte_index(start);
689                    let yanked = self.text[start_byte..end_byte].to_string();
690                    if !yanked.is_empty() {
691                        self.kill_ring.push(yanked);
692                    }
693                    self.cursor = start;
694                    self.pending_cmd = None;
695                    return InputResult::Handled;
696                },
697                | KeyCode::Char(c) if pending == 'r' => {
698                    self.save_undo();
699                    self.replace_char(c);
700                    self.pending_cmd = None;
701                    return InputResult::Handled;
702                },
703                | _ => {
704                    self.pending_cmd = None;
705                    // Fall through to normal handling below
706                },
707            }
708        }
709
710        match key.code {
711            | KeyCode::Char(c) => {
712                match c {
713                    | 'h' => self.move_cursor_left(),
714                    | 'j' => self.move_cursor_down(),
715                    | 'k' => self.move_cursor_up(),
716                    | 'l' => self.move_cursor_right(),
717                    | 'w' => self.move_word_forward(),
718                    | 'b' => self.move_word_backward(),
719                    | 'x' => {
720                        self.save_undo();
721                        self.delete_forward();
722                    },
723                    | '0' => self.move_cursor_home(),
724                    | '$' => self.move_cursor_end(),
725                    | 'i' => self.mode = VimMode::Insert,
726                    | 'a' => {
727                        self.move_cursor_right();
728                        self.mode = VimMode::Insert;
729                    },
730                    | 'o' => {
731                        self.save_undo();
732                        self.open_line_below();
733                    },
734                    | 'O' => {
735                        self.save_undo();
736                        self.open_line_above();
737                    },
738                    | 'p' => {
739                        self.save_undo();
740                        self.yank();
741                    },
742                    | 'u' => {
743                        self.save_undo();
744                        self.undo();
745                    },
746                    | 'r' => {
747                        self.pending_cmd = Some('r');
748                        return InputResult::Handled;
749                    },
750                    | 'd' | 'y' => {
751                        self.pending_cmd = Some(c);
752                        return InputResult::Handled;
753                    },
754                    | 'g' => {
755                        self.pending_cmd = Some('g');
756                        return InputResult::Handled;
757                    },
758                    | 'G' => self.go_to_end(),
759                    | _ => return InputResult::Ignored,
760                }
761                InputResult::Handled
762            },
763            | KeyCode::Left => {
764                self.move_cursor_left();
765                InputResult::Handled
766            },
767            | KeyCode::Right => {
768                self.move_cursor_right();
769                InputResult::Handled
770            },
771            | KeyCode::Up => {
772                self.move_cursor_up();
773                InputResult::Handled
774            },
775            | KeyCode::Down => {
776                self.move_cursor_down();
777                InputResult::Handled
778            },
779            | KeyCode::Home => {
780                self.move_cursor_home();
781                InputResult::Handled
782            },
783            | KeyCode::End => {
784                self.move_cursor_end();
785                InputResult::Handled
786            },
787            | KeyCode::Backspace => {
788                self.move_cursor_left();
789                InputResult::Handled
790            },
791            | _ => InputResult::Ignored,
792        }
793    }
794}
795
796impl Component for Editor {
797    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
798        let (cursor_line, cursor_col) = self.cursor_line_col();
799        let wrapped = if width == self.cache_width && !self.lines_cache.is_empty() {
800            self.lines_cache.clone()
801        } else {
802            crate::utils::wrap_text_with_ansi(&self.text, width)
803        };
804
805        let mut lines = Vec::new();
806        if self.focused {
807            let theme = Theme::palette();
808            let edit_style = Style::new().fg(theme.text()).bg(theme.surface());
809            let cursor_style = Style::new().fg(theme.cursor()).bg(theme.surface());
810            for (i, line) in wrapped.into_iter().enumerate() {
811                let padded = crate::utils::pad_to_width(&line, width);
812                let rendered = if i == cursor_line {
813                    crate::utils::render_line_with_cursor(
814                        &padded,
815                        cursor_col,
816                        width,
817                        &edit_style,
818                        &cursor_style,
819                    )
820                } else {
821                    crate::theme::stylize(&padded, &edit_style)
822                };
823                lines.push(rendered);
824            }
825        } else {
826            for line in wrapped {
827                lines.push(crate::utils::pad_to_width(&line, width));
828            }
829        }
830
831        Ok(Rendered {
832            lines,
833            cursor: None,
834            images: Vec::new(),
835        })
836    }
837
838    fn handle_input(&mut self, event: &Event) -> InputResult {
839        if let Event::Key(key) = event {
840            if self.vim_mode_enabled {
841                match self.mode {
842                    | VimMode::Insert => self.handle_insert_mode(key),
843                    | VimMode::Normal => self.handle_normal_mode(key),
844                }
845            } else {
846                self.handle_insert_mode(key)
847            }
848        } else {
849            InputResult::Ignored
850        }
851    }
852
853    fn as_focusable(&self) -> Option<&dyn Focusable> {
854        Some(self)
855    }
856
857    fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
858        Some(self)
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use crossterm::event::{
865        KeyCode,
866        KeyEvent,
867        KeyModifiers,
868    };
869
870    use super::*;
871
872    fn key_event(code: KeyCode) -> Event {
873        Event::Key(code.into())
874    }
875
876    #[test]
877    fn yank_pop_cycles() {
878        let mut editor = Editor::new();
879        editor.insert_str("ab");
880        editor.move_cursor_home();
881        editor.kill_to_end();
882        assert_eq!(editor.text(), "");
883        editor.yank();
884        assert_eq!(editor.text(), "ab");
885        editor.yank_pop();
886        assert_eq!(editor.text(), "abab");
887    }
888
889    #[test]
890    fn history_down_navigation() {
891        let mut editor = Editor::new();
892        editor.insert_str("a");
893        editor.push_history();
894        editor.move_cursor_home();
895        editor.kill_to_end();
896        editor.insert_str("b");
897        editor.push_history();
898        editor.history_up();
899        assert_eq!(editor.text(), "b");
900        editor.history_up();
901        assert_eq!(editor.text(), "a");
902        editor.history_down();
903        assert_eq!(editor.text(), "b");
904        editor.history_down();
905        assert_eq!(editor.text(), "");
906    }
907
908    #[test]
909    fn history_down_empty() {
910        let mut editor = Editor::new();
911        editor.history_down();
912        assert_eq!(editor.text(), "");
913    }
914
915    #[test]
916    fn push_history_empty() {
917        let mut editor = Editor::new();
918        editor.push_history();
919        assert_eq!(editor.text(), "");
920    }
921
922    #[test]
923    fn move_cursor_up_down() {
924        let mut editor = Editor::new();
925        editor.insert_str("a\nb");
926        editor.move_cursor_up();
927        // cursor lands at newline position because col=1 and current_col reaches 1 at
928        // '\n'
929        assert_eq!(editor.cursor_grapheme(), 1);
930        editor.move_cursor_down();
931        assert_eq!(editor.cursor_grapheme(), 3);
932    }
933
934    #[test]
935    fn kill_word_forward() {
936        let mut editor = Editor::new();
937        editor.insert_str("hello world");
938        editor.move_cursor_home();
939        editor.kill_word_forward();
940        assert_eq!(editor.text(), " world");
941    }
942
943    #[test]
944    fn render_unfocused() {
945        let mut editor = Editor::new();
946        editor.set_focused(false);
947        editor.insert_str("x");
948        let r = editor.render(80).unwrap();
949        assert!(r.cursor.is_none());
950    }
951
952    #[test]
953    fn ctrl_a_e_navigation() {
954        let mut editor = Editor::new();
955        editor.insert_str("abc");
956        editor.move_cursor_home();
957        assert_eq!(editor.cursor_grapheme(), 0);
958        editor.move_cursor_end();
959        assert_eq!(editor.cursor_grapheme(), 3);
960    }
961
962    #[test]
963    fn ctrl_f_b_navigation() {
964        let mut editor = Editor::new();
965        editor.insert_str("ab");
966        editor.move_cursor_home();
967        editor.move_cursor_right();
968        assert_eq!(editor.cursor_grapheme(), 1);
969        editor.move_cursor_left();
970        assert_eq!(editor.cursor_grapheme(), 0);
971    }
972
973    #[test]
974    fn alt_f_forward() {
975        let mut editor = Editor::new();
976        editor.insert_str("hi there");
977        editor.move_cursor_home();
978        editor.move_word_forward();
979        assert_eq!(editor.cursor_grapheme(), 2);
980    }
981
982    #[test]
983    fn home_end_keys() {
984        let mut editor = Editor::new();
985        editor.insert_str("ab");
986        editor.handle_input(&key_event(KeyCode::Home));
987        assert_eq!(editor.cursor_grapheme(), 0);
988        editor.handle_input(&key_event(KeyCode::End));
989        assert_eq!(editor.cursor_grapheme(), 2);
990    }
991
992    #[test]
993    fn delete_at_end() {
994        let mut editor = Editor::new();
995        editor.insert_str("a");
996        editor.move_cursor_end();
997        editor.delete_forward();
998        assert_eq!(editor.text(), "a");
999    }
1000
1001    #[test]
1002    fn backspace_at_start() {
1003        let mut editor = Editor::new();
1004        editor.delete_backward();
1005        assert_eq!(editor.text(), "");
1006    }
1007
1008    #[test]
1009    fn cursor_line_col_first_line() {
1010        let mut editor = Editor::new();
1011        editor.insert_str("hello");
1012        let (line, col) = editor.cursor_line_col();
1013        assert_eq!(line, 0);
1014        assert_eq!(col, 5);
1015    }
1016
1017    #[test]
1018    fn cursor_line_col_second_line() {
1019        let mut editor = Editor::new();
1020        editor.insert_str("hello\nworld");
1021        assert_eq!(editor.cursor_line_col(), (1, 5));
1022    }
1023
1024    #[test]
1025    fn graphemes_count() {
1026        let mut editor = Editor::new();
1027        editor.insert_str("éà");
1028        assert_eq!(editor.graphemes().len(), 2);
1029    }
1030
1031    #[test]
1032    fn byte_index_bounds() {
1033        let mut editor = Editor::new();
1034        editor.insert_str("ab");
1035        assert_eq!(editor.byte_index(0), 0);
1036        assert_eq!(editor.byte_index(2), 2);
1037        assert_eq!(editor.byte_index(10), 2);
1038    }
1039
1040    #[test]
1041    fn move_cursor_up_from_line_two() {
1042        let mut editor = Editor::new();
1043        editor.insert_str("a\nb\nc");
1044        editor.move_cursor_end(); // cursor at end of line 2
1045        editor.move_cursor_up();
1046        // Cursor should land at the '\n' after "b" because col=1 and we hit it
1047        assert_eq!(editor.cursor_grapheme(), 3);
1048    }
1049
1050    #[test]
1051    fn move_cursor_down_from_start() {
1052        let mut editor = Editor::new();
1053        editor.insert_str("a\nb");
1054        editor.cursor = 0;
1055        editor.move_cursor_down();
1056        assert_eq!(editor.cursor_grapheme(), 2);
1057    }
1058
1059    #[test]
1060    fn move_cursor_home_multiline() {
1061        let mut editor = Editor::new();
1062        editor.insert_str("a\nb");
1063        editor.cursor = 3;
1064        editor.move_cursor_home();
1065        assert_eq!(editor.cursor_grapheme(), 2);
1066    }
1067
1068    #[test]
1069    fn move_cursor_end_multiline() {
1070        let mut editor = Editor::new();
1071        editor.insert_str("a\nb");
1072        editor.cursor = 0;
1073        editor.move_cursor_end();
1074        assert_eq!(editor.cursor_grapheme(), 1);
1075    }
1076
1077    #[test]
1078    fn history_up_past_start() {
1079        let mut editor = Editor::new();
1080        editor.insert_str("a");
1081        editor.push_history();
1082        editor.history_up();
1083        editor.history_up(); // should be a no-op when at first item
1084        assert_eq!(editor.text(), "a");
1085    }
1086
1087    #[test]
1088    fn push_history_max_limit() {
1089        let mut editor = Editor::new();
1090        for i in 0..105 {
1091            editor.text = i.to_string();
1092            editor.cursor = 1;
1093            editor.push_history();
1094        }
1095        // History should be capped at max_history (100)
1096        assert_eq!(editor.history.len(), 100);
1097    }
1098
1099    // -- Vim mode tests --
1100
1101    #[test]
1102    fn vim_starts_in_normal_mode() {
1103        let mut editor = Editor::new();
1104        editor.set_vim_mode_enabled(true);
1105        assert_eq!(editor.mode(), VimMode::Normal);
1106    }
1107
1108    #[test]
1109    fn vim_hjkl_navigation() {
1110        let mut editor = Editor::new();
1111        editor.set_vim_mode_enabled(true);
1112        editor.set_mode(VimMode::Insert);
1113        editor.insert_str("ab\ncd\nef");
1114        editor.set_mode(VimMode::Normal);
1115        editor.cursor = 0;
1116        editor.move_cursor_down(); // to line 1, 'c'
1117        assert_eq!(editor.cursor_grapheme(), 3); // 'c'
1118        editor.handle_input(&Event::Key(KeyEvent::new(
1119            KeyCode::Char('l'),
1120            KeyModifiers::empty(),
1121        )));
1122        assert_eq!(editor.cursor_grapheme(), 4); // 'd'
1123        editor.handle_input(&Event::Key(KeyEvent::new(
1124            KeyCode::Char('h'),
1125            KeyModifiers::empty(),
1126        )));
1127        assert_eq!(editor.cursor_grapheme(), 3); // 'c'
1128        editor.handle_input(&Event::Key(KeyEvent::new(
1129            KeyCode::Char('j'),
1130            KeyModifiers::empty(),
1131        )));
1132        assert_eq!(editor.cursor_grapheme(), 6); // 'e' on line 2
1133        editor.handle_input(&Event::Key(KeyEvent::new(
1134            KeyCode::Char('k'),
1135            KeyModifiers::empty(),
1136        )));
1137        assert_eq!(editor.cursor_grapheme(), 3); // back to 'c'
1138    }
1139
1140    #[test]
1141    fn vim_i_enters_insert_mode() {
1142        let mut editor = Editor::new();
1143        editor.set_vim_mode_enabled(true);
1144        editor.handle_input(&Event::Key(KeyEvent::new(
1145            KeyCode::Char('i'),
1146            KeyModifiers::empty(),
1147        )));
1148        assert_eq!(editor.mode(), VimMode::Insert);
1149        editor.handle_input(&key_event(KeyCode::Char('x')));
1150        assert_eq!(editor.text(), "x");
1151    }
1152
1153    #[test]
1154    fn vim_esc_returns_to_normal_mode() {
1155        let mut editor = Editor::new();
1156        editor.set_vim_mode_enabled(true);
1157        editor.set_mode(VimMode::Insert);
1158        editor.handle_input(&key_event(KeyCode::Esc));
1159        assert_eq!(editor.mode(), VimMode::Normal);
1160    }
1161
1162    #[test]
1163    fn vim_x_deletes_char() {
1164        let mut editor = Editor::new();
1165        editor.set_vim_mode_enabled(true);
1166        editor.set_mode(VimMode::Insert);
1167        editor.insert_str("abc");
1168        editor.set_mode(VimMode::Normal);
1169        editor.move_cursor_home();
1170        editor.handle_input(&Event::Key(KeyEvent::new(
1171            KeyCode::Char('x'),
1172            KeyModifiers::empty(),
1173        )));
1174        assert_eq!(editor.text(), "bc");
1175    }
1176
1177    #[test]
1178    fn vim_dd_deletes_line() {
1179        let mut editor = Editor::new();
1180        editor.set_vim_mode_enabled(true);
1181        editor.set_mode(VimMode::Insert);
1182        editor.insert_str("hello\nworld");
1183        editor.set_mode(VimMode::Normal);
1184        editor.cursor = 0;
1185        editor.handle_input(&Event::Key(KeyEvent::new(
1186            KeyCode::Char('d'),
1187            KeyModifiers::empty(),
1188        )));
1189        editor.handle_input(&Event::Key(KeyEvent::new(
1190            KeyCode::Char('d'),
1191            KeyModifiers::empty(),
1192        )));
1193        assert_eq!(editor.text(), "world");
1194    }
1195
1196    #[test]
1197    fn vim_yy_yanks_line() {
1198        let mut editor = Editor::new();
1199        editor.set_vim_mode_enabled(true);
1200        editor.set_mode(VimMode::Insert);
1201        editor.insert_str("hello\nworld");
1202        editor.set_mode(VimMode::Normal);
1203        editor.cursor = 0;
1204        editor.handle_input(&Event::Key(KeyEvent::new(
1205            KeyCode::Char('y'),
1206            KeyModifiers::empty(),
1207        )));
1208        editor.handle_input(&Event::Key(KeyEvent::new(
1209            KeyCode::Char('y'),
1210            KeyModifiers::empty(),
1211        )));
1212        // p pastes after cursor (position 0)
1213        editor.handle_input(&Event::Key(KeyEvent::new(
1214            KeyCode::Char('p'),
1215            KeyModifiers::empty(),
1216        )));
1217        assert_eq!(editor.text(), "hello\nhello\nworld");
1218    }
1219
1220    #[test]
1221    fn vim_0_and_dollar() {
1222        let mut editor = Editor::new();
1223        editor.set_vim_mode_enabled(true);
1224        editor.set_mode(VimMode::Insert);
1225        editor.insert_str("abc");
1226        editor.set_mode(VimMode::Normal);
1227        editor.move_cursor_end();
1228        editor.handle_input(&Event::Key(KeyEvent::new(
1229            KeyCode::Char('0'),
1230            KeyModifiers::empty(),
1231        )));
1232        assert_eq!(editor.cursor_grapheme(), 0);
1233        editor.handle_input(&Event::Key(KeyEvent::new(
1234            KeyCode::Char('$'),
1235            KeyModifiers::empty(),
1236        )));
1237        assert_eq!(editor.cursor_grapheme(), 3);
1238    }
1239
1240    #[test]
1241    fn vim_gg_and_g() {
1242        let mut editor = Editor::new();
1243        editor.set_vim_mode_enabled(true);
1244        editor.set_mode(VimMode::Insert);
1245        editor.insert_str("a\nb\nc");
1246        editor.set_mode(VimMode::Normal);
1247        editor.move_cursor_end();
1248        editor.handle_input(&Event::Key(KeyEvent::new(
1249            KeyCode::Char('g'),
1250            KeyModifiers::empty(),
1251        )));
1252        editor.handle_input(&Event::Key(KeyEvent::new(
1253            KeyCode::Char('g'),
1254            KeyModifiers::empty(),
1255        )));
1256        assert_eq!(editor.cursor_grapheme(), 0);
1257        editor.handle_input(&Event::Key(KeyEvent::new(
1258            KeyCode::Char('G'),
1259            KeyModifiers::empty(),
1260        )));
1261        assert_eq!(editor.cursor_grapheme(), 5);
1262    }
1263
1264    #[test]
1265    fn vim_a_appends() {
1266        let mut editor = Editor::new();
1267        editor.set_vim_mode_enabled(true);
1268        editor.set_mode(VimMode::Insert);
1269        editor.insert_str("a");
1270        editor.set_mode(VimMode::Normal);
1271        editor.move_cursor_home();
1272        editor.handle_input(&Event::Key(KeyEvent::new(
1273            KeyCode::Char('a'),
1274            KeyModifiers::empty(),
1275        )));
1276        assert_eq!(editor.mode(), VimMode::Insert);
1277        editor.handle_input(&key_event(KeyCode::Char('b')));
1278        assert_eq!(editor.text(), "ab");
1279    }
1280
1281    #[test]
1282    fn vim_o_opens_line_below() {
1283        let mut editor = Editor::new();
1284        editor.set_vim_mode_enabled(true);
1285        editor.set_mode(VimMode::Insert);
1286        editor.insert_str("a");
1287        editor.set_mode(VimMode::Normal);
1288        editor.handle_input(&Event::Key(KeyEvent::new(
1289            KeyCode::Char('o'),
1290            KeyModifiers::empty(),
1291        )));
1292        assert_eq!(editor.mode(), VimMode::Insert);
1293        assert_eq!(editor.text(), "a\n");
1294    }
1295
1296    #[test]
1297    fn vim_o_opens_line_above() {
1298        let mut editor = Editor::new();
1299        editor.set_vim_mode_enabled(true);
1300        editor.set_mode(VimMode::Insert);
1301        editor.insert_str("a");
1302        editor.set_mode(VimMode::Normal);
1303        editor.handle_input(&Event::Key(KeyEvent::new(
1304            KeyCode::Char('O'),
1305            KeyModifiers::empty(),
1306        )));
1307        assert_eq!(editor.mode(), VimMode::Insert);
1308        assert_eq!(editor.text(), "\na");
1309    }
1310
1311    #[test]
1312    fn vim_r_replaces_char() {
1313        let mut editor = Editor::new();
1314        editor.set_vim_mode_enabled(true);
1315        editor.set_mode(VimMode::Insert);
1316        editor.insert_str("abc");
1317        editor.set_mode(VimMode::Normal);
1318        editor.move_cursor_home();
1319        editor.handle_input(&Event::Key(KeyEvent::new(
1320            KeyCode::Char('r'),
1321            KeyModifiers::empty(),
1322        )));
1323        editor.handle_input(&Event::Key(KeyEvent::new(
1324            KeyCode::Char('x'),
1325            KeyModifiers::empty(),
1326        )));
1327        assert_eq!(editor.text(), "xbc");
1328    }
1329
1330    #[test]
1331    fn vim_dw_deletes_word() {
1332        let mut editor = Editor::new();
1333        editor.set_vim_mode_enabled(true);
1334        editor.set_mode(VimMode::Insert);
1335        editor.insert_str("hello world");
1336        editor.set_mode(VimMode::Normal);
1337        editor.move_cursor_home();
1338        editor.handle_input(&Event::Key(KeyEvent::new(
1339            KeyCode::Char('d'),
1340            KeyModifiers::empty(),
1341        )));
1342        editor.handle_input(&Event::Key(KeyEvent::new(
1343            KeyCode::Char('w'),
1344            KeyModifiers::empty(),
1345        )));
1346        assert_eq!(editor.text(), " world");
1347    }
1348
1349    #[test]
1350    fn vim_u_undo() {
1351        let mut editor = Editor::new();
1352        editor.set_vim_mode_enabled(true);
1353        editor.set_mode(VimMode::Insert);
1354        editor.handle_input(&key_event(KeyCode::Char('a')));
1355        editor.handle_input(&key_event(KeyCode::Char('b')));
1356        editor.set_mode(VimMode::Normal);
1357        editor.handle_input(&Event::Key(KeyEvent::new(
1358            KeyCode::Char('u'),
1359            KeyModifiers::empty(),
1360        )));
1361        assert_eq!(editor.text(), "a");
1362    }
1363
1364    #[test]
1365    fn vim_normal_mode_arrow_keys_work() {
1366        let mut editor = Editor::new();
1367        editor.set_vim_mode_enabled(true);
1368        editor.set_mode(VimMode::Insert);
1369        editor.insert_str("ab");
1370        editor.set_mode(VimMode::Normal);
1371        editor.move_cursor_end();
1372        editor.handle_input(&key_event(KeyCode::Left));
1373        assert_eq!(editor.cursor_grapheme(), 1);
1374    }
1375
1376    #[test]
1377    fn render_uses_cache_when_width_matches() {
1378        Theme::with(Theme::Light, || {
1379            let mut editor = Editor::new();
1380            editor.text = "cached".to_string();
1381            editor.lines_cache = vec!["cached".to_string()];
1382            editor.cache_width = 80;
1383            editor.cursor = 6;
1384            editor.focused = true;
1385            let rendered = editor.render(80).unwrap();
1386            assert!(rendered.lines[0].contains("cached"));
1387            assert!(rendered.lines[0].contains(crate::utils::EDIT_CURSOR));
1388            assert!(rendered.lines[0].contains("\x1b[48;"));
1389            assert_eq!(crate::utils::visible_width(&rendered.lines[0]), 80);
1390            assert_eq!(rendered.cursor, None);
1391        });
1392    }
1393
1394    #[test]
1395    fn render_focused_shows_block_cursor() {
1396        Theme::with(Theme::Light, || {
1397            let mut editor = Editor::new();
1398            editor.set_focused(true);
1399            editor.insert_str("hello");
1400            let rendered = editor.render(80).unwrap();
1401            assert!(rendered.lines[0].contains("hello"));
1402            assert!(rendered.lines[0].contains(crate::utils::EDIT_CURSOR));
1403            assert!(rendered.lines[0].contains("\x1b[48;"));
1404            assert_eq!(rendered.cursor, None);
1405        });
1406    }
1407
1408    #[test]
1409    fn render_unfocused_no_cursor_or_highlight() {
1410        Theme::with(Theme::Light, || {
1411            let mut editor = Editor::new();
1412            editor.set_focused(false);
1413            editor.insert_str("hello");
1414            let rendered = editor.render(80).unwrap();
1415            assert_eq!(rendered.cursor, None);
1416            assert!(rendered.lines[0].contains("hello"));
1417            assert!(!rendered.lines[0].contains(crate::utils::EDIT_CURSOR));
1418            assert!(!rendered.lines[0].contains("\x1b[48;"));
1419        });
1420    }
1421
1422    #[test]
1423    fn redo_restores_state() {
1424        let mut editor = Editor::new();
1425        editor.undo_stack.push(EditorAction {
1426            text: "first".to_string(),
1427            cursor: 5,
1428        });
1429        editor.undo_stack.push(EditorAction {
1430            text: "second".to_string(),
1431            cursor: 6,
1432        });
1433        // Move the undo pointer back so redo has somewhere to go.
1434        editor.undo_stack.undo();
1435        editor.redo();
1436        assert_eq!(editor.text(), "second");
1437        assert_eq!(editor.cursor_grapheme(), 6);
1438    }
1439}