1use crate::input::{Input, Key};
10use crate::vim::{self, VimState};
11use crate::{KeybindingMode, VimMode};
12#[cfg(feature = "crossterm")]
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14#[cfg(feature = "ratatui")]
15use ratatui::layout::Rect;
16use std::sync::atomic::{AtomicU16, Ordering};
17
18#[cfg(feature = "ratatui")]
25pub(crate) fn engine_style_to_ratatui(s: crate::types::Style) -> ratatui::style::Style {
26 use crate::types::Attrs;
27 use ratatui::style::{Color as RColor, Modifier as RMod, Style as RStyle};
28 let mut out = RStyle::default();
29 if let Some(c) = s.fg {
30 out = out.fg(RColor::Rgb(c.0, c.1, c.2));
31 }
32 if let Some(c) = s.bg {
33 out = out.bg(RColor::Rgb(c.0, c.1, c.2));
34 }
35 let mut m = RMod::empty();
36 if s.attrs.contains(Attrs::BOLD) {
37 m |= RMod::BOLD;
38 }
39 if s.attrs.contains(Attrs::ITALIC) {
40 m |= RMod::ITALIC;
41 }
42 if s.attrs.contains(Attrs::UNDERLINE) {
43 m |= RMod::UNDERLINED;
44 }
45 if s.attrs.contains(Attrs::REVERSE) {
46 m |= RMod::REVERSED;
47 }
48 if s.attrs.contains(Attrs::DIM) {
49 m |= RMod::DIM;
50 }
51 if s.attrs.contains(Attrs::STRIKE) {
52 m |= RMod::CROSSED_OUT;
53 }
54 out.add_modifier(m)
55}
56
57#[cfg(feature = "ratatui")]
61pub(crate) fn ratatui_style_to_engine(s: ratatui::style::Style) -> crate::types::Style {
62 use crate::types::{Attrs, Color, Style};
63 use ratatui::style::{Color as RColor, Modifier as RMod};
64 fn c(rc: RColor) -> Color {
65 match rc {
66 RColor::Rgb(r, g, b) => Color(r, g, b),
67 RColor::Black => Color(0, 0, 0),
68 RColor::Red => Color(205, 49, 49),
69 RColor::Green => Color(13, 188, 121),
70 RColor::Yellow => Color(229, 229, 16),
71 RColor::Blue => Color(36, 114, 200),
72 RColor::Magenta => Color(188, 63, 188),
73 RColor::Cyan => Color(17, 168, 205),
74 RColor::Gray => Color(229, 229, 229),
75 RColor::DarkGray => Color(102, 102, 102),
76 RColor::LightRed => Color(241, 76, 76),
77 RColor::LightGreen => Color(35, 209, 139),
78 RColor::LightYellow => Color(245, 245, 67),
79 RColor::LightBlue => Color(59, 142, 234),
80 RColor::LightMagenta => Color(214, 112, 214),
81 RColor::LightCyan => Color(41, 184, 219),
82 RColor::White => Color(255, 255, 255),
83 _ => Color(0, 0, 0),
84 }
85 }
86 let mut attrs = Attrs::empty();
87 if s.add_modifier.contains(RMod::BOLD) {
88 attrs |= Attrs::BOLD;
89 }
90 if s.add_modifier.contains(RMod::ITALIC) {
91 attrs |= Attrs::ITALIC;
92 }
93 if s.add_modifier.contains(RMod::UNDERLINED) {
94 attrs |= Attrs::UNDERLINE;
95 }
96 if s.add_modifier.contains(RMod::REVERSED) {
97 attrs |= Attrs::REVERSE;
98 }
99 if s.add_modifier.contains(RMod::DIM) {
100 attrs |= Attrs::DIM;
101 }
102 if s.add_modifier.contains(RMod::CROSSED_OUT) {
103 attrs |= Attrs::STRIKE;
104 }
105 Style {
106 fg: s.fg.map(c),
107 bg: s.bg.map(c),
108 attrs,
109 }
110}
111
112fn edit_to_editops(edit: &hjkl_buffer::Edit) -> Vec<crate::types::Edit> {
124 use crate::types::{Edit as Op, Pos};
125 use hjkl_buffer::Edit as B;
126 let to_pos = |p: hjkl_buffer::Position| Pos {
127 line: p.row as u32,
128 col: p.col as u32,
129 };
130 match edit {
131 B::InsertChar { at, ch } => vec![Op {
132 range: to_pos(*at)..to_pos(*at),
133 replacement: ch.to_string(),
134 }],
135 B::InsertStr { at, text } => vec![Op {
136 range: to_pos(*at)..to_pos(*at),
137 replacement: text.clone(),
138 }],
139 B::DeleteRange { start, end, .. } => vec![Op {
140 range: to_pos(*start)..to_pos(*end),
141 replacement: String::new(),
142 }],
143 B::Replace { start, end, with } => vec![Op {
144 range: to_pos(*start)..to_pos(*end),
145 replacement: with.clone(),
146 }],
147 B::JoinLines {
148 row,
149 count,
150 with_space,
151 } => {
152 let start = Pos {
157 line: *row as u32 + 1,
158 col: 0,
159 };
160 let end = Pos {
161 line: (*row + *count) as u32,
162 col: u32::MAX, };
164 vec![Op {
165 range: start..end,
166 replacement: if *with_space {
167 " ".into()
168 } else {
169 String::new()
170 },
171 }]
172 }
173 B::SplitLines {
174 row,
175 cols,
176 inserted_space: _,
177 } => {
178 cols.iter()
181 .map(|c| {
182 let p = Pos {
183 line: *row as u32,
184 col: *c as u32,
185 };
186 Op {
187 range: p..p,
188 replacement: "\n".into(),
189 }
190 })
191 .collect()
192 }
193 B::InsertBlock { at, chunks } => {
194 chunks
196 .iter()
197 .enumerate()
198 .map(|(i, chunk)| {
199 let p = Pos {
200 line: at.row as u32 + i as u32,
201 col: at.col as u32,
202 };
203 Op {
204 range: p..p,
205 replacement: chunk.clone(),
206 }
207 })
208 .collect()
209 }
210 B::DeleteBlockChunks { at, widths } => {
211 widths
214 .iter()
215 .enumerate()
216 .map(|(i, w)| {
217 let start = Pos {
218 line: at.row as u32 + i as u32,
219 col: at.col as u32,
220 };
221 let end = Pos {
222 line: at.row as u32 + i as u32,
223 col: at.col as u32 + *w as u32,
224 };
225 Op {
226 range: start..end,
227 replacement: String::new(),
228 }
229 })
230 .collect()
231 }
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub(super) enum CursorScrollTarget {
239 Center,
240 Top,
241 Bottom,
242}
243
244use crate::buf_helpers::{
252 apply_buffer_edit, buf_cursor_pos, buf_cursor_rc, buf_cursor_row, buf_line, buf_lines_to_vec,
253 buf_row_count, buf_set_cursor_rc,
254};
255
256pub struct Editor<
257 B: crate::types::Buffer = hjkl_buffer::Buffer,
258 H: crate::types::Host = crate::types::DefaultHost,
259> {
260 pub keybinding_mode: KeybindingMode,
261 pub last_yank: Option<String>,
263 pub(crate) vim: VimState,
268 pub(crate) undo_stack: Vec<(Vec<String>, (usize, usize))>,
272 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
274 pub(super) content_dirty: bool,
276 pub(super) cached_content: Option<std::sync::Arc<String>>,
281 pub(super) viewport_height: AtomicU16,
286 pub(super) pending_lsp: Option<LspIntent>,
290 pub(super) pending_fold_ops: Vec<crate::types::FoldOp>,
298 pub(super) buffer: B,
305 #[cfg(feature = "ratatui")]
317 pub(super) style_table: Vec<ratatui::style::Style>,
318 #[cfg(not(feature = "ratatui"))]
323 pub(super) engine_style_table: Vec<crate::types::Style>,
324 pub(crate) registers: crate::registers::Registers,
329 #[cfg(feature = "ratatui")]
336 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
337 pub(crate) settings: Settings,
342 pub(crate) marks: std::collections::BTreeMap<char, (usize, usize)>,
357 pub(crate) syntax_fold_ranges: Vec<(usize, usize)>,
363 pub(crate) change_log: Vec<crate::types::Edit>,
372 pub(crate) sticky_col: Option<usize>,
381 pub(crate) host: H,
389 pub(crate) last_emitted_mode: crate::VimMode,
394 pub(crate) search_state: crate::search::SearchState,
401 pub(crate) buffer_spans: Vec<Vec<hjkl_buffer::Span>>,
412}
413
414#[derive(Debug, Clone)]
417pub struct Settings {
418 pub shiftwidth: usize,
420 pub tabstop: usize,
423 pub ignore_case: bool,
426 pub smartcase: bool,
430 pub wrapscan: bool,
433 pub textwidth: usize,
435 pub expandtab: bool,
439 pub wrap: hjkl_buffer::Wrap,
445 pub readonly: bool,
449 pub autoindent: bool,
453 pub undo_levels: u32,
457 pub undo_break_on_motion: bool,
464 pub iskeyword: String,
470 pub timeout_len: core::time::Duration,
475}
476
477impl Default for Settings {
478 fn default() -> Self {
479 Self {
480 shiftwidth: 2,
481 tabstop: 8,
482 ignore_case: false,
483 smartcase: false,
484 wrapscan: true,
485 textwidth: 79,
486 expandtab: false,
487 wrap: hjkl_buffer::Wrap::None,
488 readonly: false,
489 autoindent: true,
490 undo_levels: 1000,
491 undo_break_on_motion: true,
492 iskeyword: "@,48-57,_,192-255".to_string(),
493 timeout_len: core::time::Duration::from_millis(1000),
494 }
495 }
496}
497
498fn settings_from_options(o: &crate::types::Options) -> Settings {
506 Settings {
507 shiftwidth: o.shiftwidth as usize,
508 tabstop: o.tabstop as usize,
509 ignore_case: o.ignorecase,
510 smartcase: o.smartcase,
511 wrapscan: o.wrapscan,
512 textwidth: o.textwidth as usize,
513 expandtab: o.expandtab,
514 wrap: match o.wrap {
515 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
516 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
517 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
518 },
519 readonly: o.readonly,
520 autoindent: o.autoindent,
521 undo_levels: o.undo_levels,
522 undo_break_on_motion: o.undo_break_on_motion,
523 iskeyword: o.iskeyword.clone(),
524 timeout_len: o.timeout_len,
525 }
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum LspIntent {
533 GotoDefinition,
535}
536
537impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
538 pub fn new(buffer: hjkl_buffer::Buffer, host: H, options: crate::types::Options) -> Self {
548 let settings = settings_from_options(&options);
549 Self {
550 keybinding_mode: KeybindingMode::Vim,
551 last_yank: None,
552 vim: VimState::default(),
553 undo_stack: Vec::new(),
554 redo_stack: Vec::new(),
555 content_dirty: false,
556 cached_content: None,
557 viewport_height: AtomicU16::new(0),
558 pending_lsp: None,
559 pending_fold_ops: Vec::new(),
560 buffer,
561 #[cfg(feature = "ratatui")]
562 style_table: Vec::new(),
563 #[cfg(not(feature = "ratatui"))]
564 engine_style_table: Vec::new(),
565 registers: crate::registers::Registers::default(),
566 #[cfg(feature = "ratatui")]
567 styled_spans: Vec::new(),
568 settings,
569 marks: std::collections::BTreeMap::new(),
570 syntax_fold_ranges: Vec::new(),
571 change_log: Vec::new(),
572 sticky_col: None,
573 host,
574 last_emitted_mode: crate::VimMode::Normal,
575 search_state: crate::search::SearchState::new(),
576 buffer_spans: Vec::new(),
577 }
578 }
579}
580
581impl<B: crate::types::Buffer, H: crate::types::Host> Editor<B, H> {
582 pub fn buffer(&self) -> &B {
585 &self.buffer
586 }
587
588 pub fn buffer_mut(&mut self) -> &mut B {
590 &mut self.buffer
591 }
592
593 pub fn host(&self) -> &H {
595 &self.host
596 }
597
598 pub fn host_mut(&mut self) -> &mut H {
600 &mut self.host
601 }
602}
603
604impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
605 pub fn set_iskeyword(&mut self, spec: impl Into<String>) {
612 self.settings.iskeyword = spec.into();
613 }
614
615 pub(crate) fn emit_cursor_shape_if_changed(&mut self) {
620 let mode = self.vim_mode();
621 if mode == self.last_emitted_mode {
622 return;
623 }
624 let shape = match mode {
625 crate::VimMode::Insert => crate::types::CursorShape::Bar,
626 _ => crate::types::CursorShape::Block,
627 };
628 self.host.emit_cursor_shape(shape);
629 self.last_emitted_mode = mode;
630 }
631
632 pub(crate) fn record_yank_to_host(&mut self, text: String) {
639 self.host.write_clipboard(text.clone());
640 self.last_yank = Some(text);
641 }
642
643 pub fn sticky_col(&self) -> Option<usize> {
648 self.sticky_col
649 }
650
651 pub fn set_sticky_col(&mut self, col: Option<usize>) {
655 self.sticky_col = col;
656 }
657
658 pub fn mark(&self, c: char) -> Option<(usize, usize)> {
666 self.marks.get(&c).copied()
667 }
668
669 pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
672 self.marks.insert(c, pos);
673 }
674
675 pub fn clear_mark(&mut self, c: char) {
677 self.marks.remove(&c);
678 }
679
680 #[deprecated(
685 since = "0.0.36",
686 note = "use Editor::mark — lowercase + uppercase marks now live in a single map"
687 )]
688 pub fn buffer_mark(&self, c: char) -> Option<(usize, usize)> {
689 self.mark(c)
690 }
691
692 pub fn pop_last_undo(&mut self) -> bool {
699 self.undo_stack.pop().is_some()
700 }
701
702 pub fn marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
707 self.marks.iter().map(|(c, p)| (*c, *p))
708 }
709
710 #[deprecated(
715 since = "0.0.36",
716 note = "use Editor::marks — lowercase + uppercase marks now live in a single map"
717 )]
718 pub fn buffer_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
719 self.marks
720 .iter()
721 .filter(|(c, _)| c.is_ascii_lowercase())
722 .map(|(c, p)| (*c, *p))
723 }
724
725 pub fn last_jump_back(&self) -> Option<(usize, usize)> {
728 self.vim.jump_back.last().copied()
729 }
730
731 pub fn last_edit_pos(&self) -> Option<(usize, usize)> {
734 self.vim.last_edit_pos
735 }
736
737 pub fn file_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
748 self.marks
749 .iter()
750 .filter(|(c, _)| c.is_ascii_uppercase())
751 .map(|(c, p)| (*c, *p))
752 }
753
754 pub fn syntax_fold_ranges(&self) -> &[(usize, usize)] {
759 &self.syntax_fold_ranges
760 }
761
762 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
763 self.syntax_fold_ranges = ranges;
764 }
765
766 pub fn settings(&self) -> &Settings {
769 &self.settings
770 }
771
772 pub fn settings_mut(&mut self) -> &mut Settings {
777 &mut self.settings
778 }
779
780 pub fn search_state(&self) -> &crate::search::SearchState {
785 &self.search_state
786 }
787
788 pub fn search_state_mut(&mut self) -> &mut crate::search::SearchState {
792 &mut self.search_state
793 }
794
795 pub fn set_search_pattern(&mut self, pattern: Option<regex::Regex>) {
801 self.search_state.set_pattern(pattern);
802 }
803
804 pub fn search_advance_forward(&mut self, skip_current: bool) -> bool {
809 crate::search::search_forward(&mut self.buffer, &mut self.search_state, skip_current)
810 }
811
812 pub fn search_advance_backward(&mut self, skip_current: bool) -> bool {
814 crate::search::search_backward(&mut self.buffer, &mut self.search_state, skip_current)
815 }
816
817 #[cfg(feature = "ratatui")]
828 pub fn install_ratatui_syntax_spans(
829 &mut self,
830 spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
831 ) {
832 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
833 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
834 .collect();
835 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
836 for (row, row_spans) in spans.iter().enumerate() {
837 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
838 let mut translated = Vec::with_capacity(row_spans.len());
839 for (start, end, style) in row_spans {
840 let end_clamped = (*end).min(line_len);
841 if end_clamped <= *start {
842 continue;
843 }
844 let id = self.intern_ratatui_style(*style);
845 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
846 }
847 by_row.push(translated);
848 }
849 self.buffer_spans = by_row;
850 self.styled_spans = spans;
851 }
852
853 pub fn yank(&self) -> &str {
855 &self.registers.unnamed.text
856 }
857
858 pub fn registers(&self) -> &crate::registers::Registers {
860 &self.registers
861 }
862
863 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
868 self.registers.set_clipboard(text, linewise);
869 }
870
871 pub fn pending_register_is_clipboard(&self) -> bool {
875 matches!(self.vim.pending_register, Some('+') | Some('*'))
876 }
877
878 pub fn set_yank(&mut self, text: impl Into<String>) {
882 let text = text.into();
883 let linewise = self.vim.yank_linewise;
884 self.registers.unnamed = crate::registers::Slot { text, linewise };
885 }
886
887 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
891 self.vim.yank_linewise = linewise;
892 let target = self.vim.pending_register.take();
893 self.registers.record_yank(text, linewise, target);
894 }
895
896 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
901 if let Some(slot) = match reg {
902 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
903 'A'..='Z' => {
904 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
905 }
906 _ => None,
907 } {
908 slot.text = text;
909 slot.linewise = false;
910 }
911 }
912
913 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
916 self.vim.yank_linewise = linewise;
917 let target = self.vim.pending_register.take();
918 self.registers.record_delete(text, linewise, target);
919 }
920
921 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
930 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
931 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
932 .collect();
933 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
934 #[cfg(feature = "ratatui")]
935 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
936 Vec::with_capacity(spans.len());
937 for (row, row_spans) in spans.iter().enumerate() {
938 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
939 let mut translated = Vec::with_capacity(row_spans.len());
940 #[cfg(feature = "ratatui")]
941 let mut translated_r = Vec::with_capacity(row_spans.len());
942 for (start, end, style) in row_spans {
943 let end_clamped = (*end).min(line_len);
944 if end_clamped <= *start {
945 continue;
946 }
947 let id = self.intern_style(*style);
948 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
949 #[cfg(feature = "ratatui")]
950 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
951 }
952 by_row.push(translated);
953 #[cfg(feature = "ratatui")]
954 ratatui_spans.push(translated_r);
955 }
956 self.buffer_spans = by_row;
957 #[cfg(feature = "ratatui")]
958 {
959 self.styled_spans = ratatui_spans;
960 }
961 }
962
963 #[cfg(feature = "ratatui")]
972 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
973 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
974 return idx as u32;
975 }
976 self.style_table.push(style);
977 (self.style_table.len() - 1) as u32
978 }
979
980 #[cfg(feature = "ratatui")]
984 pub fn style_table(&self) -> &[ratatui::style::Style] {
985 &self.style_table
986 }
987
988 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
997 &self.buffer_spans
998 }
999
1000 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1015 #[cfg(feature = "ratatui")]
1016 {
1017 let r = engine_style_to_ratatui(style);
1018 self.intern_ratatui_style(r)
1019 }
1020 #[cfg(not(feature = "ratatui"))]
1021 {
1022 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1023 return idx as u32;
1024 }
1025 self.engine_style_table.push(style);
1026 (self.engine_style_table.len() - 1) as u32
1027 }
1028 }
1029
1030 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1034 #[cfg(feature = "ratatui")]
1035 {
1036 let r = self.style_table.get(id as usize).copied()?;
1037 Some(ratatui_style_to_engine(r))
1038 }
1039 #[cfg(not(feature = "ratatui"))]
1040 {
1041 self.engine_style_table.get(id as usize).copied()
1042 }
1043 }
1044
1045 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1049
1050 pub fn set_viewport_top(&mut self, row: usize) {
1058 let last = buf_row_count(&self.buffer).saturating_sub(1);
1059 let target = row.min(last);
1060 self.host.viewport_mut().top_row = target;
1061 }
1062
1063 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1067 buf_set_cursor_rc(&mut self.buffer, row, col);
1068 }
1069
1070 pub fn cursor(&self) -> (usize, usize) {
1078 buf_cursor_rc(&self.buffer)
1079 }
1080
1081 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1084 self.pending_lsp.take()
1085 }
1086
1087 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1101 std::mem::take(&mut self.pending_fold_ops)
1102 }
1103
1104 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1114 use crate::types::FoldProvider;
1115 self.pending_fold_ops.push(op);
1116 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1117 provider.apply(op);
1118 }
1119
1120 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1127 let height = self.viewport_height_value();
1128 self.host.viewport_mut().height = height;
1129 }
1130
1131 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1135 self.sync_buffer_from_textarea();
1136 }
1137
1138 pub fn record_jump(&mut self, pos: (usize, usize)) {
1143 const JUMPLIST_MAX: usize = 100;
1144 self.vim.jump_back.push(pos);
1145 if self.vim.jump_back.len() > JUMPLIST_MAX {
1146 self.vim.jump_back.remove(0);
1147 }
1148 self.vim.jump_fwd.clear();
1149 }
1150
1151 pub fn set_viewport_height(&self, height: u16) {
1154 self.viewport_height.store(height, Ordering::Relaxed);
1155 }
1156
1157 pub fn viewport_height_value(&self) -> u16 {
1159 self.viewport_height.load(Ordering::Relaxed)
1160 }
1161
1162 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1171 if self.settings.readonly {
1178 let _ = edit;
1179 return hjkl_buffer::Edit::InsertStr {
1180 at: buf_cursor_pos(&self.buffer),
1181 text: String::new(),
1182 };
1183 }
1184 let pre_row = buf_cursor_row(&self.buffer);
1185 let pre_rows = buf_row_count(&self.buffer);
1186 self.change_log.extend(edit_to_editops(&edit));
1190 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1196 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1197 let lo = pre_row.min(pos_row);
1203 let hi = pre_row.max(pos_row);
1204 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1205 start_row: lo,
1206 end_row: hi,
1207 });
1208 self.vim.last_edit_pos = Some((pos_row, pos_col));
1209 let entry = (pos_row, pos_col);
1214 if self.vim.change_list.last() != Some(&entry) {
1215 if let Some(idx) = self.vim.change_list_cursor.take() {
1216 self.vim.change_list.truncate(idx + 1);
1217 }
1218 self.vim.change_list.push(entry);
1219 let len = self.vim.change_list.len();
1220 if len > crate::vim::CHANGE_LIST_MAX {
1221 self.vim
1222 .change_list
1223 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1224 }
1225 }
1226 self.vim.change_list_cursor = None;
1227 let post_rows = buf_row_count(&self.buffer);
1231 let delta = post_rows as isize - pre_rows as isize;
1232 if delta != 0 {
1233 self.shift_marks_after_edit(pre_row, delta);
1234 }
1235 self.push_buffer_content_to_textarea();
1236 self.mark_content_dirty();
1237 inverse
1238 }
1239
1240 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1245 if delta == 0 {
1246 return;
1247 }
1248 let drop_end = if delta < 0 {
1251 edit_start.saturating_add((-delta) as usize)
1252 } else {
1253 edit_start
1254 };
1255 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1256
1257 let mut to_drop: Vec<char> = Vec::new();
1260 for (c, (row, _col)) in self.marks.iter_mut() {
1261 if (edit_start..drop_end).contains(row) {
1262 to_drop.push(*c);
1263 } else if *row >= shift_threshold {
1264 *row = ((*row as isize) + delta).max(0) as usize;
1265 }
1266 }
1267 for c in to_drop {
1268 self.marks.remove(&c);
1269 }
1270
1271 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1272 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1273 for (row, _) in entries.iter_mut() {
1274 if *row >= shift_threshold {
1275 *row = ((*row as isize) + delta).max(0) as usize;
1276 }
1277 }
1278 };
1279 shift_jumps(&mut self.vim.jump_back);
1280 shift_jumps(&mut self.vim.jump_fwd);
1281 }
1282
1283 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1291
1292 pub fn mark_content_dirty(&mut self) {
1298 self.content_dirty = true;
1299 self.cached_content = None;
1300 }
1301
1302 pub fn take_dirty(&mut self) -> bool {
1304 let dirty = self.content_dirty;
1305 self.content_dirty = false;
1306 dirty
1307 }
1308
1309 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1319 if !self.content_dirty {
1320 return None;
1321 }
1322 let arc = self.content_arc();
1323 self.content_dirty = false;
1324 Some(arc)
1325 }
1326
1327 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1330 let cursor = buf_cursor_row(&self.buffer);
1331 let top = self.host.viewport().top_row;
1332 cursor.saturating_sub(top).min(height as usize - 1) as u16
1333 }
1334
1335 pub fn cursor_screen_pos(
1345 &self,
1346 area_x: u16,
1347 area_y: u16,
1348 area_width: u16,
1349 area_height: u16,
1350 ) -> Option<(u16, u16)> {
1351 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1352 let v = self.host.viewport();
1353 if pos_row < v.top_row || pos_col < v.top_col {
1354 return None;
1355 }
1356 let lnum_width = buf_row_count(&self.buffer).to_string().len() as u16 + 2;
1357 let dy = (pos_row - v.top_row) as u16;
1358 let dx = (pos_col - v.top_col) as u16;
1359 if dy >= area_height || dx + lnum_width >= area_width {
1360 return None;
1361 }
1362 Some((area_x + lnum_width + dx, area_y + dy))
1363 }
1364
1365 #[cfg(feature = "ratatui")]
1371 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1372 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1373 }
1374
1375 pub fn vim_mode(&self) -> VimMode {
1376 self.vim.public_mode()
1377 }
1378
1379 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1385 self.vim.search_prompt.as_ref()
1386 }
1387
1388 pub fn last_search(&self) -> Option<&str> {
1391 self.vim.last_search.as_deref()
1392 }
1393
1394 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1398 if self.vim_mode() != VimMode::Visual {
1399 return None;
1400 }
1401 let anchor = self.vim.visual_anchor;
1402 let cursor = self.cursor();
1403 let (start, end) = if anchor <= cursor {
1404 (anchor, cursor)
1405 } else {
1406 (cursor, anchor)
1407 };
1408 Some((start, end))
1409 }
1410
1411 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1414 if self.vim_mode() != VimMode::VisualLine {
1415 return None;
1416 }
1417 let anchor = self.vim.visual_line_anchor;
1418 let cursor = buf_cursor_row(&self.buffer);
1419 Some((anchor.min(cursor), anchor.max(cursor)))
1420 }
1421
1422 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1423 if self.vim_mode() != VimMode::VisualBlock {
1424 return None;
1425 }
1426 let (ar, ac) = self.vim.block_anchor;
1427 let cr = buf_cursor_row(&self.buffer);
1428 let cc = self.vim.block_vcol;
1429 let top = ar.min(cr);
1430 let bot = ar.max(cr);
1431 let left = ac.min(cc);
1432 let right = ac.max(cc);
1433 Some((top, bot, left, right))
1434 }
1435
1436 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1442 use hjkl_buffer::{Position, Selection};
1443 match self.vim_mode() {
1444 VimMode::Visual => {
1445 let (ar, ac) = self.vim.visual_anchor;
1446 let head = buf_cursor_pos(&self.buffer);
1447 Some(Selection::Char {
1448 anchor: Position::new(ar, ac),
1449 head,
1450 })
1451 }
1452 VimMode::VisualLine => {
1453 let anchor_row = self.vim.visual_line_anchor;
1454 let head_row = buf_cursor_row(&self.buffer);
1455 Some(Selection::Line {
1456 anchor_row,
1457 head_row,
1458 })
1459 }
1460 VimMode::VisualBlock => {
1461 let (ar, ac) = self.vim.block_anchor;
1462 let cr = buf_cursor_row(&self.buffer);
1463 let cc = self.vim.block_vcol;
1464 Some(Selection::Block {
1465 anchor: Position::new(ar, ac),
1466 head: Position::new(cr, cc),
1467 })
1468 }
1469 _ => None,
1470 }
1471 }
1472
1473 pub fn force_normal(&mut self) {
1475 self.vim.force_normal();
1476 }
1477
1478 pub fn content(&self) -> String {
1479 let n = buf_row_count(&self.buffer);
1480 let mut s = String::new();
1481 for r in 0..n {
1482 if r > 0 {
1483 s.push('\n');
1484 }
1485 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1486 }
1487 s.push('\n');
1488 s
1489 }
1490
1491 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
1496 if let Some(arc) = &self.cached_content {
1497 return std::sync::Arc::clone(arc);
1498 }
1499 let arc = std::sync::Arc::new(self.content());
1500 self.cached_content = Some(std::sync::Arc::clone(&arc));
1501 arc
1502 }
1503
1504 pub fn set_content(&mut self, text: &str) {
1505 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
1506 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
1507 lines.pop();
1508 }
1509 if lines.is_empty() {
1510 lines.push(String::new());
1511 }
1512 let _ = lines;
1513 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
1514 self.undo_stack.clear();
1515 self.redo_stack.clear();
1516 self.mark_content_dirty();
1517 }
1518
1519 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
1535 use crate::{PlannedInput, SpecialKey};
1536 let (key, mods) = match input {
1537 PlannedInput::Char(c, m) => (Key::Char(c), m),
1538 PlannedInput::Key(k, m) => {
1539 let key = match k {
1540 SpecialKey::Esc => Key::Esc,
1541 SpecialKey::Enter => Key::Enter,
1542 SpecialKey::Backspace => Key::Backspace,
1543 SpecialKey::Tab => Key::Tab,
1544 SpecialKey::BackTab => Key::Tab,
1548 SpecialKey::Up => Key::Up,
1549 SpecialKey::Down => Key::Down,
1550 SpecialKey::Left => Key::Left,
1551 SpecialKey::Right => Key::Right,
1552 SpecialKey::Home => Key::Home,
1553 SpecialKey::End => Key::End,
1554 SpecialKey::PageUp => Key::PageUp,
1555 SpecialKey::PageDown => Key::PageDown,
1556 SpecialKey::Insert => Key::Null,
1560 SpecialKey::Delete => Key::Delete,
1561 SpecialKey::F(_) => Key::Null,
1562 };
1563 let m = if matches!(k, SpecialKey::BackTab) {
1564 crate::Modifiers { shift: true, ..m }
1565 } else {
1566 m
1567 };
1568 (key, m)
1569 }
1570 PlannedInput::Mouse(_)
1572 | PlannedInput::Paste(_)
1573 | PlannedInput::FocusGained
1574 | PlannedInput::FocusLost
1575 | PlannedInput::Resize(_, _) => return false,
1576 };
1577 if key == Key::Null {
1578 return false;
1579 }
1580 let event = Input {
1581 key,
1582 ctrl: mods.ctrl,
1583 alt: mods.alt,
1584 shift: mods.shift,
1585 };
1586 let consumed = vim::step(self, event);
1587 self.emit_cursor_shape_if_changed();
1588 consumed
1589 }
1590
1591 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
1608 std::mem::take(&mut self.change_log)
1609 }
1610
1611 pub fn current_options(&self) -> crate::types::Options {
1621 crate::types::Options {
1622 shiftwidth: self.settings.shiftwidth as u32,
1623 tabstop: self.settings.tabstop as u32,
1624 textwidth: self.settings.textwidth as u32,
1625 expandtab: self.settings.expandtab,
1626 ignorecase: self.settings.ignore_case,
1627 smartcase: self.settings.smartcase,
1628 wrapscan: self.settings.wrapscan,
1629 wrap: match self.settings.wrap {
1630 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
1631 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
1632 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
1633 },
1634 readonly: self.settings.readonly,
1635 autoindent: self.settings.autoindent,
1636 undo_levels: self.settings.undo_levels,
1637 undo_break_on_motion: self.settings.undo_break_on_motion,
1638 iskeyword: self.settings.iskeyword.clone(),
1639 timeout_len: self.settings.timeout_len,
1640 ..crate::types::Options::default()
1641 }
1642 }
1643
1644 pub fn apply_options(&mut self, opts: &crate::types::Options) {
1649 self.settings.shiftwidth = opts.shiftwidth as usize;
1650 self.settings.tabstop = opts.tabstop as usize;
1651 self.settings.textwidth = opts.textwidth as usize;
1652 self.settings.expandtab = opts.expandtab;
1653 self.settings.ignore_case = opts.ignorecase;
1654 self.settings.smartcase = opts.smartcase;
1655 self.settings.wrapscan = opts.wrapscan;
1656 self.settings.wrap = match opts.wrap {
1657 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
1658 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
1659 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
1660 };
1661 self.settings.readonly = opts.readonly;
1662 self.settings.autoindent = opts.autoindent;
1663 self.settings.undo_levels = opts.undo_levels;
1664 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
1665 self.set_iskeyword(opts.iskeyword.clone());
1666 self.settings.timeout_len = opts.timeout_len;
1667 }
1668
1669 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
1679 use crate::types::{Highlight, HighlightKind, Pos};
1680 let sel = self.buffer_selection()?;
1681 let (start, end) = match sel {
1682 hjkl_buffer::Selection::Char { anchor, head } => {
1683 let a = (anchor.row, anchor.col);
1684 let h = (head.row, head.col);
1685 if a <= h { (a, h) } else { (h, a) }
1686 }
1687 hjkl_buffer::Selection::Line {
1688 anchor_row,
1689 head_row,
1690 } => {
1691 let (top, bot) = if anchor_row <= head_row {
1692 (anchor_row, head_row)
1693 } else {
1694 (head_row, anchor_row)
1695 };
1696 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
1697 ((top, 0), (bot, last_col))
1698 }
1699 hjkl_buffer::Selection::Block { anchor, head } => {
1700 let (top, bot) = if anchor.row <= head.row {
1701 (anchor.row, head.row)
1702 } else {
1703 (head.row, anchor.row)
1704 };
1705 let (left, right) = if anchor.col <= head.col {
1706 (anchor.col, head.col)
1707 } else {
1708 (head.col, anchor.col)
1709 };
1710 ((top, left), (bot, right))
1711 }
1712 };
1713 Some(Highlight {
1714 range: Pos {
1715 line: start.0 as u32,
1716 col: start.1 as u32,
1717 }..Pos {
1718 line: end.0 as u32,
1719 col: end.1 as u32,
1720 },
1721 kind: HighlightKind::Selection,
1722 })
1723 }
1724
1725 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
1744 use crate::types::{Highlight, HighlightKind, Pos};
1745 let row = line as usize;
1746 if row >= buf_row_count(&self.buffer) {
1747 return Vec::new();
1748 }
1749
1750 if let Some(prompt) = self.search_prompt() {
1753 if prompt.text.is_empty() {
1754 return Vec::new();
1755 }
1756 let Ok(re) = regex::Regex::new(&prompt.text) else {
1757 return Vec::new();
1758 };
1759 let Some(haystack) = buf_line(&self.buffer, row) else {
1760 return Vec::new();
1761 };
1762 return re
1763 .find_iter(haystack)
1764 .map(|m| Highlight {
1765 range: Pos {
1766 line,
1767 col: m.start() as u32,
1768 }..Pos {
1769 line,
1770 col: m.end() as u32,
1771 },
1772 kind: HighlightKind::IncSearch,
1773 })
1774 .collect();
1775 }
1776
1777 if self.search_state.pattern.is_none() {
1778 return Vec::new();
1779 }
1780 let dgen = crate::types::Query::dirty_gen(&self.buffer);
1781 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
1782 .into_iter()
1783 .map(|(start, end)| Highlight {
1784 range: Pos {
1785 line,
1786 col: start as u32,
1787 }..Pos {
1788 line,
1789 col: end as u32,
1790 },
1791 kind: HighlightKind::SearchMatch,
1792 })
1793 .collect()
1794 }
1795
1796 pub fn render_frame(&self) -> crate::types::RenderFrame {
1806 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
1807 let (cursor_row, cursor_col) = self.cursor();
1808 let (mode, shape) = match self.vim_mode() {
1809 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
1810 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
1811 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
1812 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
1813 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
1814 };
1815 RenderFrame {
1816 mode,
1817 cursor_row: cursor_row as u32,
1818 cursor_col: cursor_col as u32,
1819 cursor_shape: shape,
1820 viewport_top: self.host.viewport().top_row as u32,
1821 line_count: crate::types::Query::line_count(&self.buffer),
1822 }
1823 }
1824
1825 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
1838 use crate::types::{EditorSnapshot, SnapshotMode};
1839 let mode = match self.vim_mode() {
1840 crate::VimMode::Normal => SnapshotMode::Normal,
1841 crate::VimMode::Insert => SnapshotMode::Insert,
1842 crate::VimMode::Visual => SnapshotMode::Visual,
1843 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
1844 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
1845 };
1846 let cursor = self.cursor();
1847 let cursor = (cursor.0 as u32, cursor.1 as u32);
1848 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
1849 let viewport_top = self.host.viewport().top_row as u32;
1850 let marks = self
1851 .marks
1852 .iter()
1853 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
1854 .collect();
1855 EditorSnapshot {
1856 version: EditorSnapshot::VERSION,
1857 mode,
1858 cursor,
1859 lines,
1860 viewport_top,
1861 registers: self.registers.clone(),
1862 marks,
1863 }
1864 }
1865
1866 pub fn restore_snapshot(
1874 &mut self,
1875 snap: crate::types::EditorSnapshot,
1876 ) -> Result<(), crate::EngineError> {
1877 use crate::types::EditorSnapshot;
1878 if snap.version != EditorSnapshot::VERSION {
1879 return Err(crate::EngineError::SnapshotVersion(
1880 snap.version,
1881 EditorSnapshot::VERSION,
1882 ));
1883 }
1884 let text = snap.lines.join("\n");
1885 self.set_content(&text);
1886 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1887 self.host.viewport_mut().top_row = snap.viewport_top as usize;
1888 self.registers = snap.registers;
1889 self.marks = snap
1890 .marks
1891 .into_iter()
1892 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1893 .collect();
1894 Ok(())
1895 }
1896
1897 pub fn seed_yank(&mut self, text: String) {
1901 let linewise = text.ends_with('\n');
1902 self.vim.yank_linewise = linewise;
1903 self.registers.unnamed = crate::registers::Slot { text, linewise };
1904 }
1905
1906 pub fn scroll_down(&mut self, rows: i16) {
1911 self.scroll_viewport(rows);
1912 }
1913
1914 pub fn scroll_up(&mut self, rows: i16) {
1918 self.scroll_viewport(-rows);
1919 }
1920
1921 const SCROLLOFF: usize = 5;
1925
1926 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1931 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1932 if height == 0 {
1933 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
1940 crate::viewport_math::ensure_cursor_visible(
1941 &self.buffer,
1942 &folds,
1943 self.host.viewport_mut(),
1944 );
1945 return;
1946 }
1947 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1951 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
1954 self.ensure_scrolloff_wrap(height, margin);
1955 return;
1956 }
1957 let cursor_row = buf_cursor_row(&self.buffer);
1958 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
1959 let v = self.host.viewport_mut();
1960 if cursor_row < v.top_row + margin {
1962 v.top_row = cursor_row.saturating_sub(margin);
1963 }
1964 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1966 if cursor_row > v.top_row + max_bottom {
1967 v.top_row = cursor_row.saturating_sub(max_bottom);
1968 }
1969 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1971 if v.top_row > max_top {
1972 v.top_row = max_top;
1973 }
1974 let cursor = buf_cursor_pos(&self.buffer);
1977 self.host.viewport_mut().ensure_visible(cursor);
1978 }
1979
1980 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1985 let cursor_row = buf_cursor_row(&self.buffer);
1986 if cursor_row < self.host.viewport().top_row {
1989 let v = self.host.viewport_mut();
1990 v.top_row = cursor_row;
1991 v.top_col = 0;
1992 }
1993 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2002 loop {
2003 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2004 let csr =
2005 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2006 .unwrap_or(0);
2007 if csr <= max_csr {
2008 break;
2009 }
2010 let top = self.host.viewport().top_row;
2011 let row_count = buf_row_count(&self.buffer);
2012 let next = {
2013 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2014 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2015 };
2016 let Some(next) = next else {
2017 break;
2018 };
2019 if next > cursor_row {
2021 self.host.viewport_mut().top_row = cursor_row;
2022 break;
2023 }
2024 self.host.viewport_mut().top_row = next;
2025 }
2026 loop {
2029 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2030 let csr =
2031 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2032 .unwrap_or(0);
2033 if csr >= margin {
2034 break;
2035 }
2036 let top = self.host.viewport().top_row;
2037 let prev = {
2038 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2039 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2040 };
2041 let Some(prev) = prev else {
2042 break;
2043 };
2044 self.host.viewport_mut().top_row = prev;
2045 }
2046 let max_top = {
2051 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2052 crate::viewport_math::max_top_for_height(
2053 &self.buffer,
2054 &folds,
2055 self.host.viewport(),
2056 height,
2057 )
2058 };
2059 if self.host.viewport().top_row > max_top {
2060 self.host.viewport_mut().top_row = max_top;
2061 }
2062 self.host.viewport_mut().top_col = 0;
2063 }
2064
2065 fn scroll_viewport(&mut self, delta: i16) {
2066 if delta == 0 {
2067 return;
2068 }
2069 let total_rows = buf_row_count(&self.buffer) as isize;
2071 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2072 let cur_top = self.host.viewport().top_row as isize;
2073 let new_top = (cur_top + delta as isize)
2074 .max(0)
2075 .min((total_rows - 1).max(0)) as usize;
2076 self.host.viewport_mut().top_row = new_top;
2077 let _ = cur_top;
2080 if height == 0 {
2081 return;
2082 }
2083 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2086 let margin = Self::SCROLLOFF.min(height / 2);
2087 let min_row = new_top + margin;
2088 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2089 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2090 if target_row != cursor_row {
2091 let line_len = buf_line(&self.buffer, target_row)
2092 .map(|l| l.chars().count())
2093 .unwrap_or(0);
2094 let target_col = cursor_col.min(line_len.saturating_sub(1));
2095 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2096 }
2097 }
2098
2099 pub fn goto_line(&mut self, line: usize) {
2100 let row = line.saturating_sub(1);
2101 let max = buf_row_count(&self.buffer).saturating_sub(1);
2102 let target = row.min(max);
2103 buf_set_cursor_rc(&mut self.buffer, target, 0);
2104 }
2105
2106 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2110 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2111 if height == 0 {
2112 return;
2113 }
2114 let cur_row = buf_cursor_row(&self.buffer);
2115 let cur_top = self.host.viewport().top_row;
2116 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2122 let new_top = match pos {
2123 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2124 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2125 CursorScrollTarget::Bottom => {
2126 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2127 }
2128 };
2129 if new_top == cur_top {
2130 return;
2131 }
2132 self.host.viewport_mut().top_row = new_top;
2133 }
2134
2135 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2146 let n = buf_row_count(&self.buffer);
2147 let inner_top = area_y.saturating_add(1); let lnum_width = n.to_string().len() as u16 + 2;
2149 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2150 let rel_row = row.saturating_sub(inner_top) as usize;
2151 let top = self.host.viewport().top_row;
2152 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2153 let rel_col = col.saturating_sub(content_x) as usize;
2154 let line_chars = buf_line(&self.buffer, doc_row)
2155 .map(|l| l.chars().count())
2156 .unwrap_or(0);
2157 let last_col = line_chars.saturating_sub(1);
2158 (doc_row, rel_col.min(last_col))
2159 }
2160
2161 pub fn jump_to(&mut self, line: usize, col: usize) {
2163 let r = line.saturating_sub(1);
2164 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2165 let r = r.min(max_row);
2166 let line_len = buf_line(&self.buffer, r)
2167 .map(|l| l.chars().count())
2168 .unwrap_or(0);
2169 let c = col.saturating_sub(1).min(line_len);
2170 buf_set_cursor_rc(&mut self.buffer, r, c);
2171 }
2172
2173 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2181 if self.vim.is_visual() {
2182 self.vim.force_normal();
2183 }
2184 crate::vim::break_undo_group_in_insert(self);
2187 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2188 buf_set_cursor_rc(&mut self.buffer, r, c);
2189 }
2190
2191 #[cfg(feature = "ratatui")]
2197 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2198 self.mouse_click(area.x, area.y, col, row);
2199 }
2200
2201 pub fn mouse_begin_drag(&mut self) {
2203 if !self.vim.is_visual_char() {
2204 let cursor = self.cursor();
2205 self.vim.enter_visual(cursor);
2206 }
2207 }
2208
2209 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2215 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2216 buf_set_cursor_rc(&mut self.buffer, r, c);
2217 }
2218
2219 #[cfg(feature = "ratatui")]
2225 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2226 self.mouse_extend_drag(area.x, area.y, col, row);
2227 }
2228
2229 pub fn insert_str(&mut self, text: &str) {
2230 let pos = crate::types::Cursor::cursor(&self.buffer);
2231 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2232 self.push_buffer_content_to_textarea();
2233 self.mark_content_dirty();
2234 }
2235
2236 pub fn accept_completion(&mut self, completion: &str) {
2237 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2238 let cursor_pos = CursorTrait::cursor(&self.buffer);
2239 let cursor_row = cursor_pos.line as usize;
2240 let cursor_col = cursor_pos.col as usize;
2241 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2242 let chars: Vec<char> = line.chars().collect();
2243 let prefix_len = chars[..cursor_col.min(chars.len())]
2244 .iter()
2245 .rev()
2246 .take_while(|c| c.is_alphanumeric() || **c == '_')
2247 .count();
2248 if prefix_len > 0 {
2249 let start = Pos {
2250 line: cursor_row as u32,
2251 col: (cursor_col - prefix_len) as u32,
2252 };
2253 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2254 }
2255 let cursor = CursorTrait::cursor(&self.buffer);
2256 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2257 self.push_buffer_content_to_textarea();
2258 self.mark_content_dirty();
2259 }
2260
2261 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2262 let rc = buf_cursor_rc(&self.buffer);
2263 (buf_lines_to_vec(&self.buffer), rc)
2264 }
2265
2266 pub fn undo(&mut self) {
2270 crate::vim::do_undo(self);
2271 }
2272
2273 pub fn redo(&mut self) {
2276 crate::vim::do_redo(self);
2277 }
2278
2279 pub fn push_undo(&mut self) {
2284 let snap = self.snapshot();
2285 self.undo_stack.push(snap);
2286 self.cap_undo();
2287 self.redo_stack.clear();
2288 }
2289
2290 pub(crate) fn cap_undo(&mut self) {
2296 let cap = self.settings.undo_levels as usize;
2297 if cap > 0 && self.undo_stack.len() > cap {
2298 let diff = self.undo_stack.len() - cap;
2299 self.undo_stack.drain(..diff);
2300 }
2301 }
2302
2303 #[doc(hidden)]
2305 pub fn undo_stack_len(&self) -> usize {
2306 self.undo_stack.len()
2307 }
2308
2309 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2313 let text = lines.join("\n");
2314 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2315 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2316 self.mark_content_dirty();
2317 }
2318
2319 #[cfg(feature = "crossterm")]
2321 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
2322 let input = crossterm_to_input(key);
2323 if input.key == Key::Null {
2324 return false;
2325 }
2326 let consumed = vim::step(self, input);
2327 self.emit_cursor_shape_if_changed();
2328 consumed
2329 }
2330}
2331
2332#[cfg(feature = "crossterm")]
2333impl From<KeyEvent> for Input {
2334 fn from(key: KeyEvent) -> Self {
2335 let k = match key.code {
2336 KeyCode::Char(c) => Key::Char(c),
2337 KeyCode::Backspace => Key::Backspace,
2338 KeyCode::Delete => Key::Delete,
2339 KeyCode::Enter => Key::Enter,
2340 KeyCode::Left => Key::Left,
2341 KeyCode::Right => Key::Right,
2342 KeyCode::Up => Key::Up,
2343 KeyCode::Down => Key::Down,
2344 KeyCode::Home => Key::Home,
2345 KeyCode::End => Key::End,
2346 KeyCode::Tab => Key::Tab,
2347 KeyCode::Esc => Key::Esc,
2348 _ => Key::Null,
2349 };
2350 Input {
2351 key: k,
2352 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
2353 alt: key.modifiers.contains(KeyModifiers::ALT),
2354 shift: key.modifiers.contains(KeyModifiers::SHIFT),
2355 }
2356 }
2357}
2358
2359#[cfg(feature = "crossterm")]
2363pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
2364 Input::from(key)
2365}
2366
2367#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
2368mod tests {
2369 use super::*;
2370 use crate::types::Host;
2371 use crossterm::event::KeyEvent;
2372
2373 fn key(code: KeyCode) -> KeyEvent {
2374 KeyEvent::new(code, KeyModifiers::NONE)
2375 }
2376 fn shift_key(code: KeyCode) -> KeyEvent {
2377 KeyEvent::new(code, KeyModifiers::SHIFT)
2378 }
2379 fn ctrl_key(code: KeyCode) -> KeyEvent {
2380 KeyEvent::new(code, KeyModifiers::CONTROL)
2381 }
2382
2383 #[test]
2384 fn vim_normal_to_insert() {
2385 let mut e = Editor::new(
2386 hjkl_buffer::Buffer::new(),
2387 crate::types::DefaultHost::new(),
2388 crate::types::Options::default(),
2389 );
2390 e.handle_key(key(KeyCode::Char('i')));
2391 assert_eq!(e.vim_mode(), VimMode::Insert);
2392 }
2393
2394 #[test]
2395 fn with_options_constructs_from_spec_options() {
2396 let opts = crate::types::Options {
2400 shiftwidth: 4,
2401 tabstop: 4,
2402 expandtab: true,
2403 iskeyword: "@,a-z".to_string(),
2404 wrap: crate::types::WrapMode::Word,
2405 ..crate::types::Options::default()
2406 };
2407 let mut e = Editor::new(
2408 hjkl_buffer::Buffer::new(),
2409 crate::types::DefaultHost::new(),
2410 opts,
2411 );
2412 assert_eq!(e.settings().shiftwidth, 4);
2413 assert_eq!(e.settings().tabstop, 4);
2414 assert!(e.settings().expandtab);
2415 assert_eq!(e.settings().iskeyword, "@,a-z");
2416 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
2417 e.handle_key(key(KeyCode::Char('i')));
2419 assert_eq!(e.vim_mode(), VimMode::Insert);
2420 }
2421
2422 #[test]
2423 fn feed_input_char_routes_through_handle_key() {
2424 use crate::{Modifiers, PlannedInput};
2425 let mut e = Editor::new(
2426 hjkl_buffer::Buffer::new(),
2427 crate::types::DefaultHost::new(),
2428 crate::types::Options::default(),
2429 );
2430 e.set_content("abc");
2431 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2433 assert_eq!(e.vim_mode(), VimMode::Insert);
2434 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
2436 assert!(e.content().contains('X'));
2437 }
2438
2439 #[test]
2440 fn feed_input_special_key_routes() {
2441 use crate::{Modifiers, PlannedInput, SpecialKey};
2442 let mut e = Editor::new(
2443 hjkl_buffer::Buffer::new(),
2444 crate::types::DefaultHost::new(),
2445 crate::types::Options::default(),
2446 );
2447 e.set_content("abc");
2448 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2449 assert_eq!(e.vim_mode(), VimMode::Insert);
2450 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
2451 assert_eq!(e.vim_mode(), VimMode::Normal);
2452 }
2453
2454 #[test]
2455 fn feed_input_mouse_paste_focus_resize_no_op() {
2456 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
2457 let mut e = Editor::new(
2458 hjkl_buffer::Buffer::new(),
2459 crate::types::DefaultHost::new(),
2460 crate::types::Options::default(),
2461 );
2462 e.set_content("abc");
2463 let mode_before = e.vim_mode();
2464 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
2465 kind: MouseKind::Press,
2466 pos: Pos::new(0, 0),
2467 mods: Default::default(),
2468 }));
2469 assert!(!consumed);
2470 assert_eq!(e.vim_mode(), mode_before);
2471 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
2472 assert!(!e.feed_input(PlannedInput::FocusGained));
2473 assert!(!e.feed_input(PlannedInput::FocusLost));
2474 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
2475 }
2476
2477 #[test]
2478 fn intern_style_dedups_engine_native_styles() {
2479 use crate::types::{Attrs, Color, Style};
2480 let mut e = Editor::new(
2481 hjkl_buffer::Buffer::new(),
2482 crate::types::DefaultHost::new(),
2483 crate::types::Options::default(),
2484 );
2485 let s = Style {
2486 fg: Some(Color(255, 0, 0)),
2487 bg: None,
2488 attrs: Attrs::BOLD,
2489 };
2490 let id_a = e.intern_style(s);
2491 let id_b = e.intern_style(s);
2493 assert_eq!(id_a, id_b);
2494 let back = e.engine_style_at(id_a).expect("interned");
2496 assert_eq!(back, s);
2497 }
2498
2499 #[test]
2500 fn engine_style_at_out_of_range_returns_none() {
2501 let e = Editor::new(
2502 hjkl_buffer::Buffer::new(),
2503 crate::types::DefaultHost::new(),
2504 crate::types::Options::default(),
2505 );
2506 assert!(e.engine_style_at(99).is_none());
2507 }
2508
2509 #[test]
2510 fn take_changes_emits_per_row_for_block_insert() {
2511 let mut e = Editor::new(
2516 hjkl_buffer::Buffer::new(),
2517 crate::types::DefaultHost::new(),
2518 crate::types::Options::default(),
2519 );
2520 e.set_content("aaa\nbbb\nccc\nddd");
2521 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
2523 e.handle_key(key(KeyCode::Char('j')));
2524 e.handle_key(key(KeyCode::Char('j')));
2525 e.handle_key(shift_key(KeyCode::Char('I')));
2527 e.handle_key(key(KeyCode::Char('X')));
2528 e.handle_key(key(KeyCode::Esc));
2529
2530 let changes = e.take_changes();
2531 assert!(
2535 changes.len() >= 3,
2536 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
2537 changes.len()
2538 );
2539 }
2540
2541 #[test]
2542 fn take_changes_drains_after_insert() {
2543 let mut e = Editor::new(
2544 hjkl_buffer::Buffer::new(),
2545 crate::types::DefaultHost::new(),
2546 crate::types::Options::default(),
2547 );
2548 e.set_content("abc");
2549 assert!(e.take_changes().is_empty());
2551 e.handle_key(key(KeyCode::Char('i')));
2553 e.handle_key(key(KeyCode::Char('X')));
2554 let changes = e.take_changes();
2555 assert!(
2556 !changes.is_empty(),
2557 "insert mode keystroke should produce a change"
2558 );
2559 assert!(e.take_changes().is_empty());
2561 }
2562
2563 #[test]
2564 fn options_bridge_roundtrip() {
2565 let mut e = Editor::new(
2566 hjkl_buffer::Buffer::new(),
2567 crate::types::DefaultHost::new(),
2568 crate::types::Options::default(),
2569 );
2570 let opts = e.current_options();
2571 assert_eq!(opts.shiftwidth, 8);
2573 assert_eq!(opts.tabstop, 8);
2574
2575 let new_opts = crate::types::Options {
2576 shiftwidth: 4,
2577 tabstop: 2,
2578 ignorecase: true,
2579 ..crate::types::Options::default()
2580 };
2581 e.apply_options(&new_opts);
2582
2583 let after = e.current_options();
2584 assert_eq!(after.shiftwidth, 4);
2585 assert_eq!(after.tabstop, 2);
2586 assert!(after.ignorecase);
2587 }
2588
2589 #[test]
2590 fn selection_highlight_none_in_normal() {
2591 let mut e = Editor::new(
2592 hjkl_buffer::Buffer::new(),
2593 crate::types::DefaultHost::new(),
2594 crate::types::Options::default(),
2595 );
2596 e.set_content("hello");
2597 assert!(e.selection_highlight().is_none());
2598 }
2599
2600 #[test]
2601 fn selection_highlight_some_in_visual() {
2602 use crate::types::HighlightKind;
2603 let mut e = Editor::new(
2604 hjkl_buffer::Buffer::new(),
2605 crate::types::DefaultHost::new(),
2606 crate::types::Options::default(),
2607 );
2608 e.set_content("hello world");
2609 e.handle_key(key(KeyCode::Char('v')));
2610 e.handle_key(key(KeyCode::Char('l')));
2611 e.handle_key(key(KeyCode::Char('l')));
2612 let h = e
2613 .selection_highlight()
2614 .expect("visual mode should produce a highlight");
2615 assert_eq!(h.kind, HighlightKind::Selection);
2616 assert_eq!(h.range.start.line, 0);
2617 assert_eq!(h.range.end.line, 0);
2618 }
2619
2620 #[test]
2621 fn highlights_emit_incsearch_during_active_prompt() {
2622 use crate::types::HighlightKind;
2623 let mut e = Editor::new(
2624 hjkl_buffer::Buffer::new(),
2625 crate::types::DefaultHost::new(),
2626 crate::types::Options::default(),
2627 );
2628 e.set_content("foo bar foo\nbaz\n");
2629 e.handle_key(key(KeyCode::Char('/')));
2631 e.handle_key(key(KeyCode::Char('f')));
2632 e.handle_key(key(KeyCode::Char('o')));
2633 e.handle_key(key(KeyCode::Char('o')));
2634 assert!(e.search_prompt().is_some());
2636 let hs = e.highlights_for_line(0);
2637 assert_eq!(hs.len(), 2);
2638 for h in &hs {
2639 assert_eq!(h.kind, HighlightKind::IncSearch);
2640 }
2641 }
2642
2643 #[test]
2644 fn highlights_empty_for_blank_prompt() {
2645 let mut e = Editor::new(
2646 hjkl_buffer::Buffer::new(),
2647 crate::types::DefaultHost::new(),
2648 crate::types::Options::default(),
2649 );
2650 e.set_content("foo");
2651 e.handle_key(key(KeyCode::Char('/')));
2652 assert!(e.search_prompt().is_some());
2654 assert!(e.highlights_for_line(0).is_empty());
2655 }
2656
2657 #[test]
2658 fn highlights_emit_search_matches() {
2659 use crate::types::HighlightKind;
2660 let mut e = Editor::new(
2661 hjkl_buffer::Buffer::new(),
2662 crate::types::DefaultHost::new(),
2663 crate::types::Options::default(),
2664 );
2665 e.set_content("foo bar foo\nbaz qux\n");
2666 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
2670 let hs = e.highlights_for_line(0);
2671 assert_eq!(hs.len(), 2);
2672 for h in &hs {
2673 assert_eq!(h.kind, HighlightKind::SearchMatch);
2674 assert_eq!(h.range.start.line, 0);
2675 assert_eq!(h.range.end.line, 0);
2676 }
2677 }
2678
2679 #[test]
2680 fn highlights_empty_without_pattern() {
2681 let mut e = Editor::new(
2682 hjkl_buffer::Buffer::new(),
2683 crate::types::DefaultHost::new(),
2684 crate::types::Options::default(),
2685 );
2686 e.set_content("foo bar");
2687 assert!(e.highlights_for_line(0).is_empty());
2688 }
2689
2690 #[test]
2691 fn highlights_empty_for_out_of_range_line() {
2692 let mut e = Editor::new(
2693 hjkl_buffer::Buffer::new(),
2694 crate::types::DefaultHost::new(),
2695 crate::types::Options::default(),
2696 );
2697 e.set_content("foo");
2698 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
2699 assert!(e.highlights_for_line(99).is_empty());
2700 }
2701
2702 #[test]
2703 fn render_frame_reflects_mode_and_cursor() {
2704 use crate::types::{CursorShape, SnapshotMode};
2705 let mut e = Editor::new(
2706 hjkl_buffer::Buffer::new(),
2707 crate::types::DefaultHost::new(),
2708 crate::types::Options::default(),
2709 );
2710 e.set_content("alpha\nbeta");
2711 let f = e.render_frame();
2712 assert_eq!(f.mode, SnapshotMode::Normal);
2713 assert_eq!(f.cursor_shape, CursorShape::Block);
2714 assert_eq!(f.line_count, 2);
2715
2716 e.handle_key(key(KeyCode::Char('i')));
2717 let f = e.render_frame();
2718 assert_eq!(f.mode, SnapshotMode::Insert);
2719 assert_eq!(f.cursor_shape, CursorShape::Bar);
2720 }
2721
2722 #[test]
2723 fn snapshot_roundtrips_through_restore() {
2724 use crate::types::SnapshotMode;
2725 let mut e = Editor::new(
2726 hjkl_buffer::Buffer::new(),
2727 crate::types::DefaultHost::new(),
2728 crate::types::Options::default(),
2729 );
2730 e.set_content("alpha\nbeta\ngamma");
2731 e.jump_cursor(2, 3);
2732 let snap = e.take_snapshot();
2733 assert_eq!(snap.mode, SnapshotMode::Normal);
2734 assert_eq!(snap.cursor, (2, 3));
2735 assert_eq!(snap.lines.len(), 3);
2736
2737 let mut other = Editor::new(
2738 hjkl_buffer::Buffer::new(),
2739 crate::types::DefaultHost::new(),
2740 crate::types::Options::default(),
2741 );
2742 other.restore_snapshot(snap).expect("restore");
2743 assert_eq!(other.cursor(), (2, 3));
2744 assert_eq!(other.buffer().lines().len(), 3);
2745 }
2746
2747 #[test]
2748 fn restore_snapshot_rejects_version_mismatch() {
2749 let mut e = Editor::new(
2750 hjkl_buffer::Buffer::new(),
2751 crate::types::DefaultHost::new(),
2752 crate::types::Options::default(),
2753 );
2754 let mut snap = e.take_snapshot();
2755 snap.version = 9999;
2756 match e.restore_snapshot(snap) {
2757 Err(crate::EngineError::SnapshotVersion(got, want)) => {
2758 assert_eq!(got, 9999);
2759 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
2760 }
2761 other => panic!("expected SnapshotVersion err, got {other:?}"),
2762 }
2763 }
2764
2765 #[test]
2766 fn take_content_change_returns_some_on_first_dirty() {
2767 let mut e = Editor::new(
2768 hjkl_buffer::Buffer::new(),
2769 crate::types::DefaultHost::new(),
2770 crate::types::Options::default(),
2771 );
2772 e.set_content("hello");
2773 let first = e.take_content_change();
2774 assert!(first.is_some());
2775 let second = e.take_content_change();
2776 assert!(second.is_none());
2777 }
2778
2779 #[test]
2780 fn take_content_change_none_until_mutation() {
2781 let mut e = Editor::new(
2782 hjkl_buffer::Buffer::new(),
2783 crate::types::DefaultHost::new(),
2784 crate::types::Options::default(),
2785 );
2786 e.set_content("hello");
2787 e.take_content_change();
2789 assert!(e.take_content_change().is_none());
2790 e.handle_key(key(KeyCode::Char('i')));
2792 e.handle_key(key(KeyCode::Char('x')));
2793 let after = e.take_content_change();
2794 assert!(after.is_some());
2795 assert!(after.unwrap().contains('x'));
2796 }
2797
2798 #[test]
2799 fn vim_insert_to_normal() {
2800 let mut e = Editor::new(
2801 hjkl_buffer::Buffer::new(),
2802 crate::types::DefaultHost::new(),
2803 crate::types::Options::default(),
2804 );
2805 e.handle_key(key(KeyCode::Char('i')));
2806 e.handle_key(key(KeyCode::Esc));
2807 assert_eq!(e.vim_mode(), VimMode::Normal);
2808 }
2809
2810 #[test]
2811 fn vim_normal_to_visual() {
2812 let mut e = Editor::new(
2813 hjkl_buffer::Buffer::new(),
2814 crate::types::DefaultHost::new(),
2815 crate::types::Options::default(),
2816 );
2817 e.handle_key(key(KeyCode::Char('v')));
2818 assert_eq!(e.vim_mode(), VimMode::Visual);
2819 }
2820
2821 #[test]
2822 fn vim_visual_to_normal() {
2823 let mut e = Editor::new(
2824 hjkl_buffer::Buffer::new(),
2825 crate::types::DefaultHost::new(),
2826 crate::types::Options::default(),
2827 );
2828 e.handle_key(key(KeyCode::Char('v')));
2829 e.handle_key(key(KeyCode::Esc));
2830 assert_eq!(e.vim_mode(), VimMode::Normal);
2831 }
2832
2833 #[test]
2834 fn vim_shift_i_moves_to_first_non_whitespace() {
2835 let mut e = Editor::new(
2836 hjkl_buffer::Buffer::new(),
2837 crate::types::DefaultHost::new(),
2838 crate::types::Options::default(),
2839 );
2840 e.set_content(" hello");
2841 e.jump_cursor(0, 8);
2842 e.handle_key(shift_key(KeyCode::Char('I')));
2843 assert_eq!(e.vim_mode(), VimMode::Insert);
2844 assert_eq!(e.cursor(), (0, 3));
2845 }
2846
2847 #[test]
2848 fn vim_shift_a_moves_to_end_and_insert() {
2849 let mut e = Editor::new(
2850 hjkl_buffer::Buffer::new(),
2851 crate::types::DefaultHost::new(),
2852 crate::types::Options::default(),
2853 );
2854 e.set_content("hello");
2855 e.handle_key(shift_key(KeyCode::Char('A')));
2856 assert_eq!(e.vim_mode(), VimMode::Insert);
2857 assert_eq!(e.cursor().1, 5);
2858 }
2859
2860 #[test]
2861 fn count_10j_moves_down_10() {
2862 let mut e = Editor::new(
2863 hjkl_buffer::Buffer::new(),
2864 crate::types::DefaultHost::new(),
2865 crate::types::Options::default(),
2866 );
2867 e.set_content(
2868 (0..20)
2869 .map(|i| format!("line{i}"))
2870 .collect::<Vec<_>>()
2871 .join("\n")
2872 .as_str(),
2873 );
2874 for d in "10".chars() {
2875 e.handle_key(key(KeyCode::Char(d)));
2876 }
2877 e.handle_key(key(KeyCode::Char('j')));
2878 assert_eq!(e.cursor().0, 10);
2879 }
2880
2881 #[test]
2882 fn count_o_repeats_insert_on_esc() {
2883 let mut e = Editor::new(
2884 hjkl_buffer::Buffer::new(),
2885 crate::types::DefaultHost::new(),
2886 crate::types::Options::default(),
2887 );
2888 e.set_content("hello");
2889 for d in "3".chars() {
2890 e.handle_key(key(KeyCode::Char(d)));
2891 }
2892 e.handle_key(key(KeyCode::Char('o')));
2893 assert_eq!(e.vim_mode(), VimMode::Insert);
2894 for c in "world".chars() {
2895 e.handle_key(key(KeyCode::Char(c)));
2896 }
2897 e.handle_key(key(KeyCode::Esc));
2898 assert_eq!(e.vim_mode(), VimMode::Normal);
2899 assert_eq!(e.buffer().lines().len(), 4);
2900 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
2901 }
2902
2903 #[test]
2904 fn count_i_repeats_text_on_esc() {
2905 let mut e = Editor::new(
2906 hjkl_buffer::Buffer::new(),
2907 crate::types::DefaultHost::new(),
2908 crate::types::Options::default(),
2909 );
2910 e.set_content("");
2911 for d in "3".chars() {
2912 e.handle_key(key(KeyCode::Char(d)));
2913 }
2914 e.handle_key(key(KeyCode::Char('i')));
2915 for c in "ab".chars() {
2916 e.handle_key(key(KeyCode::Char(c)));
2917 }
2918 e.handle_key(key(KeyCode::Esc));
2919 assert_eq!(e.vim_mode(), VimMode::Normal);
2920 assert_eq!(e.buffer().lines()[0], "ababab");
2921 }
2922
2923 #[test]
2924 fn vim_shift_o_opens_line_above() {
2925 let mut e = Editor::new(
2926 hjkl_buffer::Buffer::new(),
2927 crate::types::DefaultHost::new(),
2928 crate::types::Options::default(),
2929 );
2930 e.set_content("hello");
2931 e.handle_key(shift_key(KeyCode::Char('O')));
2932 assert_eq!(e.vim_mode(), VimMode::Insert);
2933 assert_eq!(e.cursor(), (0, 0));
2934 assert_eq!(e.buffer().lines().len(), 2);
2935 }
2936
2937 #[test]
2938 fn vim_gg_goes_to_top() {
2939 let mut e = Editor::new(
2940 hjkl_buffer::Buffer::new(),
2941 crate::types::DefaultHost::new(),
2942 crate::types::Options::default(),
2943 );
2944 e.set_content("a\nb\nc");
2945 e.jump_cursor(2, 0);
2946 e.handle_key(key(KeyCode::Char('g')));
2947 e.handle_key(key(KeyCode::Char('g')));
2948 assert_eq!(e.cursor().0, 0);
2949 }
2950
2951 #[test]
2952 fn vim_shift_g_goes_to_bottom() {
2953 let mut e = Editor::new(
2954 hjkl_buffer::Buffer::new(),
2955 crate::types::DefaultHost::new(),
2956 crate::types::Options::default(),
2957 );
2958 e.set_content("a\nb\nc");
2959 e.handle_key(shift_key(KeyCode::Char('G')));
2960 assert_eq!(e.cursor().0, 2);
2961 }
2962
2963 #[test]
2964 fn vim_dd_deletes_line() {
2965 let mut e = Editor::new(
2966 hjkl_buffer::Buffer::new(),
2967 crate::types::DefaultHost::new(),
2968 crate::types::Options::default(),
2969 );
2970 e.set_content("first\nsecond");
2971 e.handle_key(key(KeyCode::Char('d')));
2972 e.handle_key(key(KeyCode::Char('d')));
2973 assert_eq!(e.buffer().lines().len(), 1);
2974 assert_eq!(e.buffer().lines()[0], "second");
2975 }
2976
2977 #[test]
2978 fn vim_dw_deletes_word() {
2979 let mut e = Editor::new(
2980 hjkl_buffer::Buffer::new(),
2981 crate::types::DefaultHost::new(),
2982 crate::types::Options::default(),
2983 );
2984 e.set_content("hello world");
2985 e.handle_key(key(KeyCode::Char('d')));
2986 e.handle_key(key(KeyCode::Char('w')));
2987 assert_eq!(e.vim_mode(), VimMode::Normal);
2988 assert!(!e.buffer().lines()[0].starts_with("hello"));
2989 }
2990
2991 #[test]
2992 fn vim_yy_yanks_line() {
2993 let mut e = Editor::new(
2994 hjkl_buffer::Buffer::new(),
2995 crate::types::DefaultHost::new(),
2996 crate::types::Options::default(),
2997 );
2998 e.set_content("hello\nworld");
2999 e.handle_key(key(KeyCode::Char('y')));
3000 e.handle_key(key(KeyCode::Char('y')));
3001 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3002 }
3003
3004 #[test]
3005 fn vim_yy_does_not_move_cursor() {
3006 let mut e = Editor::new(
3007 hjkl_buffer::Buffer::new(),
3008 crate::types::DefaultHost::new(),
3009 crate::types::Options::default(),
3010 );
3011 e.set_content("first\nsecond\nthird");
3012 e.jump_cursor(1, 0);
3013 let before = e.cursor();
3014 e.handle_key(key(KeyCode::Char('y')));
3015 e.handle_key(key(KeyCode::Char('y')));
3016 assert_eq!(e.cursor(), before);
3017 assert_eq!(e.vim_mode(), VimMode::Normal);
3018 }
3019
3020 #[test]
3021 fn vim_yw_yanks_word() {
3022 let mut e = Editor::new(
3023 hjkl_buffer::Buffer::new(),
3024 crate::types::DefaultHost::new(),
3025 crate::types::Options::default(),
3026 );
3027 e.set_content("hello world");
3028 e.handle_key(key(KeyCode::Char('y')));
3029 e.handle_key(key(KeyCode::Char('w')));
3030 assert_eq!(e.vim_mode(), VimMode::Normal);
3031 assert!(e.last_yank.is_some());
3032 }
3033
3034 #[test]
3035 fn vim_cc_changes_line() {
3036 let mut e = Editor::new(
3037 hjkl_buffer::Buffer::new(),
3038 crate::types::DefaultHost::new(),
3039 crate::types::Options::default(),
3040 );
3041 e.set_content("hello\nworld");
3042 e.handle_key(key(KeyCode::Char('c')));
3043 e.handle_key(key(KeyCode::Char('c')));
3044 assert_eq!(e.vim_mode(), VimMode::Insert);
3045 }
3046
3047 #[test]
3048 fn vim_u_undoes_insert_session_as_chunk() {
3049 let mut e = Editor::new(
3050 hjkl_buffer::Buffer::new(),
3051 crate::types::DefaultHost::new(),
3052 crate::types::Options::default(),
3053 );
3054 e.set_content("hello");
3055 e.handle_key(key(KeyCode::Char('i')));
3056 e.handle_key(key(KeyCode::Enter));
3057 e.handle_key(key(KeyCode::Enter));
3058 e.handle_key(key(KeyCode::Esc));
3059 assert_eq!(e.buffer().lines().len(), 3);
3060 e.handle_key(key(KeyCode::Char('u')));
3061 assert_eq!(e.buffer().lines().len(), 1);
3062 assert_eq!(e.buffer().lines()[0], "hello");
3063 }
3064
3065 #[test]
3066 fn vim_undo_redo_roundtrip() {
3067 let mut e = Editor::new(
3068 hjkl_buffer::Buffer::new(),
3069 crate::types::DefaultHost::new(),
3070 crate::types::Options::default(),
3071 );
3072 e.set_content("hello");
3073 e.handle_key(key(KeyCode::Char('i')));
3074 for c in "world".chars() {
3075 e.handle_key(key(KeyCode::Char(c)));
3076 }
3077 e.handle_key(key(KeyCode::Esc));
3078 let after = e.buffer().lines()[0].clone();
3079 e.handle_key(key(KeyCode::Char('u')));
3080 assert_eq!(e.buffer().lines()[0], "hello");
3081 e.handle_key(ctrl_key(KeyCode::Char('r')));
3082 assert_eq!(e.buffer().lines()[0], after);
3083 }
3084
3085 #[test]
3086 fn vim_u_undoes_dd() {
3087 let mut e = Editor::new(
3088 hjkl_buffer::Buffer::new(),
3089 crate::types::DefaultHost::new(),
3090 crate::types::Options::default(),
3091 );
3092 e.set_content("first\nsecond");
3093 e.handle_key(key(KeyCode::Char('d')));
3094 e.handle_key(key(KeyCode::Char('d')));
3095 assert_eq!(e.buffer().lines().len(), 1);
3096 e.handle_key(key(KeyCode::Char('u')));
3097 assert_eq!(e.buffer().lines().len(), 2);
3098 assert_eq!(e.buffer().lines()[0], "first");
3099 }
3100
3101 #[test]
3102 fn vim_ctrl_r_redoes() {
3103 let mut e = Editor::new(
3104 hjkl_buffer::Buffer::new(),
3105 crate::types::DefaultHost::new(),
3106 crate::types::Options::default(),
3107 );
3108 e.set_content("hello");
3109 e.handle_key(ctrl_key(KeyCode::Char('r')));
3110 }
3111
3112 #[test]
3113 fn vim_r_replaces_char() {
3114 let mut e = Editor::new(
3115 hjkl_buffer::Buffer::new(),
3116 crate::types::DefaultHost::new(),
3117 crate::types::Options::default(),
3118 );
3119 e.set_content("hello");
3120 e.handle_key(key(KeyCode::Char('r')));
3121 e.handle_key(key(KeyCode::Char('x')));
3122 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
3123 }
3124
3125 #[test]
3126 fn vim_tilde_toggles_case() {
3127 let mut e = Editor::new(
3128 hjkl_buffer::Buffer::new(),
3129 crate::types::DefaultHost::new(),
3130 crate::types::Options::default(),
3131 );
3132 e.set_content("hello");
3133 e.handle_key(key(KeyCode::Char('~')));
3134 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
3135 }
3136
3137 #[test]
3138 fn vim_visual_d_cuts() {
3139 let mut e = Editor::new(
3140 hjkl_buffer::Buffer::new(),
3141 crate::types::DefaultHost::new(),
3142 crate::types::Options::default(),
3143 );
3144 e.set_content("hello");
3145 e.handle_key(key(KeyCode::Char('v')));
3146 e.handle_key(key(KeyCode::Char('l')));
3147 e.handle_key(key(KeyCode::Char('l')));
3148 e.handle_key(key(KeyCode::Char('d')));
3149 assert_eq!(e.vim_mode(), VimMode::Normal);
3150 assert!(e.last_yank.is_some());
3151 }
3152
3153 #[test]
3154 fn vim_visual_c_enters_insert() {
3155 let mut e = Editor::new(
3156 hjkl_buffer::Buffer::new(),
3157 crate::types::DefaultHost::new(),
3158 crate::types::Options::default(),
3159 );
3160 e.set_content("hello");
3161 e.handle_key(key(KeyCode::Char('v')));
3162 e.handle_key(key(KeyCode::Char('l')));
3163 e.handle_key(key(KeyCode::Char('c')));
3164 assert_eq!(e.vim_mode(), VimMode::Insert);
3165 }
3166
3167 #[test]
3168 fn vim_normal_unknown_key_consumed() {
3169 let mut e = Editor::new(
3170 hjkl_buffer::Buffer::new(),
3171 crate::types::DefaultHost::new(),
3172 crate::types::Options::default(),
3173 );
3174 let consumed = e.handle_key(key(KeyCode::Char('z')));
3176 assert!(consumed);
3177 }
3178
3179 #[test]
3180 fn force_normal_clears_operator() {
3181 let mut e = Editor::new(
3182 hjkl_buffer::Buffer::new(),
3183 crate::types::DefaultHost::new(),
3184 crate::types::Options::default(),
3185 );
3186 e.handle_key(key(KeyCode::Char('d')));
3187 e.force_normal();
3188 assert_eq!(e.vim_mode(), VimMode::Normal);
3189 }
3190
3191 fn many_lines(n: usize) -> String {
3192 (0..n)
3193 .map(|i| format!("line{i}"))
3194 .collect::<Vec<_>>()
3195 .join("\n")
3196 }
3197
3198 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
3199 e.set_viewport_height(height);
3200 }
3201
3202 #[test]
3203 fn zz_centers_cursor_in_viewport() {
3204 let mut e = Editor::new(
3205 hjkl_buffer::Buffer::new(),
3206 crate::types::DefaultHost::new(),
3207 crate::types::Options::default(),
3208 );
3209 e.set_content(&many_lines(100));
3210 prime_viewport(&mut e, 20);
3211 e.jump_cursor(50, 0);
3212 e.handle_key(key(KeyCode::Char('z')));
3213 e.handle_key(key(KeyCode::Char('z')));
3214 assert_eq!(e.host().viewport().top_row, 40);
3215 assert_eq!(e.cursor().0, 50);
3216 }
3217
3218 #[test]
3219 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
3220 let mut e = Editor::new(
3221 hjkl_buffer::Buffer::new(),
3222 crate::types::DefaultHost::new(),
3223 crate::types::Options::default(),
3224 );
3225 e.set_content(&many_lines(100));
3226 prime_viewport(&mut e, 20);
3227 e.jump_cursor(50, 0);
3228 e.handle_key(key(KeyCode::Char('z')));
3229 e.handle_key(key(KeyCode::Char('t')));
3230 assert_eq!(e.host().viewport().top_row, 45);
3233 assert_eq!(e.cursor().0, 50);
3234 }
3235
3236 #[test]
3237 fn ctrl_a_increments_number_at_cursor() {
3238 let mut e = Editor::new(
3239 hjkl_buffer::Buffer::new(),
3240 crate::types::DefaultHost::new(),
3241 crate::types::Options::default(),
3242 );
3243 e.set_content("x = 41");
3244 e.handle_key(ctrl_key(KeyCode::Char('a')));
3245 assert_eq!(e.buffer().lines()[0], "x = 42");
3246 assert_eq!(e.cursor(), (0, 5));
3247 }
3248
3249 #[test]
3250 fn ctrl_a_finds_number_to_right_of_cursor() {
3251 let mut e = Editor::new(
3252 hjkl_buffer::Buffer::new(),
3253 crate::types::DefaultHost::new(),
3254 crate::types::Options::default(),
3255 );
3256 e.set_content("foo 99 bar");
3257 e.handle_key(ctrl_key(KeyCode::Char('a')));
3258 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
3259 assert_eq!(e.cursor(), (0, 6));
3260 }
3261
3262 #[test]
3263 fn ctrl_a_with_count_adds_count() {
3264 let mut e = Editor::new(
3265 hjkl_buffer::Buffer::new(),
3266 crate::types::DefaultHost::new(),
3267 crate::types::Options::default(),
3268 );
3269 e.set_content("x = 10");
3270 for d in "5".chars() {
3271 e.handle_key(key(KeyCode::Char(d)));
3272 }
3273 e.handle_key(ctrl_key(KeyCode::Char('a')));
3274 assert_eq!(e.buffer().lines()[0], "x = 15");
3275 }
3276
3277 #[test]
3278 fn ctrl_x_decrements_number() {
3279 let mut e = Editor::new(
3280 hjkl_buffer::Buffer::new(),
3281 crate::types::DefaultHost::new(),
3282 crate::types::Options::default(),
3283 );
3284 e.set_content("n=5");
3285 e.handle_key(ctrl_key(KeyCode::Char('x')));
3286 assert_eq!(e.buffer().lines()[0], "n=4");
3287 }
3288
3289 #[test]
3290 fn ctrl_x_crosses_zero_into_negative() {
3291 let mut e = Editor::new(
3292 hjkl_buffer::Buffer::new(),
3293 crate::types::DefaultHost::new(),
3294 crate::types::Options::default(),
3295 );
3296 e.set_content("v=0");
3297 e.handle_key(ctrl_key(KeyCode::Char('x')));
3298 assert_eq!(e.buffer().lines()[0], "v=-1");
3299 }
3300
3301 #[test]
3302 fn ctrl_a_on_negative_number_increments_toward_zero() {
3303 let mut e = Editor::new(
3304 hjkl_buffer::Buffer::new(),
3305 crate::types::DefaultHost::new(),
3306 crate::types::Options::default(),
3307 );
3308 e.set_content("a = -5");
3309 e.handle_key(ctrl_key(KeyCode::Char('a')));
3310 assert_eq!(e.buffer().lines()[0], "a = -4");
3311 }
3312
3313 #[test]
3314 fn ctrl_a_noop_when_no_digit_on_line() {
3315 let mut e = Editor::new(
3316 hjkl_buffer::Buffer::new(),
3317 crate::types::DefaultHost::new(),
3318 crate::types::Options::default(),
3319 );
3320 e.set_content("no digits here");
3321 e.handle_key(ctrl_key(KeyCode::Char('a')));
3322 assert_eq!(e.buffer().lines()[0], "no digits here");
3323 }
3324
3325 #[test]
3326 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
3327 let mut e = Editor::new(
3328 hjkl_buffer::Buffer::new(),
3329 crate::types::DefaultHost::new(),
3330 crate::types::Options::default(),
3331 );
3332 e.set_content(&many_lines(100));
3333 prime_viewport(&mut e, 20);
3334 e.jump_cursor(50, 0);
3335 e.handle_key(key(KeyCode::Char('z')));
3336 e.handle_key(key(KeyCode::Char('b')));
3337 assert_eq!(e.host().viewport().top_row, 36);
3341 assert_eq!(e.cursor().0, 50);
3342 }
3343
3344 #[test]
3351 fn set_content_dirties_then_take_dirty_clears() {
3352 let mut e = Editor::new(
3353 hjkl_buffer::Buffer::new(),
3354 crate::types::DefaultHost::new(),
3355 crate::types::Options::default(),
3356 );
3357 e.set_content("hello");
3358 assert!(
3359 e.take_dirty(),
3360 "set_content should leave content_dirty=true"
3361 );
3362 assert!(!e.take_dirty(), "take_dirty should clear the flag");
3363 }
3364
3365 #[test]
3366 fn content_arc_returns_same_arc_until_mutation() {
3367 let mut e = Editor::new(
3368 hjkl_buffer::Buffer::new(),
3369 crate::types::DefaultHost::new(),
3370 crate::types::Options::default(),
3371 );
3372 e.set_content("hello");
3373 let a = e.content_arc();
3374 let b = e.content_arc();
3375 assert!(
3376 std::sync::Arc::ptr_eq(&a, &b),
3377 "repeated content_arc() should hit the cache"
3378 );
3379
3380 e.handle_key(key(KeyCode::Char('i')));
3382 e.handle_key(key(KeyCode::Char('!')));
3383 let c = e.content_arc();
3384 assert!(
3385 !std::sync::Arc::ptr_eq(&a, &c),
3386 "mutation should invalidate content_arc() cache"
3387 );
3388 assert!(c.contains('!'));
3389 }
3390
3391 #[test]
3392 fn content_arc_cache_invalidated_by_set_content() {
3393 let mut e = Editor::new(
3394 hjkl_buffer::Buffer::new(),
3395 crate::types::DefaultHost::new(),
3396 crate::types::Options::default(),
3397 );
3398 e.set_content("one");
3399 let a = e.content_arc();
3400 e.set_content("two");
3401 let b = e.content_arc();
3402 assert!(!std::sync::Arc::ptr_eq(&a, &b));
3403 assert!(b.starts_with("two"));
3404 }
3405
3406 #[test]
3412 fn mouse_click_past_eol_lands_on_last_char() {
3413 let mut e = Editor::new(
3414 hjkl_buffer::Buffer::new(),
3415 crate::types::DefaultHost::new(),
3416 crate::types::Options::default(),
3417 );
3418 e.set_content("hello");
3419 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3423 e.mouse_click_in_rect(area, 78, 1);
3424 assert_eq!(e.cursor(), (0, 4));
3425 }
3426
3427 #[test]
3428 fn mouse_click_past_eol_handles_multibyte_line() {
3429 let mut e = Editor::new(
3430 hjkl_buffer::Buffer::new(),
3431 crate::types::DefaultHost::new(),
3432 crate::types::Options::default(),
3433 );
3434 e.set_content("héllo");
3437 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3438 e.mouse_click_in_rect(area, 78, 1);
3439 assert_eq!(e.cursor(), (0, 4));
3440 }
3441
3442 #[test]
3443 fn mouse_click_inside_line_lands_on_clicked_char() {
3444 let mut e = Editor::new(
3445 hjkl_buffer::Buffer::new(),
3446 crate::types::DefaultHost::new(),
3447 crate::types::Options::default(),
3448 );
3449 e.set_content("hello world");
3450 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3453 e.mouse_click_in_rect(area, 4, 1);
3454 assert_eq!(e.cursor(), (0, 0));
3455 e.mouse_click_in_rect(area, 6, 1);
3456 assert_eq!(e.cursor(), (0, 2));
3457 }
3458
3459 #[test]
3464 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
3465 let mut e = Editor::new(
3466 hjkl_buffer::Buffer::new(),
3467 crate::types::DefaultHost::new(),
3468 crate::types::Options::default(),
3469 );
3470 e.set_content("hello world");
3471 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3472 assert!(e.settings().undo_break_on_motion);
3474 e.handle_key(key(KeyCode::Char('i')));
3476 e.handle_key(key(KeyCode::Char('A')));
3477 e.handle_key(key(KeyCode::Char('A')));
3478 e.handle_key(key(KeyCode::Char('A')));
3479 e.mouse_click_in_rect(area, 10, 1);
3481 e.handle_key(key(KeyCode::Char('B')));
3483 e.handle_key(key(KeyCode::Char('B')));
3484 e.handle_key(key(KeyCode::Char('B')));
3485 e.handle_key(key(KeyCode::Esc));
3487 e.handle_key(key(KeyCode::Char('u')));
3488 let line = e.buffer().line(0).unwrap_or("").to_string();
3489 assert!(
3490 line.contains("AAA"),
3491 "AAA must survive undo (separate group): {line:?}"
3492 );
3493 assert!(
3494 !line.contains("BBB"),
3495 "BBB must be undone (post-click group): {line:?}"
3496 );
3497 }
3498
3499 #[test]
3503 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
3504 let mut e = Editor::new(
3505 hjkl_buffer::Buffer::new(),
3506 crate::types::DefaultHost::new(),
3507 crate::types::Options::default(),
3508 );
3509 e.set_content("hello world");
3510 e.settings_mut().undo_break_on_motion = false;
3511 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3512 e.handle_key(key(KeyCode::Char('i')));
3513 e.handle_key(key(KeyCode::Char('A')));
3514 e.handle_key(key(KeyCode::Char('A')));
3515 e.mouse_click_in_rect(area, 10, 1);
3516 e.handle_key(key(KeyCode::Char('B')));
3517 e.handle_key(key(KeyCode::Char('B')));
3518 e.handle_key(key(KeyCode::Esc));
3519 e.handle_key(key(KeyCode::Char('u')));
3520 let line = e.buffer().line(0).unwrap_or("").to_string();
3521 assert!(
3522 !line.contains("AA") && !line.contains("BB"),
3523 "with undobreak off, single `u` must reverse whole insert: {line:?}"
3524 );
3525 assert_eq!(line, "hello world");
3526 }
3527
3528 #[test]
3531 fn host_clipboard_round_trip_via_default_host() {
3532 let mut e = Editor::new(
3535 hjkl_buffer::Buffer::new(),
3536 crate::types::DefaultHost::new(),
3537 crate::types::Options::default(),
3538 );
3539 e.host_mut().write_clipboard("payload".to_string());
3540 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
3541 }
3542
3543 #[test]
3544 fn host_records_clipboard_on_yank() {
3545 let mut e = Editor::new(
3549 hjkl_buffer::Buffer::new(),
3550 crate::types::DefaultHost::new(),
3551 crate::types::Options::default(),
3552 );
3553 e.set_content("hello\n");
3554 e.handle_key(key(KeyCode::Char('y')));
3555 e.handle_key(key(KeyCode::Char('y')));
3556 let clip = e.host_mut().read_clipboard();
3558 assert!(
3559 clip.as_deref().unwrap_or("").starts_with("hello"),
3560 "host clipboard should carry the yank: {clip:?}"
3561 );
3562 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3564 }
3565
3566 #[test]
3567 fn host_cursor_shape_via_shared_recorder() {
3568 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
3572 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
3573 struct LeakHost {
3574 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
3575 viewport: crate::types::Viewport,
3576 }
3577 impl crate::types::Host for LeakHost {
3578 type Intent = ();
3579 fn write_clipboard(&mut self, _: String) {}
3580 fn read_clipboard(&mut self) -> Option<String> {
3581 None
3582 }
3583 fn now(&self) -> core::time::Duration {
3584 core::time::Duration::ZERO
3585 }
3586 fn prompt_search(&mut self) -> Option<String> {
3587 None
3588 }
3589 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
3590 self.shapes.lock().unwrap().push(s);
3591 }
3592 fn viewport(&self) -> &crate::types::Viewport {
3593 &self.viewport
3594 }
3595 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
3596 &mut self.viewport
3597 }
3598 fn emit_intent(&mut self, _: Self::Intent) {}
3599 }
3600 let mut e = Editor::new(
3601 hjkl_buffer::Buffer::new(),
3602 LeakHost {
3603 shapes: shapes_ptr,
3604 viewport: crate::types::Viewport::default(),
3605 },
3606 crate::types::Options::default(),
3607 );
3608 e.set_content("abc");
3609 e.handle_key(key(KeyCode::Char('i')));
3611 e.handle_key(key(KeyCode::Esc));
3613 let shapes = shapes_ptr.lock().unwrap().clone();
3614 assert_eq!(
3615 shapes,
3616 vec![
3617 crate::types::CursorShape::Bar,
3618 crate::types::CursorShape::Block,
3619 ],
3620 "host should observe Insert(Bar) → Normal(Block) transitions"
3621 );
3622 }
3623
3624 #[test]
3625 fn host_now_drives_chord_timeout_deterministically() {
3626 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
3631 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
3632 struct ClockHost {
3633 now: &'static std::sync::Mutex<core::time::Duration>,
3634 viewport: crate::types::Viewport,
3635 }
3636 impl crate::types::Host for ClockHost {
3637 type Intent = ();
3638 fn write_clipboard(&mut self, _: String) {}
3639 fn read_clipboard(&mut self) -> Option<String> {
3640 None
3641 }
3642 fn now(&self) -> core::time::Duration {
3643 *self.now.lock().unwrap()
3644 }
3645 fn prompt_search(&mut self) -> Option<String> {
3646 None
3647 }
3648 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
3649 fn viewport(&self) -> &crate::types::Viewport {
3650 &self.viewport
3651 }
3652 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
3653 &mut self.viewport
3654 }
3655 fn emit_intent(&mut self, _: Self::Intent) {}
3656 }
3657 let mut e = Editor::new(
3658 hjkl_buffer::Buffer::new(),
3659 ClockHost {
3660 now: now_ptr,
3661 viewport: crate::types::Viewport::default(),
3662 },
3663 crate::types::Options::default(),
3664 );
3665 e.set_content("a\nb\nc\n");
3666 e.jump_cursor(2, 0);
3667 e.handle_key(key(KeyCode::Char('g')));
3669 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
3671 e.handle_key(key(KeyCode::Char('g')));
3674 assert_eq!(
3675 e.cursor().0,
3676 2,
3677 "Host::now() must drive `:set timeoutlen` deterministically"
3678 );
3679 }
3680}