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 crate::types::Options {
1062 shiftwidth: self.settings.shiftwidth as u32,
1063 tabstop: self.settings.tabstop as u32,
1064 ignorecase: self.settings.ignore_case,
1065 ..crate::types::Options::default()
1066 }
1067 }
1068
1069 pub fn apply_options(&mut self, opts: &crate::types::Options) {
1074 self.settings.shiftwidth = opts.shiftwidth as usize;
1075 self.settings.tabstop = opts.tabstop as usize;
1076 self.settings.ignore_case = opts.ignorecase;
1077 }
1078
1079 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
1089 use crate::types::{Highlight, HighlightKind, Pos};
1090 let sel = self.buffer_selection()?;
1091 let (start, end) = match sel {
1092 hjkl_buffer::Selection::Char { anchor, head } => {
1093 let a = (anchor.row, anchor.col);
1094 let h = (head.row, head.col);
1095 if a <= h { (a, h) } else { (h, a) }
1096 }
1097 hjkl_buffer::Selection::Line {
1098 anchor_row,
1099 head_row,
1100 } => {
1101 let (top, bot) = if anchor_row <= head_row {
1102 (anchor_row, head_row)
1103 } else {
1104 (head_row, anchor_row)
1105 };
1106 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
1107 ((top, 0), (bot, last_col))
1108 }
1109 hjkl_buffer::Selection::Block { anchor, head } => {
1110 let (top, bot) = if anchor.row <= head.row {
1111 (anchor.row, head.row)
1112 } else {
1113 (head.row, anchor.row)
1114 };
1115 let (left, right) = if anchor.col <= head.col {
1116 (anchor.col, head.col)
1117 } else {
1118 (head.col, anchor.col)
1119 };
1120 ((top, left), (bot, right))
1121 }
1122 };
1123 Some(Highlight {
1124 range: Pos {
1125 line: start.0 as u32,
1126 col: start.1 as u32,
1127 }..Pos {
1128 line: end.0 as u32,
1129 col: end.1 as u32,
1130 },
1131 kind: HighlightKind::Selection,
1132 })
1133 }
1134
1135 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
1154 use crate::types::{Highlight, HighlightKind, Pos};
1155 let row = line as usize;
1156 if row >= self.buffer.lines().len() {
1157 return Vec::new();
1158 }
1159
1160 if let Some(prompt) = self.search_prompt() {
1163 if prompt.text.is_empty() {
1164 return Vec::new();
1165 }
1166 let Ok(re) = regex::Regex::new(&prompt.text) else {
1167 return Vec::new();
1168 };
1169 let Some(haystack) = self.buffer.line(row) else {
1170 return Vec::new();
1171 };
1172 return re
1173 .find_iter(haystack)
1174 .map(|m| Highlight {
1175 range: Pos {
1176 line,
1177 col: m.start() as u32,
1178 }..Pos {
1179 line,
1180 col: m.end() as u32,
1181 },
1182 kind: HighlightKind::IncSearch,
1183 })
1184 .collect();
1185 }
1186
1187 if self.buffer.search_pattern().is_none() {
1188 return Vec::new();
1189 }
1190 self.buffer
1191 .search_matches(row)
1192 .into_iter()
1193 .map(|(start, end)| Highlight {
1194 range: Pos {
1195 line,
1196 col: start as u32,
1197 }..Pos {
1198 line,
1199 col: end as u32,
1200 },
1201 kind: HighlightKind::SearchMatch,
1202 })
1203 .collect()
1204 }
1205
1206 pub fn render_frame(&self) -> crate::types::RenderFrame {
1216 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
1217 let (cursor_row, cursor_col) = self.cursor();
1218 let (mode, shape) = match self.vim_mode() {
1219 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
1220 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
1221 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
1222 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
1223 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
1224 };
1225 RenderFrame {
1226 mode,
1227 cursor_row: cursor_row as u32,
1228 cursor_col: cursor_col as u32,
1229 cursor_shape: shape,
1230 viewport_top: self.buffer.viewport().top_row as u32,
1231 line_count: self.buffer.lines().len() as u32,
1232 }
1233 }
1234
1235 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
1248 use crate::types::{EditorSnapshot, SnapshotMode};
1249 let mode = match self.vim_mode() {
1250 crate::VimMode::Normal => SnapshotMode::Normal,
1251 crate::VimMode::Insert => SnapshotMode::Insert,
1252 crate::VimMode::Visual => SnapshotMode::Visual,
1253 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
1254 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
1255 };
1256 let cursor = self.cursor();
1257 let cursor = (cursor.0 as u32, cursor.1 as u32);
1258 let lines: Vec<String> = self.buffer.lines().to_vec();
1259 let viewport_top = self.buffer.viewport().top_row as u32;
1260 let file_marks = self
1261 .file_marks
1262 .iter()
1263 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
1264 .collect();
1265 EditorSnapshot {
1266 version: EditorSnapshot::VERSION,
1267 mode,
1268 cursor,
1269 lines,
1270 viewport_top,
1271 registers: self.registers.clone(),
1272 file_marks,
1273 }
1274 }
1275
1276 pub fn restore_snapshot(
1284 &mut self,
1285 snap: crate::types::EditorSnapshot,
1286 ) -> Result<(), crate::EngineError> {
1287 use crate::types::EditorSnapshot;
1288 if snap.version != EditorSnapshot::VERSION {
1289 return Err(crate::EngineError::SnapshotVersion(
1290 snap.version,
1291 EditorSnapshot::VERSION,
1292 ));
1293 }
1294 let text = snap.lines.join("\n");
1295 self.set_content(&text);
1296 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1297 let mut vp = self.buffer.viewport();
1298 vp.top_row = snap.viewport_top as usize;
1299 *self.buffer.viewport_mut() = vp;
1300 self.registers = snap.registers;
1301 self.file_marks = snap
1302 .file_marks
1303 .into_iter()
1304 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1305 .collect();
1306 Ok(())
1307 }
1308
1309 pub fn seed_yank(&mut self, text: String) {
1313 let linewise = text.ends_with('\n');
1314 self.vim.yank_linewise = linewise;
1315 self.registers.unnamed = crate::registers::Slot { text, linewise };
1316 }
1317
1318 pub fn scroll_down(&mut self, rows: i16) {
1323 self.scroll_viewport(rows);
1324 }
1325
1326 pub fn scroll_up(&mut self, rows: i16) {
1330 self.scroll_viewport(-rows);
1331 }
1332
1333 const SCROLLOFF: usize = 5;
1337
1338 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1343 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1344 if height == 0 {
1345 self.buffer.ensure_cursor_visible();
1346 return;
1347 }
1348 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1352 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
1355 self.ensure_scrolloff_wrap(height, margin);
1356 return;
1357 }
1358 let cursor_row = self.buffer.cursor().row;
1359 let last_row = self.buffer.row_count().saturating_sub(1);
1360 let v = self.buffer.viewport_mut();
1361 if cursor_row < v.top_row + margin {
1363 v.top_row = cursor_row.saturating_sub(margin);
1364 }
1365 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1367 if cursor_row > v.top_row + max_bottom {
1368 v.top_row = cursor_row.saturating_sub(max_bottom);
1369 }
1370 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1372 if v.top_row > max_top {
1373 v.top_row = max_top;
1374 }
1375 let cursor = self.buffer.cursor();
1378 self.buffer.viewport_mut().ensure_visible(cursor);
1379 }
1380
1381 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1386 let cursor_row = self.buffer.cursor().row;
1387 if cursor_row < self.buffer.viewport().top_row {
1390 self.buffer.viewport_mut().top_row = cursor_row;
1391 self.buffer.viewport_mut().top_col = 0;
1392 }
1393 let max_csr = height.saturating_sub(1).saturating_sub(margin);
1396 loop {
1397 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1398 if csr <= max_csr {
1399 break;
1400 }
1401 let top = self.buffer.viewport().top_row;
1402 let Some(next) = self.buffer.next_visible_row(top) else {
1403 break;
1404 };
1405 if next > cursor_row {
1407 self.buffer.viewport_mut().top_row = cursor_row;
1408 break;
1409 }
1410 self.buffer.viewport_mut().top_row = next;
1411 }
1412 loop {
1415 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1416 if csr >= margin {
1417 break;
1418 }
1419 let top = self.buffer.viewport().top_row;
1420 let Some(prev) = self.buffer.prev_visible_row(top) else {
1421 break;
1422 };
1423 self.buffer.viewport_mut().top_row = prev;
1424 }
1425 let max_top = self.buffer.max_top_for_height(height);
1430 if self.buffer.viewport().top_row > max_top {
1431 self.buffer.viewport_mut().top_row = max_top;
1432 }
1433 self.buffer.viewport_mut().top_col = 0;
1434 }
1435
1436 fn scroll_viewport(&mut self, delta: i16) {
1437 if delta == 0 {
1438 return;
1439 }
1440 let total_rows = self.buffer.row_count() as isize;
1442 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1443 let cur_top = self.buffer.viewport().top_row as isize;
1444 let new_top = (cur_top + delta as isize)
1445 .max(0)
1446 .min((total_rows - 1).max(0)) as usize;
1447 self.buffer.viewport_mut().top_row = new_top;
1448 let _ = cur_top;
1451 if height == 0 {
1452 return;
1453 }
1454 let cursor = self.buffer.cursor();
1457 let margin = Self::SCROLLOFF.min(height / 2);
1458 let min_row = new_top + margin;
1459 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1460 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1461 if target_row != cursor.row {
1462 let line_len = self
1463 .buffer
1464 .line(target_row)
1465 .map(|l| l.chars().count())
1466 .unwrap_or(0);
1467 let target_col = cursor.col.min(line_len.saturating_sub(1));
1468 self.buffer
1469 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1470 }
1471 }
1472
1473 pub fn goto_line(&mut self, line: usize) {
1474 let row = line.saturating_sub(1);
1475 let max = self.buffer.row_count().saturating_sub(1);
1476 let target = row.min(max);
1477 self.buffer
1478 .set_cursor(hjkl_buffer::Position::new(target, 0));
1479 }
1480
1481 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1485 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1486 if height == 0 {
1487 return;
1488 }
1489 let cur_row = self.buffer.cursor().row;
1490 let cur_top = self.buffer.viewport().top_row;
1491 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1497 let new_top = match pos {
1498 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1499 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1500 CursorScrollTarget::Bottom => {
1501 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1502 }
1503 };
1504 if new_top == cur_top {
1505 return;
1506 }
1507 self.buffer.viewport_mut().top_row = new_top;
1508 }
1509
1510 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1517 let lines = self.buffer.lines();
1518 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1520 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1521 let rel_row = row.saturating_sub(inner_top) as usize;
1522 let top = self.buffer.viewport().top_row;
1523 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1524 let rel_col = col.saturating_sub(content_x) as usize;
1525 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1526 let last_col = line_chars.saturating_sub(1);
1527 (doc_row, rel_col.min(last_col))
1528 }
1529
1530 pub fn jump_to(&mut self, line: usize, col: usize) {
1532 let r = line.saturating_sub(1);
1533 let max_row = self.buffer.row_count().saturating_sub(1);
1534 let r = r.min(max_row);
1535 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1536 let c = col.saturating_sub(1).min(line_len);
1537 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1538 }
1539
1540 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1542 if self.vim.is_visual() {
1543 self.vim.force_normal();
1544 }
1545 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1546 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1547 }
1548
1549 pub fn mouse_begin_drag(&mut self) {
1551 if !self.vim.is_visual_char() {
1552 let cursor = self.cursor();
1553 self.vim.enter_visual(cursor);
1554 }
1555 }
1556
1557 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1559 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1560 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1561 }
1562
1563 pub fn insert_str(&mut self, text: &str) {
1564 let pos = self.buffer.cursor();
1565 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1566 at: pos,
1567 text: text.to_string(),
1568 });
1569 self.push_buffer_content_to_textarea();
1570 self.mark_content_dirty();
1571 }
1572
1573 pub fn accept_completion(&mut self, completion: &str) {
1574 use hjkl_buffer::{Edit, MotionKind, Position};
1575 let cursor = self.buffer.cursor();
1576 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1577 let chars: Vec<char> = line.chars().collect();
1578 let prefix_len = chars[..cursor.col.min(chars.len())]
1579 .iter()
1580 .rev()
1581 .take_while(|c| c.is_alphanumeric() || **c == '_')
1582 .count();
1583 if prefix_len > 0 {
1584 let start = Position::new(cursor.row, cursor.col - prefix_len);
1585 self.buffer.apply_edit(Edit::DeleteRange {
1586 start,
1587 end: cursor,
1588 kind: MotionKind::Char,
1589 });
1590 }
1591 let cursor = self.buffer.cursor();
1592 self.buffer.apply_edit(Edit::InsertStr {
1593 at: cursor,
1594 text: completion.to_string(),
1595 });
1596 self.push_buffer_content_to_textarea();
1597 self.mark_content_dirty();
1598 }
1599
1600 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1601 let pos = self.buffer.cursor();
1602 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1603 }
1604
1605 pub fn undo(&mut self) {
1609 crate::vim::do_undo(self);
1610 }
1611
1612 pub fn redo(&mut self) {
1615 crate::vim::do_redo(self);
1616 }
1617
1618 pub fn push_undo(&mut self) {
1623 let snap = self.snapshot();
1624 if self.undo_stack.len() >= 200 {
1625 self.undo_stack.remove(0);
1626 }
1627 self.undo_stack.push(snap);
1628 self.redo_stack.clear();
1629 }
1630
1631 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1635 let text = lines.join("\n");
1636 self.buffer.replace_all(&text);
1637 self.buffer
1638 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1639 self.mark_content_dirty();
1640 }
1641
1642 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1644 let input = crossterm_to_input(key);
1645 if input.key == Key::Null {
1646 return false;
1647 }
1648 vim::step(self, input)
1649 }
1650}
1651
1652pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1653 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1654 let alt = key.modifiers.contains(KeyModifiers::ALT);
1655 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1656 let k = match key.code {
1657 KeyCode::Char(c) => Key::Char(c),
1658 KeyCode::Backspace => Key::Backspace,
1659 KeyCode::Delete => Key::Delete,
1660 KeyCode::Enter => Key::Enter,
1661 KeyCode::Left => Key::Left,
1662 KeyCode::Right => Key::Right,
1663 KeyCode::Up => Key::Up,
1664 KeyCode::Down => Key::Down,
1665 KeyCode::Home => Key::Home,
1666 KeyCode::End => Key::End,
1667 KeyCode::Tab => Key::Tab,
1668 KeyCode::Esc => Key::Esc,
1669 _ => Key::Null,
1670 };
1671 Input {
1672 key: k,
1673 ctrl,
1674 alt,
1675 shift,
1676 }
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681 use super::*;
1682 use crossterm::event::KeyEvent;
1683
1684 fn key(code: KeyCode) -> KeyEvent {
1685 KeyEvent::new(code, KeyModifiers::NONE)
1686 }
1687 fn shift_key(code: KeyCode) -> KeyEvent {
1688 KeyEvent::new(code, KeyModifiers::SHIFT)
1689 }
1690 fn ctrl_key(code: KeyCode) -> KeyEvent {
1691 KeyEvent::new(code, KeyModifiers::CONTROL)
1692 }
1693
1694 #[test]
1695 fn vim_normal_to_insert() {
1696 let mut e = Editor::new(KeybindingMode::Vim);
1697 e.handle_key(key(KeyCode::Char('i')));
1698 assert_eq!(e.vim_mode(), VimMode::Insert);
1699 }
1700
1701 #[test]
1702 fn feed_input_char_routes_through_handle_key() {
1703 use crate::{Modifiers, PlannedInput};
1704 let mut e = Editor::new(KeybindingMode::Vim);
1705 e.set_content("abc");
1706 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1708 assert_eq!(e.vim_mode(), VimMode::Insert);
1709 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
1711 assert!(e.content().contains('X'));
1712 }
1713
1714 #[test]
1715 fn feed_input_special_key_routes() {
1716 use crate::{Modifiers, PlannedInput, SpecialKey};
1717 let mut e = Editor::new(KeybindingMode::Vim);
1718 e.set_content("abc");
1719 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
1720 assert_eq!(e.vim_mode(), VimMode::Insert);
1721 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
1722 assert_eq!(e.vim_mode(), VimMode::Normal);
1723 }
1724
1725 #[test]
1726 fn feed_input_mouse_paste_focus_resize_no_op() {
1727 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
1728 let mut e = Editor::new(KeybindingMode::Vim);
1729 e.set_content("abc");
1730 let mode_before = e.vim_mode();
1731 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
1732 kind: MouseKind::Press,
1733 pos: Pos::new(0, 0),
1734 mods: Default::default(),
1735 }));
1736 assert!(!consumed);
1737 assert_eq!(e.vim_mode(), mode_before);
1738 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
1739 assert!(!e.feed_input(PlannedInput::FocusGained));
1740 assert!(!e.feed_input(PlannedInput::FocusLost));
1741 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
1742 }
1743
1744 #[test]
1745 fn intern_engine_style_dedups_with_intern_style() {
1746 use crate::types::{Attrs, Color, Style};
1747 let mut e = Editor::new(KeybindingMode::Vim);
1748 let s = Style {
1749 fg: Some(Color(255, 0, 0)),
1750 bg: None,
1751 attrs: Attrs::BOLD,
1752 };
1753 let id_a = e.intern_engine_style(s);
1754 let id_b = e.intern_engine_style(s);
1756 assert_eq!(id_a, id_b);
1757 let back = e.engine_style_at(id_a).expect("interned");
1759 assert_eq!(back, s);
1760 }
1761
1762 #[test]
1763 fn engine_style_at_out_of_range_returns_none() {
1764 let e = Editor::new(KeybindingMode::Vim);
1765 assert!(e.engine_style_at(99).is_none());
1766 }
1767
1768 #[test]
1769 fn take_changes_drains_after_insert() {
1770 let mut e = Editor::new(KeybindingMode::Vim);
1771 e.set_content("abc");
1772 assert!(e.take_changes().is_empty());
1774 e.handle_key(key(KeyCode::Char('i')));
1776 e.handle_key(key(KeyCode::Char('X')));
1777 let changes = e.take_changes();
1778 assert!(
1779 !changes.is_empty(),
1780 "insert mode keystroke should produce a change"
1781 );
1782 assert!(e.take_changes().is_empty());
1784 }
1785
1786 #[test]
1787 fn options_bridge_roundtrip() {
1788 let mut e = Editor::new(KeybindingMode::Vim);
1789 let opts = e.current_options();
1790 assert_eq!(opts.shiftwidth, 2); assert_eq!(opts.tabstop, 8);
1792
1793 let new_opts = crate::types::Options {
1794 shiftwidth: 4,
1795 tabstop: 2,
1796 ignorecase: true,
1797 ..crate::types::Options::default()
1798 };
1799 e.apply_options(&new_opts);
1800
1801 let after = e.current_options();
1802 assert_eq!(after.shiftwidth, 4);
1803 assert_eq!(after.tabstop, 2);
1804 assert!(after.ignorecase);
1805 }
1806
1807 #[test]
1808 fn selection_highlight_none_in_normal() {
1809 let mut e = Editor::new(KeybindingMode::Vim);
1810 e.set_content("hello");
1811 assert!(e.selection_highlight().is_none());
1812 }
1813
1814 #[test]
1815 fn selection_highlight_some_in_visual() {
1816 use crate::types::HighlightKind;
1817 let mut e = Editor::new(KeybindingMode::Vim);
1818 e.set_content("hello world");
1819 e.handle_key(key(KeyCode::Char('v')));
1820 e.handle_key(key(KeyCode::Char('l')));
1821 e.handle_key(key(KeyCode::Char('l')));
1822 let h = e
1823 .selection_highlight()
1824 .expect("visual mode should produce a highlight");
1825 assert_eq!(h.kind, HighlightKind::Selection);
1826 assert_eq!(h.range.start.line, 0);
1827 assert_eq!(h.range.end.line, 0);
1828 }
1829
1830 #[test]
1831 fn highlights_emit_incsearch_during_active_prompt() {
1832 use crate::types::HighlightKind;
1833 let mut e = Editor::new(KeybindingMode::Vim);
1834 e.set_content("foo bar foo\nbaz\n");
1835 e.handle_key(key(KeyCode::Char('/')));
1837 e.handle_key(key(KeyCode::Char('f')));
1838 e.handle_key(key(KeyCode::Char('o')));
1839 e.handle_key(key(KeyCode::Char('o')));
1840 assert!(e.search_prompt().is_some());
1842 let hs = e.highlights_for_line(0);
1843 assert_eq!(hs.len(), 2);
1844 for h in &hs {
1845 assert_eq!(h.kind, HighlightKind::IncSearch);
1846 }
1847 }
1848
1849 #[test]
1850 fn highlights_empty_for_blank_prompt() {
1851 let mut e = Editor::new(KeybindingMode::Vim);
1852 e.set_content("foo");
1853 e.handle_key(key(KeyCode::Char('/')));
1854 assert!(e.search_prompt().is_some());
1856 assert!(e.highlights_for_line(0).is_empty());
1857 }
1858
1859 #[test]
1860 fn highlights_emit_search_matches() {
1861 use crate::types::HighlightKind;
1862 let mut e = Editor::new(KeybindingMode::Vim);
1863 e.set_content("foo bar foo\nbaz qux\n");
1864 e.buffer_mut()
1866 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1867 let hs = e.highlights_for_line(0);
1868 assert_eq!(hs.len(), 2);
1869 for h in &hs {
1870 assert_eq!(h.kind, HighlightKind::SearchMatch);
1871 assert_eq!(h.range.start.line, 0);
1872 assert_eq!(h.range.end.line, 0);
1873 }
1874 }
1875
1876 #[test]
1877 fn highlights_empty_without_pattern() {
1878 let mut e = Editor::new(KeybindingMode::Vim);
1879 e.set_content("foo bar");
1880 assert!(e.highlights_for_line(0).is_empty());
1881 }
1882
1883 #[test]
1884 fn highlights_empty_for_out_of_range_line() {
1885 let mut e = Editor::new(KeybindingMode::Vim);
1886 e.set_content("foo");
1887 e.buffer_mut()
1888 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1889 assert!(e.highlights_for_line(99).is_empty());
1890 }
1891
1892 #[test]
1893 fn render_frame_reflects_mode_and_cursor() {
1894 use crate::types::{CursorShape, SnapshotMode};
1895 let mut e = Editor::new(KeybindingMode::Vim);
1896 e.set_content("alpha\nbeta");
1897 let f = e.render_frame();
1898 assert_eq!(f.mode, SnapshotMode::Normal);
1899 assert_eq!(f.cursor_shape, CursorShape::Block);
1900 assert_eq!(f.line_count, 2);
1901
1902 e.handle_key(key(KeyCode::Char('i')));
1903 let f = e.render_frame();
1904 assert_eq!(f.mode, SnapshotMode::Insert);
1905 assert_eq!(f.cursor_shape, CursorShape::Bar);
1906 }
1907
1908 #[test]
1909 fn snapshot_roundtrips_through_restore() {
1910 use crate::types::SnapshotMode;
1911 let mut e = Editor::new(KeybindingMode::Vim);
1912 e.set_content("alpha\nbeta\ngamma");
1913 e.jump_cursor(2, 3);
1914 let snap = e.take_snapshot();
1915 assert_eq!(snap.mode, SnapshotMode::Normal);
1916 assert_eq!(snap.cursor, (2, 3));
1917 assert_eq!(snap.lines.len(), 3);
1918
1919 let mut other = Editor::new(KeybindingMode::Vim);
1920 other.restore_snapshot(snap).expect("restore");
1921 assert_eq!(other.cursor(), (2, 3));
1922 assert_eq!(other.buffer().lines().len(), 3);
1923 }
1924
1925 #[test]
1926 fn restore_snapshot_rejects_version_mismatch() {
1927 let mut e = Editor::new(KeybindingMode::Vim);
1928 let mut snap = e.take_snapshot();
1929 snap.version = 9999;
1930 match e.restore_snapshot(snap) {
1931 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1932 assert_eq!(got, 9999);
1933 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1934 }
1935 other => panic!("expected SnapshotVersion err, got {other:?}"),
1936 }
1937 }
1938
1939 #[test]
1940 fn take_content_change_returns_some_on_first_dirty() {
1941 let mut e = Editor::new(KeybindingMode::Vim);
1942 e.set_content("hello");
1943 let first = e.take_content_change();
1944 assert!(first.is_some());
1945 let second = e.take_content_change();
1946 assert!(second.is_none());
1947 }
1948
1949 #[test]
1950 fn take_content_change_none_until_mutation() {
1951 let mut e = Editor::new(KeybindingMode::Vim);
1952 e.set_content("hello");
1953 e.take_content_change();
1955 assert!(e.take_content_change().is_none());
1956 e.handle_key(key(KeyCode::Char('i')));
1958 e.handle_key(key(KeyCode::Char('x')));
1959 let after = e.take_content_change();
1960 assert!(after.is_some());
1961 assert!(after.unwrap().contains('x'));
1962 }
1963
1964 #[test]
1965 fn vim_insert_to_normal() {
1966 let mut e = Editor::new(KeybindingMode::Vim);
1967 e.handle_key(key(KeyCode::Char('i')));
1968 e.handle_key(key(KeyCode::Esc));
1969 assert_eq!(e.vim_mode(), VimMode::Normal);
1970 }
1971
1972 #[test]
1973 fn vim_normal_to_visual() {
1974 let mut e = Editor::new(KeybindingMode::Vim);
1975 e.handle_key(key(KeyCode::Char('v')));
1976 assert_eq!(e.vim_mode(), VimMode::Visual);
1977 }
1978
1979 #[test]
1980 fn vim_visual_to_normal() {
1981 let mut e = Editor::new(KeybindingMode::Vim);
1982 e.handle_key(key(KeyCode::Char('v')));
1983 e.handle_key(key(KeyCode::Esc));
1984 assert_eq!(e.vim_mode(), VimMode::Normal);
1985 }
1986
1987 #[test]
1988 fn vim_shift_i_moves_to_first_non_whitespace() {
1989 let mut e = Editor::new(KeybindingMode::Vim);
1990 e.set_content(" hello");
1991 e.jump_cursor(0, 8);
1992 e.handle_key(shift_key(KeyCode::Char('I')));
1993 assert_eq!(e.vim_mode(), VimMode::Insert);
1994 assert_eq!(e.cursor(), (0, 3));
1995 }
1996
1997 #[test]
1998 fn vim_shift_a_moves_to_end_and_insert() {
1999 let mut e = Editor::new(KeybindingMode::Vim);
2000 e.set_content("hello");
2001 e.handle_key(shift_key(KeyCode::Char('A')));
2002 assert_eq!(e.vim_mode(), VimMode::Insert);
2003 assert_eq!(e.cursor().1, 5);
2004 }
2005
2006 #[test]
2007 fn count_10j_moves_down_10() {
2008 let mut e = Editor::new(KeybindingMode::Vim);
2009 e.set_content(
2010 (0..20)
2011 .map(|i| format!("line{i}"))
2012 .collect::<Vec<_>>()
2013 .join("\n")
2014 .as_str(),
2015 );
2016 for d in "10".chars() {
2017 e.handle_key(key(KeyCode::Char(d)));
2018 }
2019 e.handle_key(key(KeyCode::Char('j')));
2020 assert_eq!(e.cursor().0, 10);
2021 }
2022
2023 #[test]
2024 fn count_o_repeats_insert_on_esc() {
2025 let mut e = Editor::new(KeybindingMode::Vim);
2026 e.set_content("hello");
2027 for d in "3".chars() {
2028 e.handle_key(key(KeyCode::Char(d)));
2029 }
2030 e.handle_key(key(KeyCode::Char('o')));
2031 assert_eq!(e.vim_mode(), VimMode::Insert);
2032 for c in "world".chars() {
2033 e.handle_key(key(KeyCode::Char(c)));
2034 }
2035 e.handle_key(key(KeyCode::Esc));
2036 assert_eq!(e.vim_mode(), VimMode::Normal);
2037 assert_eq!(e.buffer().lines().len(), 4);
2038 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
2039 }
2040
2041 #[test]
2042 fn count_i_repeats_text_on_esc() {
2043 let mut e = Editor::new(KeybindingMode::Vim);
2044 e.set_content("");
2045 for d in "3".chars() {
2046 e.handle_key(key(KeyCode::Char(d)));
2047 }
2048 e.handle_key(key(KeyCode::Char('i')));
2049 for c in "ab".chars() {
2050 e.handle_key(key(KeyCode::Char(c)));
2051 }
2052 e.handle_key(key(KeyCode::Esc));
2053 assert_eq!(e.vim_mode(), VimMode::Normal);
2054 assert_eq!(e.buffer().lines()[0], "ababab");
2055 }
2056
2057 #[test]
2058 fn vim_shift_o_opens_line_above() {
2059 let mut e = Editor::new(KeybindingMode::Vim);
2060 e.set_content("hello");
2061 e.handle_key(shift_key(KeyCode::Char('O')));
2062 assert_eq!(e.vim_mode(), VimMode::Insert);
2063 assert_eq!(e.cursor(), (0, 0));
2064 assert_eq!(e.buffer().lines().len(), 2);
2065 }
2066
2067 #[test]
2068 fn vim_gg_goes_to_top() {
2069 let mut e = Editor::new(KeybindingMode::Vim);
2070 e.set_content("a\nb\nc");
2071 e.jump_cursor(2, 0);
2072 e.handle_key(key(KeyCode::Char('g')));
2073 e.handle_key(key(KeyCode::Char('g')));
2074 assert_eq!(e.cursor().0, 0);
2075 }
2076
2077 #[test]
2078 fn vim_shift_g_goes_to_bottom() {
2079 let mut e = Editor::new(KeybindingMode::Vim);
2080 e.set_content("a\nb\nc");
2081 e.handle_key(shift_key(KeyCode::Char('G')));
2082 assert_eq!(e.cursor().0, 2);
2083 }
2084
2085 #[test]
2086 fn vim_dd_deletes_line() {
2087 let mut e = Editor::new(KeybindingMode::Vim);
2088 e.set_content("first\nsecond");
2089 e.handle_key(key(KeyCode::Char('d')));
2090 e.handle_key(key(KeyCode::Char('d')));
2091 assert_eq!(e.buffer().lines().len(), 1);
2092 assert_eq!(e.buffer().lines()[0], "second");
2093 }
2094
2095 #[test]
2096 fn vim_dw_deletes_word() {
2097 let mut e = Editor::new(KeybindingMode::Vim);
2098 e.set_content("hello world");
2099 e.handle_key(key(KeyCode::Char('d')));
2100 e.handle_key(key(KeyCode::Char('w')));
2101 assert_eq!(e.vim_mode(), VimMode::Normal);
2102 assert!(!e.buffer().lines()[0].starts_with("hello"));
2103 }
2104
2105 #[test]
2106 fn vim_yy_yanks_line() {
2107 let mut e = Editor::new(KeybindingMode::Vim);
2108 e.set_content("hello\nworld");
2109 e.handle_key(key(KeyCode::Char('y')));
2110 e.handle_key(key(KeyCode::Char('y')));
2111 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
2112 }
2113
2114 #[test]
2115 fn vim_yy_does_not_move_cursor() {
2116 let mut e = Editor::new(KeybindingMode::Vim);
2117 e.set_content("first\nsecond\nthird");
2118 e.jump_cursor(1, 0);
2119 let before = e.cursor();
2120 e.handle_key(key(KeyCode::Char('y')));
2121 e.handle_key(key(KeyCode::Char('y')));
2122 assert_eq!(e.cursor(), before);
2123 assert_eq!(e.vim_mode(), VimMode::Normal);
2124 }
2125
2126 #[test]
2127 fn vim_yw_yanks_word() {
2128 let mut e = Editor::new(KeybindingMode::Vim);
2129 e.set_content("hello world");
2130 e.handle_key(key(KeyCode::Char('y')));
2131 e.handle_key(key(KeyCode::Char('w')));
2132 assert_eq!(e.vim_mode(), VimMode::Normal);
2133 assert!(e.last_yank.is_some());
2134 }
2135
2136 #[test]
2137 fn vim_cc_changes_line() {
2138 let mut e = Editor::new(KeybindingMode::Vim);
2139 e.set_content("hello\nworld");
2140 e.handle_key(key(KeyCode::Char('c')));
2141 e.handle_key(key(KeyCode::Char('c')));
2142 assert_eq!(e.vim_mode(), VimMode::Insert);
2143 }
2144
2145 #[test]
2146 fn vim_u_undoes_insert_session_as_chunk() {
2147 let mut e = Editor::new(KeybindingMode::Vim);
2148 e.set_content("hello");
2149 e.handle_key(key(KeyCode::Char('i')));
2150 e.handle_key(key(KeyCode::Enter));
2151 e.handle_key(key(KeyCode::Enter));
2152 e.handle_key(key(KeyCode::Esc));
2153 assert_eq!(e.buffer().lines().len(), 3);
2154 e.handle_key(key(KeyCode::Char('u')));
2155 assert_eq!(e.buffer().lines().len(), 1);
2156 assert_eq!(e.buffer().lines()[0], "hello");
2157 }
2158
2159 #[test]
2160 fn vim_undo_redo_roundtrip() {
2161 let mut e = Editor::new(KeybindingMode::Vim);
2162 e.set_content("hello");
2163 e.handle_key(key(KeyCode::Char('i')));
2164 for c in "world".chars() {
2165 e.handle_key(key(KeyCode::Char(c)));
2166 }
2167 e.handle_key(key(KeyCode::Esc));
2168 let after = e.buffer().lines()[0].clone();
2169 e.handle_key(key(KeyCode::Char('u')));
2170 assert_eq!(e.buffer().lines()[0], "hello");
2171 e.handle_key(ctrl_key(KeyCode::Char('r')));
2172 assert_eq!(e.buffer().lines()[0], after);
2173 }
2174
2175 #[test]
2176 fn vim_u_undoes_dd() {
2177 let mut e = Editor::new(KeybindingMode::Vim);
2178 e.set_content("first\nsecond");
2179 e.handle_key(key(KeyCode::Char('d')));
2180 e.handle_key(key(KeyCode::Char('d')));
2181 assert_eq!(e.buffer().lines().len(), 1);
2182 e.handle_key(key(KeyCode::Char('u')));
2183 assert_eq!(e.buffer().lines().len(), 2);
2184 assert_eq!(e.buffer().lines()[0], "first");
2185 }
2186
2187 #[test]
2188 fn vim_ctrl_r_redoes() {
2189 let mut e = Editor::new(KeybindingMode::Vim);
2190 e.set_content("hello");
2191 e.handle_key(ctrl_key(KeyCode::Char('r')));
2192 }
2193
2194 #[test]
2195 fn vim_r_replaces_char() {
2196 let mut e = Editor::new(KeybindingMode::Vim);
2197 e.set_content("hello");
2198 e.handle_key(key(KeyCode::Char('r')));
2199 e.handle_key(key(KeyCode::Char('x')));
2200 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
2201 }
2202
2203 #[test]
2204 fn vim_tilde_toggles_case() {
2205 let mut e = Editor::new(KeybindingMode::Vim);
2206 e.set_content("hello");
2207 e.handle_key(key(KeyCode::Char('~')));
2208 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
2209 }
2210
2211 #[test]
2212 fn vim_visual_d_cuts() {
2213 let mut e = Editor::new(KeybindingMode::Vim);
2214 e.set_content("hello");
2215 e.handle_key(key(KeyCode::Char('v')));
2216 e.handle_key(key(KeyCode::Char('l')));
2217 e.handle_key(key(KeyCode::Char('l')));
2218 e.handle_key(key(KeyCode::Char('d')));
2219 assert_eq!(e.vim_mode(), VimMode::Normal);
2220 assert!(e.last_yank.is_some());
2221 }
2222
2223 #[test]
2224 fn vim_visual_c_enters_insert() {
2225 let mut e = Editor::new(KeybindingMode::Vim);
2226 e.set_content("hello");
2227 e.handle_key(key(KeyCode::Char('v')));
2228 e.handle_key(key(KeyCode::Char('l')));
2229 e.handle_key(key(KeyCode::Char('c')));
2230 assert_eq!(e.vim_mode(), VimMode::Insert);
2231 }
2232
2233 #[test]
2234 fn vim_normal_unknown_key_consumed() {
2235 let mut e = Editor::new(KeybindingMode::Vim);
2236 let consumed = e.handle_key(key(KeyCode::Char('z')));
2238 assert!(consumed);
2239 }
2240
2241 #[test]
2242 fn force_normal_clears_operator() {
2243 let mut e = Editor::new(KeybindingMode::Vim);
2244 e.handle_key(key(KeyCode::Char('d')));
2245 e.force_normal();
2246 assert_eq!(e.vim_mode(), VimMode::Normal);
2247 }
2248
2249 fn many_lines(n: usize) -> String {
2250 (0..n)
2251 .map(|i| format!("line{i}"))
2252 .collect::<Vec<_>>()
2253 .join("\n")
2254 }
2255
2256 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
2257 e.set_viewport_height(height);
2258 }
2259
2260 #[test]
2261 fn zz_centers_cursor_in_viewport() {
2262 let mut e = Editor::new(KeybindingMode::Vim);
2263 e.set_content(&many_lines(100));
2264 prime_viewport(&mut e, 20);
2265 e.jump_cursor(50, 0);
2266 e.handle_key(key(KeyCode::Char('z')));
2267 e.handle_key(key(KeyCode::Char('z')));
2268 assert_eq!(e.buffer().viewport().top_row, 40);
2269 assert_eq!(e.cursor().0, 50);
2270 }
2271
2272 #[test]
2273 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
2274 let mut e = Editor::new(KeybindingMode::Vim);
2275 e.set_content(&many_lines(100));
2276 prime_viewport(&mut e, 20);
2277 e.jump_cursor(50, 0);
2278 e.handle_key(key(KeyCode::Char('z')));
2279 e.handle_key(key(KeyCode::Char('t')));
2280 assert_eq!(e.buffer().viewport().top_row, 45);
2283 assert_eq!(e.cursor().0, 50);
2284 }
2285
2286 #[test]
2287 fn ctrl_a_increments_number_at_cursor() {
2288 let mut e = Editor::new(KeybindingMode::Vim);
2289 e.set_content("x = 41");
2290 e.handle_key(ctrl_key(KeyCode::Char('a')));
2291 assert_eq!(e.buffer().lines()[0], "x = 42");
2292 assert_eq!(e.cursor(), (0, 5));
2293 }
2294
2295 #[test]
2296 fn ctrl_a_finds_number_to_right_of_cursor() {
2297 let mut e = Editor::new(KeybindingMode::Vim);
2298 e.set_content("foo 99 bar");
2299 e.handle_key(ctrl_key(KeyCode::Char('a')));
2300 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
2301 assert_eq!(e.cursor(), (0, 6));
2302 }
2303
2304 #[test]
2305 fn ctrl_a_with_count_adds_count() {
2306 let mut e = Editor::new(KeybindingMode::Vim);
2307 e.set_content("x = 10");
2308 for d in "5".chars() {
2309 e.handle_key(key(KeyCode::Char(d)));
2310 }
2311 e.handle_key(ctrl_key(KeyCode::Char('a')));
2312 assert_eq!(e.buffer().lines()[0], "x = 15");
2313 }
2314
2315 #[test]
2316 fn ctrl_x_decrements_number() {
2317 let mut e = Editor::new(KeybindingMode::Vim);
2318 e.set_content("n=5");
2319 e.handle_key(ctrl_key(KeyCode::Char('x')));
2320 assert_eq!(e.buffer().lines()[0], "n=4");
2321 }
2322
2323 #[test]
2324 fn ctrl_x_crosses_zero_into_negative() {
2325 let mut e = Editor::new(KeybindingMode::Vim);
2326 e.set_content("v=0");
2327 e.handle_key(ctrl_key(KeyCode::Char('x')));
2328 assert_eq!(e.buffer().lines()[0], "v=-1");
2329 }
2330
2331 #[test]
2332 fn ctrl_a_on_negative_number_increments_toward_zero() {
2333 let mut e = Editor::new(KeybindingMode::Vim);
2334 e.set_content("a = -5");
2335 e.handle_key(ctrl_key(KeyCode::Char('a')));
2336 assert_eq!(e.buffer().lines()[0], "a = -4");
2337 }
2338
2339 #[test]
2340 fn ctrl_a_noop_when_no_digit_on_line() {
2341 let mut e = Editor::new(KeybindingMode::Vim);
2342 e.set_content("no digits here");
2343 e.handle_key(ctrl_key(KeyCode::Char('a')));
2344 assert_eq!(e.buffer().lines()[0], "no digits here");
2345 }
2346
2347 #[test]
2348 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
2349 let mut e = Editor::new(KeybindingMode::Vim);
2350 e.set_content(&many_lines(100));
2351 prime_viewport(&mut e, 20);
2352 e.jump_cursor(50, 0);
2353 e.handle_key(key(KeyCode::Char('z')));
2354 e.handle_key(key(KeyCode::Char('b')));
2355 assert_eq!(e.buffer().viewport().top_row, 36);
2359 assert_eq!(e.cursor().0, 50);
2360 }
2361
2362 #[test]
2369 fn set_content_dirties_then_take_dirty_clears() {
2370 let mut e = Editor::new(KeybindingMode::Vim);
2371 e.set_content("hello");
2372 assert!(
2373 e.take_dirty(),
2374 "set_content should leave content_dirty=true"
2375 );
2376 assert!(!e.take_dirty(), "take_dirty should clear the flag");
2377 }
2378
2379 #[test]
2380 fn content_arc_returns_same_arc_until_mutation() {
2381 let mut e = Editor::new(KeybindingMode::Vim);
2382 e.set_content("hello");
2383 let a = e.content_arc();
2384 let b = e.content_arc();
2385 assert!(
2386 std::sync::Arc::ptr_eq(&a, &b),
2387 "repeated content_arc() should hit the cache"
2388 );
2389
2390 e.handle_key(key(KeyCode::Char('i')));
2392 e.handle_key(key(KeyCode::Char('!')));
2393 let c = e.content_arc();
2394 assert!(
2395 !std::sync::Arc::ptr_eq(&a, &c),
2396 "mutation should invalidate content_arc() cache"
2397 );
2398 assert!(c.contains('!'));
2399 }
2400
2401 #[test]
2402 fn content_arc_cache_invalidated_by_set_content() {
2403 let mut e = Editor::new(KeybindingMode::Vim);
2404 e.set_content("one");
2405 let a = e.content_arc();
2406 e.set_content("two");
2407 let b = e.content_arc();
2408 assert!(!std::sync::Arc::ptr_eq(&a, &b));
2409 assert!(b.starts_with("two"));
2410 }
2411
2412 #[test]
2418 fn mouse_click_past_eol_lands_on_last_char() {
2419 let mut e = Editor::new(KeybindingMode::Vim);
2420 e.set_content("hello");
2421 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2425 e.mouse_click(area, 78, 1);
2426 assert_eq!(e.cursor(), (0, 4));
2427 }
2428
2429 #[test]
2430 fn mouse_click_past_eol_handles_multibyte_line() {
2431 let mut e = Editor::new(KeybindingMode::Vim);
2432 e.set_content("héllo");
2435 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2436 e.mouse_click(area, 78, 1);
2437 assert_eq!(e.cursor(), (0, 4));
2438 }
2439
2440 #[test]
2441 fn mouse_click_inside_line_lands_on_clicked_char() {
2442 let mut e = Editor::new(KeybindingMode::Vim);
2443 e.set_content("hello world");
2444 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2447 e.mouse_click(area, 4, 1);
2448 assert_eq!(e.cursor(), (0, 0));
2449 e.mouse_click(area, 6, 1);
2450 assert_eq!(e.cursor(), (0, 2));
2451 }
2452}