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 feed_input(&mut self, input: crate::PlannedInput) -> bool {
920 use crate::{Modifiers, PlannedInput, SpecialKey};
921 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
922 let to_mods = |m: Modifiers| {
923 let mut k = KeyModifiers::NONE;
924 if m.ctrl {
925 k |= KeyModifiers::CONTROL;
926 }
927 if m.shift {
928 k |= KeyModifiers::SHIFT;
929 }
930 if m.alt {
931 k |= KeyModifiers::ALT;
932 }
933 if m.super_ {
934 k |= KeyModifiers::SUPER;
935 }
936 k
937 };
938 let (code, mods) = match input {
939 PlannedInput::Char(c, m) => (KeyCode::Char(c), to_mods(m)),
940 PlannedInput::Key(k, m) => {
941 let code = match k {
942 SpecialKey::Esc => KeyCode::Esc,
943 SpecialKey::Enter => KeyCode::Enter,
944 SpecialKey::Backspace => KeyCode::Backspace,
945 SpecialKey::Tab => KeyCode::Tab,
946 SpecialKey::BackTab => KeyCode::BackTab,
947 SpecialKey::Up => KeyCode::Up,
948 SpecialKey::Down => KeyCode::Down,
949 SpecialKey::Left => KeyCode::Left,
950 SpecialKey::Right => KeyCode::Right,
951 SpecialKey::Home => KeyCode::Home,
952 SpecialKey::End => KeyCode::End,
953 SpecialKey::PageUp => KeyCode::PageUp,
954 SpecialKey::PageDown => KeyCode::PageDown,
955 SpecialKey::Insert => KeyCode::Insert,
956 SpecialKey::Delete => KeyCode::Delete,
957 SpecialKey::F(n) => KeyCode::F(n),
958 };
959 (code, to_mods(m))
960 }
961 PlannedInput::Mouse(_)
963 | PlannedInput::Paste(_)
964 | PlannedInput::FocusGained
965 | PlannedInput::FocusLost
966 | PlannedInput::Resize(_, _) => return false,
967 };
968 self.handle_key(KeyEvent::new(code, mods))
969 }
970
971 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
988 std::mem::take(&mut self.change_log)
989 }
990
991 pub fn current_options(&self) -> crate::types::Options {
1001 let mut o = crate::types::Options::default();
1002 o.shiftwidth = self.settings.shiftwidth as u32;
1003 o.tabstop = self.settings.tabstop as u32;
1004 o.ignorecase = self.settings.ignore_case;
1005 o
1006 }
1007
1008 pub fn apply_options(&mut self, opts: &crate::types::Options) {
1013 self.settings.shiftwidth = opts.shiftwidth as usize;
1014 self.settings.tabstop = opts.tabstop as usize;
1015 self.settings.ignore_case = opts.ignorecase;
1016 }
1017
1018 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
1028 use crate::types::{Highlight, HighlightKind, Pos};
1029 let sel = self.buffer_selection()?;
1030 let (start, end) = match sel {
1031 hjkl_buffer::Selection::Char { anchor, head } => {
1032 let a = (anchor.row, anchor.col);
1033 let h = (head.row, head.col);
1034 if a <= h { (a, h) } else { (h, a) }
1035 }
1036 hjkl_buffer::Selection::Line {
1037 anchor_row,
1038 head_row,
1039 } => {
1040 let (top, bot) = if anchor_row <= head_row {
1041 (anchor_row, head_row)
1042 } else {
1043 (head_row, anchor_row)
1044 };
1045 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
1046 ((top, 0), (bot, last_col))
1047 }
1048 hjkl_buffer::Selection::Block { anchor, head } => {
1049 let (top, bot) = if anchor.row <= head.row {
1050 (anchor.row, head.row)
1051 } else {
1052 (head.row, anchor.row)
1053 };
1054 let (left, right) = if anchor.col <= head.col {
1055 (anchor.col, head.col)
1056 } else {
1057 (head.col, anchor.col)
1058 };
1059 ((top, left), (bot, right))
1060 }
1061 };
1062 Some(Highlight {
1063 range: Pos {
1064 line: start.0 as u32,
1065 col: start.1 as u32,
1066 }..Pos {
1067 line: end.0 as u32,
1068 col: end.1 as u32,
1069 },
1070 kind: HighlightKind::Selection,
1071 })
1072 }
1073
1074 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
1087 use crate::types::{Highlight, HighlightKind, Pos};
1088 let row = line as usize;
1089 if row >= self.buffer.lines().len() {
1090 return Vec::new();
1091 }
1092 if self.buffer.search_pattern().is_none() {
1093 return Vec::new();
1094 }
1095 self.buffer
1096 .search_matches(row)
1097 .into_iter()
1098 .map(|(start, end)| Highlight {
1099 range: Pos {
1100 line,
1101 col: start as u32,
1102 }..Pos {
1103 line,
1104 col: end as u32,
1105 },
1106 kind: HighlightKind::SearchMatch,
1107 })
1108 .collect()
1109 }
1110
1111 pub fn render_frame(&self) -> crate::types::RenderFrame {
1121 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
1122 let (cursor_row, cursor_col) = self.cursor();
1123 let (mode, shape) = match self.vim_mode() {
1124 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
1125 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
1126 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
1127 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
1128 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
1129 };
1130 RenderFrame {
1131 mode,
1132 cursor_row: cursor_row as u32,
1133 cursor_col: cursor_col as u32,
1134 cursor_shape: shape,
1135 viewport_top: self.buffer.viewport().top_row as u32,
1136 line_count: self.buffer.lines().len() as u32,
1137 }
1138 }
1139
1140 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
1153 use crate::types::{EditorSnapshot, SnapshotMode};
1154 let mode = match self.vim_mode() {
1155 crate::VimMode::Normal => SnapshotMode::Normal,
1156 crate::VimMode::Insert => SnapshotMode::Insert,
1157 crate::VimMode::Visual => SnapshotMode::Visual,
1158 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
1159 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
1160 };
1161 let cursor = self.cursor();
1162 let cursor = (cursor.0 as u32, cursor.1 as u32);
1163 let lines: Vec<String> = self.buffer.lines().to_vec();
1164 let viewport_top = self.buffer.viewport().top_row as u32;
1165 let file_marks = self
1166 .file_marks
1167 .iter()
1168 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
1169 .collect();
1170 EditorSnapshot {
1171 version: EditorSnapshot::VERSION,
1172 mode,
1173 cursor,
1174 lines,
1175 viewport_top,
1176 registers: self.registers.clone(),
1177 file_marks,
1178 }
1179 }
1180
1181 pub fn restore_snapshot(
1189 &mut self,
1190 snap: crate::types::EditorSnapshot,
1191 ) -> Result<(), crate::EngineError> {
1192 use crate::types::EditorSnapshot;
1193 if snap.version != EditorSnapshot::VERSION {
1194 return Err(crate::EngineError::SnapshotVersion(
1195 snap.version,
1196 EditorSnapshot::VERSION,
1197 ));
1198 }
1199 let text = snap.lines.join("\n");
1200 self.set_content(&text);
1201 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1202 let mut vp = self.buffer.viewport();
1203 vp.top_row = snap.viewport_top as usize;
1204 *self.buffer.viewport_mut() = vp;
1205 self.registers = snap.registers;
1206 self.file_marks = snap
1207 .file_marks
1208 .into_iter()
1209 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1210 .collect();
1211 Ok(())
1212 }
1213
1214 pub fn seed_yank(&mut self, text: String) {
1218 let linewise = text.ends_with('\n');
1219 self.vim.yank_linewise = linewise;
1220 self.registers.unnamed = crate::registers::Slot { text, linewise };
1221 }
1222
1223 pub fn scroll_down(&mut self, rows: i16) {
1228 self.scroll_viewport(rows);
1229 }
1230
1231 pub fn scroll_up(&mut self, rows: i16) {
1235 self.scroll_viewport(-rows);
1236 }
1237
1238 const SCROLLOFF: usize = 5;
1242
1243 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1248 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1249 if height == 0 {
1250 self.buffer.ensure_cursor_visible();
1251 return;
1252 }
1253 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1257 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
1260 self.ensure_scrolloff_wrap(height, margin);
1261 return;
1262 }
1263 let cursor_row = self.buffer.cursor().row;
1264 let last_row = self.buffer.row_count().saturating_sub(1);
1265 let v = self.buffer.viewport_mut();
1266 if cursor_row < v.top_row + margin {
1268 v.top_row = cursor_row.saturating_sub(margin);
1269 }
1270 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1272 if cursor_row > v.top_row + max_bottom {
1273 v.top_row = cursor_row.saturating_sub(max_bottom);
1274 }
1275 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1277 if v.top_row > max_top {
1278 v.top_row = max_top;
1279 }
1280 let cursor = self.buffer.cursor();
1283 self.buffer.viewport_mut().ensure_visible(cursor);
1284 }
1285
1286 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1291 let cursor_row = self.buffer.cursor().row;
1292 if cursor_row < self.buffer.viewport().top_row {
1295 self.buffer.viewport_mut().top_row = cursor_row;
1296 self.buffer.viewport_mut().top_col = 0;
1297 }
1298 let max_csr = height.saturating_sub(1).saturating_sub(margin);
1301 loop {
1302 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1303 if csr <= max_csr {
1304 break;
1305 }
1306 let top = self.buffer.viewport().top_row;
1307 let Some(next) = self.buffer.next_visible_row(top) else {
1308 break;
1309 };
1310 if next > cursor_row {
1312 self.buffer.viewport_mut().top_row = cursor_row;
1313 break;
1314 }
1315 self.buffer.viewport_mut().top_row = next;
1316 }
1317 loop {
1320 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1321 if csr >= margin {
1322 break;
1323 }
1324 let top = self.buffer.viewport().top_row;
1325 let Some(prev) = self.buffer.prev_visible_row(top) else {
1326 break;
1327 };
1328 self.buffer.viewport_mut().top_row = prev;
1329 }
1330 let max_top = self.buffer.max_top_for_height(height);
1335 if self.buffer.viewport().top_row > max_top {
1336 self.buffer.viewport_mut().top_row = max_top;
1337 }
1338 self.buffer.viewport_mut().top_col = 0;
1339 }
1340
1341 fn scroll_viewport(&mut self, delta: i16) {
1342 if delta == 0 {
1343 return;
1344 }
1345 let total_rows = self.buffer.row_count() as isize;
1347 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1348 let cur_top = self.buffer.viewport().top_row as isize;
1349 let new_top = (cur_top + delta as isize)
1350 .max(0)
1351 .min((total_rows - 1).max(0)) as usize;
1352 self.buffer.viewport_mut().top_row = new_top;
1353 let _ = cur_top;
1356 if height == 0 {
1357 return;
1358 }
1359 let cursor = self.buffer.cursor();
1362 let margin = Self::SCROLLOFF.min(height / 2);
1363 let min_row = new_top + margin;
1364 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1365 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1366 if target_row != cursor.row {
1367 let line_len = self
1368 .buffer
1369 .line(target_row)
1370 .map(|l| l.chars().count())
1371 .unwrap_or(0);
1372 let target_col = cursor.col.min(line_len.saturating_sub(1));
1373 self.buffer
1374 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1375 }
1376 }
1377
1378 pub fn goto_line(&mut self, line: usize) {
1379 let row = line.saturating_sub(1);
1380 let max = self.buffer.row_count().saturating_sub(1);
1381 let target = row.min(max);
1382 self.buffer
1383 .set_cursor(hjkl_buffer::Position::new(target, 0));
1384 }
1385
1386 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1390 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1391 if height == 0 {
1392 return;
1393 }
1394 let cur_row = self.buffer.cursor().row;
1395 let cur_top = self.buffer.viewport().top_row;
1396 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1402 let new_top = match pos {
1403 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1404 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1405 CursorScrollTarget::Bottom => {
1406 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1407 }
1408 };
1409 if new_top == cur_top {
1410 return;
1411 }
1412 self.buffer.viewport_mut().top_row = new_top;
1413 }
1414
1415 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1422 let lines = self.buffer.lines();
1423 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1425 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1426 let rel_row = row.saturating_sub(inner_top) as usize;
1427 let top = self.buffer.viewport().top_row;
1428 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1429 let rel_col = col.saturating_sub(content_x) as usize;
1430 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1431 let last_col = line_chars.saturating_sub(1);
1432 (doc_row, rel_col.min(last_col))
1433 }
1434
1435 pub fn jump_to(&mut self, line: usize, col: usize) {
1437 let r = line.saturating_sub(1);
1438 let max_row = self.buffer.row_count().saturating_sub(1);
1439 let r = r.min(max_row);
1440 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1441 let c = col.saturating_sub(1).min(line_len);
1442 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1443 }
1444
1445 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1447 if self.vim.is_visual() {
1448 self.vim.force_normal();
1449 }
1450 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1451 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1452 }
1453
1454 pub fn mouse_begin_drag(&mut self) {
1456 if !self.vim.is_visual_char() {
1457 let cursor = self.cursor();
1458 self.vim.enter_visual(cursor);
1459 }
1460 }
1461
1462 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1464 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1465 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1466 }
1467
1468 pub fn insert_str(&mut self, text: &str) {
1469 let pos = self.buffer.cursor();
1470 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1471 at: pos,
1472 text: text.to_string(),
1473 });
1474 self.push_buffer_content_to_textarea();
1475 self.mark_content_dirty();
1476 }
1477
1478 pub fn accept_completion(&mut self, completion: &str) {
1479 use hjkl_buffer::{Edit, MotionKind, Position};
1480 let cursor = self.buffer.cursor();
1481 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1482 let chars: Vec<char> = line.chars().collect();
1483 let prefix_len = chars[..cursor.col.min(chars.len())]
1484 .iter()
1485 .rev()
1486 .take_while(|c| c.is_alphanumeric() || **c == '_')
1487 .count();
1488 if prefix_len > 0 {
1489 let start = Position::new(cursor.row, cursor.col - prefix_len);
1490 self.buffer.apply_edit(Edit::DeleteRange {
1491 start,
1492 end: cursor,
1493 kind: MotionKind::Char,
1494 });
1495 }
1496 let cursor = self.buffer.cursor();
1497 self.buffer.apply_edit(Edit::InsertStr {
1498 at: cursor,
1499 text: completion.to_string(),
1500 });
1501 self.push_buffer_content_to_textarea();
1502 self.mark_content_dirty();
1503 }
1504
1505 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1506 let pos = self.buffer.cursor();
1507 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1508 }
1509
1510 #[doc(hidden)]
1511 pub fn push_undo(&mut self) {
1512 let snap = self.snapshot();
1513 if self.undo_stack.len() >= 200 {
1514 self.undo_stack.remove(0);
1515 }
1516 self.undo_stack.push(snap);
1517 self.redo_stack.clear();
1518 }
1519
1520 #[doc(hidden)]
1521 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1522 let text = lines.join("\n");
1523 self.buffer.replace_all(&text);
1524 self.buffer
1525 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1526 self.mark_content_dirty();
1527 }
1528
1529 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1531 let input = crossterm_to_input(key);
1532 if input.key == Key::Null {
1533 return false;
1534 }
1535 vim::step(self, input)
1536 }
1537}
1538
1539pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1540 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1541 let alt = key.modifiers.contains(KeyModifiers::ALT);
1542 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1543 let k = match key.code {
1544 KeyCode::Char(c) => Key::Char(c),
1545 KeyCode::Backspace => Key::Backspace,
1546 KeyCode::Delete => Key::Delete,
1547 KeyCode::Enter => Key::Enter,
1548 KeyCode::Left => Key::Left,
1549 KeyCode::Right => Key::Right,
1550 KeyCode::Up => Key::Up,
1551 KeyCode::Down => Key::Down,
1552 KeyCode::Home => Key::Home,
1553 KeyCode::End => Key::End,
1554 KeyCode::Tab => Key::Tab,
1555 KeyCode::Esc => Key::Esc,
1556 _ => Key::Null,
1557 };
1558 Input {
1559 key: k,
1560 ctrl,
1561 alt,
1562 shift,
1563 }
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568 use super::*;
1569 use crossterm::event::KeyEvent;
1570
1571 fn key(code: KeyCode) -> KeyEvent {
1572 KeyEvent::new(code, KeyModifiers::NONE)
1573 }
1574 fn shift_key(code: KeyCode) -> KeyEvent {
1575 KeyEvent::new(code, KeyModifiers::SHIFT)
1576 }
1577 fn ctrl_key(code: KeyCode) -> KeyEvent {
1578 KeyEvent::new(code, KeyModifiers::CONTROL)
1579 }
1580
1581 #[test]
1582 fn vim_normal_to_insert() {
1583 let mut e = Editor::new(KeybindingMode::Vim);
1584 e.handle_key(key(KeyCode::Char('i')));
1585 assert_eq!(e.vim_mode(), VimMode::Insert);
1586 }
1587
1588 #[test]
1589 fn feed_input_char_routes_through_handle_key() {
1590 use crate::{Modifiers, PlannedInput};
1591 let mut e = Editor::new(KeybindingMode::Vim);
1592 e.set_content("abc");
1593 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1595 assert_eq!(e.vim_mode(), VimMode::Insert);
1596 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
1598 assert!(e.content().contains('X'));
1599 }
1600
1601 #[test]
1602 fn feed_input_special_key_routes() {
1603 use crate::{Modifiers, PlannedInput, SpecialKey};
1604 let mut e = Editor::new(KeybindingMode::Vim);
1605 e.set_content("abc");
1606 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1607 assert_eq!(e.vim_mode(), VimMode::Insert);
1608 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
1609 assert_eq!(e.vim_mode(), VimMode::Normal);
1610 }
1611
1612 #[test]
1613 fn feed_input_mouse_paste_focus_resize_no_op() {
1614 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
1615 let mut e = Editor::new(KeybindingMode::Vim);
1616 e.set_content("abc");
1617 let mode_before = e.vim_mode();
1618 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
1619 kind: MouseKind::Press,
1620 pos: Pos::new(0, 0),
1621 mods: Default::default(),
1622 }));
1623 assert!(!consumed);
1624 assert_eq!(e.vim_mode(), mode_before);
1625 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
1626 assert!(!e.feed_input(PlannedInput::FocusGained));
1627 assert!(!e.feed_input(PlannedInput::FocusLost));
1628 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
1629 }
1630
1631 #[test]
1632 fn intern_engine_style_dedups_with_intern_style() {
1633 use crate::types::{Attrs, Color, Style};
1634 let mut e = Editor::new(KeybindingMode::Vim);
1635 let s = Style {
1636 fg: Some(Color(255, 0, 0)),
1637 bg: None,
1638 attrs: Attrs::BOLD,
1639 };
1640 let id_a = e.intern_engine_style(s);
1641 let id_b = e.intern_engine_style(s);
1643 assert_eq!(id_a, id_b);
1644 let back = e.engine_style_at(id_a).expect("interned");
1646 assert_eq!(back, s);
1647 }
1648
1649 #[test]
1650 fn engine_style_at_out_of_range_returns_none() {
1651 let e = Editor::new(KeybindingMode::Vim);
1652 assert!(e.engine_style_at(99).is_none());
1653 }
1654
1655 #[test]
1656 fn take_changes_drains_after_insert() {
1657 let mut e = Editor::new(KeybindingMode::Vim);
1658 e.set_content("abc");
1659 assert!(e.take_changes().is_empty());
1661 e.handle_key(key(KeyCode::Char('i')));
1663 e.handle_key(key(KeyCode::Char('X')));
1664 let changes = e.take_changes();
1665 assert!(
1666 !changes.is_empty(),
1667 "insert mode keystroke should produce a change"
1668 );
1669 assert!(e.take_changes().is_empty());
1671 }
1672
1673 #[test]
1674 fn options_bridge_roundtrip() {
1675 let mut e = Editor::new(KeybindingMode::Vim);
1676 let opts = e.current_options();
1677 assert_eq!(opts.shiftwidth, 2); assert_eq!(opts.tabstop, 8);
1679
1680 let mut new_opts = crate::types::Options::default();
1681 new_opts.shiftwidth = 4;
1682 new_opts.tabstop = 2;
1683 new_opts.ignorecase = true;
1684 e.apply_options(&new_opts);
1685
1686 let after = e.current_options();
1687 assert_eq!(after.shiftwidth, 4);
1688 assert_eq!(after.tabstop, 2);
1689 assert!(after.ignorecase);
1690 }
1691
1692 #[test]
1693 fn selection_highlight_none_in_normal() {
1694 let mut e = Editor::new(KeybindingMode::Vim);
1695 e.set_content("hello");
1696 assert!(e.selection_highlight().is_none());
1697 }
1698
1699 #[test]
1700 fn selection_highlight_some_in_visual() {
1701 use crate::types::HighlightKind;
1702 let mut e = Editor::new(KeybindingMode::Vim);
1703 e.set_content("hello world");
1704 e.handle_key(key(KeyCode::Char('v')));
1705 e.handle_key(key(KeyCode::Char('l')));
1706 e.handle_key(key(KeyCode::Char('l')));
1707 let h = e
1708 .selection_highlight()
1709 .expect("visual mode should produce a highlight");
1710 assert_eq!(h.kind, HighlightKind::Selection);
1711 assert_eq!(h.range.start.line, 0);
1712 assert_eq!(h.range.end.line, 0);
1713 }
1714
1715 #[test]
1716 fn highlights_emit_search_matches() {
1717 use crate::types::HighlightKind;
1718 let mut e = Editor::new(KeybindingMode::Vim);
1719 e.set_content("foo bar foo\nbaz qux\n");
1720 e.buffer_mut()
1722 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1723 let hs = e.highlights_for_line(0);
1724 assert_eq!(hs.len(), 2);
1725 for h in &hs {
1726 assert_eq!(h.kind, HighlightKind::SearchMatch);
1727 assert_eq!(h.range.start.line, 0);
1728 assert_eq!(h.range.end.line, 0);
1729 }
1730 }
1731
1732 #[test]
1733 fn highlights_empty_without_pattern() {
1734 let mut e = Editor::new(KeybindingMode::Vim);
1735 e.set_content("foo bar");
1736 assert!(e.highlights_for_line(0).is_empty());
1737 }
1738
1739 #[test]
1740 fn highlights_empty_for_out_of_range_line() {
1741 let mut e = Editor::new(KeybindingMode::Vim);
1742 e.set_content("foo");
1743 e.buffer_mut()
1744 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1745 assert!(e.highlights_for_line(99).is_empty());
1746 }
1747
1748 #[test]
1749 fn render_frame_reflects_mode_and_cursor() {
1750 use crate::types::{CursorShape, SnapshotMode};
1751 let mut e = Editor::new(KeybindingMode::Vim);
1752 e.set_content("alpha\nbeta");
1753 let f = e.render_frame();
1754 assert_eq!(f.mode, SnapshotMode::Normal);
1755 assert_eq!(f.cursor_shape, CursorShape::Block);
1756 assert_eq!(f.line_count, 2);
1757
1758 e.handle_key(key(KeyCode::Char('i')));
1759 let f = e.render_frame();
1760 assert_eq!(f.mode, SnapshotMode::Insert);
1761 assert_eq!(f.cursor_shape, CursorShape::Bar);
1762 }
1763
1764 #[test]
1765 fn snapshot_roundtrips_through_restore() {
1766 use crate::types::SnapshotMode;
1767 let mut e = Editor::new(KeybindingMode::Vim);
1768 e.set_content("alpha\nbeta\ngamma");
1769 e.jump_cursor(2, 3);
1770 let snap = e.take_snapshot();
1771 assert_eq!(snap.mode, SnapshotMode::Normal);
1772 assert_eq!(snap.cursor, (2, 3));
1773 assert_eq!(snap.lines.len(), 3);
1774
1775 let mut other = Editor::new(KeybindingMode::Vim);
1776 other.restore_snapshot(snap).expect("restore");
1777 assert_eq!(other.cursor(), (2, 3));
1778 assert_eq!(other.buffer().lines().len(), 3);
1779 }
1780
1781 #[test]
1782 fn restore_snapshot_rejects_version_mismatch() {
1783 let mut e = Editor::new(KeybindingMode::Vim);
1784 let mut snap = e.take_snapshot();
1785 snap.version = 9999;
1786 match e.restore_snapshot(snap) {
1787 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1788 assert_eq!(got, 9999);
1789 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1790 }
1791 other => panic!("expected SnapshotVersion err, got {other:?}"),
1792 }
1793 }
1794
1795 #[test]
1796 fn take_content_change_returns_some_on_first_dirty() {
1797 let mut e = Editor::new(KeybindingMode::Vim);
1798 e.set_content("hello");
1799 let first = e.take_content_change();
1800 assert!(first.is_some());
1801 let second = e.take_content_change();
1802 assert!(second.is_none());
1803 }
1804
1805 #[test]
1806 fn take_content_change_none_until_mutation() {
1807 let mut e = Editor::new(KeybindingMode::Vim);
1808 e.set_content("hello");
1809 e.take_content_change();
1811 assert!(e.take_content_change().is_none());
1812 e.handle_key(key(KeyCode::Char('i')));
1814 e.handle_key(key(KeyCode::Char('x')));
1815 let after = e.take_content_change();
1816 assert!(after.is_some());
1817 assert!(after.unwrap().contains('x'));
1818 }
1819
1820 #[test]
1821 fn vim_insert_to_normal() {
1822 let mut e = Editor::new(KeybindingMode::Vim);
1823 e.handle_key(key(KeyCode::Char('i')));
1824 e.handle_key(key(KeyCode::Esc));
1825 assert_eq!(e.vim_mode(), VimMode::Normal);
1826 }
1827
1828 #[test]
1829 fn vim_normal_to_visual() {
1830 let mut e = Editor::new(KeybindingMode::Vim);
1831 e.handle_key(key(KeyCode::Char('v')));
1832 assert_eq!(e.vim_mode(), VimMode::Visual);
1833 }
1834
1835 #[test]
1836 fn vim_visual_to_normal() {
1837 let mut e = Editor::new(KeybindingMode::Vim);
1838 e.handle_key(key(KeyCode::Char('v')));
1839 e.handle_key(key(KeyCode::Esc));
1840 assert_eq!(e.vim_mode(), VimMode::Normal);
1841 }
1842
1843 #[test]
1844 fn vim_shift_i_moves_to_first_non_whitespace() {
1845 let mut e = Editor::new(KeybindingMode::Vim);
1846 e.set_content(" hello");
1847 e.jump_cursor(0, 8);
1848 e.handle_key(shift_key(KeyCode::Char('I')));
1849 assert_eq!(e.vim_mode(), VimMode::Insert);
1850 assert_eq!(e.cursor(), (0, 3));
1851 }
1852
1853 #[test]
1854 fn vim_shift_a_moves_to_end_and_insert() {
1855 let mut e = Editor::new(KeybindingMode::Vim);
1856 e.set_content("hello");
1857 e.handle_key(shift_key(KeyCode::Char('A')));
1858 assert_eq!(e.vim_mode(), VimMode::Insert);
1859 assert_eq!(e.cursor().1, 5);
1860 }
1861
1862 #[test]
1863 fn count_10j_moves_down_10() {
1864 let mut e = Editor::new(KeybindingMode::Vim);
1865 e.set_content(
1866 (0..20)
1867 .map(|i| format!("line{i}"))
1868 .collect::<Vec<_>>()
1869 .join("\n")
1870 .as_str(),
1871 );
1872 for d in "10".chars() {
1873 e.handle_key(key(KeyCode::Char(d)));
1874 }
1875 e.handle_key(key(KeyCode::Char('j')));
1876 assert_eq!(e.cursor().0, 10);
1877 }
1878
1879 #[test]
1880 fn count_o_repeats_insert_on_esc() {
1881 let mut e = Editor::new(KeybindingMode::Vim);
1882 e.set_content("hello");
1883 for d in "3".chars() {
1884 e.handle_key(key(KeyCode::Char(d)));
1885 }
1886 e.handle_key(key(KeyCode::Char('o')));
1887 assert_eq!(e.vim_mode(), VimMode::Insert);
1888 for c in "world".chars() {
1889 e.handle_key(key(KeyCode::Char(c)));
1890 }
1891 e.handle_key(key(KeyCode::Esc));
1892 assert_eq!(e.vim_mode(), VimMode::Normal);
1893 assert_eq!(e.buffer().lines().len(), 4);
1894 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1895 }
1896
1897 #[test]
1898 fn count_i_repeats_text_on_esc() {
1899 let mut e = Editor::new(KeybindingMode::Vim);
1900 e.set_content("");
1901 for d in "3".chars() {
1902 e.handle_key(key(KeyCode::Char(d)));
1903 }
1904 e.handle_key(key(KeyCode::Char('i')));
1905 for c in "ab".chars() {
1906 e.handle_key(key(KeyCode::Char(c)));
1907 }
1908 e.handle_key(key(KeyCode::Esc));
1909 assert_eq!(e.vim_mode(), VimMode::Normal);
1910 assert_eq!(e.buffer().lines()[0], "ababab");
1911 }
1912
1913 #[test]
1914 fn vim_shift_o_opens_line_above() {
1915 let mut e = Editor::new(KeybindingMode::Vim);
1916 e.set_content("hello");
1917 e.handle_key(shift_key(KeyCode::Char('O')));
1918 assert_eq!(e.vim_mode(), VimMode::Insert);
1919 assert_eq!(e.cursor(), (0, 0));
1920 assert_eq!(e.buffer().lines().len(), 2);
1921 }
1922
1923 #[test]
1924 fn vim_gg_goes_to_top() {
1925 let mut e = Editor::new(KeybindingMode::Vim);
1926 e.set_content("a\nb\nc");
1927 e.jump_cursor(2, 0);
1928 e.handle_key(key(KeyCode::Char('g')));
1929 e.handle_key(key(KeyCode::Char('g')));
1930 assert_eq!(e.cursor().0, 0);
1931 }
1932
1933 #[test]
1934 fn vim_shift_g_goes_to_bottom() {
1935 let mut e = Editor::new(KeybindingMode::Vim);
1936 e.set_content("a\nb\nc");
1937 e.handle_key(shift_key(KeyCode::Char('G')));
1938 assert_eq!(e.cursor().0, 2);
1939 }
1940
1941 #[test]
1942 fn vim_dd_deletes_line() {
1943 let mut e = Editor::new(KeybindingMode::Vim);
1944 e.set_content("first\nsecond");
1945 e.handle_key(key(KeyCode::Char('d')));
1946 e.handle_key(key(KeyCode::Char('d')));
1947 assert_eq!(e.buffer().lines().len(), 1);
1948 assert_eq!(e.buffer().lines()[0], "second");
1949 }
1950
1951 #[test]
1952 fn vim_dw_deletes_word() {
1953 let mut e = Editor::new(KeybindingMode::Vim);
1954 e.set_content("hello world");
1955 e.handle_key(key(KeyCode::Char('d')));
1956 e.handle_key(key(KeyCode::Char('w')));
1957 assert_eq!(e.vim_mode(), VimMode::Normal);
1958 assert!(!e.buffer().lines()[0].starts_with("hello"));
1959 }
1960
1961 #[test]
1962 fn vim_yy_yanks_line() {
1963 let mut e = Editor::new(KeybindingMode::Vim);
1964 e.set_content("hello\nworld");
1965 e.handle_key(key(KeyCode::Char('y')));
1966 e.handle_key(key(KeyCode::Char('y')));
1967 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1968 }
1969
1970 #[test]
1971 fn vim_yy_does_not_move_cursor() {
1972 let mut e = Editor::new(KeybindingMode::Vim);
1973 e.set_content("first\nsecond\nthird");
1974 e.jump_cursor(1, 0);
1975 let before = e.cursor();
1976 e.handle_key(key(KeyCode::Char('y')));
1977 e.handle_key(key(KeyCode::Char('y')));
1978 assert_eq!(e.cursor(), before);
1979 assert_eq!(e.vim_mode(), VimMode::Normal);
1980 }
1981
1982 #[test]
1983 fn vim_yw_yanks_word() {
1984 let mut e = Editor::new(KeybindingMode::Vim);
1985 e.set_content("hello world");
1986 e.handle_key(key(KeyCode::Char('y')));
1987 e.handle_key(key(KeyCode::Char('w')));
1988 assert_eq!(e.vim_mode(), VimMode::Normal);
1989 assert!(e.last_yank.is_some());
1990 }
1991
1992 #[test]
1993 fn vim_cc_changes_line() {
1994 let mut e = Editor::new(KeybindingMode::Vim);
1995 e.set_content("hello\nworld");
1996 e.handle_key(key(KeyCode::Char('c')));
1997 e.handle_key(key(KeyCode::Char('c')));
1998 assert_eq!(e.vim_mode(), VimMode::Insert);
1999 }
2000
2001 #[test]
2002 fn vim_u_undoes_insert_session_as_chunk() {
2003 let mut e = Editor::new(KeybindingMode::Vim);
2004 e.set_content("hello");
2005 e.handle_key(key(KeyCode::Char('i')));
2006 e.handle_key(key(KeyCode::Enter));
2007 e.handle_key(key(KeyCode::Enter));
2008 e.handle_key(key(KeyCode::Esc));
2009 assert_eq!(e.buffer().lines().len(), 3);
2010 e.handle_key(key(KeyCode::Char('u')));
2011 assert_eq!(e.buffer().lines().len(), 1);
2012 assert_eq!(e.buffer().lines()[0], "hello");
2013 }
2014
2015 #[test]
2016 fn vim_undo_redo_roundtrip() {
2017 let mut e = Editor::new(KeybindingMode::Vim);
2018 e.set_content("hello");
2019 e.handle_key(key(KeyCode::Char('i')));
2020 for c in "world".chars() {
2021 e.handle_key(key(KeyCode::Char(c)));
2022 }
2023 e.handle_key(key(KeyCode::Esc));
2024 let after = e.buffer().lines()[0].clone();
2025 e.handle_key(key(KeyCode::Char('u')));
2026 assert_eq!(e.buffer().lines()[0], "hello");
2027 e.handle_key(ctrl_key(KeyCode::Char('r')));
2028 assert_eq!(e.buffer().lines()[0], after);
2029 }
2030
2031 #[test]
2032 fn vim_u_undoes_dd() {
2033 let mut e = Editor::new(KeybindingMode::Vim);
2034 e.set_content("first\nsecond");
2035 e.handle_key(key(KeyCode::Char('d')));
2036 e.handle_key(key(KeyCode::Char('d')));
2037 assert_eq!(e.buffer().lines().len(), 1);
2038 e.handle_key(key(KeyCode::Char('u')));
2039 assert_eq!(e.buffer().lines().len(), 2);
2040 assert_eq!(e.buffer().lines()[0], "first");
2041 }
2042
2043 #[test]
2044 fn vim_ctrl_r_redoes() {
2045 let mut e = Editor::new(KeybindingMode::Vim);
2046 e.set_content("hello");
2047 e.handle_key(ctrl_key(KeyCode::Char('r')));
2048 }
2049
2050 #[test]
2051 fn vim_r_replaces_char() {
2052 let mut e = Editor::new(KeybindingMode::Vim);
2053 e.set_content("hello");
2054 e.handle_key(key(KeyCode::Char('r')));
2055 e.handle_key(key(KeyCode::Char('x')));
2056 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
2057 }
2058
2059 #[test]
2060 fn vim_tilde_toggles_case() {
2061 let mut e = Editor::new(KeybindingMode::Vim);
2062 e.set_content("hello");
2063 e.handle_key(key(KeyCode::Char('~')));
2064 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
2065 }
2066
2067 #[test]
2068 fn vim_visual_d_cuts() {
2069 let mut e = Editor::new(KeybindingMode::Vim);
2070 e.set_content("hello");
2071 e.handle_key(key(KeyCode::Char('v')));
2072 e.handle_key(key(KeyCode::Char('l')));
2073 e.handle_key(key(KeyCode::Char('l')));
2074 e.handle_key(key(KeyCode::Char('d')));
2075 assert_eq!(e.vim_mode(), VimMode::Normal);
2076 assert!(e.last_yank.is_some());
2077 }
2078
2079 #[test]
2080 fn vim_visual_c_enters_insert() {
2081 let mut e = Editor::new(KeybindingMode::Vim);
2082 e.set_content("hello");
2083 e.handle_key(key(KeyCode::Char('v')));
2084 e.handle_key(key(KeyCode::Char('l')));
2085 e.handle_key(key(KeyCode::Char('c')));
2086 assert_eq!(e.vim_mode(), VimMode::Insert);
2087 }
2088
2089 #[test]
2090 fn vim_normal_unknown_key_consumed() {
2091 let mut e = Editor::new(KeybindingMode::Vim);
2092 let consumed = e.handle_key(key(KeyCode::Char('z')));
2094 assert!(consumed);
2095 }
2096
2097 #[test]
2098 fn force_normal_clears_operator() {
2099 let mut e = Editor::new(KeybindingMode::Vim);
2100 e.handle_key(key(KeyCode::Char('d')));
2101 e.force_normal();
2102 assert_eq!(e.vim_mode(), VimMode::Normal);
2103 }
2104
2105 fn many_lines(n: usize) -> String {
2106 (0..n)
2107 .map(|i| format!("line{i}"))
2108 .collect::<Vec<_>>()
2109 .join("\n")
2110 }
2111
2112 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
2113 e.set_viewport_height(height);
2114 }
2115
2116 #[test]
2117 fn zz_centers_cursor_in_viewport() {
2118 let mut e = Editor::new(KeybindingMode::Vim);
2119 e.set_content(&many_lines(100));
2120 prime_viewport(&mut e, 20);
2121 e.jump_cursor(50, 0);
2122 e.handle_key(key(KeyCode::Char('z')));
2123 e.handle_key(key(KeyCode::Char('z')));
2124 assert_eq!(e.buffer().viewport().top_row, 40);
2125 assert_eq!(e.cursor().0, 50);
2126 }
2127
2128 #[test]
2129 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
2130 let mut e = Editor::new(KeybindingMode::Vim);
2131 e.set_content(&many_lines(100));
2132 prime_viewport(&mut e, 20);
2133 e.jump_cursor(50, 0);
2134 e.handle_key(key(KeyCode::Char('z')));
2135 e.handle_key(key(KeyCode::Char('t')));
2136 assert_eq!(e.buffer().viewport().top_row, 45);
2139 assert_eq!(e.cursor().0, 50);
2140 }
2141
2142 #[test]
2143 fn ctrl_a_increments_number_at_cursor() {
2144 let mut e = Editor::new(KeybindingMode::Vim);
2145 e.set_content("x = 41");
2146 e.handle_key(ctrl_key(KeyCode::Char('a')));
2147 assert_eq!(e.buffer().lines()[0], "x = 42");
2148 assert_eq!(e.cursor(), (0, 5));
2149 }
2150
2151 #[test]
2152 fn ctrl_a_finds_number_to_right_of_cursor() {
2153 let mut e = Editor::new(KeybindingMode::Vim);
2154 e.set_content("foo 99 bar");
2155 e.handle_key(ctrl_key(KeyCode::Char('a')));
2156 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
2157 assert_eq!(e.cursor(), (0, 6));
2158 }
2159
2160 #[test]
2161 fn ctrl_a_with_count_adds_count() {
2162 let mut e = Editor::new(KeybindingMode::Vim);
2163 e.set_content("x = 10");
2164 for d in "5".chars() {
2165 e.handle_key(key(KeyCode::Char(d)));
2166 }
2167 e.handle_key(ctrl_key(KeyCode::Char('a')));
2168 assert_eq!(e.buffer().lines()[0], "x = 15");
2169 }
2170
2171 #[test]
2172 fn ctrl_x_decrements_number() {
2173 let mut e = Editor::new(KeybindingMode::Vim);
2174 e.set_content("n=5");
2175 e.handle_key(ctrl_key(KeyCode::Char('x')));
2176 assert_eq!(e.buffer().lines()[0], "n=4");
2177 }
2178
2179 #[test]
2180 fn ctrl_x_crosses_zero_into_negative() {
2181 let mut e = Editor::new(KeybindingMode::Vim);
2182 e.set_content("v=0");
2183 e.handle_key(ctrl_key(KeyCode::Char('x')));
2184 assert_eq!(e.buffer().lines()[0], "v=-1");
2185 }
2186
2187 #[test]
2188 fn ctrl_a_on_negative_number_increments_toward_zero() {
2189 let mut e = Editor::new(KeybindingMode::Vim);
2190 e.set_content("a = -5");
2191 e.handle_key(ctrl_key(KeyCode::Char('a')));
2192 assert_eq!(e.buffer().lines()[0], "a = -4");
2193 }
2194
2195 #[test]
2196 fn ctrl_a_noop_when_no_digit_on_line() {
2197 let mut e = Editor::new(KeybindingMode::Vim);
2198 e.set_content("no digits here");
2199 e.handle_key(ctrl_key(KeyCode::Char('a')));
2200 assert_eq!(e.buffer().lines()[0], "no digits here");
2201 }
2202
2203 #[test]
2204 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
2205 let mut e = Editor::new(KeybindingMode::Vim);
2206 e.set_content(&many_lines(100));
2207 prime_viewport(&mut e, 20);
2208 e.jump_cursor(50, 0);
2209 e.handle_key(key(KeyCode::Char('z')));
2210 e.handle_key(key(KeyCode::Char('b')));
2211 assert_eq!(e.buffer().viewport().top_row, 36);
2215 assert_eq!(e.cursor().0, 50);
2216 }
2217
2218 #[test]
2225 fn set_content_dirties_then_take_dirty_clears() {
2226 let mut e = Editor::new(KeybindingMode::Vim);
2227 e.set_content("hello");
2228 assert!(
2229 e.take_dirty(),
2230 "set_content should leave content_dirty=true"
2231 );
2232 assert!(!e.take_dirty(), "take_dirty should clear the flag");
2233 }
2234
2235 #[test]
2236 fn content_arc_returns_same_arc_until_mutation() {
2237 let mut e = Editor::new(KeybindingMode::Vim);
2238 e.set_content("hello");
2239 let a = e.content_arc();
2240 let b = e.content_arc();
2241 assert!(
2242 std::sync::Arc::ptr_eq(&a, &b),
2243 "repeated content_arc() should hit the cache"
2244 );
2245
2246 e.handle_key(key(KeyCode::Char('i')));
2248 e.handle_key(key(KeyCode::Char('!')));
2249 let c = e.content_arc();
2250 assert!(
2251 !std::sync::Arc::ptr_eq(&a, &c),
2252 "mutation should invalidate content_arc() cache"
2253 );
2254 assert!(c.contains('!'));
2255 }
2256
2257 #[test]
2258 fn content_arc_cache_invalidated_by_set_content() {
2259 let mut e = Editor::new(KeybindingMode::Vim);
2260 e.set_content("one");
2261 let a = e.content_arc();
2262 e.set_content("two");
2263 let b = e.content_arc();
2264 assert!(!std::sync::Arc::ptr_eq(&a, &b));
2265 assert!(b.starts_with("two"));
2266 }
2267
2268 #[test]
2274 fn mouse_click_past_eol_lands_on_last_char() {
2275 let mut e = Editor::new(KeybindingMode::Vim);
2276 e.set_content("hello");
2277 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2281 e.mouse_click(area, 78, 1);
2282 assert_eq!(e.cursor(), (0, 4));
2283 }
2284
2285 #[test]
2286 fn mouse_click_past_eol_handles_multibyte_line() {
2287 let mut e = Editor::new(KeybindingMode::Vim);
2288 e.set_content("héllo");
2291 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2292 e.mouse_click(area, 78, 1);
2293 assert_eq!(e.cursor(), (0, 4));
2294 }
2295
2296 #[test]
2297 fn mouse_click_inside_line_lands_on_clicked_char() {
2298 let mut e = Editor::new(KeybindingMode::Vim);
2299 e.set_content("hello world");
2300 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2303 e.mouse_click(area, 4, 1);
2304 assert_eq!(e.cursor(), (0, 0));
2305 e.mouse_click(area, 6, 1);
2306 assert_eq!(e.cursor(), (0, 2));
2307 }
2308}