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#[derive(Clone)]
26pub struct EditorAction {
27 pub text: String,
29 pub cursor: usize,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum VimMode {
36 Normal,
38 Insert,
40}
41
42#[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 vim_mode_enabled: bool,
64 mode: VimMode,
66 pending_cmd: Option<char>,
68}
69
70impl Editor {
71 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 pub fn vim_mode_enabled(&self) -> bool {
92 self.vim_mode_enabled
93 }
94
95 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 pub fn mode(&self) -> VimMode {
108 self.mode
109 }
110
111 pub fn set_mode(&mut self, mode: VimMode) {
113 self.mode = mode;
114 self.pending_cmd = None;
115 }
116
117 pub fn text(&self) -> &str {
119 &self.text
120 }
121
122 pub fn cursor_grapheme(&self) -> usize {
124 self.cursor
125 }
126
127 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 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 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 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 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 fn open_line_below(&mut self) {
485 self.move_cursor_end();
486 self.insert_newline();
487 self.mode = VimMode::Insert;
488 }
489
490 fn open_line_above(&mut self) {
492 self.move_cursor_home();
493 if self.cursor > 0 {
494 self.cursor -= 1; self.insert_newline();
496 } else {
497 self.insert_newline();
498 self.cursor = 0;
499 }
500 self.mode = VimMode::Insert;
501 }
502
503 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 fn go_to_start(&mut self) {
516 self.cursor = 0;
517 }
518
519 fn go_to_end(&mut self) {
521 self.cursor = self.graphemes().len();
522 }
523
524 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 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 },
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 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(); editor.move_cursor_up();
1046 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(); 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 assert_eq!(editor.history.len(), 100);
1097 }
1098
1099 #[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(); assert_eq!(editor.cursor_grapheme(), 3); editor.handle_input(&Event::Key(KeyEvent::new(
1119 KeyCode::Char('l'),
1120 KeyModifiers::empty(),
1121 )));
1122 assert_eq!(editor.cursor_grapheme(), 4); editor.handle_input(&Event::Key(KeyEvent::new(
1124 KeyCode::Char('h'),
1125 KeyModifiers::empty(),
1126 )));
1127 assert_eq!(editor.cursor_grapheme(), 3); editor.handle_input(&Event::Key(KeyEvent::new(
1129 KeyCode::Char('j'),
1130 KeyModifiers::empty(),
1131 )));
1132 assert_eq!(editor.cursor_grapheme(), 6); editor.handle_input(&Event::Key(KeyEvent::new(
1134 KeyCode::Char('k'),
1135 KeyModifiers::empty(),
1136 )));
1137 assert_eq!(editor.cursor_grapheme(), 3); }
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 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 editor.undo_stack.undo();
1435 editor.redo();
1436 assert_eq!(editor.text(), "second");
1437 assert_eq!(editor.cursor_grapheme(), 6);
1438 }
1439}