1use crate::input::{Input, Key};
10use crate::vim::{self, VimState};
11use crate::{KeybindingMode, VimMode};
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13use ratatui::layout::Rect;
14use std::sync::atomic::{AtomicU16, Ordering};
15
16pub(crate) fn engine_style_to_ratatui(s: crate::types::Style) -> ratatui::style::Style {
23 use crate::types::Attrs;
24 use ratatui::style::{Color as RColor, Modifier as RMod, Style as RStyle};
25 let mut out = RStyle::default();
26 if let Some(c) = s.fg {
27 out = out.fg(RColor::Rgb(c.0, c.1, c.2));
28 }
29 if let Some(c) = s.bg {
30 out = out.bg(RColor::Rgb(c.0, c.1, c.2));
31 }
32 let mut m = RMod::empty();
33 if s.attrs.contains(Attrs::BOLD) {
34 m |= RMod::BOLD;
35 }
36 if s.attrs.contains(Attrs::ITALIC) {
37 m |= RMod::ITALIC;
38 }
39 if s.attrs.contains(Attrs::UNDERLINE) {
40 m |= RMod::UNDERLINED;
41 }
42 if s.attrs.contains(Attrs::REVERSE) {
43 m |= RMod::REVERSED;
44 }
45 if s.attrs.contains(Attrs::DIM) {
46 m |= RMod::DIM;
47 }
48 if s.attrs.contains(Attrs::STRIKE) {
49 m |= RMod::CROSSED_OUT;
50 }
51 out.add_modifier(m)
52}
53
54pub(crate) fn ratatui_style_to_engine(s: ratatui::style::Style) -> crate::types::Style {
58 use crate::types::{Attrs, Color, Style};
59 use ratatui::style::{Color as RColor, Modifier as RMod};
60 fn c(rc: RColor) -> Color {
61 match rc {
62 RColor::Rgb(r, g, b) => Color(r, g, b),
63 RColor::Black => Color(0, 0, 0),
64 RColor::Red => Color(205, 49, 49),
65 RColor::Green => Color(13, 188, 121),
66 RColor::Yellow => Color(229, 229, 16),
67 RColor::Blue => Color(36, 114, 200),
68 RColor::Magenta => Color(188, 63, 188),
69 RColor::Cyan => Color(17, 168, 205),
70 RColor::Gray => Color(229, 229, 229),
71 RColor::DarkGray => Color(102, 102, 102),
72 RColor::LightRed => Color(241, 76, 76),
73 RColor::LightGreen => Color(35, 209, 139),
74 RColor::LightYellow => Color(245, 245, 67),
75 RColor::LightBlue => Color(59, 142, 234),
76 RColor::LightMagenta => Color(214, 112, 214),
77 RColor::LightCyan => Color(41, 184, 219),
78 RColor::White => Color(255, 255, 255),
79 _ => Color(0, 0, 0),
80 }
81 }
82 let mut attrs = Attrs::empty();
83 if s.add_modifier.contains(RMod::BOLD) {
84 attrs |= Attrs::BOLD;
85 }
86 if s.add_modifier.contains(RMod::ITALIC) {
87 attrs |= Attrs::ITALIC;
88 }
89 if s.add_modifier.contains(RMod::UNDERLINED) {
90 attrs |= Attrs::UNDERLINE;
91 }
92 if s.add_modifier.contains(RMod::REVERSED) {
93 attrs |= Attrs::REVERSE;
94 }
95 if s.add_modifier.contains(RMod::DIM) {
96 attrs |= Attrs::DIM;
97 }
98 if s.add_modifier.contains(RMod::CROSSED_OUT) {
99 attrs |= Attrs::STRIKE;
100 }
101 Style {
102 fg: s.fg.map(c),
103 bg: s.bg.map(c),
104 attrs,
105 }
106}
107
108fn edit_to_editop(edit: &hjkl_buffer::Edit) -> Option<crate::types::Edit> {
114 use crate::types::{Edit as Op, Pos};
115 use hjkl_buffer::Edit as B;
116 let to_pos = |p: hjkl_buffer::Position| Pos {
117 line: p.row as u32,
118 col: p.col as u32,
119 };
120 Some(match edit {
121 B::InsertChar { at, ch } => Op {
122 range: to_pos(*at)..to_pos(*at),
123 replacement: ch.to_string(),
124 },
125 B::InsertStr { at, text } => Op {
126 range: to_pos(*at)..to_pos(*at),
127 replacement: text.clone(),
128 },
129 B::DeleteRange { start, end, .. } => Op {
130 range: to_pos(*start)..to_pos(*end),
131 replacement: String::new(),
132 },
133 B::Replace { start, end, with } => Op {
134 range: to_pos(*start)..to_pos(*end),
135 replacement: with.clone(),
136 },
137 B::JoinLines { row, count, .. } => {
138 let start = Pos {
139 line: *row as u32,
140 col: 0,
141 };
142 let end = Pos {
143 line: (*row + *count) as u32,
144 col: 0,
145 };
146 Op {
147 range: start..end,
148 replacement: String::new(),
149 }
150 }
151 B::SplitLines { row, .. } => {
152 let p = Pos {
153 line: *row as u32,
154 col: 0,
155 };
156 Op {
157 range: p..p,
158 replacement: String::new(),
159 }
160 }
161 B::InsertBlock { at, .. } => {
162 let p = to_pos(*at);
163 Op {
164 range: p..p,
165 replacement: String::new(),
166 }
167 }
168 B::DeleteBlockChunks { at, .. } => {
169 let p = to_pos(*at);
170 Op {
171 range: p..p,
172 replacement: String::new(),
173 }
174 }
175 })
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub(super) enum CursorScrollTarget {
182 Center,
183 Top,
184 Bottom,
185}
186
187pub struct Editor<'a> {
188 pub keybinding_mode: KeybindingMode,
189 _marker: std::marker::PhantomData<&'a ()>,
194 pub last_yank: Option<String>,
196 #[doc(hidden)]
198 pub vim: VimState,
199 #[doc(hidden)]
201 pub undo_stack: Vec<(Vec<String>, (usize, usize))>,
202 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
204 pub(super) content_dirty: bool,
206 pub(super) cached_content: Option<std::sync::Arc<String>>,
211 pub(super) viewport_height: AtomicU16,
216 pub(super) pending_lsp: Option<LspIntent>,
220 pub(super) buffer: hjkl_buffer::Buffer,
225 pub(super) style_table: Vec<ratatui::style::Style>,
232 #[doc(hidden)]
235 pub registers: crate::registers::Registers,
236 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
242 #[doc(hidden)]
246 pub settings: Settings,
247 #[doc(hidden)]
253 pub file_marks: std::collections::HashMap<char, (usize, usize)>,
254 #[doc(hidden)]
259 pub syntax_fold_ranges: Vec<(usize, usize)>,
260 #[doc(hidden)]
268 pub change_log: Vec<crate::types::Edit>,
269}
270
271#[derive(Debug, Clone)]
274pub struct Settings {
275 pub shiftwidth: usize,
277 pub tabstop: usize,
280 pub ignore_case: bool,
283 pub textwidth: usize,
285 pub wrap: hjkl_buffer::Wrap,
291}
292
293impl Default for Settings {
294 fn default() -> Self {
295 Self {
296 shiftwidth: 2,
297 tabstop: 8,
298 ignore_case: false,
299 textwidth: 79,
300 wrap: hjkl_buffer::Wrap::None,
301 }
302 }
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum LspIntent {
310 GotoDefinition,
312}
313
314impl<'a> Editor<'a> {
315 pub fn new(keybinding_mode: KeybindingMode) -> Self {
316 Self {
317 _marker: std::marker::PhantomData,
318 keybinding_mode,
319 last_yank: None,
320 vim: VimState::default(),
321 undo_stack: Vec::new(),
322 redo_stack: Vec::new(),
323 content_dirty: false,
324 cached_content: None,
325 viewport_height: AtomicU16::new(0),
326 pending_lsp: None,
327 buffer: hjkl_buffer::Buffer::new(),
328 style_table: Vec::new(),
329 registers: crate::registers::Registers::default(),
330 styled_spans: Vec::new(),
331 settings: Settings::default(),
332 file_marks: std::collections::HashMap::new(),
333 syntax_fold_ranges: Vec::new(),
334 change_log: Vec::new(),
335 }
336 }
337
338 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
342 self.syntax_fold_ranges = ranges;
343 }
344
345 pub fn settings(&self) -> &Settings {
348 &self.settings
349 }
350
351 #[doc(hidden)]
352 pub fn settings_mut(&mut self) -> &mut Settings {
353 &mut self.settings
354 }
355
356 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>) {
363 let line_byte_lens: Vec<usize> = self.buffer.lines().iter().map(|l| l.len()).collect();
364 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
365 for (row, row_spans) in spans.iter().enumerate() {
366 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
367 let mut translated = Vec::with_capacity(row_spans.len());
368 for (start, end, style) in row_spans {
369 let end_clamped = (*end).min(line_len);
370 if end_clamped <= *start {
371 continue;
372 }
373 let id = self.intern_style(*style);
374 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
375 }
376 by_row.push(translated);
377 }
378 self.buffer.set_spans(by_row);
379 self.styled_spans = spans;
380 }
381
382 pub fn yank(&self) -> &str {
384 &self.registers.unnamed.text
385 }
386
387 pub fn registers(&self) -> &crate::registers::Registers {
389 &self.registers
390 }
391
392 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
397 self.registers.set_clipboard(text, linewise);
398 }
399
400 pub fn pending_register_is_clipboard(&self) -> bool {
404 matches!(self.vim.pending_register, Some('+') | Some('*'))
405 }
406
407 pub fn set_yank(&mut self, text: impl Into<String>) {
411 let text = text.into();
412 let linewise = self.vim.yank_linewise;
413 self.registers.unnamed = crate::registers::Slot { text, linewise };
414 }
415
416 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
420 self.vim.yank_linewise = linewise;
421 let target = self.vim.pending_register.take();
422 self.registers.record_yank(text, linewise, target);
423 }
424
425 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
430 if let Some(slot) = match reg {
431 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
432 'A'..='Z' => {
433 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
434 }
435 _ => None,
436 } {
437 slot.text = text;
438 slot.linewise = false;
439 }
440 }
441
442 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
445 self.vim.yank_linewise = linewise;
446 let target = self.vim.pending_register.take();
447 self.registers.record_delete(text, linewise, target);
448 }
449
450 pub fn intern_style(&mut self, style: ratatui::style::Style) -> u32 {
456 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
457 return idx as u32;
458 }
459 self.style_table.push(style);
460 (self.style_table.len() - 1) as u32
461 }
462
463 pub fn style_table(&self) -> &[ratatui::style::Style] {
467 &self.style_table
468 }
469
470 pub fn intern_engine_style(&mut self, style: crate::types::Style) -> u32 {
480 let r = engine_style_to_ratatui(style);
481 self.intern_style(r)
482 }
483
484 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
488 let r = self.style_table.get(id as usize).copied()?;
489 Some(ratatui_style_to_engine(r))
490 }
491
492 pub fn buffer(&self) -> &hjkl_buffer::Buffer {
495 &self.buffer
496 }
497
498 pub fn buffer_mut(&mut self) -> &mut hjkl_buffer::Buffer {
499 &mut self.buffer
500 }
501
502 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
506
507 pub fn set_viewport_top(&mut self, row: usize) {
515 let last = self.buffer.row_count().saturating_sub(1);
516 let target = row.min(last);
517 self.buffer.viewport_mut().top_row = target;
518 }
519
520 #[doc(hidden)]
525 pub fn jump_cursor(&mut self, row: usize, col: usize) {
526 self.buffer.set_cursor(hjkl_buffer::Position::new(row, col));
527 }
528
529 pub fn cursor(&self) -> (usize, usize) {
537 let pos = self.buffer.cursor();
538 (pos.row, pos.col)
539 }
540
541 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
544 self.pending_lsp.take()
545 }
546
547 pub(crate) fn sync_buffer_from_textarea(&mut self) {
551 self.buffer.set_sticky_col(self.vim.sticky_col);
552 let height = self.viewport_height_value();
553 self.buffer.viewport_mut().height = height;
554 }
555
556 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
560 self.sync_buffer_from_textarea();
561 }
562
563 pub fn record_jump(&mut self, pos: (usize, usize)) {
568 const JUMPLIST_MAX: usize = 100;
569 self.vim.jump_back.push(pos);
570 if self.vim.jump_back.len() > JUMPLIST_MAX {
571 self.vim.jump_back.remove(0);
572 }
573 self.vim.jump_fwd.clear();
574 }
575
576 pub fn set_viewport_height(&self, height: u16) {
579 self.viewport_height.store(height, Ordering::Relaxed);
580 }
581
582 pub fn viewport_height_value(&self) -> u16 {
584 self.viewport_height.load(Ordering::Relaxed)
585 }
586
587 #[doc(hidden)]
593 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
594 let pre_row = self.buffer.cursor().row;
595 let pre_rows = self.buffer.row_count();
596 if let Some(op) = edit_to_editop(&edit) {
600 self.change_log.push(op);
601 }
602 let inverse = self.buffer.apply_edit(edit);
603 let pos = self.buffer.cursor();
604 let lo = pre_row.min(pos.row);
610 let hi = pre_row.max(pos.row);
611 self.buffer.invalidate_folds_in_range(lo, hi);
612 self.vim.last_edit_pos = Some((pos.row, pos.col));
613 let entry = (pos.row, pos.col);
618 if self.vim.change_list.last() != Some(&entry) {
619 if let Some(idx) = self.vim.change_list_cursor.take() {
620 self.vim.change_list.truncate(idx + 1);
621 }
622 self.vim.change_list.push(entry);
623 let len = self.vim.change_list.len();
624 if len > crate::vim::CHANGE_LIST_MAX {
625 self.vim
626 .change_list
627 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
628 }
629 }
630 self.vim.change_list_cursor = None;
631 let post_rows = self.buffer.row_count();
635 let delta = post_rows as isize - pre_rows as isize;
636 if delta != 0 {
637 self.shift_marks_after_edit(pre_row, delta);
638 }
639 self.push_buffer_content_to_textarea();
640 self.mark_content_dirty();
641 inverse
642 }
643
644 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
649 if delta == 0 {
650 return;
651 }
652 let drop_end = if delta < 0 {
655 edit_start.saturating_add((-delta) as usize)
656 } else {
657 edit_start
658 };
659 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
660
661 let mut to_drop: Vec<char> = Vec::new();
662 for (c, (row, _col)) in self.vim.marks.iter_mut() {
663 if (edit_start..drop_end).contains(row) {
664 to_drop.push(*c);
665 } else if *row >= shift_threshold {
666 *row = ((*row as isize) + delta).max(0) as usize;
667 }
668 }
669 for c in to_drop {
670 self.vim.marks.remove(&c);
671 }
672
673 let mut to_drop: Vec<char> = Vec::new();
675 for (c, (row, _col)) in self.file_marks.iter_mut() {
676 if (edit_start..drop_end).contains(row) {
677 to_drop.push(*c);
678 } else if *row >= shift_threshold {
679 *row = ((*row as isize) + delta).max(0) as usize;
680 }
681 }
682 for c in to_drop {
683 self.file_marks.remove(&c);
684 }
685
686 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
687 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
688 for (row, _) in entries.iter_mut() {
689 if *row >= shift_threshold {
690 *row = ((*row as isize) + delta).max(0) as usize;
691 }
692 }
693 };
694 shift_jumps(&mut self.vim.jump_back);
695 shift_jumps(&mut self.vim.jump_fwd);
696 }
697
698 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
706
707 pub fn mark_content_dirty(&mut self) {
713 self.content_dirty = true;
714 self.cached_content = None;
715 }
716
717 pub fn take_dirty(&mut self) -> bool {
719 let dirty = self.content_dirty;
720 self.content_dirty = false;
721 dirty
722 }
723
724 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
734 if !self.content_dirty {
735 return None;
736 }
737 let arc = self.content_arc();
738 self.content_dirty = false;
739 Some(arc)
740 }
741
742 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
745 let cursor = self.buffer.cursor().row;
746 let top = self.buffer.viewport().top_row;
747 cursor.saturating_sub(top).min(height as usize - 1) as u16
748 }
749
750 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
754 let pos = self.buffer.cursor();
755 let v = self.buffer.viewport();
756 if pos.row < v.top_row || pos.col < v.top_col {
757 return None;
758 }
759 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
760 let dy = (pos.row - v.top_row) as u16;
761 let dx = (pos.col - v.top_col) as u16;
762 if dy >= area.height || dx + lnum_width >= area.width {
763 return None;
764 }
765 Some((area.x + lnum_width + dx, area.y + dy))
766 }
767
768 pub fn vim_mode(&self) -> VimMode {
769 self.vim.public_mode()
770 }
771
772 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
778 self.vim.search_prompt.as_ref()
779 }
780
781 pub fn last_search(&self) -> Option<&str> {
784 self.vim.last_search.as_deref()
785 }
786
787 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
791 if self.vim_mode() != VimMode::Visual {
792 return None;
793 }
794 let anchor = self.vim.visual_anchor;
795 let cursor = self.cursor();
796 let (start, end) = if anchor <= cursor {
797 (anchor, cursor)
798 } else {
799 (cursor, anchor)
800 };
801 Some((start, end))
802 }
803
804 pub fn line_highlight(&self) -> Option<(usize, usize)> {
807 if self.vim_mode() != VimMode::VisualLine {
808 return None;
809 }
810 let anchor = self.vim.visual_line_anchor;
811 let cursor = self.buffer.cursor().row;
812 Some((anchor.min(cursor), anchor.max(cursor)))
813 }
814
815 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
816 if self.vim_mode() != VimMode::VisualBlock {
817 return None;
818 }
819 let (ar, ac) = self.vim.block_anchor;
820 let cr = self.buffer.cursor().row;
821 let cc = self.vim.block_vcol;
822 let top = ar.min(cr);
823 let bot = ar.max(cr);
824 let left = ac.min(cc);
825 let right = ac.max(cc);
826 Some((top, bot, left, right))
827 }
828
829 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
835 use hjkl_buffer::{Position, Selection};
836 match self.vim_mode() {
837 VimMode::Visual => {
838 let (ar, ac) = self.vim.visual_anchor;
839 let head = self.buffer.cursor();
840 Some(Selection::Char {
841 anchor: Position::new(ar, ac),
842 head,
843 })
844 }
845 VimMode::VisualLine => {
846 let anchor_row = self.vim.visual_line_anchor;
847 let head_row = self.buffer.cursor().row;
848 Some(Selection::Line {
849 anchor_row,
850 head_row,
851 })
852 }
853 VimMode::VisualBlock => {
854 let (ar, ac) = self.vim.block_anchor;
855 let cr = self.buffer.cursor().row;
856 let cc = self.vim.block_vcol;
857 Some(Selection::Block {
858 anchor: Position::new(ar, ac),
859 head: Position::new(cr, cc),
860 })
861 }
862 _ => None,
863 }
864 }
865
866 pub fn force_normal(&mut self) {
868 self.vim.force_normal();
869 }
870
871 pub fn content(&self) -> String {
872 let mut s = self.buffer.lines().join("\n");
873 s.push('\n');
874 s
875 }
876
877 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
882 if let Some(arc) = &self.cached_content {
883 return std::sync::Arc::clone(arc);
884 }
885 let arc = std::sync::Arc::new(self.content());
886 self.cached_content = Some(std::sync::Arc::clone(&arc));
887 arc
888 }
889
890 pub fn set_content(&mut self, text: &str) {
891 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
892 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
893 lines.pop();
894 }
895 if lines.is_empty() {
896 lines.push(String::new());
897 }
898 let _ = lines;
899 self.buffer = hjkl_buffer::Buffer::from_str(text);
900 self.undo_stack.clear();
901 self.redo_stack.clear();
902 self.mark_content_dirty();
903 }
904
905 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
922 std::mem::take(&mut self.change_log)
923 }
924
925 pub fn current_options(&self) -> crate::types::Options {
935 let mut o = crate::types::Options::default();
936 o.shiftwidth = self.settings.shiftwidth as u32;
937 o.tabstop = self.settings.tabstop as u32;
938 o.ignorecase = self.settings.ignore_case;
939 o
940 }
941
942 pub fn apply_options(&mut self, opts: &crate::types::Options) {
947 self.settings.shiftwidth = opts.shiftwidth as usize;
948 self.settings.tabstop = opts.tabstop as usize;
949 self.settings.ignore_case = opts.ignorecase;
950 }
951
952 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
962 use crate::types::{Highlight, HighlightKind, Pos};
963 let sel = self.buffer_selection()?;
964 let (start, end) = match sel {
965 hjkl_buffer::Selection::Char { anchor, head } => {
966 let a = (anchor.row, anchor.col);
967 let h = (head.row, head.col);
968 if a <= h { (a, h) } else { (h, a) }
969 }
970 hjkl_buffer::Selection::Line {
971 anchor_row,
972 head_row,
973 } => {
974 let (top, bot) = if anchor_row <= head_row {
975 (anchor_row, head_row)
976 } else {
977 (head_row, anchor_row)
978 };
979 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
980 ((top, 0), (bot, last_col))
981 }
982 hjkl_buffer::Selection::Block { anchor, head } => {
983 let (top, bot) = if anchor.row <= head.row {
984 (anchor.row, head.row)
985 } else {
986 (head.row, anchor.row)
987 };
988 let (left, right) = if anchor.col <= head.col {
989 (anchor.col, head.col)
990 } else {
991 (head.col, anchor.col)
992 };
993 ((top, left), (bot, right))
994 }
995 };
996 Some(Highlight {
997 range: Pos {
998 line: start.0 as u32,
999 col: start.1 as u32,
1000 }..Pos {
1001 line: end.0 as u32,
1002 col: end.1 as u32,
1003 },
1004 kind: HighlightKind::Selection,
1005 })
1006 }
1007
1008 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
1021 use crate::types::{Highlight, HighlightKind, Pos};
1022 let row = line as usize;
1023 if row >= self.buffer.lines().len() {
1024 return Vec::new();
1025 }
1026 if self.buffer.search_pattern().is_none() {
1027 return Vec::new();
1028 }
1029 self.buffer
1030 .search_matches(row)
1031 .into_iter()
1032 .map(|(start, end)| Highlight {
1033 range: Pos {
1034 line,
1035 col: start as u32,
1036 }..Pos {
1037 line,
1038 col: end as u32,
1039 },
1040 kind: HighlightKind::SearchMatch,
1041 })
1042 .collect()
1043 }
1044
1045 pub fn render_frame(&self) -> crate::types::RenderFrame {
1055 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
1056 let (cursor_row, cursor_col) = self.cursor();
1057 let (mode, shape) = match self.vim_mode() {
1058 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
1059 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
1060 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
1061 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
1062 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
1063 };
1064 RenderFrame {
1065 mode,
1066 cursor_row: cursor_row as u32,
1067 cursor_col: cursor_col as u32,
1068 cursor_shape: shape,
1069 viewport_top: self.buffer.viewport().top_row as u32,
1070 line_count: self.buffer.lines().len() as u32,
1071 }
1072 }
1073
1074 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
1087 use crate::types::{EditorSnapshot, SnapshotMode};
1088 let mode = match self.vim_mode() {
1089 crate::VimMode::Normal => SnapshotMode::Normal,
1090 crate::VimMode::Insert => SnapshotMode::Insert,
1091 crate::VimMode::Visual => SnapshotMode::Visual,
1092 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
1093 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
1094 };
1095 let cursor = self.cursor();
1096 let cursor = (cursor.0 as u32, cursor.1 as u32);
1097 let lines: Vec<String> = self.buffer.lines().to_vec();
1098 let viewport_top = self.buffer.viewport().top_row as u32;
1099 let file_marks = self
1100 .file_marks
1101 .iter()
1102 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
1103 .collect();
1104 EditorSnapshot {
1105 version: EditorSnapshot::VERSION,
1106 mode,
1107 cursor,
1108 lines,
1109 viewport_top,
1110 registers: self.registers.clone(),
1111 file_marks,
1112 }
1113 }
1114
1115 pub fn restore_snapshot(
1123 &mut self,
1124 snap: crate::types::EditorSnapshot,
1125 ) -> Result<(), crate::EngineError> {
1126 use crate::types::EditorSnapshot;
1127 if snap.version != EditorSnapshot::VERSION {
1128 return Err(crate::EngineError::SnapshotVersion(
1129 snap.version,
1130 EditorSnapshot::VERSION,
1131 ));
1132 }
1133 let text = snap.lines.join("\n");
1134 self.set_content(&text);
1135 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1136 let mut vp = self.buffer.viewport();
1137 vp.top_row = snap.viewport_top as usize;
1138 *self.buffer.viewport_mut() = vp;
1139 self.registers = snap.registers;
1140 self.file_marks = snap
1141 .file_marks
1142 .into_iter()
1143 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1144 .collect();
1145 Ok(())
1146 }
1147
1148 pub fn seed_yank(&mut self, text: String) {
1152 let linewise = text.ends_with('\n');
1153 self.vim.yank_linewise = linewise;
1154 self.registers.unnamed = crate::registers::Slot { text, linewise };
1155 }
1156
1157 pub fn scroll_down(&mut self, rows: i16) {
1162 self.scroll_viewport(rows);
1163 }
1164
1165 pub fn scroll_up(&mut self, rows: i16) {
1169 self.scroll_viewport(-rows);
1170 }
1171
1172 const SCROLLOFF: usize = 5;
1176
1177 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1182 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1183 if height == 0 {
1184 self.buffer.ensure_cursor_visible();
1185 return;
1186 }
1187 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1191 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
1194 self.ensure_scrolloff_wrap(height, margin);
1195 return;
1196 }
1197 let cursor_row = self.buffer.cursor().row;
1198 let last_row = self.buffer.row_count().saturating_sub(1);
1199 let v = self.buffer.viewport_mut();
1200 if cursor_row < v.top_row + margin {
1202 v.top_row = cursor_row.saturating_sub(margin);
1203 }
1204 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1206 if cursor_row > v.top_row + max_bottom {
1207 v.top_row = cursor_row.saturating_sub(max_bottom);
1208 }
1209 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1211 if v.top_row > max_top {
1212 v.top_row = max_top;
1213 }
1214 let cursor = self.buffer.cursor();
1217 self.buffer.viewport_mut().ensure_visible(cursor);
1218 }
1219
1220 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1225 let cursor_row = self.buffer.cursor().row;
1226 if cursor_row < self.buffer.viewport().top_row {
1229 self.buffer.viewport_mut().top_row = cursor_row;
1230 self.buffer.viewport_mut().top_col = 0;
1231 }
1232 let max_csr = height.saturating_sub(1).saturating_sub(margin);
1235 loop {
1236 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1237 if csr <= max_csr {
1238 break;
1239 }
1240 let top = self.buffer.viewport().top_row;
1241 let Some(next) = self.buffer.next_visible_row(top) else {
1242 break;
1243 };
1244 if next > cursor_row {
1246 self.buffer.viewport_mut().top_row = cursor_row;
1247 break;
1248 }
1249 self.buffer.viewport_mut().top_row = next;
1250 }
1251 loop {
1254 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1255 if csr >= margin {
1256 break;
1257 }
1258 let top = self.buffer.viewport().top_row;
1259 let Some(prev) = self.buffer.prev_visible_row(top) else {
1260 break;
1261 };
1262 self.buffer.viewport_mut().top_row = prev;
1263 }
1264 let max_top = self.buffer.max_top_for_height(height);
1269 if self.buffer.viewport().top_row > max_top {
1270 self.buffer.viewport_mut().top_row = max_top;
1271 }
1272 self.buffer.viewport_mut().top_col = 0;
1273 }
1274
1275 fn scroll_viewport(&mut self, delta: i16) {
1276 if delta == 0 {
1277 return;
1278 }
1279 let total_rows = self.buffer.row_count() as isize;
1281 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1282 let cur_top = self.buffer.viewport().top_row as isize;
1283 let new_top = (cur_top + delta as isize)
1284 .max(0)
1285 .min((total_rows - 1).max(0)) as usize;
1286 self.buffer.viewport_mut().top_row = new_top;
1287 let _ = cur_top;
1290 if height == 0 {
1291 return;
1292 }
1293 let cursor = self.buffer.cursor();
1296 let margin = Self::SCROLLOFF.min(height / 2);
1297 let min_row = new_top + margin;
1298 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1299 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1300 if target_row != cursor.row {
1301 let line_len = self
1302 .buffer
1303 .line(target_row)
1304 .map(|l| l.chars().count())
1305 .unwrap_or(0);
1306 let target_col = cursor.col.min(line_len.saturating_sub(1));
1307 self.buffer
1308 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1309 }
1310 }
1311
1312 pub fn goto_line(&mut self, line: usize) {
1313 let row = line.saturating_sub(1);
1314 let max = self.buffer.row_count().saturating_sub(1);
1315 let target = row.min(max);
1316 self.buffer
1317 .set_cursor(hjkl_buffer::Position::new(target, 0));
1318 }
1319
1320 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1324 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1325 if height == 0 {
1326 return;
1327 }
1328 let cur_row = self.buffer.cursor().row;
1329 let cur_top = self.buffer.viewport().top_row;
1330 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1336 let new_top = match pos {
1337 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1338 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1339 CursorScrollTarget::Bottom => {
1340 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1341 }
1342 };
1343 if new_top == cur_top {
1344 return;
1345 }
1346 self.buffer.viewport_mut().top_row = new_top;
1347 }
1348
1349 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1356 let lines = self.buffer.lines();
1357 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1359 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1360 let rel_row = row.saturating_sub(inner_top) as usize;
1361 let top = self.buffer.viewport().top_row;
1362 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1363 let rel_col = col.saturating_sub(content_x) as usize;
1364 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1365 let last_col = line_chars.saturating_sub(1);
1366 (doc_row, rel_col.min(last_col))
1367 }
1368
1369 pub fn jump_to(&mut self, line: usize, col: usize) {
1371 let r = line.saturating_sub(1);
1372 let max_row = self.buffer.row_count().saturating_sub(1);
1373 let r = r.min(max_row);
1374 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1375 let c = col.saturating_sub(1).min(line_len);
1376 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1377 }
1378
1379 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1381 if self.vim.is_visual() {
1382 self.vim.force_normal();
1383 }
1384 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1385 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1386 }
1387
1388 pub fn mouse_begin_drag(&mut self) {
1390 if !self.vim.is_visual_char() {
1391 let cursor = self.cursor();
1392 self.vim.enter_visual(cursor);
1393 }
1394 }
1395
1396 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1398 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1399 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1400 }
1401
1402 pub fn insert_str(&mut self, text: &str) {
1403 let pos = self.buffer.cursor();
1404 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1405 at: pos,
1406 text: text.to_string(),
1407 });
1408 self.push_buffer_content_to_textarea();
1409 self.mark_content_dirty();
1410 }
1411
1412 pub fn accept_completion(&mut self, completion: &str) {
1413 use hjkl_buffer::{Edit, MotionKind, Position};
1414 let cursor = self.buffer.cursor();
1415 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1416 let chars: Vec<char> = line.chars().collect();
1417 let prefix_len = chars[..cursor.col.min(chars.len())]
1418 .iter()
1419 .rev()
1420 .take_while(|c| c.is_alphanumeric() || **c == '_')
1421 .count();
1422 if prefix_len > 0 {
1423 let start = Position::new(cursor.row, cursor.col - prefix_len);
1424 self.buffer.apply_edit(Edit::DeleteRange {
1425 start,
1426 end: cursor,
1427 kind: MotionKind::Char,
1428 });
1429 }
1430 let cursor = self.buffer.cursor();
1431 self.buffer.apply_edit(Edit::InsertStr {
1432 at: cursor,
1433 text: completion.to_string(),
1434 });
1435 self.push_buffer_content_to_textarea();
1436 self.mark_content_dirty();
1437 }
1438
1439 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1440 let pos = self.buffer.cursor();
1441 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1442 }
1443
1444 #[doc(hidden)]
1445 pub fn push_undo(&mut self) {
1446 let snap = self.snapshot();
1447 if self.undo_stack.len() >= 200 {
1448 self.undo_stack.remove(0);
1449 }
1450 self.undo_stack.push(snap);
1451 self.redo_stack.clear();
1452 }
1453
1454 #[doc(hidden)]
1455 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1456 let text = lines.join("\n");
1457 self.buffer.replace_all(&text);
1458 self.buffer
1459 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1460 self.mark_content_dirty();
1461 }
1462
1463 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1465 let input = crossterm_to_input(key);
1466 if input.key == Key::Null {
1467 return false;
1468 }
1469 vim::step(self, input)
1470 }
1471}
1472
1473pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1474 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1475 let alt = key.modifiers.contains(KeyModifiers::ALT);
1476 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1477 let k = match key.code {
1478 KeyCode::Char(c) => Key::Char(c),
1479 KeyCode::Backspace => Key::Backspace,
1480 KeyCode::Delete => Key::Delete,
1481 KeyCode::Enter => Key::Enter,
1482 KeyCode::Left => Key::Left,
1483 KeyCode::Right => Key::Right,
1484 KeyCode::Up => Key::Up,
1485 KeyCode::Down => Key::Down,
1486 KeyCode::Home => Key::Home,
1487 KeyCode::End => Key::End,
1488 KeyCode::Tab => Key::Tab,
1489 KeyCode::Esc => Key::Esc,
1490 _ => Key::Null,
1491 };
1492 Input {
1493 key: k,
1494 ctrl,
1495 alt,
1496 shift,
1497 }
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502 use super::*;
1503 use crossterm::event::KeyEvent;
1504
1505 fn key(code: KeyCode) -> KeyEvent {
1506 KeyEvent::new(code, KeyModifiers::NONE)
1507 }
1508 fn shift_key(code: KeyCode) -> KeyEvent {
1509 KeyEvent::new(code, KeyModifiers::SHIFT)
1510 }
1511 fn ctrl_key(code: KeyCode) -> KeyEvent {
1512 KeyEvent::new(code, KeyModifiers::CONTROL)
1513 }
1514
1515 #[test]
1516 fn vim_normal_to_insert() {
1517 let mut e = Editor::new(KeybindingMode::Vim);
1518 e.handle_key(key(KeyCode::Char('i')));
1519 assert_eq!(e.vim_mode(), VimMode::Insert);
1520 }
1521
1522 #[test]
1523 fn intern_engine_style_dedups_with_intern_style() {
1524 use crate::types::{Attrs, Color, Style};
1525 let mut e = Editor::new(KeybindingMode::Vim);
1526 let s = Style {
1527 fg: Some(Color(255, 0, 0)),
1528 bg: None,
1529 attrs: Attrs::BOLD,
1530 };
1531 let id_a = e.intern_engine_style(s);
1532 let id_b = e.intern_engine_style(s);
1534 assert_eq!(id_a, id_b);
1535 let back = e.engine_style_at(id_a).expect("interned");
1537 assert_eq!(back, s);
1538 }
1539
1540 #[test]
1541 fn engine_style_at_out_of_range_returns_none() {
1542 let e = Editor::new(KeybindingMode::Vim);
1543 assert!(e.engine_style_at(99).is_none());
1544 }
1545
1546 #[test]
1547 fn take_changes_drains_after_insert() {
1548 let mut e = Editor::new(KeybindingMode::Vim);
1549 e.set_content("abc");
1550 assert!(e.take_changes().is_empty());
1552 e.handle_key(key(KeyCode::Char('i')));
1554 e.handle_key(key(KeyCode::Char('X')));
1555 let changes = e.take_changes();
1556 assert!(
1557 !changes.is_empty(),
1558 "insert mode keystroke should produce a change"
1559 );
1560 assert!(e.take_changes().is_empty());
1562 }
1563
1564 #[test]
1565 fn options_bridge_roundtrip() {
1566 let mut e = Editor::new(KeybindingMode::Vim);
1567 let opts = e.current_options();
1568 assert_eq!(opts.shiftwidth, 2); assert_eq!(opts.tabstop, 8);
1570
1571 let mut new_opts = crate::types::Options::default();
1572 new_opts.shiftwidth = 4;
1573 new_opts.tabstop = 2;
1574 new_opts.ignorecase = true;
1575 e.apply_options(&new_opts);
1576
1577 let after = e.current_options();
1578 assert_eq!(after.shiftwidth, 4);
1579 assert_eq!(after.tabstop, 2);
1580 assert!(after.ignorecase);
1581 }
1582
1583 #[test]
1584 fn selection_highlight_none_in_normal() {
1585 let mut e = Editor::new(KeybindingMode::Vim);
1586 e.set_content("hello");
1587 assert!(e.selection_highlight().is_none());
1588 }
1589
1590 #[test]
1591 fn selection_highlight_some_in_visual() {
1592 use crate::types::HighlightKind;
1593 let mut e = Editor::new(KeybindingMode::Vim);
1594 e.set_content("hello world");
1595 e.handle_key(key(KeyCode::Char('v')));
1596 e.handle_key(key(KeyCode::Char('l')));
1597 e.handle_key(key(KeyCode::Char('l')));
1598 let h = e
1599 .selection_highlight()
1600 .expect("visual mode should produce a highlight");
1601 assert_eq!(h.kind, HighlightKind::Selection);
1602 assert_eq!(h.range.start.line, 0);
1603 assert_eq!(h.range.end.line, 0);
1604 }
1605
1606 #[test]
1607 fn highlights_emit_search_matches() {
1608 use crate::types::HighlightKind;
1609 let mut e = Editor::new(KeybindingMode::Vim);
1610 e.set_content("foo bar foo\nbaz qux\n");
1611 e.buffer_mut()
1613 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1614 let hs = e.highlights_for_line(0);
1615 assert_eq!(hs.len(), 2);
1616 for h in &hs {
1617 assert_eq!(h.kind, HighlightKind::SearchMatch);
1618 assert_eq!(h.range.start.line, 0);
1619 assert_eq!(h.range.end.line, 0);
1620 }
1621 }
1622
1623 #[test]
1624 fn highlights_empty_without_pattern() {
1625 let mut e = Editor::new(KeybindingMode::Vim);
1626 e.set_content("foo bar");
1627 assert!(e.highlights_for_line(0).is_empty());
1628 }
1629
1630 #[test]
1631 fn highlights_empty_for_out_of_range_line() {
1632 let mut e = Editor::new(KeybindingMode::Vim);
1633 e.set_content("foo");
1634 e.buffer_mut()
1635 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1636 assert!(e.highlights_for_line(99).is_empty());
1637 }
1638
1639 #[test]
1640 fn render_frame_reflects_mode_and_cursor() {
1641 use crate::types::{CursorShape, SnapshotMode};
1642 let mut e = Editor::new(KeybindingMode::Vim);
1643 e.set_content("alpha\nbeta");
1644 let f = e.render_frame();
1645 assert_eq!(f.mode, SnapshotMode::Normal);
1646 assert_eq!(f.cursor_shape, CursorShape::Block);
1647 assert_eq!(f.line_count, 2);
1648
1649 e.handle_key(key(KeyCode::Char('i')));
1650 let f = e.render_frame();
1651 assert_eq!(f.mode, SnapshotMode::Insert);
1652 assert_eq!(f.cursor_shape, CursorShape::Bar);
1653 }
1654
1655 #[test]
1656 fn snapshot_roundtrips_through_restore() {
1657 use crate::types::SnapshotMode;
1658 let mut e = Editor::new(KeybindingMode::Vim);
1659 e.set_content("alpha\nbeta\ngamma");
1660 e.jump_cursor(2, 3);
1661 let snap = e.take_snapshot();
1662 assert_eq!(snap.mode, SnapshotMode::Normal);
1663 assert_eq!(snap.cursor, (2, 3));
1664 assert_eq!(snap.lines.len(), 3);
1665
1666 let mut other = Editor::new(KeybindingMode::Vim);
1667 other.restore_snapshot(snap).expect("restore");
1668 assert_eq!(other.cursor(), (2, 3));
1669 assert_eq!(other.buffer().lines().len(), 3);
1670 }
1671
1672 #[test]
1673 fn restore_snapshot_rejects_version_mismatch() {
1674 let mut e = Editor::new(KeybindingMode::Vim);
1675 let mut snap = e.take_snapshot();
1676 snap.version = 9999;
1677 match e.restore_snapshot(snap) {
1678 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1679 assert_eq!(got, 9999);
1680 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1681 }
1682 other => panic!("expected SnapshotVersion err, got {other:?}"),
1683 }
1684 }
1685
1686 #[test]
1687 fn take_content_change_returns_some_on_first_dirty() {
1688 let mut e = Editor::new(KeybindingMode::Vim);
1689 e.set_content("hello");
1690 let first = e.take_content_change();
1691 assert!(first.is_some());
1692 let second = e.take_content_change();
1693 assert!(second.is_none());
1694 }
1695
1696 #[test]
1697 fn take_content_change_none_until_mutation() {
1698 let mut e = Editor::new(KeybindingMode::Vim);
1699 e.set_content("hello");
1700 e.take_content_change();
1702 assert!(e.take_content_change().is_none());
1703 e.handle_key(key(KeyCode::Char('i')));
1705 e.handle_key(key(KeyCode::Char('x')));
1706 let after = e.take_content_change();
1707 assert!(after.is_some());
1708 assert!(after.unwrap().contains('x'));
1709 }
1710
1711 #[test]
1712 fn vim_insert_to_normal() {
1713 let mut e = Editor::new(KeybindingMode::Vim);
1714 e.handle_key(key(KeyCode::Char('i')));
1715 e.handle_key(key(KeyCode::Esc));
1716 assert_eq!(e.vim_mode(), VimMode::Normal);
1717 }
1718
1719 #[test]
1720 fn vim_normal_to_visual() {
1721 let mut e = Editor::new(KeybindingMode::Vim);
1722 e.handle_key(key(KeyCode::Char('v')));
1723 assert_eq!(e.vim_mode(), VimMode::Visual);
1724 }
1725
1726 #[test]
1727 fn vim_visual_to_normal() {
1728 let mut e = Editor::new(KeybindingMode::Vim);
1729 e.handle_key(key(KeyCode::Char('v')));
1730 e.handle_key(key(KeyCode::Esc));
1731 assert_eq!(e.vim_mode(), VimMode::Normal);
1732 }
1733
1734 #[test]
1735 fn vim_shift_i_moves_to_first_non_whitespace() {
1736 let mut e = Editor::new(KeybindingMode::Vim);
1737 e.set_content(" hello");
1738 e.jump_cursor(0, 8);
1739 e.handle_key(shift_key(KeyCode::Char('I')));
1740 assert_eq!(e.vim_mode(), VimMode::Insert);
1741 assert_eq!(e.cursor(), (0, 3));
1742 }
1743
1744 #[test]
1745 fn vim_shift_a_moves_to_end_and_insert() {
1746 let mut e = Editor::new(KeybindingMode::Vim);
1747 e.set_content("hello");
1748 e.handle_key(shift_key(KeyCode::Char('A')));
1749 assert_eq!(e.vim_mode(), VimMode::Insert);
1750 assert_eq!(e.cursor().1, 5);
1751 }
1752
1753 #[test]
1754 fn count_10j_moves_down_10() {
1755 let mut e = Editor::new(KeybindingMode::Vim);
1756 e.set_content(
1757 (0..20)
1758 .map(|i| format!("line{i}"))
1759 .collect::<Vec<_>>()
1760 .join("\n")
1761 .as_str(),
1762 );
1763 for d in "10".chars() {
1764 e.handle_key(key(KeyCode::Char(d)));
1765 }
1766 e.handle_key(key(KeyCode::Char('j')));
1767 assert_eq!(e.cursor().0, 10);
1768 }
1769
1770 #[test]
1771 fn count_o_repeats_insert_on_esc() {
1772 let mut e = Editor::new(KeybindingMode::Vim);
1773 e.set_content("hello");
1774 for d in "3".chars() {
1775 e.handle_key(key(KeyCode::Char(d)));
1776 }
1777 e.handle_key(key(KeyCode::Char('o')));
1778 assert_eq!(e.vim_mode(), VimMode::Insert);
1779 for c in "world".chars() {
1780 e.handle_key(key(KeyCode::Char(c)));
1781 }
1782 e.handle_key(key(KeyCode::Esc));
1783 assert_eq!(e.vim_mode(), VimMode::Normal);
1784 assert_eq!(e.buffer().lines().len(), 4);
1785 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1786 }
1787
1788 #[test]
1789 fn count_i_repeats_text_on_esc() {
1790 let mut e = Editor::new(KeybindingMode::Vim);
1791 e.set_content("");
1792 for d in "3".chars() {
1793 e.handle_key(key(KeyCode::Char(d)));
1794 }
1795 e.handle_key(key(KeyCode::Char('i')));
1796 for c in "ab".chars() {
1797 e.handle_key(key(KeyCode::Char(c)));
1798 }
1799 e.handle_key(key(KeyCode::Esc));
1800 assert_eq!(e.vim_mode(), VimMode::Normal);
1801 assert_eq!(e.buffer().lines()[0], "ababab");
1802 }
1803
1804 #[test]
1805 fn vim_shift_o_opens_line_above() {
1806 let mut e = Editor::new(KeybindingMode::Vim);
1807 e.set_content("hello");
1808 e.handle_key(shift_key(KeyCode::Char('O')));
1809 assert_eq!(e.vim_mode(), VimMode::Insert);
1810 assert_eq!(e.cursor(), (0, 0));
1811 assert_eq!(e.buffer().lines().len(), 2);
1812 }
1813
1814 #[test]
1815 fn vim_gg_goes_to_top() {
1816 let mut e = Editor::new(KeybindingMode::Vim);
1817 e.set_content("a\nb\nc");
1818 e.jump_cursor(2, 0);
1819 e.handle_key(key(KeyCode::Char('g')));
1820 e.handle_key(key(KeyCode::Char('g')));
1821 assert_eq!(e.cursor().0, 0);
1822 }
1823
1824 #[test]
1825 fn vim_shift_g_goes_to_bottom() {
1826 let mut e = Editor::new(KeybindingMode::Vim);
1827 e.set_content("a\nb\nc");
1828 e.handle_key(shift_key(KeyCode::Char('G')));
1829 assert_eq!(e.cursor().0, 2);
1830 }
1831
1832 #[test]
1833 fn vim_dd_deletes_line() {
1834 let mut e = Editor::new(KeybindingMode::Vim);
1835 e.set_content("first\nsecond");
1836 e.handle_key(key(KeyCode::Char('d')));
1837 e.handle_key(key(KeyCode::Char('d')));
1838 assert_eq!(e.buffer().lines().len(), 1);
1839 assert_eq!(e.buffer().lines()[0], "second");
1840 }
1841
1842 #[test]
1843 fn vim_dw_deletes_word() {
1844 let mut e = Editor::new(KeybindingMode::Vim);
1845 e.set_content("hello world");
1846 e.handle_key(key(KeyCode::Char('d')));
1847 e.handle_key(key(KeyCode::Char('w')));
1848 assert_eq!(e.vim_mode(), VimMode::Normal);
1849 assert!(!e.buffer().lines()[0].starts_with("hello"));
1850 }
1851
1852 #[test]
1853 fn vim_yy_yanks_line() {
1854 let mut e = Editor::new(KeybindingMode::Vim);
1855 e.set_content("hello\nworld");
1856 e.handle_key(key(KeyCode::Char('y')));
1857 e.handle_key(key(KeyCode::Char('y')));
1858 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1859 }
1860
1861 #[test]
1862 fn vim_yy_does_not_move_cursor() {
1863 let mut e = Editor::new(KeybindingMode::Vim);
1864 e.set_content("first\nsecond\nthird");
1865 e.jump_cursor(1, 0);
1866 let before = e.cursor();
1867 e.handle_key(key(KeyCode::Char('y')));
1868 e.handle_key(key(KeyCode::Char('y')));
1869 assert_eq!(e.cursor(), before);
1870 assert_eq!(e.vim_mode(), VimMode::Normal);
1871 }
1872
1873 #[test]
1874 fn vim_yw_yanks_word() {
1875 let mut e = Editor::new(KeybindingMode::Vim);
1876 e.set_content("hello world");
1877 e.handle_key(key(KeyCode::Char('y')));
1878 e.handle_key(key(KeyCode::Char('w')));
1879 assert_eq!(e.vim_mode(), VimMode::Normal);
1880 assert!(e.last_yank.is_some());
1881 }
1882
1883 #[test]
1884 fn vim_cc_changes_line() {
1885 let mut e = Editor::new(KeybindingMode::Vim);
1886 e.set_content("hello\nworld");
1887 e.handle_key(key(KeyCode::Char('c')));
1888 e.handle_key(key(KeyCode::Char('c')));
1889 assert_eq!(e.vim_mode(), VimMode::Insert);
1890 }
1891
1892 #[test]
1893 fn vim_u_undoes_insert_session_as_chunk() {
1894 let mut e = Editor::new(KeybindingMode::Vim);
1895 e.set_content("hello");
1896 e.handle_key(key(KeyCode::Char('i')));
1897 e.handle_key(key(KeyCode::Enter));
1898 e.handle_key(key(KeyCode::Enter));
1899 e.handle_key(key(KeyCode::Esc));
1900 assert_eq!(e.buffer().lines().len(), 3);
1901 e.handle_key(key(KeyCode::Char('u')));
1902 assert_eq!(e.buffer().lines().len(), 1);
1903 assert_eq!(e.buffer().lines()[0], "hello");
1904 }
1905
1906 #[test]
1907 fn vim_undo_redo_roundtrip() {
1908 let mut e = Editor::new(KeybindingMode::Vim);
1909 e.set_content("hello");
1910 e.handle_key(key(KeyCode::Char('i')));
1911 for c in "world".chars() {
1912 e.handle_key(key(KeyCode::Char(c)));
1913 }
1914 e.handle_key(key(KeyCode::Esc));
1915 let after = e.buffer().lines()[0].clone();
1916 e.handle_key(key(KeyCode::Char('u')));
1917 assert_eq!(e.buffer().lines()[0], "hello");
1918 e.handle_key(ctrl_key(KeyCode::Char('r')));
1919 assert_eq!(e.buffer().lines()[0], after);
1920 }
1921
1922 #[test]
1923 fn vim_u_undoes_dd() {
1924 let mut e = Editor::new(KeybindingMode::Vim);
1925 e.set_content("first\nsecond");
1926 e.handle_key(key(KeyCode::Char('d')));
1927 e.handle_key(key(KeyCode::Char('d')));
1928 assert_eq!(e.buffer().lines().len(), 1);
1929 e.handle_key(key(KeyCode::Char('u')));
1930 assert_eq!(e.buffer().lines().len(), 2);
1931 assert_eq!(e.buffer().lines()[0], "first");
1932 }
1933
1934 #[test]
1935 fn vim_ctrl_r_redoes() {
1936 let mut e = Editor::new(KeybindingMode::Vim);
1937 e.set_content("hello");
1938 e.handle_key(ctrl_key(KeyCode::Char('r')));
1939 }
1940
1941 #[test]
1942 fn vim_r_replaces_char() {
1943 let mut e = Editor::new(KeybindingMode::Vim);
1944 e.set_content("hello");
1945 e.handle_key(key(KeyCode::Char('r')));
1946 e.handle_key(key(KeyCode::Char('x')));
1947 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1948 }
1949
1950 #[test]
1951 fn vim_tilde_toggles_case() {
1952 let mut e = Editor::new(KeybindingMode::Vim);
1953 e.set_content("hello");
1954 e.handle_key(key(KeyCode::Char('~')));
1955 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1956 }
1957
1958 #[test]
1959 fn vim_visual_d_cuts() {
1960 let mut e = Editor::new(KeybindingMode::Vim);
1961 e.set_content("hello");
1962 e.handle_key(key(KeyCode::Char('v')));
1963 e.handle_key(key(KeyCode::Char('l')));
1964 e.handle_key(key(KeyCode::Char('l')));
1965 e.handle_key(key(KeyCode::Char('d')));
1966 assert_eq!(e.vim_mode(), VimMode::Normal);
1967 assert!(e.last_yank.is_some());
1968 }
1969
1970 #[test]
1971 fn vim_visual_c_enters_insert() {
1972 let mut e = Editor::new(KeybindingMode::Vim);
1973 e.set_content("hello");
1974 e.handle_key(key(KeyCode::Char('v')));
1975 e.handle_key(key(KeyCode::Char('l')));
1976 e.handle_key(key(KeyCode::Char('c')));
1977 assert_eq!(e.vim_mode(), VimMode::Insert);
1978 }
1979
1980 #[test]
1981 fn vim_normal_unknown_key_consumed() {
1982 let mut e = Editor::new(KeybindingMode::Vim);
1983 let consumed = e.handle_key(key(KeyCode::Char('z')));
1985 assert!(consumed);
1986 }
1987
1988 #[test]
1989 fn force_normal_clears_operator() {
1990 let mut e = Editor::new(KeybindingMode::Vim);
1991 e.handle_key(key(KeyCode::Char('d')));
1992 e.force_normal();
1993 assert_eq!(e.vim_mode(), VimMode::Normal);
1994 }
1995
1996 fn many_lines(n: usize) -> String {
1997 (0..n)
1998 .map(|i| format!("line{i}"))
1999 .collect::<Vec<_>>()
2000 .join("\n")
2001 }
2002
2003 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
2004 e.set_viewport_height(height);
2005 }
2006
2007 #[test]
2008 fn zz_centers_cursor_in_viewport() {
2009 let mut e = Editor::new(KeybindingMode::Vim);
2010 e.set_content(&many_lines(100));
2011 prime_viewport(&mut e, 20);
2012 e.jump_cursor(50, 0);
2013 e.handle_key(key(KeyCode::Char('z')));
2014 e.handle_key(key(KeyCode::Char('z')));
2015 assert_eq!(e.buffer().viewport().top_row, 40);
2016 assert_eq!(e.cursor().0, 50);
2017 }
2018
2019 #[test]
2020 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
2021 let mut e = Editor::new(KeybindingMode::Vim);
2022 e.set_content(&many_lines(100));
2023 prime_viewport(&mut e, 20);
2024 e.jump_cursor(50, 0);
2025 e.handle_key(key(KeyCode::Char('z')));
2026 e.handle_key(key(KeyCode::Char('t')));
2027 assert_eq!(e.buffer().viewport().top_row, 45);
2030 assert_eq!(e.cursor().0, 50);
2031 }
2032
2033 #[test]
2034 fn ctrl_a_increments_number_at_cursor() {
2035 let mut e = Editor::new(KeybindingMode::Vim);
2036 e.set_content("x = 41");
2037 e.handle_key(ctrl_key(KeyCode::Char('a')));
2038 assert_eq!(e.buffer().lines()[0], "x = 42");
2039 assert_eq!(e.cursor(), (0, 5));
2040 }
2041
2042 #[test]
2043 fn ctrl_a_finds_number_to_right_of_cursor() {
2044 let mut e = Editor::new(KeybindingMode::Vim);
2045 e.set_content("foo 99 bar");
2046 e.handle_key(ctrl_key(KeyCode::Char('a')));
2047 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
2048 assert_eq!(e.cursor(), (0, 6));
2049 }
2050
2051 #[test]
2052 fn ctrl_a_with_count_adds_count() {
2053 let mut e = Editor::new(KeybindingMode::Vim);
2054 e.set_content("x = 10");
2055 for d in "5".chars() {
2056 e.handle_key(key(KeyCode::Char(d)));
2057 }
2058 e.handle_key(ctrl_key(KeyCode::Char('a')));
2059 assert_eq!(e.buffer().lines()[0], "x = 15");
2060 }
2061
2062 #[test]
2063 fn ctrl_x_decrements_number() {
2064 let mut e = Editor::new(KeybindingMode::Vim);
2065 e.set_content("n=5");
2066 e.handle_key(ctrl_key(KeyCode::Char('x')));
2067 assert_eq!(e.buffer().lines()[0], "n=4");
2068 }
2069
2070 #[test]
2071 fn ctrl_x_crosses_zero_into_negative() {
2072 let mut e = Editor::new(KeybindingMode::Vim);
2073 e.set_content("v=0");
2074 e.handle_key(ctrl_key(KeyCode::Char('x')));
2075 assert_eq!(e.buffer().lines()[0], "v=-1");
2076 }
2077
2078 #[test]
2079 fn ctrl_a_on_negative_number_increments_toward_zero() {
2080 let mut e = Editor::new(KeybindingMode::Vim);
2081 e.set_content("a = -5");
2082 e.handle_key(ctrl_key(KeyCode::Char('a')));
2083 assert_eq!(e.buffer().lines()[0], "a = -4");
2084 }
2085
2086 #[test]
2087 fn ctrl_a_noop_when_no_digit_on_line() {
2088 let mut e = Editor::new(KeybindingMode::Vim);
2089 e.set_content("no digits here");
2090 e.handle_key(ctrl_key(KeyCode::Char('a')));
2091 assert_eq!(e.buffer().lines()[0], "no digits here");
2092 }
2093
2094 #[test]
2095 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
2096 let mut e = Editor::new(KeybindingMode::Vim);
2097 e.set_content(&many_lines(100));
2098 prime_viewport(&mut e, 20);
2099 e.jump_cursor(50, 0);
2100 e.handle_key(key(KeyCode::Char('z')));
2101 e.handle_key(key(KeyCode::Char('b')));
2102 assert_eq!(e.buffer().viewport().top_row, 36);
2106 assert_eq!(e.cursor().0, 50);
2107 }
2108
2109 #[test]
2116 fn set_content_dirties_then_take_dirty_clears() {
2117 let mut e = Editor::new(KeybindingMode::Vim);
2118 e.set_content("hello");
2119 assert!(
2120 e.take_dirty(),
2121 "set_content should leave content_dirty=true"
2122 );
2123 assert!(!e.take_dirty(), "take_dirty should clear the flag");
2124 }
2125
2126 #[test]
2127 fn content_arc_returns_same_arc_until_mutation() {
2128 let mut e = Editor::new(KeybindingMode::Vim);
2129 e.set_content("hello");
2130 let a = e.content_arc();
2131 let b = e.content_arc();
2132 assert!(
2133 std::sync::Arc::ptr_eq(&a, &b),
2134 "repeated content_arc() should hit the cache"
2135 );
2136
2137 e.handle_key(key(KeyCode::Char('i')));
2139 e.handle_key(key(KeyCode::Char('!')));
2140 let c = e.content_arc();
2141 assert!(
2142 !std::sync::Arc::ptr_eq(&a, &c),
2143 "mutation should invalidate content_arc() cache"
2144 );
2145 assert!(c.contains('!'));
2146 }
2147
2148 #[test]
2149 fn content_arc_cache_invalidated_by_set_content() {
2150 let mut e = Editor::new(KeybindingMode::Vim);
2151 e.set_content("one");
2152 let a = e.content_arc();
2153 e.set_content("two");
2154 let b = e.content_arc();
2155 assert!(!std::sync::Arc::ptr_eq(&a, &b));
2156 assert!(b.starts_with("two"));
2157 }
2158
2159 #[test]
2165 fn mouse_click_past_eol_lands_on_last_char() {
2166 let mut e = Editor::new(KeybindingMode::Vim);
2167 e.set_content("hello");
2168 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2172 e.mouse_click(area, 78, 1);
2173 assert_eq!(e.cursor(), (0, 4));
2174 }
2175
2176 #[test]
2177 fn mouse_click_past_eol_handles_multibyte_line() {
2178 let mut e = Editor::new(KeybindingMode::Vim);
2179 e.set_content("héllo");
2182 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2183 e.mouse_click(area, 78, 1);
2184 assert_eq!(e.cursor(), (0, 4));
2185 }
2186
2187 #[test]
2188 fn mouse_click_inside_line_lands_on_clicked_char() {
2189 let mut e = Editor::new(KeybindingMode::Vim);
2190 e.set_content("hello world");
2191 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2194 e.mouse_click(area, 4, 1);
2195 assert_eq!(e.cursor(), (0, 0));
2196 e.mouse_click(area, 6, 1);
2197 assert_eq!(e.cursor(), (0, 2));
2198 }
2199}