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 pub(crate) vim: VimState,
201 pub(crate) undo_stack: Vec<(Vec<String>, (usize, usize))>,
205 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
207 pub(super) content_dirty: bool,
209 pub(super) cached_content: Option<std::sync::Arc<String>>,
214 pub(super) viewport_height: AtomicU16,
219 pub(super) pending_lsp: Option<LspIntent>,
223 pub(super) buffer: hjkl_buffer::Buffer,
228 pub(super) style_table: Vec<ratatui::style::Style>,
235 pub(crate) registers: crate::registers::Registers,
240 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
246 pub(crate) settings: Settings,
251 pub(crate) file_marks: std::collections::HashMap<char, (usize, usize)>,
259 pub(crate) syntax_fold_ranges: Vec<(usize, usize)>,
265 pub(crate) change_log: Vec<crate::types::Edit>,
274}
275
276#[derive(Debug, Clone)]
279pub struct Settings {
280 pub shiftwidth: usize,
282 pub tabstop: usize,
285 pub ignore_case: bool,
288 pub textwidth: usize,
290 pub wrap: hjkl_buffer::Wrap,
296}
297
298impl Default for Settings {
299 fn default() -> Self {
300 Self {
301 shiftwidth: 2,
302 tabstop: 8,
303 ignore_case: false,
304 textwidth: 79,
305 wrap: hjkl_buffer::Wrap::None,
306 }
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum LspIntent {
315 GotoDefinition,
317}
318
319impl<'a> Editor<'a> {
320 pub fn new(keybinding_mode: KeybindingMode) -> Self {
321 Self {
322 _marker: std::marker::PhantomData,
323 keybinding_mode,
324 last_yank: None,
325 vim: VimState::default(),
326 undo_stack: Vec::new(),
327 redo_stack: Vec::new(),
328 content_dirty: false,
329 cached_content: None,
330 viewport_height: AtomicU16::new(0),
331 pending_lsp: None,
332 buffer: hjkl_buffer::Buffer::new(),
333 style_table: Vec::new(),
334 registers: crate::registers::Registers::default(),
335 styled_spans: Vec::new(),
336 settings: Settings::default(),
337 file_marks: std::collections::HashMap::new(),
338 syntax_fold_ranges: Vec::new(),
339 change_log: Vec::new(),
340 }
341 }
342
343 pub fn buffer_mark(&self, c: char) -> Option<(usize, usize)> {
350 self.vim.marks.get(&c).copied()
351 }
352
353 pub fn pop_last_undo(&mut self) -> bool {
360 self.undo_stack.pop().is_some()
361 }
362
363 pub fn buffer_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
365 self.vim.marks.iter().map(|(c, p)| (*c, *p))
366 }
367
368 pub fn last_jump_back(&self) -> Option<(usize, usize)> {
371 self.vim.jump_back.last().copied()
372 }
373
374 pub fn last_edit_pos(&self) -> Option<(usize, usize)> {
377 self.vim.last_edit_pos
378 }
379
380 pub fn file_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
387 self.file_marks.iter().map(|(c, p)| (*c, *p))
388 }
389
390 pub fn syntax_fold_ranges(&self) -> &[(usize, usize)] {
395 &self.syntax_fold_ranges
396 }
397
398 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
399 self.syntax_fold_ranges = ranges;
400 }
401
402 pub fn settings(&self) -> &Settings {
405 &self.settings
406 }
407
408 pub fn settings_mut(&mut self) -> &mut Settings {
413 &mut self.settings
414 }
415
416 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>) {
423 let line_byte_lens: Vec<usize> = self.buffer.lines().iter().map(|l| l.len()).collect();
424 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
425 for (row, row_spans) in spans.iter().enumerate() {
426 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
427 let mut translated = Vec::with_capacity(row_spans.len());
428 for (start, end, style) in row_spans {
429 let end_clamped = (*end).min(line_len);
430 if end_clamped <= *start {
431 continue;
432 }
433 let id = self.intern_style(*style);
434 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
435 }
436 by_row.push(translated);
437 }
438 self.buffer.set_spans(by_row);
439 self.styled_spans = spans;
440 }
441
442 pub fn yank(&self) -> &str {
444 &self.registers.unnamed.text
445 }
446
447 pub fn registers(&self) -> &crate::registers::Registers {
449 &self.registers
450 }
451
452 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
457 self.registers.set_clipboard(text, linewise);
458 }
459
460 pub fn pending_register_is_clipboard(&self) -> bool {
464 matches!(self.vim.pending_register, Some('+') | Some('*'))
465 }
466
467 pub fn set_yank(&mut self, text: impl Into<String>) {
471 let text = text.into();
472 let linewise = self.vim.yank_linewise;
473 self.registers.unnamed = crate::registers::Slot { text, linewise };
474 }
475
476 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
480 self.vim.yank_linewise = linewise;
481 let target = self.vim.pending_register.take();
482 self.registers.record_yank(text, linewise, target);
483 }
484
485 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
490 if let Some(slot) = match reg {
491 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
492 'A'..='Z' => {
493 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
494 }
495 _ => None,
496 } {
497 slot.text = text;
498 slot.linewise = false;
499 }
500 }
501
502 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
505 self.vim.yank_linewise = linewise;
506 let target = self.vim.pending_register.take();
507 self.registers.record_delete(text, linewise, target);
508 }
509
510 pub fn intern_style(&mut self, style: ratatui::style::Style) -> u32 {
516 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
517 return idx as u32;
518 }
519 self.style_table.push(style);
520 (self.style_table.len() - 1) as u32
521 }
522
523 pub fn style_table(&self) -> &[ratatui::style::Style] {
527 &self.style_table
528 }
529
530 pub fn intern_engine_style(&mut self, style: crate::types::Style) -> u32 {
540 let r = engine_style_to_ratatui(style);
541 self.intern_style(r)
542 }
543
544 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
548 let r = self.style_table.get(id as usize).copied()?;
549 Some(ratatui_style_to_engine(r))
550 }
551
552 pub fn buffer(&self) -> &hjkl_buffer::Buffer {
555 &self.buffer
556 }
557
558 pub fn buffer_mut(&mut self) -> &mut hjkl_buffer::Buffer {
559 &mut self.buffer
560 }
561
562 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
566
567 pub fn set_viewport_top(&mut self, row: usize) {
575 let last = self.buffer.row_count().saturating_sub(1);
576 let target = row.min(last);
577 self.buffer.viewport_mut().top_row = target;
578 }
579
580 pub fn jump_cursor(&mut self, row: usize, col: usize) {
584 self.buffer.set_cursor(hjkl_buffer::Position::new(row, col));
585 }
586
587 pub fn cursor(&self) -> (usize, usize) {
595 let pos = self.buffer.cursor();
596 (pos.row, pos.col)
597 }
598
599 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
602 self.pending_lsp.take()
603 }
604
605 pub(crate) fn sync_buffer_from_textarea(&mut self) {
609 self.buffer.set_sticky_col(self.vim.sticky_col);
610 let height = self.viewport_height_value();
611 self.buffer.viewport_mut().height = height;
612 }
613
614 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
618 self.sync_buffer_from_textarea();
619 }
620
621 pub fn record_jump(&mut self, pos: (usize, usize)) {
626 const JUMPLIST_MAX: usize = 100;
627 self.vim.jump_back.push(pos);
628 if self.vim.jump_back.len() > JUMPLIST_MAX {
629 self.vim.jump_back.remove(0);
630 }
631 self.vim.jump_fwd.clear();
632 }
633
634 pub fn set_viewport_height(&self, height: u16) {
637 self.viewport_height.store(height, Ordering::Relaxed);
638 }
639
640 pub fn viewport_height_value(&self) -> u16 {
642 self.viewport_height.load(Ordering::Relaxed)
643 }
644
645 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
654 let pre_row = self.buffer.cursor().row;
655 let pre_rows = self.buffer.row_count();
656 if let Some(op) = edit_to_editop(&edit) {
660 self.change_log.push(op);
661 }
662 let inverse = self.buffer.apply_edit(edit);
663 let pos = self.buffer.cursor();
664 let lo = pre_row.min(pos.row);
670 let hi = pre_row.max(pos.row);
671 self.buffer.invalidate_folds_in_range(lo, hi);
672 self.vim.last_edit_pos = Some((pos.row, pos.col));
673 let entry = (pos.row, pos.col);
678 if self.vim.change_list.last() != Some(&entry) {
679 if let Some(idx) = self.vim.change_list_cursor.take() {
680 self.vim.change_list.truncate(idx + 1);
681 }
682 self.vim.change_list.push(entry);
683 let len = self.vim.change_list.len();
684 if len > crate::vim::CHANGE_LIST_MAX {
685 self.vim
686 .change_list
687 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
688 }
689 }
690 self.vim.change_list_cursor = None;
691 let post_rows = self.buffer.row_count();
695 let delta = post_rows as isize - pre_rows as isize;
696 if delta != 0 {
697 self.shift_marks_after_edit(pre_row, delta);
698 }
699 self.push_buffer_content_to_textarea();
700 self.mark_content_dirty();
701 inverse
702 }
703
704 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
709 if delta == 0 {
710 return;
711 }
712 let drop_end = if delta < 0 {
715 edit_start.saturating_add((-delta) as usize)
716 } else {
717 edit_start
718 };
719 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
720
721 let mut to_drop: Vec<char> = Vec::new();
722 for (c, (row, _col)) in self.vim.marks.iter_mut() {
723 if (edit_start..drop_end).contains(row) {
724 to_drop.push(*c);
725 } else if *row >= shift_threshold {
726 *row = ((*row as isize) + delta).max(0) as usize;
727 }
728 }
729 for c in to_drop {
730 self.vim.marks.remove(&c);
731 }
732
733 let mut to_drop: Vec<char> = Vec::new();
735 for (c, (row, _col)) in self.file_marks.iter_mut() {
736 if (edit_start..drop_end).contains(row) {
737 to_drop.push(*c);
738 } else if *row >= shift_threshold {
739 *row = ((*row as isize) + delta).max(0) as usize;
740 }
741 }
742 for c in to_drop {
743 self.file_marks.remove(&c);
744 }
745
746 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
747 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
748 for (row, _) in entries.iter_mut() {
749 if *row >= shift_threshold {
750 *row = ((*row as isize) + delta).max(0) as usize;
751 }
752 }
753 };
754 shift_jumps(&mut self.vim.jump_back);
755 shift_jumps(&mut self.vim.jump_fwd);
756 }
757
758 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
766
767 pub fn mark_content_dirty(&mut self) {
773 self.content_dirty = true;
774 self.cached_content = None;
775 }
776
777 pub fn take_dirty(&mut self) -> bool {
779 let dirty = self.content_dirty;
780 self.content_dirty = false;
781 dirty
782 }
783
784 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
794 if !self.content_dirty {
795 return None;
796 }
797 let arc = self.content_arc();
798 self.content_dirty = false;
799 Some(arc)
800 }
801
802 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
805 let cursor = self.buffer.cursor().row;
806 let top = self.buffer.viewport().top_row;
807 cursor.saturating_sub(top).min(height as usize - 1) as u16
808 }
809
810 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
814 let pos = self.buffer.cursor();
815 let v = self.buffer.viewport();
816 if pos.row < v.top_row || pos.col < v.top_col {
817 return None;
818 }
819 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
820 let dy = (pos.row - v.top_row) as u16;
821 let dx = (pos.col - v.top_col) as u16;
822 if dy >= area.height || dx + lnum_width >= area.width {
823 return None;
824 }
825 Some((area.x + lnum_width + dx, area.y + dy))
826 }
827
828 pub fn vim_mode(&self) -> VimMode {
829 self.vim.public_mode()
830 }
831
832 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
838 self.vim.search_prompt.as_ref()
839 }
840
841 pub fn last_search(&self) -> Option<&str> {
844 self.vim.last_search.as_deref()
845 }
846
847 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
851 if self.vim_mode() != VimMode::Visual {
852 return None;
853 }
854 let anchor = self.vim.visual_anchor;
855 let cursor = self.cursor();
856 let (start, end) = if anchor <= cursor {
857 (anchor, cursor)
858 } else {
859 (cursor, anchor)
860 };
861 Some((start, end))
862 }
863
864 pub fn line_highlight(&self) -> Option<(usize, usize)> {
867 if self.vim_mode() != VimMode::VisualLine {
868 return None;
869 }
870 let anchor = self.vim.visual_line_anchor;
871 let cursor = self.buffer.cursor().row;
872 Some((anchor.min(cursor), anchor.max(cursor)))
873 }
874
875 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
876 if self.vim_mode() != VimMode::VisualBlock {
877 return None;
878 }
879 let (ar, ac) = self.vim.block_anchor;
880 let cr = self.buffer.cursor().row;
881 let cc = self.vim.block_vcol;
882 let top = ar.min(cr);
883 let bot = ar.max(cr);
884 let left = ac.min(cc);
885 let right = ac.max(cc);
886 Some((top, bot, left, right))
887 }
888
889 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
895 use hjkl_buffer::{Position, Selection};
896 match self.vim_mode() {
897 VimMode::Visual => {
898 let (ar, ac) = self.vim.visual_anchor;
899 let head = self.buffer.cursor();
900 Some(Selection::Char {
901 anchor: Position::new(ar, ac),
902 head,
903 })
904 }
905 VimMode::VisualLine => {
906 let anchor_row = self.vim.visual_line_anchor;
907 let head_row = self.buffer.cursor().row;
908 Some(Selection::Line {
909 anchor_row,
910 head_row,
911 })
912 }
913 VimMode::VisualBlock => {
914 let (ar, ac) = self.vim.block_anchor;
915 let cr = self.buffer.cursor().row;
916 let cc = self.vim.block_vcol;
917 Some(Selection::Block {
918 anchor: Position::new(ar, ac),
919 head: Position::new(cr, cc),
920 })
921 }
922 _ => None,
923 }
924 }
925
926 pub fn force_normal(&mut self) {
928 self.vim.force_normal();
929 }
930
931 pub fn content(&self) -> String {
932 let mut s = self.buffer.lines().join("\n");
933 s.push('\n');
934 s
935 }
936
937 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
942 if let Some(arc) = &self.cached_content {
943 return std::sync::Arc::clone(arc);
944 }
945 let arc = std::sync::Arc::new(self.content());
946 self.cached_content = Some(std::sync::Arc::clone(&arc));
947 arc
948 }
949
950 pub fn set_content(&mut self, text: &str) {
951 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
952 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
953 lines.pop();
954 }
955 if lines.is_empty() {
956 lines.push(String::new());
957 }
958 let _ = lines;
959 self.buffer = hjkl_buffer::Buffer::from_str(text);
960 self.undo_stack.clear();
961 self.redo_stack.clear();
962 self.mark_content_dirty();
963 }
964
965 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
980 use crate::{Modifiers, PlannedInput, SpecialKey};
981 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
982 let to_mods = |m: Modifiers| {
983 let mut k = KeyModifiers::NONE;
984 if m.ctrl {
985 k |= KeyModifiers::CONTROL;
986 }
987 if m.shift {
988 k |= KeyModifiers::SHIFT;
989 }
990 if m.alt {
991 k |= KeyModifiers::ALT;
992 }
993 if m.super_ {
994 k |= KeyModifiers::SUPER;
995 }
996 k
997 };
998 let (code, mods) = match input {
999 PlannedInput::Char(c, m) => (KeyCode::Char(c), to_mods(m)),
1000 PlannedInput::Key(k, m) => {
1001 let code = match k {
1002 SpecialKey::Esc => KeyCode::Esc,
1003 SpecialKey::Enter => KeyCode::Enter,
1004 SpecialKey::Backspace => KeyCode::Backspace,
1005 SpecialKey::Tab => KeyCode::Tab,
1006 SpecialKey::BackTab => KeyCode::BackTab,
1007 SpecialKey::Up => KeyCode::Up,
1008 SpecialKey::Down => KeyCode::Down,
1009 SpecialKey::Left => KeyCode::Left,
1010 SpecialKey::Right => KeyCode::Right,
1011 SpecialKey::Home => KeyCode::Home,
1012 SpecialKey::End => KeyCode::End,
1013 SpecialKey::PageUp => KeyCode::PageUp,
1014 SpecialKey::PageDown => KeyCode::PageDown,
1015 SpecialKey::Insert => KeyCode::Insert,
1016 SpecialKey::Delete => KeyCode::Delete,
1017 SpecialKey::F(n) => KeyCode::F(n),
1018 };
1019 (code, to_mods(m))
1020 }
1021 PlannedInput::Mouse(_)
1023 | PlannedInput::Paste(_)
1024 | PlannedInput::FocusGained
1025 | PlannedInput::FocusLost
1026 | PlannedInput::Resize(_, _) => return false,
1027 };
1028 self.handle_key(KeyEvent::new(code, mods))
1029 }
1030
1031 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
1048 std::mem::take(&mut self.change_log)
1049 }
1050
1051 pub fn current_options(&self) -> crate::types::Options {
1061 let mut o = crate::types::Options::default();
1062 o.shiftwidth = self.settings.shiftwidth as u32;
1063 o.tabstop = self.settings.tabstop as u32;
1064 o.ignorecase = self.settings.ignore_case;
1065 o
1066 }
1067
1068 pub fn apply_options(&mut self, opts: &crate::types::Options) {
1073 self.settings.shiftwidth = opts.shiftwidth as usize;
1074 self.settings.tabstop = opts.tabstop as usize;
1075 self.settings.ignore_case = opts.ignorecase;
1076 }
1077
1078 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
1088 use crate::types::{Highlight, HighlightKind, Pos};
1089 let sel = self.buffer_selection()?;
1090 let (start, end) = match sel {
1091 hjkl_buffer::Selection::Char { anchor, head } => {
1092 let a = (anchor.row, anchor.col);
1093 let h = (head.row, head.col);
1094 if a <= h { (a, h) } else { (h, a) }
1095 }
1096 hjkl_buffer::Selection::Line {
1097 anchor_row,
1098 head_row,
1099 } => {
1100 let (top, bot) = if anchor_row <= head_row {
1101 (anchor_row, head_row)
1102 } else {
1103 (head_row, anchor_row)
1104 };
1105 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
1106 ((top, 0), (bot, last_col))
1107 }
1108 hjkl_buffer::Selection::Block { anchor, head } => {
1109 let (top, bot) = if anchor.row <= head.row {
1110 (anchor.row, head.row)
1111 } else {
1112 (head.row, anchor.row)
1113 };
1114 let (left, right) = if anchor.col <= head.col {
1115 (anchor.col, head.col)
1116 } else {
1117 (head.col, anchor.col)
1118 };
1119 ((top, left), (bot, right))
1120 }
1121 };
1122 Some(Highlight {
1123 range: Pos {
1124 line: start.0 as u32,
1125 col: start.1 as u32,
1126 }..Pos {
1127 line: end.0 as u32,
1128 col: end.1 as u32,
1129 },
1130 kind: HighlightKind::Selection,
1131 })
1132 }
1133
1134 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
1147 use crate::types::{Highlight, HighlightKind, Pos};
1148 let row = line as usize;
1149 if row >= self.buffer.lines().len() {
1150 return Vec::new();
1151 }
1152 if self.buffer.search_pattern().is_none() {
1153 return Vec::new();
1154 }
1155 self.buffer
1156 .search_matches(row)
1157 .into_iter()
1158 .map(|(start, end)| Highlight {
1159 range: Pos {
1160 line,
1161 col: start as u32,
1162 }..Pos {
1163 line,
1164 col: end as u32,
1165 },
1166 kind: HighlightKind::SearchMatch,
1167 })
1168 .collect()
1169 }
1170
1171 pub fn render_frame(&self) -> crate::types::RenderFrame {
1181 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
1182 let (cursor_row, cursor_col) = self.cursor();
1183 let (mode, shape) = match self.vim_mode() {
1184 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
1185 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
1186 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
1187 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
1188 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
1189 };
1190 RenderFrame {
1191 mode,
1192 cursor_row: cursor_row as u32,
1193 cursor_col: cursor_col as u32,
1194 cursor_shape: shape,
1195 viewport_top: self.buffer.viewport().top_row as u32,
1196 line_count: self.buffer.lines().len() as u32,
1197 }
1198 }
1199
1200 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
1213 use crate::types::{EditorSnapshot, SnapshotMode};
1214 let mode = match self.vim_mode() {
1215 crate::VimMode::Normal => SnapshotMode::Normal,
1216 crate::VimMode::Insert => SnapshotMode::Insert,
1217 crate::VimMode::Visual => SnapshotMode::Visual,
1218 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
1219 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
1220 };
1221 let cursor = self.cursor();
1222 let cursor = (cursor.0 as u32, cursor.1 as u32);
1223 let lines: Vec<String> = self.buffer.lines().to_vec();
1224 let viewport_top = self.buffer.viewport().top_row as u32;
1225 let file_marks = self
1226 .file_marks
1227 .iter()
1228 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
1229 .collect();
1230 EditorSnapshot {
1231 version: EditorSnapshot::VERSION,
1232 mode,
1233 cursor,
1234 lines,
1235 viewport_top,
1236 registers: self.registers.clone(),
1237 file_marks,
1238 }
1239 }
1240
1241 pub fn restore_snapshot(
1249 &mut self,
1250 snap: crate::types::EditorSnapshot,
1251 ) -> Result<(), crate::EngineError> {
1252 use crate::types::EditorSnapshot;
1253 if snap.version != EditorSnapshot::VERSION {
1254 return Err(crate::EngineError::SnapshotVersion(
1255 snap.version,
1256 EditorSnapshot::VERSION,
1257 ));
1258 }
1259 let text = snap.lines.join("\n");
1260 self.set_content(&text);
1261 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1262 let mut vp = self.buffer.viewport();
1263 vp.top_row = snap.viewport_top as usize;
1264 *self.buffer.viewport_mut() = vp;
1265 self.registers = snap.registers;
1266 self.file_marks = snap
1267 .file_marks
1268 .into_iter()
1269 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1270 .collect();
1271 Ok(())
1272 }
1273
1274 pub fn seed_yank(&mut self, text: String) {
1278 let linewise = text.ends_with('\n');
1279 self.vim.yank_linewise = linewise;
1280 self.registers.unnamed = crate::registers::Slot { text, linewise };
1281 }
1282
1283 pub fn scroll_down(&mut self, rows: i16) {
1288 self.scroll_viewport(rows);
1289 }
1290
1291 pub fn scroll_up(&mut self, rows: i16) {
1295 self.scroll_viewport(-rows);
1296 }
1297
1298 const SCROLLOFF: usize = 5;
1302
1303 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1308 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1309 if height == 0 {
1310 self.buffer.ensure_cursor_visible();
1311 return;
1312 }
1313 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1317 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
1320 self.ensure_scrolloff_wrap(height, margin);
1321 return;
1322 }
1323 let cursor_row = self.buffer.cursor().row;
1324 let last_row = self.buffer.row_count().saturating_sub(1);
1325 let v = self.buffer.viewport_mut();
1326 if cursor_row < v.top_row + margin {
1328 v.top_row = cursor_row.saturating_sub(margin);
1329 }
1330 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1332 if cursor_row > v.top_row + max_bottom {
1333 v.top_row = cursor_row.saturating_sub(max_bottom);
1334 }
1335 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1337 if v.top_row > max_top {
1338 v.top_row = max_top;
1339 }
1340 let cursor = self.buffer.cursor();
1343 self.buffer.viewport_mut().ensure_visible(cursor);
1344 }
1345
1346 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1351 let cursor_row = self.buffer.cursor().row;
1352 if cursor_row < self.buffer.viewport().top_row {
1355 self.buffer.viewport_mut().top_row = cursor_row;
1356 self.buffer.viewport_mut().top_col = 0;
1357 }
1358 let max_csr = height.saturating_sub(1).saturating_sub(margin);
1361 loop {
1362 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1363 if csr <= max_csr {
1364 break;
1365 }
1366 let top = self.buffer.viewport().top_row;
1367 let Some(next) = self.buffer.next_visible_row(top) else {
1368 break;
1369 };
1370 if next > cursor_row {
1372 self.buffer.viewport_mut().top_row = cursor_row;
1373 break;
1374 }
1375 self.buffer.viewport_mut().top_row = next;
1376 }
1377 loop {
1380 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1381 if csr >= margin {
1382 break;
1383 }
1384 let top = self.buffer.viewport().top_row;
1385 let Some(prev) = self.buffer.prev_visible_row(top) else {
1386 break;
1387 };
1388 self.buffer.viewport_mut().top_row = prev;
1389 }
1390 let max_top = self.buffer.max_top_for_height(height);
1395 if self.buffer.viewport().top_row > max_top {
1396 self.buffer.viewport_mut().top_row = max_top;
1397 }
1398 self.buffer.viewport_mut().top_col = 0;
1399 }
1400
1401 fn scroll_viewport(&mut self, delta: i16) {
1402 if delta == 0 {
1403 return;
1404 }
1405 let total_rows = self.buffer.row_count() as isize;
1407 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1408 let cur_top = self.buffer.viewport().top_row as isize;
1409 let new_top = (cur_top + delta as isize)
1410 .max(0)
1411 .min((total_rows - 1).max(0)) as usize;
1412 self.buffer.viewport_mut().top_row = new_top;
1413 let _ = cur_top;
1416 if height == 0 {
1417 return;
1418 }
1419 let cursor = self.buffer.cursor();
1422 let margin = Self::SCROLLOFF.min(height / 2);
1423 let min_row = new_top + margin;
1424 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1425 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1426 if target_row != cursor.row {
1427 let line_len = self
1428 .buffer
1429 .line(target_row)
1430 .map(|l| l.chars().count())
1431 .unwrap_or(0);
1432 let target_col = cursor.col.min(line_len.saturating_sub(1));
1433 self.buffer
1434 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1435 }
1436 }
1437
1438 pub fn goto_line(&mut self, line: usize) {
1439 let row = line.saturating_sub(1);
1440 let max = self.buffer.row_count().saturating_sub(1);
1441 let target = row.min(max);
1442 self.buffer
1443 .set_cursor(hjkl_buffer::Position::new(target, 0));
1444 }
1445
1446 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1450 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1451 if height == 0 {
1452 return;
1453 }
1454 let cur_row = self.buffer.cursor().row;
1455 let cur_top = self.buffer.viewport().top_row;
1456 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1462 let new_top = match pos {
1463 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1464 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1465 CursorScrollTarget::Bottom => {
1466 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1467 }
1468 };
1469 if new_top == cur_top {
1470 return;
1471 }
1472 self.buffer.viewport_mut().top_row = new_top;
1473 }
1474
1475 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1482 let lines = self.buffer.lines();
1483 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1485 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1486 let rel_row = row.saturating_sub(inner_top) as usize;
1487 let top = self.buffer.viewport().top_row;
1488 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1489 let rel_col = col.saturating_sub(content_x) as usize;
1490 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1491 let last_col = line_chars.saturating_sub(1);
1492 (doc_row, rel_col.min(last_col))
1493 }
1494
1495 pub fn jump_to(&mut self, line: usize, col: usize) {
1497 let r = line.saturating_sub(1);
1498 let max_row = self.buffer.row_count().saturating_sub(1);
1499 let r = r.min(max_row);
1500 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1501 let c = col.saturating_sub(1).min(line_len);
1502 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1503 }
1504
1505 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1507 if self.vim.is_visual() {
1508 self.vim.force_normal();
1509 }
1510 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1511 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1512 }
1513
1514 pub fn mouse_begin_drag(&mut self) {
1516 if !self.vim.is_visual_char() {
1517 let cursor = self.cursor();
1518 self.vim.enter_visual(cursor);
1519 }
1520 }
1521
1522 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1524 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1525 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1526 }
1527
1528 pub fn insert_str(&mut self, text: &str) {
1529 let pos = self.buffer.cursor();
1530 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1531 at: pos,
1532 text: text.to_string(),
1533 });
1534 self.push_buffer_content_to_textarea();
1535 self.mark_content_dirty();
1536 }
1537
1538 pub fn accept_completion(&mut self, completion: &str) {
1539 use hjkl_buffer::{Edit, MotionKind, Position};
1540 let cursor = self.buffer.cursor();
1541 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1542 let chars: Vec<char> = line.chars().collect();
1543 let prefix_len = chars[..cursor.col.min(chars.len())]
1544 .iter()
1545 .rev()
1546 .take_while(|c| c.is_alphanumeric() || **c == '_')
1547 .count();
1548 if prefix_len > 0 {
1549 let start = Position::new(cursor.row, cursor.col - prefix_len);
1550 self.buffer.apply_edit(Edit::DeleteRange {
1551 start,
1552 end: cursor,
1553 kind: MotionKind::Char,
1554 });
1555 }
1556 let cursor = self.buffer.cursor();
1557 self.buffer.apply_edit(Edit::InsertStr {
1558 at: cursor,
1559 text: completion.to_string(),
1560 });
1561 self.push_buffer_content_to_textarea();
1562 self.mark_content_dirty();
1563 }
1564
1565 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1566 let pos = self.buffer.cursor();
1567 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1568 }
1569
1570 pub fn undo(&mut self) {
1574 crate::vim::do_undo(self);
1575 }
1576
1577 pub fn redo(&mut self) {
1580 crate::vim::do_redo(self);
1581 }
1582
1583 pub fn push_undo(&mut self) {
1588 let snap = self.snapshot();
1589 if self.undo_stack.len() >= 200 {
1590 self.undo_stack.remove(0);
1591 }
1592 self.undo_stack.push(snap);
1593 self.redo_stack.clear();
1594 }
1595
1596 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1600 let text = lines.join("\n");
1601 self.buffer.replace_all(&text);
1602 self.buffer
1603 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1604 self.mark_content_dirty();
1605 }
1606
1607 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1609 let input = crossterm_to_input(key);
1610 if input.key == Key::Null {
1611 return false;
1612 }
1613 vim::step(self, input)
1614 }
1615}
1616
1617pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1618 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1619 let alt = key.modifiers.contains(KeyModifiers::ALT);
1620 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1621 let k = match key.code {
1622 KeyCode::Char(c) => Key::Char(c),
1623 KeyCode::Backspace => Key::Backspace,
1624 KeyCode::Delete => Key::Delete,
1625 KeyCode::Enter => Key::Enter,
1626 KeyCode::Left => Key::Left,
1627 KeyCode::Right => Key::Right,
1628 KeyCode::Up => Key::Up,
1629 KeyCode::Down => Key::Down,
1630 KeyCode::Home => Key::Home,
1631 KeyCode::End => Key::End,
1632 KeyCode::Tab => Key::Tab,
1633 KeyCode::Esc => Key::Esc,
1634 _ => Key::Null,
1635 };
1636 Input {
1637 key: k,
1638 ctrl,
1639 alt,
1640 shift,
1641 }
1642}
1643
1644#[cfg(test)]
1645mod tests {
1646 use super::*;
1647 use crossterm::event::KeyEvent;
1648
1649 fn key(code: KeyCode) -> KeyEvent {
1650 KeyEvent::new(code, KeyModifiers::NONE)
1651 }
1652 fn shift_key(code: KeyCode) -> KeyEvent {
1653 KeyEvent::new(code, KeyModifiers::SHIFT)
1654 }
1655 fn ctrl_key(code: KeyCode) -> KeyEvent {
1656 KeyEvent::new(code, KeyModifiers::CONTROL)
1657 }
1658
1659 #[test]
1660 fn vim_normal_to_insert() {
1661 let mut e = Editor::new(KeybindingMode::Vim);
1662 e.handle_key(key(KeyCode::Char('i')));
1663 assert_eq!(e.vim_mode(), VimMode::Insert);
1664 }
1665
1666 #[test]
1667 fn feed_input_char_routes_through_handle_key() {
1668 use crate::{Modifiers, PlannedInput};
1669 let mut e = Editor::new(KeybindingMode::Vim);
1670 e.set_content("abc");
1671 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1673 assert_eq!(e.vim_mode(), VimMode::Insert);
1674 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
1676 assert!(e.content().contains('X'));
1677 }
1678
1679 #[test]
1680 fn feed_input_special_key_routes() {
1681 use crate::{Modifiers, PlannedInput, SpecialKey};
1682 let mut e = Editor::new(KeybindingMode::Vim);
1683 e.set_content("abc");
1684 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1685 assert_eq!(e.vim_mode(), VimMode::Insert);
1686 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
1687 assert_eq!(e.vim_mode(), VimMode::Normal);
1688 }
1689
1690 #[test]
1691 fn feed_input_mouse_paste_focus_resize_no_op() {
1692 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
1693 let mut e = Editor::new(KeybindingMode::Vim);
1694 e.set_content("abc");
1695 let mode_before = e.vim_mode();
1696 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
1697 kind: MouseKind::Press,
1698 pos: Pos::new(0, 0),
1699 mods: Default::default(),
1700 }));
1701 assert!(!consumed);
1702 assert_eq!(e.vim_mode(), mode_before);
1703 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
1704 assert!(!e.feed_input(PlannedInput::FocusGained));
1705 assert!(!e.feed_input(PlannedInput::FocusLost));
1706 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
1707 }
1708
1709 #[test]
1710 fn intern_engine_style_dedups_with_intern_style() {
1711 use crate::types::{Attrs, Color, Style};
1712 let mut e = Editor::new(KeybindingMode::Vim);
1713 let s = Style {
1714 fg: Some(Color(255, 0, 0)),
1715 bg: None,
1716 attrs: Attrs::BOLD,
1717 };
1718 let id_a = e.intern_engine_style(s);
1719 let id_b = e.intern_engine_style(s);
1721 assert_eq!(id_a, id_b);
1722 let back = e.engine_style_at(id_a).expect("interned");
1724 assert_eq!(back, s);
1725 }
1726
1727 #[test]
1728 fn engine_style_at_out_of_range_returns_none() {
1729 let e = Editor::new(KeybindingMode::Vim);
1730 assert!(e.engine_style_at(99).is_none());
1731 }
1732
1733 #[test]
1734 fn take_changes_drains_after_insert() {
1735 let mut e = Editor::new(KeybindingMode::Vim);
1736 e.set_content("abc");
1737 assert!(e.take_changes().is_empty());
1739 e.handle_key(key(KeyCode::Char('i')));
1741 e.handle_key(key(KeyCode::Char('X')));
1742 let changes = e.take_changes();
1743 assert!(
1744 !changes.is_empty(),
1745 "insert mode keystroke should produce a change"
1746 );
1747 assert!(e.take_changes().is_empty());
1749 }
1750
1751 #[test]
1752 fn options_bridge_roundtrip() {
1753 let mut e = Editor::new(KeybindingMode::Vim);
1754 let opts = e.current_options();
1755 assert_eq!(opts.shiftwidth, 2); assert_eq!(opts.tabstop, 8);
1757
1758 let mut new_opts = crate::types::Options::default();
1759 new_opts.shiftwidth = 4;
1760 new_opts.tabstop = 2;
1761 new_opts.ignorecase = true;
1762 e.apply_options(&new_opts);
1763
1764 let after = e.current_options();
1765 assert_eq!(after.shiftwidth, 4);
1766 assert_eq!(after.tabstop, 2);
1767 assert!(after.ignorecase);
1768 }
1769
1770 #[test]
1771 fn selection_highlight_none_in_normal() {
1772 let mut e = Editor::new(KeybindingMode::Vim);
1773 e.set_content("hello");
1774 assert!(e.selection_highlight().is_none());
1775 }
1776
1777 #[test]
1778 fn selection_highlight_some_in_visual() {
1779 use crate::types::HighlightKind;
1780 let mut e = Editor::new(KeybindingMode::Vim);
1781 e.set_content("hello world");
1782 e.handle_key(key(KeyCode::Char('v')));
1783 e.handle_key(key(KeyCode::Char('l')));
1784 e.handle_key(key(KeyCode::Char('l')));
1785 let h = e
1786 .selection_highlight()
1787 .expect("visual mode should produce a highlight");
1788 assert_eq!(h.kind, HighlightKind::Selection);
1789 assert_eq!(h.range.start.line, 0);
1790 assert_eq!(h.range.end.line, 0);
1791 }
1792
1793 #[test]
1794 fn highlights_emit_search_matches() {
1795 use crate::types::HighlightKind;
1796 let mut e = Editor::new(KeybindingMode::Vim);
1797 e.set_content("foo bar foo\nbaz qux\n");
1798 e.buffer_mut()
1800 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1801 let hs = e.highlights_for_line(0);
1802 assert_eq!(hs.len(), 2);
1803 for h in &hs {
1804 assert_eq!(h.kind, HighlightKind::SearchMatch);
1805 assert_eq!(h.range.start.line, 0);
1806 assert_eq!(h.range.end.line, 0);
1807 }
1808 }
1809
1810 #[test]
1811 fn highlights_empty_without_pattern() {
1812 let mut e = Editor::new(KeybindingMode::Vim);
1813 e.set_content("foo bar");
1814 assert!(e.highlights_for_line(0).is_empty());
1815 }
1816
1817 #[test]
1818 fn highlights_empty_for_out_of_range_line() {
1819 let mut e = Editor::new(KeybindingMode::Vim);
1820 e.set_content("foo");
1821 e.buffer_mut()
1822 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1823 assert!(e.highlights_for_line(99).is_empty());
1824 }
1825
1826 #[test]
1827 fn render_frame_reflects_mode_and_cursor() {
1828 use crate::types::{CursorShape, SnapshotMode};
1829 let mut e = Editor::new(KeybindingMode::Vim);
1830 e.set_content("alpha\nbeta");
1831 let f = e.render_frame();
1832 assert_eq!(f.mode, SnapshotMode::Normal);
1833 assert_eq!(f.cursor_shape, CursorShape::Block);
1834 assert_eq!(f.line_count, 2);
1835
1836 e.handle_key(key(KeyCode::Char('i')));
1837 let f = e.render_frame();
1838 assert_eq!(f.mode, SnapshotMode::Insert);
1839 assert_eq!(f.cursor_shape, CursorShape::Bar);
1840 }
1841
1842 #[test]
1843 fn snapshot_roundtrips_through_restore() {
1844 use crate::types::SnapshotMode;
1845 let mut e = Editor::new(KeybindingMode::Vim);
1846 e.set_content("alpha\nbeta\ngamma");
1847 e.jump_cursor(2, 3);
1848 let snap = e.take_snapshot();
1849 assert_eq!(snap.mode, SnapshotMode::Normal);
1850 assert_eq!(snap.cursor, (2, 3));
1851 assert_eq!(snap.lines.len(), 3);
1852
1853 let mut other = Editor::new(KeybindingMode::Vim);
1854 other.restore_snapshot(snap).expect("restore");
1855 assert_eq!(other.cursor(), (2, 3));
1856 assert_eq!(other.buffer().lines().len(), 3);
1857 }
1858
1859 #[test]
1860 fn restore_snapshot_rejects_version_mismatch() {
1861 let mut e = Editor::new(KeybindingMode::Vim);
1862 let mut snap = e.take_snapshot();
1863 snap.version = 9999;
1864 match e.restore_snapshot(snap) {
1865 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1866 assert_eq!(got, 9999);
1867 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1868 }
1869 other => panic!("expected SnapshotVersion err, got {other:?}"),
1870 }
1871 }
1872
1873 #[test]
1874 fn take_content_change_returns_some_on_first_dirty() {
1875 let mut e = Editor::new(KeybindingMode::Vim);
1876 e.set_content("hello");
1877 let first = e.take_content_change();
1878 assert!(first.is_some());
1879 let second = e.take_content_change();
1880 assert!(second.is_none());
1881 }
1882
1883 #[test]
1884 fn take_content_change_none_until_mutation() {
1885 let mut e = Editor::new(KeybindingMode::Vim);
1886 e.set_content("hello");
1887 e.take_content_change();
1889 assert!(e.take_content_change().is_none());
1890 e.handle_key(key(KeyCode::Char('i')));
1892 e.handle_key(key(KeyCode::Char('x')));
1893 let after = e.take_content_change();
1894 assert!(after.is_some());
1895 assert!(after.unwrap().contains('x'));
1896 }
1897
1898 #[test]
1899 fn vim_insert_to_normal() {
1900 let mut e = Editor::new(KeybindingMode::Vim);
1901 e.handle_key(key(KeyCode::Char('i')));
1902 e.handle_key(key(KeyCode::Esc));
1903 assert_eq!(e.vim_mode(), VimMode::Normal);
1904 }
1905
1906 #[test]
1907 fn vim_normal_to_visual() {
1908 let mut e = Editor::new(KeybindingMode::Vim);
1909 e.handle_key(key(KeyCode::Char('v')));
1910 assert_eq!(e.vim_mode(), VimMode::Visual);
1911 }
1912
1913 #[test]
1914 fn vim_visual_to_normal() {
1915 let mut e = Editor::new(KeybindingMode::Vim);
1916 e.handle_key(key(KeyCode::Char('v')));
1917 e.handle_key(key(KeyCode::Esc));
1918 assert_eq!(e.vim_mode(), VimMode::Normal);
1919 }
1920
1921 #[test]
1922 fn vim_shift_i_moves_to_first_non_whitespace() {
1923 let mut e = Editor::new(KeybindingMode::Vim);
1924 e.set_content(" hello");
1925 e.jump_cursor(0, 8);
1926 e.handle_key(shift_key(KeyCode::Char('I')));
1927 assert_eq!(e.vim_mode(), VimMode::Insert);
1928 assert_eq!(e.cursor(), (0, 3));
1929 }
1930
1931 #[test]
1932 fn vim_shift_a_moves_to_end_and_insert() {
1933 let mut e = Editor::new(KeybindingMode::Vim);
1934 e.set_content("hello");
1935 e.handle_key(shift_key(KeyCode::Char('A')));
1936 assert_eq!(e.vim_mode(), VimMode::Insert);
1937 assert_eq!(e.cursor().1, 5);
1938 }
1939
1940 #[test]
1941 fn count_10j_moves_down_10() {
1942 let mut e = Editor::new(KeybindingMode::Vim);
1943 e.set_content(
1944 (0..20)
1945 .map(|i| format!("line{i}"))
1946 .collect::<Vec<_>>()
1947 .join("\n")
1948 .as_str(),
1949 );
1950 for d in "10".chars() {
1951 e.handle_key(key(KeyCode::Char(d)));
1952 }
1953 e.handle_key(key(KeyCode::Char('j')));
1954 assert_eq!(e.cursor().0, 10);
1955 }
1956
1957 #[test]
1958 fn count_o_repeats_insert_on_esc() {
1959 let mut e = Editor::new(KeybindingMode::Vim);
1960 e.set_content("hello");
1961 for d in "3".chars() {
1962 e.handle_key(key(KeyCode::Char(d)));
1963 }
1964 e.handle_key(key(KeyCode::Char('o')));
1965 assert_eq!(e.vim_mode(), VimMode::Insert);
1966 for c in "world".chars() {
1967 e.handle_key(key(KeyCode::Char(c)));
1968 }
1969 e.handle_key(key(KeyCode::Esc));
1970 assert_eq!(e.vim_mode(), VimMode::Normal);
1971 assert_eq!(e.buffer().lines().len(), 4);
1972 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1973 }
1974
1975 #[test]
1976 fn count_i_repeats_text_on_esc() {
1977 let mut e = Editor::new(KeybindingMode::Vim);
1978 e.set_content("");
1979 for d in "3".chars() {
1980 e.handle_key(key(KeyCode::Char(d)));
1981 }
1982 e.handle_key(key(KeyCode::Char('i')));
1983 for c in "ab".chars() {
1984 e.handle_key(key(KeyCode::Char(c)));
1985 }
1986 e.handle_key(key(KeyCode::Esc));
1987 assert_eq!(e.vim_mode(), VimMode::Normal);
1988 assert_eq!(e.buffer().lines()[0], "ababab");
1989 }
1990
1991 #[test]
1992 fn vim_shift_o_opens_line_above() {
1993 let mut e = Editor::new(KeybindingMode::Vim);
1994 e.set_content("hello");
1995 e.handle_key(shift_key(KeyCode::Char('O')));
1996 assert_eq!(e.vim_mode(), VimMode::Insert);
1997 assert_eq!(e.cursor(), (0, 0));
1998 assert_eq!(e.buffer().lines().len(), 2);
1999 }
2000
2001 #[test]
2002 fn vim_gg_goes_to_top() {
2003 let mut e = Editor::new(KeybindingMode::Vim);
2004 e.set_content("a\nb\nc");
2005 e.jump_cursor(2, 0);
2006 e.handle_key(key(KeyCode::Char('g')));
2007 e.handle_key(key(KeyCode::Char('g')));
2008 assert_eq!(e.cursor().0, 0);
2009 }
2010
2011 #[test]
2012 fn vim_shift_g_goes_to_bottom() {
2013 let mut e = Editor::new(KeybindingMode::Vim);
2014 e.set_content("a\nb\nc");
2015 e.handle_key(shift_key(KeyCode::Char('G')));
2016 assert_eq!(e.cursor().0, 2);
2017 }
2018
2019 #[test]
2020 fn vim_dd_deletes_line() {
2021 let mut e = Editor::new(KeybindingMode::Vim);
2022 e.set_content("first\nsecond");
2023 e.handle_key(key(KeyCode::Char('d')));
2024 e.handle_key(key(KeyCode::Char('d')));
2025 assert_eq!(e.buffer().lines().len(), 1);
2026 assert_eq!(e.buffer().lines()[0], "second");
2027 }
2028
2029 #[test]
2030 fn vim_dw_deletes_word() {
2031 let mut e = Editor::new(KeybindingMode::Vim);
2032 e.set_content("hello world");
2033 e.handle_key(key(KeyCode::Char('d')));
2034 e.handle_key(key(KeyCode::Char('w')));
2035 assert_eq!(e.vim_mode(), VimMode::Normal);
2036 assert!(!e.buffer().lines()[0].starts_with("hello"));
2037 }
2038
2039 #[test]
2040 fn vim_yy_yanks_line() {
2041 let mut e = Editor::new(KeybindingMode::Vim);
2042 e.set_content("hello\nworld");
2043 e.handle_key(key(KeyCode::Char('y')));
2044 e.handle_key(key(KeyCode::Char('y')));
2045 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
2046 }
2047
2048 #[test]
2049 fn vim_yy_does_not_move_cursor() {
2050 let mut e = Editor::new(KeybindingMode::Vim);
2051 e.set_content("first\nsecond\nthird");
2052 e.jump_cursor(1, 0);
2053 let before = e.cursor();
2054 e.handle_key(key(KeyCode::Char('y')));
2055 e.handle_key(key(KeyCode::Char('y')));
2056 assert_eq!(e.cursor(), before);
2057 assert_eq!(e.vim_mode(), VimMode::Normal);
2058 }
2059
2060 #[test]
2061 fn vim_yw_yanks_word() {
2062 let mut e = Editor::new(KeybindingMode::Vim);
2063 e.set_content("hello world");
2064 e.handle_key(key(KeyCode::Char('y')));
2065 e.handle_key(key(KeyCode::Char('w')));
2066 assert_eq!(e.vim_mode(), VimMode::Normal);
2067 assert!(e.last_yank.is_some());
2068 }
2069
2070 #[test]
2071 fn vim_cc_changes_line() {
2072 let mut e = Editor::new(KeybindingMode::Vim);
2073 e.set_content("hello\nworld");
2074 e.handle_key(key(KeyCode::Char('c')));
2075 e.handle_key(key(KeyCode::Char('c')));
2076 assert_eq!(e.vim_mode(), VimMode::Insert);
2077 }
2078
2079 #[test]
2080 fn vim_u_undoes_insert_session_as_chunk() {
2081 let mut e = Editor::new(KeybindingMode::Vim);
2082 e.set_content("hello");
2083 e.handle_key(key(KeyCode::Char('i')));
2084 e.handle_key(key(KeyCode::Enter));
2085 e.handle_key(key(KeyCode::Enter));
2086 e.handle_key(key(KeyCode::Esc));
2087 assert_eq!(e.buffer().lines().len(), 3);
2088 e.handle_key(key(KeyCode::Char('u')));
2089 assert_eq!(e.buffer().lines().len(), 1);
2090 assert_eq!(e.buffer().lines()[0], "hello");
2091 }
2092
2093 #[test]
2094 fn vim_undo_redo_roundtrip() {
2095 let mut e = Editor::new(KeybindingMode::Vim);
2096 e.set_content("hello");
2097 e.handle_key(key(KeyCode::Char('i')));
2098 for c in "world".chars() {
2099 e.handle_key(key(KeyCode::Char(c)));
2100 }
2101 e.handle_key(key(KeyCode::Esc));
2102 let after = e.buffer().lines()[0].clone();
2103 e.handle_key(key(KeyCode::Char('u')));
2104 assert_eq!(e.buffer().lines()[0], "hello");
2105 e.handle_key(ctrl_key(KeyCode::Char('r')));
2106 assert_eq!(e.buffer().lines()[0], after);
2107 }
2108
2109 #[test]
2110 fn vim_u_undoes_dd() {
2111 let mut e = Editor::new(KeybindingMode::Vim);
2112 e.set_content("first\nsecond");
2113 e.handle_key(key(KeyCode::Char('d')));
2114 e.handle_key(key(KeyCode::Char('d')));
2115 assert_eq!(e.buffer().lines().len(), 1);
2116 e.handle_key(key(KeyCode::Char('u')));
2117 assert_eq!(e.buffer().lines().len(), 2);
2118 assert_eq!(e.buffer().lines()[0], "first");
2119 }
2120
2121 #[test]
2122 fn vim_ctrl_r_redoes() {
2123 let mut e = Editor::new(KeybindingMode::Vim);
2124 e.set_content("hello");
2125 e.handle_key(ctrl_key(KeyCode::Char('r')));
2126 }
2127
2128 #[test]
2129 fn vim_r_replaces_char() {
2130 let mut e = Editor::new(KeybindingMode::Vim);
2131 e.set_content("hello");
2132 e.handle_key(key(KeyCode::Char('r')));
2133 e.handle_key(key(KeyCode::Char('x')));
2134 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
2135 }
2136
2137 #[test]
2138 fn vim_tilde_toggles_case() {
2139 let mut e = Editor::new(KeybindingMode::Vim);
2140 e.set_content("hello");
2141 e.handle_key(key(KeyCode::Char('~')));
2142 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
2143 }
2144
2145 #[test]
2146 fn vim_visual_d_cuts() {
2147 let mut e = Editor::new(KeybindingMode::Vim);
2148 e.set_content("hello");
2149 e.handle_key(key(KeyCode::Char('v')));
2150 e.handle_key(key(KeyCode::Char('l')));
2151 e.handle_key(key(KeyCode::Char('l')));
2152 e.handle_key(key(KeyCode::Char('d')));
2153 assert_eq!(e.vim_mode(), VimMode::Normal);
2154 assert!(e.last_yank.is_some());
2155 }
2156
2157 #[test]
2158 fn vim_visual_c_enters_insert() {
2159 let mut e = Editor::new(KeybindingMode::Vim);
2160 e.set_content("hello");
2161 e.handle_key(key(KeyCode::Char('v')));
2162 e.handle_key(key(KeyCode::Char('l')));
2163 e.handle_key(key(KeyCode::Char('c')));
2164 assert_eq!(e.vim_mode(), VimMode::Insert);
2165 }
2166
2167 #[test]
2168 fn vim_normal_unknown_key_consumed() {
2169 let mut e = Editor::new(KeybindingMode::Vim);
2170 let consumed = e.handle_key(key(KeyCode::Char('z')));
2172 assert!(consumed);
2173 }
2174
2175 #[test]
2176 fn force_normal_clears_operator() {
2177 let mut e = Editor::new(KeybindingMode::Vim);
2178 e.handle_key(key(KeyCode::Char('d')));
2179 e.force_normal();
2180 assert_eq!(e.vim_mode(), VimMode::Normal);
2181 }
2182
2183 fn many_lines(n: usize) -> String {
2184 (0..n)
2185 .map(|i| format!("line{i}"))
2186 .collect::<Vec<_>>()
2187 .join("\n")
2188 }
2189
2190 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
2191 e.set_viewport_height(height);
2192 }
2193
2194 #[test]
2195 fn zz_centers_cursor_in_viewport() {
2196 let mut e = Editor::new(KeybindingMode::Vim);
2197 e.set_content(&many_lines(100));
2198 prime_viewport(&mut e, 20);
2199 e.jump_cursor(50, 0);
2200 e.handle_key(key(KeyCode::Char('z')));
2201 e.handle_key(key(KeyCode::Char('z')));
2202 assert_eq!(e.buffer().viewport().top_row, 40);
2203 assert_eq!(e.cursor().0, 50);
2204 }
2205
2206 #[test]
2207 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
2208 let mut e = Editor::new(KeybindingMode::Vim);
2209 e.set_content(&many_lines(100));
2210 prime_viewport(&mut e, 20);
2211 e.jump_cursor(50, 0);
2212 e.handle_key(key(KeyCode::Char('z')));
2213 e.handle_key(key(KeyCode::Char('t')));
2214 assert_eq!(e.buffer().viewport().top_row, 45);
2217 assert_eq!(e.cursor().0, 50);
2218 }
2219
2220 #[test]
2221 fn ctrl_a_increments_number_at_cursor() {
2222 let mut e = Editor::new(KeybindingMode::Vim);
2223 e.set_content("x = 41");
2224 e.handle_key(ctrl_key(KeyCode::Char('a')));
2225 assert_eq!(e.buffer().lines()[0], "x = 42");
2226 assert_eq!(e.cursor(), (0, 5));
2227 }
2228
2229 #[test]
2230 fn ctrl_a_finds_number_to_right_of_cursor() {
2231 let mut e = Editor::new(KeybindingMode::Vim);
2232 e.set_content("foo 99 bar");
2233 e.handle_key(ctrl_key(KeyCode::Char('a')));
2234 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
2235 assert_eq!(e.cursor(), (0, 6));
2236 }
2237
2238 #[test]
2239 fn ctrl_a_with_count_adds_count() {
2240 let mut e = Editor::new(KeybindingMode::Vim);
2241 e.set_content("x = 10");
2242 for d in "5".chars() {
2243 e.handle_key(key(KeyCode::Char(d)));
2244 }
2245 e.handle_key(ctrl_key(KeyCode::Char('a')));
2246 assert_eq!(e.buffer().lines()[0], "x = 15");
2247 }
2248
2249 #[test]
2250 fn ctrl_x_decrements_number() {
2251 let mut e = Editor::new(KeybindingMode::Vim);
2252 e.set_content("n=5");
2253 e.handle_key(ctrl_key(KeyCode::Char('x')));
2254 assert_eq!(e.buffer().lines()[0], "n=4");
2255 }
2256
2257 #[test]
2258 fn ctrl_x_crosses_zero_into_negative() {
2259 let mut e = Editor::new(KeybindingMode::Vim);
2260 e.set_content("v=0");
2261 e.handle_key(ctrl_key(KeyCode::Char('x')));
2262 assert_eq!(e.buffer().lines()[0], "v=-1");
2263 }
2264
2265 #[test]
2266 fn ctrl_a_on_negative_number_increments_toward_zero() {
2267 let mut e = Editor::new(KeybindingMode::Vim);
2268 e.set_content("a = -5");
2269 e.handle_key(ctrl_key(KeyCode::Char('a')));
2270 assert_eq!(e.buffer().lines()[0], "a = -4");
2271 }
2272
2273 #[test]
2274 fn ctrl_a_noop_when_no_digit_on_line() {
2275 let mut e = Editor::new(KeybindingMode::Vim);
2276 e.set_content("no digits here");
2277 e.handle_key(ctrl_key(KeyCode::Char('a')));
2278 assert_eq!(e.buffer().lines()[0], "no digits here");
2279 }
2280
2281 #[test]
2282 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
2283 let mut e = Editor::new(KeybindingMode::Vim);
2284 e.set_content(&many_lines(100));
2285 prime_viewport(&mut e, 20);
2286 e.jump_cursor(50, 0);
2287 e.handle_key(key(KeyCode::Char('z')));
2288 e.handle_key(key(KeyCode::Char('b')));
2289 assert_eq!(e.buffer().viewport().top_row, 36);
2293 assert_eq!(e.cursor().0, 50);
2294 }
2295
2296 #[test]
2303 fn set_content_dirties_then_take_dirty_clears() {
2304 let mut e = Editor::new(KeybindingMode::Vim);
2305 e.set_content("hello");
2306 assert!(
2307 e.take_dirty(),
2308 "set_content should leave content_dirty=true"
2309 );
2310 assert!(!e.take_dirty(), "take_dirty should clear the flag");
2311 }
2312
2313 #[test]
2314 fn content_arc_returns_same_arc_until_mutation() {
2315 let mut e = Editor::new(KeybindingMode::Vim);
2316 e.set_content("hello");
2317 let a = e.content_arc();
2318 let b = e.content_arc();
2319 assert!(
2320 std::sync::Arc::ptr_eq(&a, &b),
2321 "repeated content_arc() should hit the cache"
2322 );
2323
2324 e.handle_key(key(KeyCode::Char('i')));
2326 e.handle_key(key(KeyCode::Char('!')));
2327 let c = e.content_arc();
2328 assert!(
2329 !std::sync::Arc::ptr_eq(&a, &c),
2330 "mutation should invalidate content_arc() cache"
2331 );
2332 assert!(c.contains('!'));
2333 }
2334
2335 #[test]
2336 fn content_arc_cache_invalidated_by_set_content() {
2337 let mut e = Editor::new(KeybindingMode::Vim);
2338 e.set_content("one");
2339 let a = e.content_arc();
2340 e.set_content("two");
2341 let b = e.content_arc();
2342 assert!(!std::sync::Arc::ptr_eq(&a, &b));
2343 assert!(b.starts_with("two"));
2344 }
2345
2346 #[test]
2352 fn mouse_click_past_eol_lands_on_last_char() {
2353 let mut e = Editor::new(KeybindingMode::Vim);
2354 e.set_content("hello");
2355 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2359 e.mouse_click(area, 78, 1);
2360 assert_eq!(e.cursor(), (0, 4));
2361 }
2362
2363 #[test]
2364 fn mouse_click_past_eol_handles_multibyte_line() {
2365 let mut e = Editor::new(KeybindingMode::Vim);
2366 e.set_content("héllo");
2369 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2370 e.mouse_click(area, 78, 1);
2371 assert_eq!(e.cursor(), (0, 4));
2372 }
2373
2374 #[test]
2375 fn mouse_click_inside_line_lands_on_clicked_char() {
2376 let mut e = Editor::new(KeybindingMode::Vim);
2377 e.set_content("hello world");
2378 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2381 e.mouse_click(area, 4, 1);
2382 assert_eq!(e.cursor(), (0, 0));
2383 e.mouse_click(area, 6, 1);
2384 assert_eq!(e.cursor(), (0, 2));
2385 }
2386}