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#[inline]
239fn buffer_byte_of_row(buf: &hjkl_buffer::Buffer, row: usize) -> usize {
240 let n = buf.row_count();
241 let row = row.min(n);
242 let mut acc = 0usize;
243 for r in 0..row {
244 acc += buf.line(r).map(str::len).unwrap_or(0);
245 if r + 1 < n {
246 acc += 1; }
248 }
249 acc
250}
251
252fn position_to_byte_coords(
256 buf: &hjkl_buffer::Buffer,
257 pos: hjkl_buffer::Position,
258) -> (usize, (u32, u32)) {
259 let row = pos.row.min(buf.row_count().saturating_sub(1));
260 let line = buf.line(row).unwrap_or("");
261 let col_byte = pos.byte_offset(line);
262 let byte = buffer_byte_of_row(buf, row) + col_byte;
263 (byte, (row as u32, col_byte as u32))
264}
265
266fn advance_by_text(text: &str, start_byte: usize, start_pos: (u32, u32)) -> (usize, (u32, u32)) {
269 let new_end_byte = start_byte + text.len();
270 let newlines = text.bytes().filter(|&b| b == b'\n').count();
271 let end_pos = if newlines == 0 {
272 (start_pos.0, start_pos.1 + text.len() as u32)
273 } else {
274 let last_nl = text.rfind('\n').unwrap();
276 let tail_bytes = (text.len() - last_nl - 1) as u32;
277 (start_pos.0 + newlines as u32, tail_bytes)
278 };
279 (new_end_byte, end_pos)
280}
281
282fn content_edits_from_buffer_edit(
287 buf: &hjkl_buffer::Buffer,
288 edit: &hjkl_buffer::Edit,
289) -> Vec<crate::types::ContentEdit> {
290 use hjkl_buffer::Edit as B;
291 use hjkl_buffer::Position;
292
293 let mut out: Vec<crate::types::ContentEdit> = Vec::new();
294
295 match edit {
296 B::InsertChar { at, ch } => {
297 let (start_byte, start_pos) = position_to_byte_coords(buf, *at);
298 let new_end_byte = start_byte + ch.len_utf8();
299 let new_end_pos = (start_pos.0, start_pos.1 + ch.len_utf8() as u32);
300 out.push(crate::types::ContentEdit {
301 start_byte,
302 old_end_byte: start_byte,
303 new_end_byte,
304 start_position: start_pos,
305 old_end_position: start_pos,
306 new_end_position: new_end_pos,
307 });
308 }
309 B::InsertStr { at, text } => {
310 let (start_byte, start_pos) = position_to_byte_coords(buf, *at);
311 let (new_end_byte, new_end_pos) = advance_by_text(text, start_byte, start_pos);
312 out.push(crate::types::ContentEdit {
313 start_byte,
314 old_end_byte: start_byte,
315 new_end_byte,
316 start_position: start_pos,
317 old_end_position: start_pos,
318 new_end_position: new_end_pos,
319 });
320 }
321 B::DeleteRange { start, end, kind } => {
322 let (start, end) = if start <= end {
323 (*start, *end)
324 } else {
325 (*end, *start)
326 };
327 match kind {
328 hjkl_buffer::MotionKind::Char => {
329 let (start_byte, start_pos) = position_to_byte_coords(buf, start);
330 let (old_end_byte, old_end_pos) = position_to_byte_coords(buf, end);
331 out.push(crate::types::ContentEdit {
332 start_byte,
333 old_end_byte,
334 new_end_byte: start_byte,
335 start_position: start_pos,
336 old_end_position: old_end_pos,
337 new_end_position: start_pos,
338 });
339 }
340 hjkl_buffer::MotionKind::Line => {
341 let lo = start.row;
346 let hi = end.row.min(buf.row_count().saturating_sub(1));
347 let start_byte = buffer_byte_of_row(buf, lo);
348 let next_row_byte = if hi + 1 < buf.row_count() {
349 buffer_byte_of_row(buf, hi + 1)
350 } else {
351 buffer_byte_of_row(buf, buf.row_count())
353 + buf
354 .line(buf.row_count().saturating_sub(1))
355 .map(str::len)
356 .unwrap_or(0)
357 };
358 out.push(crate::types::ContentEdit {
359 start_byte,
360 old_end_byte: next_row_byte,
361 new_end_byte: start_byte,
362 start_position: (lo as u32, 0),
363 old_end_position: ((hi + 1) as u32, 0),
364 new_end_position: (lo as u32, 0),
365 });
366 }
367 hjkl_buffer::MotionKind::Block => {
368 let (left_col, right_col) = (start.col.min(end.col), start.col.max(end.col));
371 for row in start.row..=end.row {
372 let row_start_pos = Position::new(row, left_col);
373 let row_end_pos = Position::new(row, right_col + 1);
374 let (sb, sp) = position_to_byte_coords(buf, row_start_pos);
375 let (eb, ep) = position_to_byte_coords(buf, row_end_pos);
376 if eb <= sb {
377 continue;
378 }
379 out.push(crate::types::ContentEdit {
380 start_byte: sb,
381 old_end_byte: eb,
382 new_end_byte: sb,
383 start_position: sp,
384 old_end_position: ep,
385 new_end_position: sp,
386 });
387 }
388 }
389 }
390 }
391 B::Replace { start, end, with } => {
392 let (start, end) = if start <= end {
393 (*start, *end)
394 } else {
395 (*end, *start)
396 };
397 let (start_byte, start_pos) = position_to_byte_coords(buf, start);
398 let (old_end_byte, old_end_pos) = position_to_byte_coords(buf, end);
399 let (new_end_byte, new_end_pos) = advance_by_text(with, start_byte, start_pos);
400 out.push(crate::types::ContentEdit {
401 start_byte,
402 old_end_byte,
403 new_end_byte,
404 start_position: start_pos,
405 old_end_position: old_end_pos,
406 new_end_position: new_end_pos,
407 });
408 }
409 B::JoinLines {
410 row,
411 count,
412 with_space,
413 } => {
414 let row = (*row).min(buf.row_count().saturating_sub(1));
420 let last_join_row = (row + count).min(buf.row_count().saturating_sub(1));
421 let line = buf.line(row).unwrap_or("");
422 let row_eol_byte = buffer_byte_of_row(buf, row) + line.len();
423 let row_eol_col = line.len() as u32;
424 let next_row_after = last_join_row + 1;
425 let old_end_byte = if next_row_after < buf.row_count() {
426 buffer_byte_of_row(buf, next_row_after).saturating_sub(1)
427 } else {
428 buffer_byte_of_row(buf, buf.row_count())
429 + buf
430 .line(buf.row_count().saturating_sub(1))
431 .map(str::len)
432 .unwrap_or(0)
433 };
434 let last_line = buf.line(last_join_row).unwrap_or("");
435 let old_end_pos = (last_join_row as u32, last_line.len() as u32);
436 let replacement_len = if *with_space { 1 } else { 0 };
437 let new_end_byte = row_eol_byte + replacement_len;
438 let new_end_pos = (row as u32, row_eol_col + replacement_len as u32);
439 out.push(crate::types::ContentEdit {
440 start_byte: row_eol_byte,
441 old_end_byte,
442 new_end_byte,
443 start_position: (row as u32, row_eol_col),
444 old_end_position: old_end_pos,
445 new_end_position: new_end_pos,
446 });
447 }
448 B::SplitLines {
449 row,
450 cols,
451 inserted_space,
452 } => {
453 let row = (*row).min(buf.row_count().saturating_sub(1));
460 let line = buf.line(row).unwrap_or("");
461 let row_byte = buffer_byte_of_row(buf, row);
462 let insert = if *inserted_space { "\n " } else { "\n" };
463 for &c in cols {
464 let pos = Position::new(row, c);
465 let col_byte = pos.byte_offset(line);
466 let start_byte = row_byte + col_byte;
467 let start_pos = (row as u32, col_byte as u32);
468 let (new_end_byte, new_end_pos) = advance_by_text(insert, start_byte, start_pos);
469 out.push(crate::types::ContentEdit {
470 start_byte,
471 old_end_byte: start_byte,
472 new_end_byte,
473 start_position: start_pos,
474 old_end_position: start_pos,
475 new_end_position: new_end_pos,
476 });
477 }
478 }
479 B::InsertBlock { at, chunks } => {
480 for (i, chunk) in chunks.iter().enumerate() {
483 let pos = Position::new(at.row + i, at.col);
484 let (start_byte, start_pos) = position_to_byte_coords(buf, pos);
485 let (new_end_byte, new_end_pos) = advance_by_text(chunk, start_byte, start_pos);
486 out.push(crate::types::ContentEdit {
487 start_byte,
488 old_end_byte: start_byte,
489 new_end_byte,
490 start_position: start_pos,
491 old_end_position: start_pos,
492 new_end_position: new_end_pos,
493 });
494 }
495 }
496 B::DeleteBlockChunks { at, widths } => {
497 for (i, w) in widths.iter().enumerate() {
498 let row = at.row + i;
499 let start_pos = Position::new(row, at.col);
500 let end_pos = Position::new(row, at.col + *w);
501 let (sb, sp) = position_to_byte_coords(buf, start_pos);
502 let (eb, ep) = position_to_byte_coords(buf, end_pos);
503 if eb <= sb {
504 continue;
505 }
506 out.push(crate::types::ContentEdit {
507 start_byte: sb,
508 old_end_byte: eb,
509 new_end_byte: sb,
510 start_position: sp,
511 old_end_position: ep,
512 new_end_position: sp,
513 });
514 }
515 }
516 }
517
518 out
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub(super) enum CursorScrollTarget {
525 Center,
526 Top,
527 Bottom,
528}
529
530use crate::buf_helpers::{
538 apply_buffer_edit, buf_cursor_pos, buf_cursor_rc, buf_cursor_row, buf_line, buf_lines_to_vec,
539 buf_row_count, buf_set_cursor_rc,
540};
541
542pub struct Editor<
543 B: crate::types::Buffer = hjkl_buffer::Buffer,
544 H: crate::types::Host = crate::types::DefaultHost,
545> {
546 pub keybinding_mode: KeybindingMode,
547 pub last_yank: Option<String>,
549 pub(crate) vim: VimState,
554 pub(crate) undo_stack: Vec<(Vec<String>, (usize, usize))>,
558 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
560 pub(super) content_dirty: bool,
562 pub(super) cached_content: Option<std::sync::Arc<String>>,
567 pub(super) viewport_height: AtomicU16,
572 pub(super) pending_lsp: Option<LspIntent>,
576 pub(super) pending_fold_ops: Vec<crate::types::FoldOp>,
584 pub(super) buffer: B,
591 #[cfg(feature = "ratatui")]
603 pub(super) style_table: Vec<ratatui::style::Style>,
604 #[cfg(not(feature = "ratatui"))]
609 pub(super) engine_style_table: Vec<crate::types::Style>,
610 pub(crate) registers: crate::registers::Registers,
615 #[cfg(feature = "ratatui")]
622 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
623 pub(crate) settings: Settings,
628 pub(crate) marks: std::collections::BTreeMap<char, (usize, usize)>,
643 pub(crate) syntax_fold_ranges: Vec<(usize, usize)>,
649 pub(crate) change_log: Vec<crate::types::Edit>,
658 pub(crate) sticky_col: Option<usize>,
667 pub(crate) host: H,
675 pub(crate) last_emitted_mode: crate::VimMode,
680 pub(crate) search_state: crate::search::SearchState,
687 pub(crate) buffer_spans: Vec<Vec<hjkl_buffer::Span>>,
698 pub(crate) pending_content_edits: Vec<crate::types::ContentEdit>,
703 pub(crate) pending_content_reset: bool,
708}
709
710#[derive(Debug, Clone)]
713pub struct Settings {
714 pub shiftwidth: usize,
716 pub tabstop: usize,
719 pub ignore_case: bool,
722 pub smartcase: bool,
726 pub wrapscan: bool,
729 pub textwidth: usize,
731 pub expandtab: bool,
735 pub softtabstop: usize,
740 pub wrap: hjkl_buffer::Wrap,
746 pub readonly: bool,
750 pub autoindent: bool,
754 pub smartindent: bool,
759 pub undo_levels: u32,
763 pub undo_break_on_motion: bool,
770 pub iskeyword: String,
776 pub timeout_len: core::time::Duration,
781 pub number: bool,
784 pub relativenumber: bool,
789 pub numberwidth: usize,
794 pub cursorline: bool,
797 pub cursorcolumn: bool,
800 pub signcolumn: crate::types::SignColumnMode,
803 pub foldcolumn: u32,
806 pub colorcolumn: String,
809}
810
811impl Default for Settings {
812 fn default() -> Self {
813 Self {
814 shiftwidth: 4,
815 tabstop: 4,
816 softtabstop: 4,
817 ignore_case: false,
818 smartcase: false,
819 wrapscan: true,
820 textwidth: 79,
821 expandtab: true,
822 wrap: hjkl_buffer::Wrap::None,
823 readonly: false,
824 autoindent: true,
825 smartindent: true,
826 undo_levels: 1000,
827 undo_break_on_motion: true,
828 iskeyword: "@,48-57,_,192-255".to_string(),
829 timeout_len: core::time::Duration::from_millis(1000),
830 number: true,
831 relativenumber: false,
832 numberwidth: 4,
833 cursorline: false,
834 cursorcolumn: false,
835 signcolumn: crate::types::SignColumnMode::Auto,
836 foldcolumn: 0,
837 colorcolumn: String::new(),
838 }
839 }
840}
841
842fn settings_from_options(o: &crate::types::Options) -> Settings {
850 Settings {
851 shiftwidth: o.shiftwidth as usize,
852 tabstop: o.tabstop as usize,
853 softtabstop: o.softtabstop as usize,
854 ignore_case: o.ignorecase,
855 smartcase: o.smartcase,
856 wrapscan: o.wrapscan,
857 textwidth: o.textwidth as usize,
858 expandtab: o.expandtab,
859 wrap: match o.wrap {
860 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
861 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
862 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
863 },
864 readonly: o.readonly,
865 autoindent: o.autoindent,
866 smartindent: o.smartindent,
867 undo_levels: o.undo_levels,
868 undo_break_on_motion: o.undo_break_on_motion,
869 iskeyword: o.iskeyword.clone(),
870 timeout_len: o.timeout_len,
871 number: o.number,
872 relativenumber: o.relativenumber,
873 numberwidth: o.numberwidth,
874 cursorline: o.cursorline,
875 cursorcolumn: o.cursorcolumn,
876 signcolumn: o.signcolumn,
877 foldcolumn: o.foldcolumn,
878 colorcolumn: o.colorcolumn.clone(),
879 }
880}
881
882#[derive(Debug, Clone, Copy, PartialEq, Eq)]
886pub enum LspIntent {
887 GotoDefinition,
889}
890
891impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
892 pub fn new(buffer: hjkl_buffer::Buffer, host: H, options: crate::types::Options) -> Self {
902 let settings = settings_from_options(&options);
903 Self {
904 keybinding_mode: KeybindingMode::Vim,
905 last_yank: None,
906 vim: VimState::default(),
907 undo_stack: Vec::new(),
908 redo_stack: Vec::new(),
909 content_dirty: false,
910 cached_content: None,
911 viewport_height: AtomicU16::new(0),
912 pending_lsp: None,
913 pending_fold_ops: Vec::new(),
914 buffer,
915 #[cfg(feature = "ratatui")]
916 style_table: Vec::new(),
917 #[cfg(not(feature = "ratatui"))]
918 engine_style_table: Vec::new(),
919 registers: crate::registers::Registers::default(),
920 #[cfg(feature = "ratatui")]
921 styled_spans: Vec::new(),
922 settings,
923 marks: std::collections::BTreeMap::new(),
924 syntax_fold_ranges: Vec::new(),
925 change_log: Vec::new(),
926 sticky_col: None,
927 host,
928 last_emitted_mode: crate::VimMode::Normal,
929 search_state: crate::search::SearchState::new(),
930 buffer_spans: Vec::new(),
931 pending_content_edits: Vec::new(),
932 pending_content_reset: false,
933 }
934 }
935}
936
937impl<B: crate::types::Buffer, H: crate::types::Host> Editor<B, H> {
938 pub fn buffer(&self) -> &B {
941 &self.buffer
942 }
943
944 pub fn buffer_mut(&mut self) -> &mut B {
946 &mut self.buffer
947 }
948
949 pub fn host(&self) -> &H {
951 &self.host
952 }
953
954 pub fn host_mut(&mut self) -> &mut H {
956 &mut self.host
957 }
958}
959
960impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
961 pub fn set_iskeyword(&mut self, spec: impl Into<String>) {
968 self.settings.iskeyword = spec.into();
969 }
970
971 pub(crate) fn emit_cursor_shape_if_changed(&mut self) {
976 let mode = self.vim_mode();
977 if mode == self.last_emitted_mode {
978 return;
979 }
980 let shape = match mode {
981 crate::VimMode::Insert => crate::types::CursorShape::Bar,
982 _ => crate::types::CursorShape::Block,
983 };
984 self.host.emit_cursor_shape(shape);
985 self.last_emitted_mode = mode;
986 }
987
988 pub(crate) fn record_yank_to_host(&mut self, text: String) {
995 self.host.write_clipboard(text.clone());
996 self.last_yank = Some(text);
997 }
998
999 pub fn sticky_col(&self) -> Option<usize> {
1004 self.sticky_col
1005 }
1006
1007 pub fn set_sticky_col(&mut self, col: Option<usize>) {
1011 self.sticky_col = col;
1012 }
1013
1014 pub fn mark(&self, c: char) -> Option<(usize, usize)> {
1022 self.marks.get(&c).copied()
1023 }
1024
1025 pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
1028 self.marks.insert(c, pos);
1029 }
1030
1031 pub fn clear_mark(&mut self, c: char) {
1033 self.marks.remove(&c);
1034 }
1035
1036 #[deprecated(
1041 since = "0.0.36",
1042 note = "use Editor::mark — lowercase + uppercase marks now live in a single map"
1043 )]
1044 pub fn buffer_mark(&self, c: char) -> Option<(usize, usize)> {
1045 self.mark(c)
1046 }
1047
1048 pub fn pop_last_undo(&mut self) -> bool {
1055 self.undo_stack.pop().is_some()
1056 }
1057
1058 pub fn marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1063 self.marks.iter().map(|(c, p)| (*c, *p))
1064 }
1065
1066 #[deprecated(
1071 since = "0.0.36",
1072 note = "use Editor::marks — lowercase + uppercase marks now live in a single map"
1073 )]
1074 pub fn buffer_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1075 self.marks
1076 .iter()
1077 .filter(|(c, _)| c.is_ascii_lowercase())
1078 .map(|(c, p)| (*c, *p))
1079 }
1080
1081 pub fn last_jump_back(&self) -> Option<(usize, usize)> {
1084 self.vim.jump_back.last().copied()
1085 }
1086
1087 pub fn last_edit_pos(&self) -> Option<(usize, usize)> {
1090 self.vim.last_edit_pos
1091 }
1092
1093 pub fn file_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1104 self.marks
1105 .iter()
1106 .filter(|(c, _)| c.is_ascii_uppercase())
1107 .map(|(c, p)| (*c, *p))
1108 }
1109
1110 pub fn syntax_fold_ranges(&self) -> &[(usize, usize)] {
1115 &self.syntax_fold_ranges
1116 }
1117
1118 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
1119 self.syntax_fold_ranges = ranges;
1120 }
1121
1122 pub fn settings(&self) -> &Settings {
1125 &self.settings
1126 }
1127
1128 pub fn settings_mut(&mut self) -> &mut Settings {
1133 &mut self.settings
1134 }
1135
1136 pub fn is_readonly(&self) -> bool {
1140 self.settings.readonly
1141 }
1142
1143 pub fn search_state(&self) -> &crate::search::SearchState {
1148 &self.search_state
1149 }
1150
1151 pub fn search_state_mut(&mut self) -> &mut crate::search::SearchState {
1155 &mut self.search_state
1156 }
1157
1158 pub fn set_search_pattern(&mut self, pattern: Option<regex::Regex>) {
1164 self.search_state.set_pattern(pattern);
1165 }
1166
1167 pub fn search_advance_forward(&mut self, skip_current: bool) -> bool {
1172 crate::search::search_forward(&mut self.buffer, &mut self.search_state, skip_current)
1173 }
1174
1175 pub fn search_advance_backward(&mut self, skip_current: bool) -> bool {
1177 crate::search::search_backward(&mut self.buffer, &mut self.search_state, skip_current)
1178 }
1179
1180 #[cfg(feature = "ratatui")]
1191 pub fn install_ratatui_syntax_spans(
1192 &mut self,
1193 spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
1194 ) {
1195 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1199 for (row, row_spans) in spans.iter().enumerate() {
1200 if row_spans.is_empty() {
1201 by_row.push(Vec::new());
1202 continue;
1203 }
1204 let line_len = buf_line(&self.buffer, row).map(str::len).unwrap_or(0);
1205 let mut translated = Vec::with_capacity(row_spans.len());
1206 for (start, end, style) in row_spans {
1207 let end_clamped = (*end).min(line_len);
1208 if end_clamped <= *start {
1209 continue;
1210 }
1211 let id = self.intern_ratatui_style(*style);
1212 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1213 }
1214 by_row.push(translated);
1215 }
1216 self.buffer_spans = by_row;
1217 self.styled_spans = spans;
1218 }
1219
1220 pub fn yank(&self) -> &str {
1222 &self.registers.unnamed.text
1223 }
1224
1225 pub fn registers(&self) -> &crate::registers::Registers {
1227 &self.registers
1228 }
1229
1230 pub fn registers_mut(&mut self) -> &mut crate::registers::Registers {
1234 &mut self.registers
1235 }
1236
1237 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
1242 self.registers.set_clipboard(text, linewise);
1243 }
1244
1245 pub fn pending_register(&self) -> Option<char> {
1255 self.vim.pending_register
1256 }
1257
1258 pub fn pending_register_is_clipboard(&self) -> bool {
1262 matches!(self.vim.pending_register, Some('+') | Some('*'))
1263 }
1264
1265 pub fn recording_register(&self) -> Option<char> {
1269 self.vim.recording_macro
1270 }
1271
1272 pub fn pending_count(&self) -> Option<u32> {
1276 self.vim.pending_count_val()
1277 }
1278
1279 pub fn pending_op(&self) -> Option<char> {
1283 self.vim.pending_op_char()
1284 }
1285
1286 pub fn is_chord_pending(&self) -> bool {
1296 self.vim.is_chord_pending()
1297 }
1298
1299 #[allow(clippy::type_complexity)]
1302 pub fn jump_list(&self) -> (&[(usize, usize)], &[(usize, usize)]) {
1303 (&self.vim.jump_back, &self.vim.jump_fwd)
1304 }
1305
1306 pub fn change_list(&self) -> (&[(usize, usize)], Option<usize>) {
1309 (&self.vim.change_list, self.vim.change_list_cursor)
1310 }
1311
1312 pub fn set_yank(&mut self, text: impl Into<String>) {
1316 let text = text.into();
1317 let linewise = self.vim.yank_linewise;
1318 self.registers.unnamed = crate::registers::Slot { text, linewise };
1319 }
1320
1321 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
1325 self.vim.yank_linewise = linewise;
1326 let target = self.vim.pending_register.take();
1327 self.registers.record_yank(text, linewise, target);
1328 }
1329
1330 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
1335 if let Some(slot) = match reg {
1336 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
1337 'A'..='Z' => {
1338 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
1339 }
1340 _ => None,
1341 } {
1342 slot.text = text;
1343 slot.linewise = false;
1344 }
1345 }
1346
1347 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
1350 self.vim.yank_linewise = linewise;
1351 let target = self.vim.pending_register.take();
1352 self.registers.record_delete(text, linewise, target);
1353 }
1354
1355 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
1364 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
1365 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
1366 .collect();
1367 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1368 #[cfg(feature = "ratatui")]
1369 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
1370 Vec::with_capacity(spans.len());
1371 for (row, row_spans) in spans.iter().enumerate() {
1372 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
1373 let mut translated = Vec::with_capacity(row_spans.len());
1374 #[cfg(feature = "ratatui")]
1375 let mut translated_r = Vec::with_capacity(row_spans.len());
1376 for (start, end, style) in row_spans {
1377 let end_clamped = (*end).min(line_len);
1378 if end_clamped <= *start {
1379 continue;
1380 }
1381 let id = self.intern_style(*style);
1382 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1383 #[cfg(feature = "ratatui")]
1384 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
1385 }
1386 by_row.push(translated);
1387 #[cfg(feature = "ratatui")]
1388 ratatui_spans.push(translated_r);
1389 }
1390 self.buffer_spans = by_row;
1391 #[cfg(feature = "ratatui")]
1392 {
1393 self.styled_spans = ratatui_spans;
1394 }
1395 }
1396
1397 #[cfg(feature = "ratatui")]
1406 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
1407 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
1408 return idx as u32;
1409 }
1410 self.style_table.push(style);
1411 (self.style_table.len() - 1) as u32
1412 }
1413
1414 #[cfg(feature = "ratatui")]
1418 pub fn style_table(&self) -> &[ratatui::style::Style] {
1419 &self.style_table
1420 }
1421
1422 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
1431 &self.buffer_spans
1432 }
1433
1434 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1449 #[cfg(feature = "ratatui")]
1450 {
1451 let r = engine_style_to_ratatui(style);
1452 self.intern_ratatui_style(r)
1453 }
1454 #[cfg(not(feature = "ratatui"))]
1455 {
1456 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1457 return idx as u32;
1458 }
1459 self.engine_style_table.push(style);
1460 (self.engine_style_table.len() - 1) as u32
1461 }
1462 }
1463
1464 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1468 #[cfg(feature = "ratatui")]
1469 {
1470 let r = self.style_table.get(id as usize).copied()?;
1471 Some(ratatui_style_to_engine(r))
1472 }
1473 #[cfg(not(feature = "ratatui"))]
1474 {
1475 self.engine_style_table.get(id as usize).copied()
1476 }
1477 }
1478
1479 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1483
1484 pub fn set_viewport_top(&mut self, row: usize) {
1492 let last = buf_row_count(&self.buffer).saturating_sub(1);
1493 let target = row.min(last);
1494 self.host.viewport_mut().top_row = target;
1495 }
1496
1497 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1501 buf_set_cursor_rc(&mut self.buffer, row, col);
1502 }
1503
1504 pub fn cursor(&self) -> (usize, usize) {
1512 buf_cursor_rc(&self.buffer)
1513 }
1514
1515 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1518 self.pending_lsp.take()
1519 }
1520
1521 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1535 std::mem::take(&mut self.pending_fold_ops)
1536 }
1537
1538 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1548 use crate::types::FoldProvider;
1549 self.pending_fold_ops.push(op);
1550 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1551 provider.apply(op);
1552 }
1553
1554 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1561 let height = self.viewport_height_value();
1562 self.host.viewport_mut().height = height;
1563 }
1564
1565 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1569 self.sync_buffer_from_textarea();
1570 }
1571
1572 pub fn record_jump(&mut self, pos: (usize, usize)) {
1577 const JUMPLIST_MAX: usize = 100;
1578 self.vim.jump_back.push(pos);
1579 if self.vim.jump_back.len() > JUMPLIST_MAX {
1580 self.vim.jump_back.remove(0);
1581 }
1582 self.vim.jump_fwd.clear();
1583 }
1584
1585 pub fn set_viewport_height(&self, height: u16) {
1588 self.viewport_height.store(height, Ordering::Relaxed);
1589 }
1590
1591 pub fn viewport_height_value(&self) -> u16 {
1593 self.viewport_height.load(Ordering::Relaxed)
1594 }
1595
1596 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1605 if self.settings.readonly {
1612 let _ = edit;
1613 return hjkl_buffer::Edit::InsertStr {
1614 at: buf_cursor_pos(&self.buffer),
1615 text: String::new(),
1616 };
1617 }
1618 let pre_row = buf_cursor_row(&self.buffer);
1619 let pre_rows = buf_row_count(&self.buffer);
1620 let (pre_edit_row, pre_edit_col) = buf_cursor_rc(&self.buffer);
1625 self.change_log.extend(edit_to_editops(&edit));
1629 let content_edits = content_edits_from_buffer_edit(&self.buffer, &edit);
1635 self.pending_content_edits.extend(content_edits);
1636 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1642 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1643 let lo = pre_row.min(pos_row);
1649 let hi = pre_row.max(pos_row);
1650 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1651 start_row: lo,
1652 end_row: hi,
1653 });
1654 self.vim.last_edit_pos = Some((pre_edit_row, pre_edit_col));
1658 let entry = (pos_row, pos_col);
1663 if self.vim.change_list.last() != Some(&entry) {
1664 if let Some(idx) = self.vim.change_list_cursor.take() {
1665 self.vim.change_list.truncate(idx + 1);
1666 }
1667 self.vim.change_list.push(entry);
1668 let len = self.vim.change_list.len();
1669 if len > crate::vim::CHANGE_LIST_MAX {
1670 self.vim
1671 .change_list
1672 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1673 }
1674 }
1675 self.vim.change_list_cursor = None;
1676 let post_rows = buf_row_count(&self.buffer);
1680 let delta = post_rows as isize - pre_rows as isize;
1681 if delta != 0 {
1682 self.shift_marks_after_edit(pre_row, delta);
1683 }
1684 self.push_buffer_content_to_textarea();
1685 self.mark_content_dirty();
1686 inverse
1687 }
1688
1689 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1694 if delta == 0 {
1695 return;
1696 }
1697 let drop_end = if delta < 0 {
1700 edit_start.saturating_add((-delta) as usize)
1701 } else {
1702 edit_start
1703 };
1704 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1705
1706 let mut to_drop: Vec<char> = Vec::new();
1709 for (c, (row, _col)) in self.marks.iter_mut() {
1710 if (edit_start..drop_end).contains(row) {
1711 to_drop.push(*c);
1712 } else if *row >= shift_threshold {
1713 *row = ((*row as isize) + delta).max(0) as usize;
1714 }
1715 }
1716 for c in to_drop {
1717 self.marks.remove(&c);
1718 }
1719
1720 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1721 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1722 for (row, _) in entries.iter_mut() {
1723 if *row >= shift_threshold {
1724 *row = ((*row as isize) + delta).max(0) as usize;
1725 }
1726 }
1727 };
1728 shift_jumps(&mut self.vim.jump_back);
1729 shift_jumps(&mut self.vim.jump_fwd);
1730 }
1731
1732 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1740
1741 pub fn mark_content_dirty(&mut self) {
1747 self.content_dirty = true;
1748 self.cached_content = None;
1749 }
1750
1751 pub fn take_dirty(&mut self) -> bool {
1753 let dirty = self.content_dirty;
1754 self.content_dirty = false;
1755 dirty
1756 }
1757
1758 pub fn take_content_edits(&mut self) -> Vec<crate::types::ContentEdit> {
1766 std::mem::take(&mut self.pending_content_edits)
1767 }
1768
1769 pub fn take_content_reset(&mut self) -> bool {
1775 let r = self.pending_content_reset;
1776 self.pending_content_reset = false;
1777 r
1778 }
1779
1780 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1790 if !self.content_dirty {
1791 return None;
1792 }
1793 let arc = self.content_arc();
1794 self.content_dirty = false;
1795 Some(arc)
1796 }
1797
1798 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1801 let cursor = buf_cursor_row(&self.buffer);
1802 let top = self.host.viewport().top_row;
1803 cursor.saturating_sub(top).min(height as usize - 1) as u16
1804 }
1805
1806 pub fn cursor_screen_pos(
1816 &self,
1817 area_x: u16,
1818 area_y: u16,
1819 area_width: u16,
1820 area_height: u16,
1821 ) -> Option<(u16, u16)> {
1822 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1823 let v = self.host.viewport();
1824 if pos_row < v.top_row || pos_col < v.top_col {
1825 return None;
1826 }
1827 let lnum_width = if self.settings.number || self.settings.relativenumber {
1828 let needed = buf_row_count(&self.buffer).to_string().len() + 1;
1829 needed.max(self.settings.numberwidth) as u16
1830 } else {
1831 0
1832 };
1833 let dy = (pos_row - v.top_row) as u16;
1834 let line = self.buffer.line(pos_row).unwrap_or("");
1838 let tab_width = if v.tab_width == 0 {
1839 4
1840 } else {
1841 v.tab_width as usize
1842 };
1843 let visual_pos = visual_col_for_char(line, pos_col, tab_width);
1844 let visual_top = visual_col_for_char(line, v.top_col, tab_width);
1845 let dx = (visual_pos - visual_top) as u16;
1846 if dy >= area_height || dx + lnum_width >= area_width {
1847 return None;
1848 }
1849 Some((area_x + lnum_width + dx, area_y + dy))
1850 }
1851
1852 #[cfg(feature = "ratatui")]
1858 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1859 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1860 }
1861
1862 pub fn vim_mode(&self) -> VimMode {
1863 self.vim.public_mode()
1864 }
1865
1866 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1872 self.vim.search_prompt.as_ref()
1873 }
1874
1875 pub fn last_search(&self) -> Option<&str> {
1878 self.vim.last_search.as_deref()
1879 }
1880
1881 pub fn last_search_forward(&self) -> bool {
1885 self.vim.last_search_forward
1886 }
1887
1888 pub fn set_last_search(&mut self, text: Option<String>, forward: bool) {
1894 self.vim.last_search = text;
1895 self.vim.last_search_forward = forward;
1896 }
1897
1898 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1902 if self.vim_mode() != VimMode::Visual {
1903 return None;
1904 }
1905 let anchor = self.vim.visual_anchor;
1906 let cursor = self.cursor();
1907 let (start, end) = if anchor <= cursor {
1908 (anchor, cursor)
1909 } else {
1910 (cursor, anchor)
1911 };
1912 Some((start, end))
1913 }
1914
1915 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1918 if self.vim_mode() != VimMode::VisualLine {
1919 return None;
1920 }
1921 let anchor = self.vim.visual_line_anchor;
1922 let cursor = buf_cursor_row(&self.buffer);
1923 Some((anchor.min(cursor), anchor.max(cursor)))
1924 }
1925
1926 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1927 if self.vim_mode() != VimMode::VisualBlock {
1928 return None;
1929 }
1930 let (ar, ac) = self.vim.block_anchor;
1931 let cr = buf_cursor_row(&self.buffer);
1932 let cc = self.vim.block_vcol;
1933 let top = ar.min(cr);
1934 let bot = ar.max(cr);
1935 let left = ac.min(cc);
1936 let right = ac.max(cc);
1937 Some((top, bot, left, right))
1938 }
1939
1940 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1946 use hjkl_buffer::{Position, Selection};
1947 match self.vim_mode() {
1948 VimMode::Visual => {
1949 let (ar, ac) = self.vim.visual_anchor;
1950 let head = buf_cursor_pos(&self.buffer);
1951 Some(Selection::Char {
1952 anchor: Position::new(ar, ac),
1953 head,
1954 })
1955 }
1956 VimMode::VisualLine => {
1957 let anchor_row = self.vim.visual_line_anchor;
1958 let head_row = buf_cursor_row(&self.buffer);
1959 Some(Selection::Line {
1960 anchor_row,
1961 head_row,
1962 })
1963 }
1964 VimMode::VisualBlock => {
1965 let (ar, ac) = self.vim.block_anchor;
1966 let cr = buf_cursor_row(&self.buffer);
1967 let cc = self.vim.block_vcol;
1968 Some(Selection::Block {
1969 anchor: Position::new(ar, ac),
1970 head: Position::new(cr, cc),
1971 })
1972 }
1973 _ => None,
1974 }
1975 }
1976
1977 pub fn force_normal(&mut self) {
1979 self.vim.force_normal();
1980 }
1981
1982 pub fn content(&self) -> String {
1983 let n = buf_row_count(&self.buffer);
1984 let mut s = String::new();
1985 for r in 0..n {
1986 if r > 0 {
1987 s.push('\n');
1988 }
1989 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1990 }
1991 s.push('\n');
1992 s
1993 }
1994
1995 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
2000 if let Some(arc) = &self.cached_content {
2001 return std::sync::Arc::clone(arc);
2002 }
2003 let arc = std::sync::Arc::new(self.content());
2004 self.cached_content = Some(std::sync::Arc::clone(&arc));
2005 arc
2006 }
2007
2008 pub fn set_content(&mut self, text: &str) {
2009 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
2010 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
2011 lines.pop();
2012 }
2013 if lines.is_empty() {
2014 lines.push(String::new());
2015 }
2016 let _ = lines;
2017 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
2018 self.undo_stack.clear();
2019 self.redo_stack.clear();
2020 self.pending_content_edits.clear();
2022 self.pending_content_reset = true;
2023 self.mark_content_dirty();
2024 }
2025
2026 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
2042 use crate::{PlannedInput, SpecialKey};
2043 let (key, mods) = match input {
2044 PlannedInput::Char(c, m) => (Key::Char(c), m),
2045 PlannedInput::Key(k, m) => {
2046 let key = match k {
2047 SpecialKey::Esc => Key::Esc,
2048 SpecialKey::Enter => Key::Enter,
2049 SpecialKey::Backspace => Key::Backspace,
2050 SpecialKey::Tab => Key::Tab,
2051 SpecialKey::BackTab => Key::Tab,
2055 SpecialKey::Up => Key::Up,
2056 SpecialKey::Down => Key::Down,
2057 SpecialKey::Left => Key::Left,
2058 SpecialKey::Right => Key::Right,
2059 SpecialKey::Home => Key::Home,
2060 SpecialKey::End => Key::End,
2061 SpecialKey::PageUp => Key::PageUp,
2062 SpecialKey::PageDown => Key::PageDown,
2063 SpecialKey::Insert => Key::Null,
2067 SpecialKey::Delete => Key::Delete,
2068 SpecialKey::F(_) => Key::Null,
2069 };
2070 let m = if matches!(k, SpecialKey::BackTab) {
2071 crate::Modifiers { shift: true, ..m }
2072 } else {
2073 m
2074 };
2075 (key, m)
2076 }
2077 PlannedInput::Mouse(_)
2079 | PlannedInput::Paste(_)
2080 | PlannedInput::FocusGained
2081 | PlannedInput::FocusLost
2082 | PlannedInput::Resize(_, _) => return false,
2083 };
2084 if key == Key::Null {
2085 return false;
2086 }
2087 let event = Input {
2088 key,
2089 ctrl: mods.ctrl,
2090 alt: mods.alt,
2091 shift: mods.shift,
2092 };
2093 let consumed = vim::step(self, event);
2094 self.emit_cursor_shape_if_changed();
2095 consumed
2096 }
2097
2098 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
2115 std::mem::take(&mut self.change_log)
2116 }
2117
2118 pub fn current_options(&self) -> crate::types::Options {
2128 crate::types::Options {
2129 shiftwidth: self.settings.shiftwidth as u32,
2130 tabstop: self.settings.tabstop as u32,
2131 softtabstop: self.settings.softtabstop as u32,
2132 textwidth: self.settings.textwidth as u32,
2133 expandtab: self.settings.expandtab,
2134 ignorecase: self.settings.ignore_case,
2135 smartcase: self.settings.smartcase,
2136 wrapscan: self.settings.wrapscan,
2137 wrap: match self.settings.wrap {
2138 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
2139 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
2140 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
2141 },
2142 readonly: self.settings.readonly,
2143 autoindent: self.settings.autoindent,
2144 smartindent: self.settings.smartindent,
2145 undo_levels: self.settings.undo_levels,
2146 undo_break_on_motion: self.settings.undo_break_on_motion,
2147 iskeyword: self.settings.iskeyword.clone(),
2148 timeout_len: self.settings.timeout_len,
2149 ..crate::types::Options::default()
2150 }
2151 }
2152
2153 pub fn apply_options(&mut self, opts: &crate::types::Options) {
2158 self.settings.shiftwidth = opts.shiftwidth as usize;
2159 self.settings.tabstop = opts.tabstop as usize;
2160 self.settings.softtabstop = opts.softtabstop as usize;
2161 self.settings.textwidth = opts.textwidth as usize;
2162 self.settings.expandtab = opts.expandtab;
2163 self.settings.ignore_case = opts.ignorecase;
2164 self.settings.smartcase = opts.smartcase;
2165 self.settings.wrapscan = opts.wrapscan;
2166 self.settings.wrap = match opts.wrap {
2167 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
2168 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
2169 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
2170 };
2171 self.settings.readonly = opts.readonly;
2172 self.settings.autoindent = opts.autoindent;
2173 self.settings.smartindent = opts.smartindent;
2174 self.settings.undo_levels = opts.undo_levels;
2175 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
2176 self.set_iskeyword(opts.iskeyword.clone());
2177 self.settings.timeout_len = opts.timeout_len;
2178 self.settings.number = opts.number;
2179 self.settings.relativenumber = opts.relativenumber;
2180 self.settings.numberwidth = opts.numberwidth;
2181 self.settings.cursorline = opts.cursorline;
2182 self.settings.cursorcolumn = opts.cursorcolumn;
2183 self.settings.signcolumn = opts.signcolumn;
2184 self.settings.foldcolumn = opts.foldcolumn;
2185 self.settings.colorcolumn = opts.colorcolumn.clone();
2186 }
2187
2188 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
2198 use crate::types::{Highlight, HighlightKind, Pos};
2199 let sel = self.buffer_selection()?;
2200 let (start, end) = match sel {
2201 hjkl_buffer::Selection::Char { anchor, head } => {
2202 let a = (anchor.row, anchor.col);
2203 let h = (head.row, head.col);
2204 if a <= h { (a, h) } else { (h, a) }
2205 }
2206 hjkl_buffer::Selection::Line {
2207 anchor_row,
2208 head_row,
2209 } => {
2210 let (top, bot) = if anchor_row <= head_row {
2211 (anchor_row, head_row)
2212 } else {
2213 (head_row, anchor_row)
2214 };
2215 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
2216 ((top, 0), (bot, last_col))
2217 }
2218 hjkl_buffer::Selection::Block { anchor, head } => {
2219 let (top, bot) = if anchor.row <= head.row {
2220 (anchor.row, head.row)
2221 } else {
2222 (head.row, anchor.row)
2223 };
2224 let (left, right) = if anchor.col <= head.col {
2225 (anchor.col, head.col)
2226 } else {
2227 (head.col, anchor.col)
2228 };
2229 ((top, left), (bot, right))
2230 }
2231 };
2232 Some(Highlight {
2233 range: Pos {
2234 line: start.0 as u32,
2235 col: start.1 as u32,
2236 }..Pos {
2237 line: end.0 as u32,
2238 col: end.1 as u32,
2239 },
2240 kind: HighlightKind::Selection,
2241 })
2242 }
2243
2244 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
2263 use crate::types::{Highlight, HighlightKind, Pos};
2264 let row = line as usize;
2265 if row >= buf_row_count(&self.buffer) {
2266 return Vec::new();
2267 }
2268
2269 if let Some(prompt) = self.search_prompt() {
2272 if prompt.text.is_empty() {
2273 return Vec::new();
2274 }
2275 let Ok(re) = regex::Regex::new(&prompt.text) else {
2276 return Vec::new();
2277 };
2278 let Some(haystack) = buf_line(&self.buffer, row) else {
2279 return Vec::new();
2280 };
2281 return re
2282 .find_iter(haystack)
2283 .map(|m| Highlight {
2284 range: Pos {
2285 line,
2286 col: m.start() as u32,
2287 }..Pos {
2288 line,
2289 col: m.end() as u32,
2290 },
2291 kind: HighlightKind::IncSearch,
2292 })
2293 .collect();
2294 }
2295
2296 if self.search_state.pattern.is_none() {
2297 return Vec::new();
2298 }
2299 let dgen = crate::types::Query::dirty_gen(&self.buffer);
2300 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
2301 .into_iter()
2302 .map(|(start, end)| Highlight {
2303 range: Pos {
2304 line,
2305 col: start as u32,
2306 }..Pos {
2307 line,
2308 col: end as u32,
2309 },
2310 kind: HighlightKind::SearchMatch,
2311 })
2312 .collect()
2313 }
2314
2315 pub fn render_frame(&self) -> crate::types::RenderFrame {
2325 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
2326 let (cursor_row, cursor_col) = self.cursor();
2327 let (mode, shape) = match self.vim_mode() {
2328 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
2329 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
2330 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
2331 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
2332 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
2333 };
2334 RenderFrame {
2335 mode,
2336 cursor_row: cursor_row as u32,
2337 cursor_col: cursor_col as u32,
2338 cursor_shape: shape,
2339 viewport_top: self.host.viewport().top_row as u32,
2340 line_count: crate::types::Query::line_count(&self.buffer),
2341 }
2342 }
2343
2344 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
2357 use crate::types::{EditorSnapshot, SnapshotMode};
2358 let mode = match self.vim_mode() {
2359 crate::VimMode::Normal => SnapshotMode::Normal,
2360 crate::VimMode::Insert => SnapshotMode::Insert,
2361 crate::VimMode::Visual => SnapshotMode::Visual,
2362 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
2363 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
2364 };
2365 let cursor = self.cursor();
2366 let cursor = (cursor.0 as u32, cursor.1 as u32);
2367 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
2368 let viewport_top = self.host.viewport().top_row as u32;
2369 let marks = self
2370 .marks
2371 .iter()
2372 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
2373 .collect();
2374 EditorSnapshot {
2375 version: EditorSnapshot::VERSION,
2376 mode,
2377 cursor,
2378 lines,
2379 viewport_top,
2380 registers: self.registers.clone(),
2381 marks,
2382 }
2383 }
2384
2385 pub fn restore_snapshot(
2393 &mut self,
2394 snap: crate::types::EditorSnapshot,
2395 ) -> Result<(), crate::EngineError> {
2396 use crate::types::EditorSnapshot;
2397 if snap.version != EditorSnapshot::VERSION {
2398 return Err(crate::EngineError::SnapshotVersion(
2399 snap.version,
2400 EditorSnapshot::VERSION,
2401 ));
2402 }
2403 let text = snap.lines.join("\n");
2404 self.set_content(&text);
2405 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
2406 self.host.viewport_mut().top_row = snap.viewport_top as usize;
2407 self.registers = snap.registers;
2408 self.marks = snap
2409 .marks
2410 .into_iter()
2411 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
2412 .collect();
2413 Ok(())
2414 }
2415
2416 pub fn seed_yank(&mut self, text: String) {
2420 let linewise = text.ends_with('\n');
2421 self.vim.yank_linewise = linewise;
2422 self.registers.unnamed = crate::registers::Slot { text, linewise };
2423 }
2424
2425 pub fn scroll_down(&mut self, rows: i16) {
2430 self.scroll_viewport(rows);
2431 }
2432
2433 pub fn scroll_up(&mut self, rows: i16) {
2437 self.scroll_viewport(-rows);
2438 }
2439
2440 const SCROLLOFF: usize = 5;
2444
2445 pub fn ensure_cursor_in_scrolloff(&mut self) {
2450 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2451 if height == 0 {
2452 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2459 crate::viewport_math::ensure_cursor_visible(
2460 &self.buffer,
2461 &folds,
2462 self.host.viewport_mut(),
2463 );
2464 return;
2465 }
2466 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2470 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
2473 self.ensure_scrolloff_wrap(height, margin);
2474 return;
2475 }
2476 let cursor_row = buf_cursor_row(&self.buffer);
2477 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
2478 let v = self.host.viewport_mut();
2479 if cursor_row < v.top_row + margin {
2481 v.top_row = cursor_row.saturating_sub(margin);
2482 }
2483 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
2485 if cursor_row > v.top_row + max_bottom {
2486 v.top_row = cursor_row.saturating_sub(max_bottom);
2487 }
2488 let max_top = last_row.saturating_sub(height.saturating_sub(1));
2490 if v.top_row > max_top {
2491 v.top_row = max_top;
2492 }
2493 let cursor = buf_cursor_pos(&self.buffer);
2496 self.host.viewport_mut().ensure_visible(cursor);
2497 }
2498
2499 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
2504 let cursor_row = buf_cursor_row(&self.buffer);
2505 if cursor_row < self.host.viewport().top_row {
2508 let v = self.host.viewport_mut();
2509 v.top_row = cursor_row;
2510 v.top_col = 0;
2511 }
2512 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2521 loop {
2522 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2523 let csr =
2524 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2525 .unwrap_or(0);
2526 if csr <= max_csr {
2527 break;
2528 }
2529 let top = self.host.viewport().top_row;
2530 let row_count = buf_row_count(&self.buffer);
2531 let next = {
2532 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2533 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2534 };
2535 let Some(next) = next else {
2536 break;
2537 };
2538 if next > cursor_row {
2540 self.host.viewport_mut().top_row = cursor_row;
2541 break;
2542 }
2543 self.host.viewport_mut().top_row = next;
2544 }
2545 loop {
2548 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2549 let csr =
2550 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2551 .unwrap_or(0);
2552 if csr >= margin {
2553 break;
2554 }
2555 let top = self.host.viewport().top_row;
2556 let prev = {
2557 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2558 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2559 };
2560 let Some(prev) = prev else {
2561 break;
2562 };
2563 self.host.viewport_mut().top_row = prev;
2564 }
2565 let max_top = {
2570 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2571 crate::viewport_math::max_top_for_height(
2572 &self.buffer,
2573 &folds,
2574 self.host.viewport(),
2575 height,
2576 )
2577 };
2578 if self.host.viewport().top_row > max_top {
2579 self.host.viewport_mut().top_row = max_top;
2580 }
2581 self.host.viewport_mut().top_col = 0;
2582 }
2583
2584 fn scroll_viewport(&mut self, delta: i16) {
2585 if delta == 0 {
2586 return;
2587 }
2588 let total_rows = buf_row_count(&self.buffer) as isize;
2590 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2591 let cur_top = self.host.viewport().top_row as isize;
2592 let new_top = (cur_top + delta as isize)
2593 .max(0)
2594 .min((total_rows - 1).max(0)) as usize;
2595 self.host.viewport_mut().top_row = new_top;
2596 let _ = cur_top;
2599 if height == 0 {
2600 return;
2601 }
2602 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2605 let margin = Self::SCROLLOFF.min(height / 2);
2606 let min_row = new_top + margin;
2607 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2608 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2609 if target_row != cursor_row {
2610 let line_len = buf_line(&self.buffer, target_row)
2611 .map(|l| l.chars().count())
2612 .unwrap_or(0);
2613 let target_col = cursor_col.min(line_len.saturating_sub(1));
2614 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2615 }
2616 }
2617
2618 pub fn goto_line(&mut self, line: usize) {
2619 let row = line.saturating_sub(1);
2620 let max = buf_row_count(&self.buffer).saturating_sub(1);
2621 let target = row.min(max);
2622 buf_set_cursor_rc(&mut self.buffer, target, 0);
2623 self.ensure_cursor_in_scrolloff();
2627 }
2628
2629 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2633 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2634 if height == 0 {
2635 return;
2636 }
2637 let cur_row = buf_cursor_row(&self.buffer);
2638 let cur_top = self.host.viewport().top_row;
2639 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2645 let new_top = match pos {
2646 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2647 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2648 CursorScrollTarget::Bottom => {
2649 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2650 }
2651 };
2652 if new_top == cur_top {
2653 return;
2654 }
2655 self.host.viewport_mut().top_row = new_top;
2656 }
2657
2658 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2669 let n = buf_row_count(&self.buffer);
2670 let inner_top = area_y.saturating_add(1); let lnum_width = if self.settings.number || self.settings.relativenumber {
2672 let needed = n.to_string().len() + 1;
2673 needed.max(self.settings.numberwidth) as u16
2674 } else {
2675 0
2676 };
2677 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2678 let rel_row = row.saturating_sub(inner_top) as usize;
2679 let top = self.host.viewport().top_row;
2680 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2681 let rel_col = col.saturating_sub(content_x) as usize;
2682 let line_chars = buf_line(&self.buffer, doc_row)
2683 .map(|l| l.chars().count())
2684 .unwrap_or(0);
2685 let last_col = line_chars.saturating_sub(1);
2686 (doc_row, rel_col.min(last_col))
2687 }
2688
2689 pub fn jump_to(&mut self, line: usize, col: usize) {
2691 let r = line.saturating_sub(1);
2692 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2693 let r = r.min(max_row);
2694 let line_len = buf_line(&self.buffer, r)
2695 .map(|l| l.chars().count())
2696 .unwrap_or(0);
2697 let c = col.saturating_sub(1).min(line_len);
2698 buf_set_cursor_rc(&mut self.buffer, r, c);
2699 }
2700
2701 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2709 if self.vim.is_visual() {
2710 self.vim.force_normal();
2711 }
2712 crate::vim::break_undo_group_in_insert(self);
2715 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2716 buf_set_cursor_rc(&mut self.buffer, r, c);
2717 }
2718
2719 #[cfg(feature = "ratatui")]
2725 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2726 self.mouse_click(area.x, area.y, col, row);
2727 }
2728
2729 pub fn mouse_begin_drag(&mut self) {
2731 if !self.vim.is_visual_char() {
2732 let cursor = self.cursor();
2733 self.vim.enter_visual(cursor);
2734 }
2735 }
2736
2737 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2743 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2744 buf_set_cursor_rc(&mut self.buffer, r, c);
2745 }
2746
2747 #[cfg(feature = "ratatui")]
2753 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2754 self.mouse_extend_drag(area.x, area.y, col, row);
2755 }
2756
2757 pub fn insert_str(&mut self, text: &str) {
2758 let pos = crate::types::Cursor::cursor(&self.buffer);
2759 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2760 self.push_buffer_content_to_textarea();
2761 self.mark_content_dirty();
2762 }
2763
2764 pub fn accept_completion(&mut self, completion: &str) {
2765 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2766 let cursor_pos = CursorTrait::cursor(&self.buffer);
2767 let cursor_row = cursor_pos.line as usize;
2768 let cursor_col = cursor_pos.col as usize;
2769 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2770 let chars: Vec<char> = line.chars().collect();
2771 let prefix_len = chars[..cursor_col.min(chars.len())]
2772 .iter()
2773 .rev()
2774 .take_while(|c| c.is_alphanumeric() || **c == '_')
2775 .count();
2776 if prefix_len > 0 {
2777 let start = Pos {
2778 line: cursor_row as u32,
2779 col: (cursor_col - prefix_len) as u32,
2780 };
2781 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2782 }
2783 let cursor = CursorTrait::cursor(&self.buffer);
2784 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2785 self.push_buffer_content_to_textarea();
2786 self.mark_content_dirty();
2787 }
2788
2789 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2790 let rc = buf_cursor_rc(&self.buffer);
2791 (buf_lines_to_vec(&self.buffer), rc)
2792 }
2793
2794 pub fn undo(&mut self) {
2798 crate::vim::do_undo(self);
2799 }
2800
2801 pub fn redo(&mut self) {
2804 crate::vim::do_redo(self);
2805 }
2806
2807 pub fn push_undo(&mut self) {
2812 let snap = self.snapshot();
2813 self.undo_stack.push(snap);
2814 self.cap_undo();
2815 self.redo_stack.clear();
2816 }
2817
2818 pub(crate) fn cap_undo(&mut self) {
2824 let cap = self.settings.undo_levels as usize;
2825 if cap > 0 && self.undo_stack.len() > cap {
2826 let diff = self.undo_stack.len() - cap;
2827 self.undo_stack.drain(..diff);
2828 }
2829 }
2830
2831 #[doc(hidden)]
2833 pub fn undo_stack_len(&self) -> usize {
2834 self.undo_stack.len()
2835 }
2836
2837 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2841 let text = lines.join("\n");
2842 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2843 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2844 self.pending_content_edits.clear();
2846 self.pending_content_reset = true;
2847 self.mark_content_dirty();
2848 }
2849
2850 pub fn replace_char_at(&mut self, ch: char, count: usize) {
2857 vim::replace_char(self, ch, count);
2858 }
2859
2860 pub fn find_char(&mut self, ch: char, forward: bool, till: bool, count: usize) {
2868 vim::apply_find_char(self, ch, forward, till, count.max(1));
2869 }
2870
2871 pub fn after_g(&mut self, ch: char, count: usize) {
2879 vim::apply_after_g(self, ch, count);
2880 }
2881
2882 pub fn after_z(&mut self, ch: char, count: usize) {
2891 vim::apply_after_z(self, ch, count);
2892 }
2893
2894 pub fn apply_op_motion(
2908 &mut self,
2909 op: crate::vim::Operator,
2910 motion_key: char,
2911 total_count: usize,
2912 ) {
2913 vim::apply_op_motion_key(self, op, motion_key, total_count);
2914 }
2915
2916 pub fn apply_op_double(&mut self, op: crate::vim::Operator, total_count: usize) {
2923 vim::apply_op_double(self, op, total_count);
2924 }
2925
2926 pub fn apply_op_find(
2939 &mut self,
2940 op: crate::vim::Operator,
2941 ch: char,
2942 forward: bool,
2943 till: bool,
2944 total_count: usize,
2945 ) {
2946 vim::apply_op_find_motion(self, op, ch, forward, till, total_count);
2947 }
2948
2949 pub fn apply_op_text_obj(
2965 &mut self,
2966 op: crate::vim::Operator,
2967 ch: char,
2968 inner: bool,
2969 total_count: usize,
2970 ) {
2971 vim::apply_op_text_obj_inner(self, op, ch, inner, total_count);
2972 }
2973
2974 pub fn apply_op_g(&mut self, op: crate::vim::Operator, ch: char, total_count: usize) {
2996 vim::apply_op_g_inner(self, op, ch, total_count);
2997 }
2998
2999 pub fn delete_range(
3017 &mut self,
3018 start: (usize, usize),
3019 end: (usize, usize),
3020 kind: crate::vim::MotionKind,
3021 register: char,
3022 ) {
3023 vim::delete_range_bridge(self, start, end, kind, register);
3024 }
3025
3026 pub fn yank_range(
3037 &mut self,
3038 start: (usize, usize),
3039 end: (usize, usize),
3040 kind: crate::vim::MotionKind,
3041 register: char,
3042 ) {
3043 vim::yank_range_bridge(self, start, end, kind, register);
3044 }
3045
3046 pub fn change_range(
3058 &mut self,
3059 start: (usize, usize),
3060 end: (usize, usize),
3061 kind: crate::vim::MotionKind,
3062 register: char,
3063 ) {
3064 vim::change_range_bridge(self, start, end, kind, register);
3065 }
3066
3067 pub fn indent_range(
3080 &mut self,
3081 start: (usize, usize),
3082 end: (usize, usize),
3083 count: i32,
3084 shiftwidth: u32,
3085 ) {
3086 vim::indent_range_bridge(self, start, end, count, shiftwidth);
3087 }
3088
3089 pub fn case_range(
3102 &mut self,
3103 start: (usize, usize),
3104 end: (usize, usize),
3105 kind: crate::vim::MotionKind,
3106 op: crate::vim::Operator,
3107 ) {
3108 vim::case_range_bridge(self, start, end, kind, op);
3109 }
3110
3111 pub fn delete_block(
3131 &mut self,
3132 top_row: usize,
3133 bot_row: usize,
3134 left_col: usize,
3135 right_col: usize,
3136 register: char,
3137 ) {
3138 vim::delete_block_bridge(self, top_row, bot_row, left_col, right_col, register);
3139 }
3140
3141 pub fn yank_block(
3146 &mut self,
3147 top_row: usize,
3148 bot_row: usize,
3149 left_col: usize,
3150 right_col: usize,
3151 register: char,
3152 ) {
3153 vim::yank_block_bridge(self, top_row, bot_row, left_col, right_col, register);
3154 }
3155
3156 pub fn change_block(
3163 &mut self,
3164 top_row: usize,
3165 bot_row: usize,
3166 left_col: usize,
3167 right_col: usize,
3168 register: char,
3169 ) {
3170 vim::change_block_bridge(self, top_row, bot_row, left_col, right_col, register);
3171 }
3172
3173 pub fn indent_block(
3179 &mut self,
3180 top_row: usize,
3181 bot_row: usize,
3182 _left_col: usize,
3183 _right_col: usize,
3184 count: i32,
3185 ) {
3186 vim::indent_block_bridge(self, top_row, bot_row, count);
3187 }
3188
3189 pub fn text_object_inner_word(&self) -> Option<((usize, usize), (usize, usize))> {
3217 vim::text_object_inner_word_bridge(self)
3218 }
3219
3220 pub fn text_object_around_word(&self) -> Option<((usize, usize), (usize, usize))> {
3231 vim::text_object_around_word_bridge(self)
3232 }
3233
3234 pub fn text_object_inner_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
3245 vim::text_object_inner_big_word_bridge(self)
3246 }
3247
3248 pub fn text_object_around_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
3259 vim::text_object_around_big_word_bridge(self)
3260 }
3261
3262 pub fn text_object_inner_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))> {
3294 vim::text_object_inner_quote_bridge(self, quote)
3295 }
3296
3297 pub fn text_object_around_quote(
3309 &self,
3310 quote: char,
3311 ) -> Option<((usize, usize), (usize, usize))> {
3312 vim::text_object_around_quote_bridge(self, quote)
3313 }
3314
3315 pub fn text_object_inner_bracket(
3338 &self,
3339 open: char,
3340 ) -> Option<((usize, usize), (usize, usize))> {
3341 vim::text_object_inner_bracket_bridge(self, open)
3342 }
3343
3344 pub fn text_object_around_bracket(
3356 &self,
3357 open: char,
3358 ) -> Option<((usize, usize), (usize, usize))> {
3359 vim::text_object_around_bracket_bridge(self, open)
3360 }
3361
3362 pub fn text_object_inner_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
3375 vim::text_object_inner_sentence_bridge(self)
3376 }
3377
3378 pub fn text_object_around_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
3388 vim::text_object_around_sentence_bridge(self)
3389 }
3390
3391 pub fn text_object_inner_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
3403 vim::text_object_inner_paragraph_bridge(self)
3404 }
3405
3406 pub fn text_object_around_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
3415 vim::text_object_around_paragraph_bridge(self)
3416 }
3417
3418 pub fn text_object_inner_tag(&self) -> Option<((usize, usize), (usize, usize))> {
3431 vim::text_object_inner_tag_bridge(self)
3432 }
3433
3434 pub fn text_object_around_tag(&self) -> Option<((usize, usize), (usize, usize))> {
3443 vim::text_object_around_tag_bridge(self)
3444 }
3445
3446 pub fn apply_motion(&mut self, kind: hjkl_vim::MotionKind, count: usize) {
3462 vim::apply_motion_kind(self, kind, count);
3463 }
3464
3465 pub fn set_pending_register(&mut self, reg: char) {
3476 if reg.is_ascii_alphanumeric() || matches!(reg, '"' | '+' | '*' | '_') {
3477 self.vim.pending_register = Some(reg);
3478 }
3479 }
3481
3482 pub fn set_mark_at_cursor(&mut self, ch: char) {
3492 vim::set_mark_at_cursor(self, ch);
3493 }
3494
3495 pub fn replay_last_change(&mut self, count: usize) {
3504 vim::replay_last_change(self, count);
3505 }
3506
3507 pub fn goto_mark_line(&mut self, ch: char) {
3521 vim::goto_mark(self, ch, true);
3522 }
3523
3524 pub fn goto_mark_char(&mut self, ch: char) {
3537 vim::goto_mark(self, ch, false);
3538 }
3539
3540 pub fn start_macro_record(&mut self, reg: char) {
3558 if !(reg.is_ascii_alphabetic() || reg.is_ascii_digit()) {
3559 return;
3560 }
3561 self.vim.recording_macro = Some(reg);
3562 if reg.is_ascii_uppercase() {
3563 let lower = reg.to_ascii_lowercase();
3567 let text = self
3568 .registers
3569 .read(lower)
3570 .map(|s| s.text.clone())
3571 .unwrap_or_default();
3572 self.vim.recording_keys = crate::input::decode_macro(&text);
3573 } else {
3574 self.vim.recording_keys.clear();
3575 }
3576 }
3577
3578 pub fn stop_macro_record(&mut self) {
3586 let Some(reg) = self.vim.recording_macro.take() else {
3587 return;
3588 };
3589 let keys = std::mem::take(&mut self.vim.recording_keys);
3590 let text = crate::input::encode_macro(&keys);
3591 self.set_named_register_text(reg.to_ascii_lowercase(), text);
3592 }
3593
3594 pub fn is_recording_macro(&self) -> bool {
3599 self.vim.recording_macro.is_some()
3600 }
3601
3602 pub fn is_replaying_macro(&self) -> bool {
3606 self.vim.replaying_macro
3607 }
3608
3609 pub fn play_macro(&mut self, reg: char, count: usize) -> Vec<crate::input::Input> {
3625 let resolved = if reg == '@' {
3626 match self.vim.last_macro {
3627 Some(r) => r,
3628 None => return vec![],
3629 }
3630 } else {
3631 reg.to_ascii_lowercase()
3632 };
3633 let text = match self.registers.read(resolved) {
3634 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
3635 _ => return vec![],
3636 };
3637 let keys = crate::input::decode_macro(&text);
3638 self.vim.last_macro = Some(resolved);
3639 self.vim.replaying_macro = true;
3640 keys.repeat(count.max(1))
3642 }
3643
3644 pub fn end_macro_replay(&mut self) {
3647 self.vim.replaying_macro = false;
3648 }
3649
3650 pub fn record_input(&mut self, input: crate::input::Input) {
3656 if self.vim.recording_macro.is_some() && !self.vim.replaying_macro {
3657 self.vim.recording_keys.push(input);
3658 }
3659 }
3660
3661 #[cfg(feature = "crossterm")]
3662 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
3663 let input = crossterm_to_input(key);
3664 if input.key == Key::Null {
3665 return false;
3666 }
3667 let consumed = vim::step(self, input);
3668 self.emit_cursor_shape_if_changed();
3669 consumed
3670 }
3671
3672 pub fn insert_char(&mut self, ch: char) {
3693 let mutated = vim::insert_char_bridge(self, ch);
3694 if mutated {
3695 self.mark_content_dirty();
3696 let (row, _) = self.cursor();
3697 self.vim.widen_insert_row(row);
3698 }
3699 }
3700
3701 pub fn insert_newline(&mut self) {
3706 let mutated = vim::insert_newline_bridge(self);
3707 if mutated {
3708 self.mark_content_dirty();
3709 let (row, _) = self.cursor();
3710 self.vim.widen_insert_row(row);
3711 }
3712 }
3713
3714 pub fn insert_tab(&mut self) {
3719 let mutated = vim::insert_tab_bridge(self);
3720 if mutated {
3721 self.mark_content_dirty();
3722 let (row, _) = self.cursor();
3723 self.vim.widen_insert_row(row);
3724 }
3725 }
3726
3727 pub fn insert_backspace(&mut self) {
3733 let mutated = vim::insert_backspace_bridge(self);
3734 if mutated {
3735 self.mark_content_dirty();
3736 let (row, _) = self.cursor();
3737 self.vim.widen_insert_row(row);
3738 }
3739 }
3740
3741 pub fn insert_delete(&mut self) {
3746 let mutated = vim::insert_delete_bridge(self);
3747 if mutated {
3748 self.mark_content_dirty();
3749 let (row, _) = self.cursor();
3750 self.vim.widen_insert_row(row);
3751 }
3752 }
3753
3754 pub fn insert_arrow(&mut self, dir: vim::InsertDir) {
3759 vim::insert_arrow_bridge(self, dir);
3760 let (row, _) = self.cursor();
3761 self.vim.widen_insert_row(row);
3762 }
3763
3764 pub fn insert_home(&mut self) {
3769 vim::insert_home_bridge(self);
3770 let (row, _) = self.cursor();
3771 self.vim.widen_insert_row(row);
3772 }
3773
3774 pub fn insert_end(&mut self) {
3779 vim::insert_end_bridge(self);
3780 let (row, _) = self.cursor();
3781 self.vim.widen_insert_row(row);
3782 }
3783
3784 pub fn insert_pageup(&mut self, viewport_h: u16) {
3790 vim::insert_pageup_bridge(self, viewport_h);
3791 let (row, _) = self.cursor();
3792 self.vim.widen_insert_row(row);
3793 }
3794
3795 pub fn insert_pagedown(&mut self, viewport_h: u16) {
3800 vim::insert_pagedown_bridge(self, viewport_h);
3801 let (row, _) = self.cursor();
3802 self.vim.widen_insert_row(row);
3803 }
3804
3805 pub fn insert_ctrl_w(&mut self) {
3810 let mutated = vim::insert_ctrl_w_bridge(self);
3811 if mutated {
3812 self.mark_content_dirty();
3813 let (row, _) = self.cursor();
3814 self.vim.widen_insert_row(row);
3815 }
3816 }
3817
3818 pub fn insert_ctrl_u(&mut self) {
3823 let mutated = vim::insert_ctrl_u_bridge(self);
3824 if mutated {
3825 self.mark_content_dirty();
3826 let (row, _) = self.cursor();
3827 self.vim.widen_insert_row(row);
3828 }
3829 }
3830
3831 pub fn insert_ctrl_h(&mut self) {
3836 let mutated = vim::insert_ctrl_h_bridge(self);
3837 if mutated {
3838 self.mark_content_dirty();
3839 let (row, _) = self.cursor();
3840 self.vim.widen_insert_row(row);
3841 }
3842 }
3843
3844 pub fn insert_ctrl_o_arm(&mut self) {
3849 vim::insert_ctrl_o_bridge(self);
3850 }
3851
3852 pub fn insert_ctrl_r_arm(&mut self) {
3859 vim::insert_ctrl_r_bridge(self);
3860 }
3861
3862 pub fn insert_ctrl_t(&mut self) {
3867 let mutated = vim::insert_ctrl_t_bridge(self);
3868 if mutated {
3869 self.mark_content_dirty();
3870 let (row, _) = self.cursor();
3871 self.vim.widen_insert_row(row);
3872 }
3873 }
3874
3875 pub fn insert_ctrl_d(&mut self) {
3880 let mutated = vim::insert_ctrl_d_bridge(self);
3881 if mutated {
3882 self.mark_content_dirty();
3883 let (row, _) = self.cursor();
3884 self.vim.widen_insert_row(row);
3885 }
3886 }
3887
3888 pub fn insert_paste_register(&mut self, reg: char) {
3893 vim::insert_paste_register_bridge(self, reg);
3894 let (row, _) = self.cursor();
3895 self.vim.widen_insert_row(row);
3896 }
3897
3898 pub fn leave_insert_to_normal(&mut self) {
3904 vim::leave_insert_to_normal_bridge(self);
3905 }
3906}
3907
3908fn visual_col_for_char(line: &str, char_col: usize, tab_width: usize) -> usize {
3913 let mut visual = 0usize;
3914 for (i, ch) in line.chars().enumerate() {
3915 if i >= char_col {
3916 break;
3917 }
3918 if ch == '\t' {
3919 visual += tab_width - (visual % tab_width);
3920 } else {
3921 visual += 1;
3922 }
3923 }
3924 visual
3925}
3926
3927#[cfg(feature = "crossterm")]
3928impl From<KeyEvent> for Input {
3929 fn from(key: KeyEvent) -> Self {
3930 let k = match key.code {
3931 KeyCode::Char(c) => Key::Char(c),
3932 KeyCode::Backspace => Key::Backspace,
3933 KeyCode::Delete => Key::Delete,
3934 KeyCode::Enter => Key::Enter,
3935 KeyCode::Left => Key::Left,
3936 KeyCode::Right => Key::Right,
3937 KeyCode::Up => Key::Up,
3938 KeyCode::Down => Key::Down,
3939 KeyCode::Home => Key::Home,
3940 KeyCode::End => Key::End,
3941 KeyCode::Tab => Key::Tab,
3942 KeyCode::Esc => Key::Esc,
3943 _ => Key::Null,
3944 };
3945 Input {
3946 key: k,
3947 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
3948 alt: key.modifiers.contains(KeyModifiers::ALT),
3949 shift: key.modifiers.contains(KeyModifiers::SHIFT),
3950 }
3951 }
3952}
3953
3954#[cfg(feature = "crossterm")]
3958pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
3959 Input::from(key)
3960}
3961
3962#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
3963mod tests {
3964 use super::*;
3965 use crate::types::Host;
3966 use crossterm::event::KeyEvent;
3967
3968 fn key(code: KeyCode) -> KeyEvent {
3969 KeyEvent::new(code, KeyModifiers::NONE)
3970 }
3971 fn shift_key(code: KeyCode) -> KeyEvent {
3972 KeyEvent::new(code, KeyModifiers::SHIFT)
3973 }
3974 fn ctrl_key(code: KeyCode) -> KeyEvent {
3975 KeyEvent::new(code, KeyModifiers::CONTROL)
3976 }
3977
3978 #[test]
3979 fn vim_normal_to_insert() {
3980 let mut e = Editor::new(
3981 hjkl_buffer::Buffer::new(),
3982 crate::types::DefaultHost::new(),
3983 crate::types::Options::default(),
3984 );
3985 e.handle_key(key(KeyCode::Char('i')));
3986 assert_eq!(e.vim_mode(), VimMode::Insert);
3987 }
3988
3989 #[test]
3990 fn with_options_constructs_from_spec_options() {
3991 let opts = crate::types::Options {
3995 shiftwidth: 4,
3996 tabstop: 4,
3997 expandtab: true,
3998 iskeyword: "@,a-z".to_string(),
3999 wrap: crate::types::WrapMode::Word,
4000 ..crate::types::Options::default()
4001 };
4002 let mut e = Editor::new(
4003 hjkl_buffer::Buffer::new(),
4004 crate::types::DefaultHost::new(),
4005 opts,
4006 );
4007 assert_eq!(e.settings().shiftwidth, 4);
4008 assert_eq!(e.settings().tabstop, 4);
4009 assert!(e.settings().expandtab);
4010 assert_eq!(e.settings().iskeyword, "@,a-z");
4011 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
4012 e.handle_key(key(KeyCode::Char('i')));
4014 assert_eq!(e.vim_mode(), VimMode::Insert);
4015 }
4016
4017 #[test]
4018 fn feed_input_char_routes_through_handle_key() {
4019 use crate::{Modifiers, PlannedInput};
4020 let mut e = Editor::new(
4021 hjkl_buffer::Buffer::new(),
4022 crate::types::DefaultHost::new(),
4023 crate::types::Options::default(),
4024 );
4025 e.set_content("abc");
4026 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
4028 assert_eq!(e.vim_mode(), VimMode::Insert);
4029 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
4031 assert!(e.content().contains('X'));
4032 }
4033
4034 #[test]
4035 fn feed_input_special_key_routes() {
4036 use crate::{Modifiers, PlannedInput, SpecialKey};
4037 let mut e = Editor::new(
4038 hjkl_buffer::Buffer::new(),
4039 crate::types::DefaultHost::new(),
4040 crate::types::Options::default(),
4041 );
4042 e.set_content("abc");
4043 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
4044 assert_eq!(e.vim_mode(), VimMode::Insert);
4045 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
4046 assert_eq!(e.vim_mode(), VimMode::Normal);
4047 }
4048
4049 #[test]
4050 fn feed_input_mouse_paste_focus_resize_no_op() {
4051 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
4052 let mut e = Editor::new(
4053 hjkl_buffer::Buffer::new(),
4054 crate::types::DefaultHost::new(),
4055 crate::types::Options::default(),
4056 );
4057 e.set_content("abc");
4058 let mode_before = e.vim_mode();
4059 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
4060 kind: MouseKind::Press,
4061 pos: Pos::new(0, 0),
4062 mods: Default::default(),
4063 }));
4064 assert!(!consumed);
4065 assert_eq!(e.vim_mode(), mode_before);
4066 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
4067 assert!(!e.feed_input(PlannedInput::FocusGained));
4068 assert!(!e.feed_input(PlannedInput::FocusLost));
4069 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
4070 }
4071
4072 #[test]
4073 fn intern_style_dedups_engine_native_styles() {
4074 use crate::types::{Attrs, Color, Style};
4075 let mut e = Editor::new(
4076 hjkl_buffer::Buffer::new(),
4077 crate::types::DefaultHost::new(),
4078 crate::types::Options::default(),
4079 );
4080 let s = Style {
4081 fg: Some(Color(255, 0, 0)),
4082 bg: None,
4083 attrs: Attrs::BOLD,
4084 };
4085 let id_a = e.intern_style(s);
4086 let id_b = e.intern_style(s);
4088 assert_eq!(id_a, id_b);
4089 let back = e.engine_style_at(id_a).expect("interned");
4091 assert_eq!(back, s);
4092 }
4093
4094 #[test]
4095 fn engine_style_at_out_of_range_returns_none() {
4096 let e = Editor::new(
4097 hjkl_buffer::Buffer::new(),
4098 crate::types::DefaultHost::new(),
4099 crate::types::Options::default(),
4100 );
4101 assert!(e.engine_style_at(99).is_none());
4102 }
4103
4104 #[test]
4105 fn take_changes_emits_per_row_for_block_insert() {
4106 let mut e = Editor::new(
4111 hjkl_buffer::Buffer::new(),
4112 crate::types::DefaultHost::new(),
4113 crate::types::Options::default(),
4114 );
4115 e.set_content("aaa\nbbb\nccc\nddd");
4116 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
4118 e.handle_key(key(KeyCode::Char('j')));
4119 e.handle_key(key(KeyCode::Char('j')));
4120 e.handle_key(shift_key(KeyCode::Char('I')));
4122 e.handle_key(key(KeyCode::Char('X')));
4123 e.handle_key(key(KeyCode::Esc));
4124
4125 let changes = e.take_changes();
4126 assert!(
4130 changes.len() >= 3,
4131 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
4132 changes.len()
4133 );
4134 }
4135
4136 #[test]
4137 fn take_changes_drains_after_insert() {
4138 let mut e = Editor::new(
4139 hjkl_buffer::Buffer::new(),
4140 crate::types::DefaultHost::new(),
4141 crate::types::Options::default(),
4142 );
4143 e.set_content("abc");
4144 assert!(e.take_changes().is_empty());
4146 e.handle_key(key(KeyCode::Char('i')));
4148 e.handle_key(key(KeyCode::Char('X')));
4149 let changes = e.take_changes();
4150 assert!(
4151 !changes.is_empty(),
4152 "insert mode keystroke should produce a change"
4153 );
4154 assert!(e.take_changes().is_empty());
4156 }
4157
4158 #[test]
4159 fn options_bridge_roundtrip() {
4160 let mut e = Editor::new(
4161 hjkl_buffer::Buffer::new(),
4162 crate::types::DefaultHost::new(),
4163 crate::types::Options::default(),
4164 );
4165 let opts = e.current_options();
4166 assert_eq!(opts.shiftwidth, 4);
4168 assert_eq!(opts.tabstop, 4);
4169
4170 let new_opts = crate::types::Options {
4171 shiftwidth: 4,
4172 tabstop: 2,
4173 ignorecase: true,
4174 ..crate::types::Options::default()
4175 };
4176 e.apply_options(&new_opts);
4177
4178 let after = e.current_options();
4179 assert_eq!(after.shiftwidth, 4);
4180 assert_eq!(after.tabstop, 2);
4181 assert!(after.ignorecase);
4182 }
4183
4184 #[test]
4185 fn selection_highlight_none_in_normal() {
4186 let mut e = Editor::new(
4187 hjkl_buffer::Buffer::new(),
4188 crate::types::DefaultHost::new(),
4189 crate::types::Options::default(),
4190 );
4191 e.set_content("hello");
4192 assert!(e.selection_highlight().is_none());
4193 }
4194
4195 #[test]
4196 fn selection_highlight_some_in_visual() {
4197 use crate::types::HighlightKind;
4198 let mut e = Editor::new(
4199 hjkl_buffer::Buffer::new(),
4200 crate::types::DefaultHost::new(),
4201 crate::types::Options::default(),
4202 );
4203 e.set_content("hello world");
4204 e.handle_key(key(KeyCode::Char('v')));
4205 e.handle_key(key(KeyCode::Char('l')));
4206 e.handle_key(key(KeyCode::Char('l')));
4207 let h = e
4208 .selection_highlight()
4209 .expect("visual mode should produce a highlight");
4210 assert_eq!(h.kind, HighlightKind::Selection);
4211 assert_eq!(h.range.start.line, 0);
4212 assert_eq!(h.range.end.line, 0);
4213 }
4214
4215 #[test]
4216 fn highlights_emit_incsearch_during_active_prompt() {
4217 use crate::types::HighlightKind;
4218 let mut e = Editor::new(
4219 hjkl_buffer::Buffer::new(),
4220 crate::types::DefaultHost::new(),
4221 crate::types::Options::default(),
4222 );
4223 e.set_content("foo bar foo\nbaz\n");
4224 e.handle_key(key(KeyCode::Char('/')));
4226 e.handle_key(key(KeyCode::Char('f')));
4227 e.handle_key(key(KeyCode::Char('o')));
4228 e.handle_key(key(KeyCode::Char('o')));
4229 assert!(e.search_prompt().is_some());
4231 let hs = e.highlights_for_line(0);
4232 assert_eq!(hs.len(), 2);
4233 for h in &hs {
4234 assert_eq!(h.kind, HighlightKind::IncSearch);
4235 }
4236 }
4237
4238 #[test]
4239 fn highlights_empty_for_blank_prompt() {
4240 let mut e = Editor::new(
4241 hjkl_buffer::Buffer::new(),
4242 crate::types::DefaultHost::new(),
4243 crate::types::Options::default(),
4244 );
4245 e.set_content("foo");
4246 e.handle_key(key(KeyCode::Char('/')));
4247 assert!(e.search_prompt().is_some());
4249 assert!(e.highlights_for_line(0).is_empty());
4250 }
4251
4252 #[test]
4253 fn highlights_emit_search_matches() {
4254 use crate::types::HighlightKind;
4255 let mut e = Editor::new(
4256 hjkl_buffer::Buffer::new(),
4257 crate::types::DefaultHost::new(),
4258 crate::types::Options::default(),
4259 );
4260 e.set_content("foo bar foo\nbaz qux\n");
4261 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
4265 let hs = e.highlights_for_line(0);
4266 assert_eq!(hs.len(), 2);
4267 for h in &hs {
4268 assert_eq!(h.kind, HighlightKind::SearchMatch);
4269 assert_eq!(h.range.start.line, 0);
4270 assert_eq!(h.range.end.line, 0);
4271 }
4272 }
4273
4274 #[test]
4275 fn highlights_empty_without_pattern() {
4276 let mut e = Editor::new(
4277 hjkl_buffer::Buffer::new(),
4278 crate::types::DefaultHost::new(),
4279 crate::types::Options::default(),
4280 );
4281 e.set_content("foo bar");
4282 assert!(e.highlights_for_line(0).is_empty());
4283 }
4284
4285 #[test]
4286 fn highlights_empty_for_out_of_range_line() {
4287 let mut e = Editor::new(
4288 hjkl_buffer::Buffer::new(),
4289 crate::types::DefaultHost::new(),
4290 crate::types::Options::default(),
4291 );
4292 e.set_content("foo");
4293 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
4294 assert!(e.highlights_for_line(99).is_empty());
4295 }
4296
4297 #[test]
4298 fn render_frame_reflects_mode_and_cursor() {
4299 use crate::types::{CursorShape, SnapshotMode};
4300 let mut e = Editor::new(
4301 hjkl_buffer::Buffer::new(),
4302 crate::types::DefaultHost::new(),
4303 crate::types::Options::default(),
4304 );
4305 e.set_content("alpha\nbeta");
4306 let f = e.render_frame();
4307 assert_eq!(f.mode, SnapshotMode::Normal);
4308 assert_eq!(f.cursor_shape, CursorShape::Block);
4309 assert_eq!(f.line_count, 2);
4310
4311 e.handle_key(key(KeyCode::Char('i')));
4312 let f = e.render_frame();
4313 assert_eq!(f.mode, SnapshotMode::Insert);
4314 assert_eq!(f.cursor_shape, CursorShape::Bar);
4315 }
4316
4317 #[test]
4318 fn snapshot_roundtrips_through_restore() {
4319 use crate::types::SnapshotMode;
4320 let mut e = Editor::new(
4321 hjkl_buffer::Buffer::new(),
4322 crate::types::DefaultHost::new(),
4323 crate::types::Options::default(),
4324 );
4325 e.set_content("alpha\nbeta\ngamma");
4326 e.jump_cursor(2, 3);
4327 let snap = e.take_snapshot();
4328 assert_eq!(snap.mode, SnapshotMode::Normal);
4329 assert_eq!(snap.cursor, (2, 3));
4330 assert_eq!(snap.lines.len(), 3);
4331
4332 let mut other = Editor::new(
4333 hjkl_buffer::Buffer::new(),
4334 crate::types::DefaultHost::new(),
4335 crate::types::Options::default(),
4336 );
4337 other.restore_snapshot(snap).expect("restore");
4338 assert_eq!(other.cursor(), (2, 3));
4339 assert_eq!(other.buffer().lines().len(), 3);
4340 }
4341
4342 #[test]
4343 fn restore_snapshot_rejects_version_mismatch() {
4344 let mut e = Editor::new(
4345 hjkl_buffer::Buffer::new(),
4346 crate::types::DefaultHost::new(),
4347 crate::types::Options::default(),
4348 );
4349 let mut snap = e.take_snapshot();
4350 snap.version = 9999;
4351 match e.restore_snapshot(snap) {
4352 Err(crate::EngineError::SnapshotVersion(got, want)) => {
4353 assert_eq!(got, 9999);
4354 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
4355 }
4356 other => panic!("expected SnapshotVersion err, got {other:?}"),
4357 }
4358 }
4359
4360 #[test]
4361 fn take_content_change_returns_some_on_first_dirty() {
4362 let mut e = Editor::new(
4363 hjkl_buffer::Buffer::new(),
4364 crate::types::DefaultHost::new(),
4365 crate::types::Options::default(),
4366 );
4367 e.set_content("hello");
4368 let first = e.take_content_change();
4369 assert!(first.is_some());
4370 let second = e.take_content_change();
4371 assert!(second.is_none());
4372 }
4373
4374 #[test]
4375 fn take_content_change_none_until_mutation() {
4376 let mut e = Editor::new(
4377 hjkl_buffer::Buffer::new(),
4378 crate::types::DefaultHost::new(),
4379 crate::types::Options::default(),
4380 );
4381 e.set_content("hello");
4382 e.take_content_change();
4384 assert!(e.take_content_change().is_none());
4385 e.handle_key(key(KeyCode::Char('i')));
4387 e.handle_key(key(KeyCode::Char('x')));
4388 let after = e.take_content_change();
4389 assert!(after.is_some());
4390 assert!(after.unwrap().contains('x'));
4391 }
4392
4393 #[test]
4394 fn vim_insert_to_normal() {
4395 let mut e = Editor::new(
4396 hjkl_buffer::Buffer::new(),
4397 crate::types::DefaultHost::new(),
4398 crate::types::Options::default(),
4399 );
4400 e.handle_key(key(KeyCode::Char('i')));
4401 e.handle_key(key(KeyCode::Esc));
4402 assert_eq!(e.vim_mode(), VimMode::Normal);
4403 }
4404
4405 #[test]
4406 fn vim_normal_to_visual() {
4407 let mut e = Editor::new(
4408 hjkl_buffer::Buffer::new(),
4409 crate::types::DefaultHost::new(),
4410 crate::types::Options::default(),
4411 );
4412 e.handle_key(key(KeyCode::Char('v')));
4413 assert_eq!(e.vim_mode(), VimMode::Visual);
4414 }
4415
4416 #[test]
4417 fn vim_visual_to_normal() {
4418 let mut e = Editor::new(
4419 hjkl_buffer::Buffer::new(),
4420 crate::types::DefaultHost::new(),
4421 crate::types::Options::default(),
4422 );
4423 e.handle_key(key(KeyCode::Char('v')));
4424 e.handle_key(key(KeyCode::Esc));
4425 assert_eq!(e.vim_mode(), VimMode::Normal);
4426 }
4427
4428 #[test]
4429 fn vim_shift_i_moves_to_first_non_whitespace() {
4430 let mut e = Editor::new(
4431 hjkl_buffer::Buffer::new(),
4432 crate::types::DefaultHost::new(),
4433 crate::types::Options::default(),
4434 );
4435 e.set_content(" hello");
4436 e.jump_cursor(0, 8);
4437 e.handle_key(shift_key(KeyCode::Char('I')));
4438 assert_eq!(e.vim_mode(), VimMode::Insert);
4439 assert_eq!(e.cursor(), (0, 3));
4440 }
4441
4442 #[test]
4443 fn vim_shift_a_moves_to_end_and_insert() {
4444 let mut e = Editor::new(
4445 hjkl_buffer::Buffer::new(),
4446 crate::types::DefaultHost::new(),
4447 crate::types::Options::default(),
4448 );
4449 e.set_content("hello");
4450 e.handle_key(shift_key(KeyCode::Char('A')));
4451 assert_eq!(e.vim_mode(), VimMode::Insert);
4452 assert_eq!(e.cursor().1, 5);
4453 }
4454
4455 #[test]
4456 fn count_10j_moves_down_10() {
4457 let mut e = Editor::new(
4458 hjkl_buffer::Buffer::new(),
4459 crate::types::DefaultHost::new(),
4460 crate::types::Options::default(),
4461 );
4462 e.set_content(
4463 (0..20)
4464 .map(|i| format!("line{i}"))
4465 .collect::<Vec<_>>()
4466 .join("\n")
4467 .as_str(),
4468 );
4469 for d in "10".chars() {
4470 e.handle_key(key(KeyCode::Char(d)));
4471 }
4472 e.handle_key(key(KeyCode::Char('j')));
4473 assert_eq!(e.cursor().0, 10);
4474 }
4475
4476 #[test]
4477 fn count_o_repeats_insert_on_esc() {
4478 let mut e = Editor::new(
4479 hjkl_buffer::Buffer::new(),
4480 crate::types::DefaultHost::new(),
4481 crate::types::Options::default(),
4482 );
4483 e.set_content("hello");
4484 for d in "3".chars() {
4485 e.handle_key(key(KeyCode::Char(d)));
4486 }
4487 e.handle_key(key(KeyCode::Char('o')));
4488 assert_eq!(e.vim_mode(), VimMode::Insert);
4489 for c in "world".chars() {
4490 e.handle_key(key(KeyCode::Char(c)));
4491 }
4492 e.handle_key(key(KeyCode::Esc));
4493 assert_eq!(e.vim_mode(), VimMode::Normal);
4494 assert_eq!(e.buffer().lines().len(), 4);
4495 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
4496 }
4497
4498 #[test]
4499 fn count_i_repeats_text_on_esc() {
4500 let mut e = Editor::new(
4501 hjkl_buffer::Buffer::new(),
4502 crate::types::DefaultHost::new(),
4503 crate::types::Options::default(),
4504 );
4505 e.set_content("");
4506 for d in "3".chars() {
4507 e.handle_key(key(KeyCode::Char(d)));
4508 }
4509 e.handle_key(key(KeyCode::Char('i')));
4510 for c in "ab".chars() {
4511 e.handle_key(key(KeyCode::Char(c)));
4512 }
4513 e.handle_key(key(KeyCode::Esc));
4514 assert_eq!(e.vim_mode(), VimMode::Normal);
4515 assert_eq!(e.buffer().lines()[0], "ababab");
4516 }
4517
4518 #[test]
4519 fn vim_shift_o_opens_line_above() {
4520 let mut e = Editor::new(
4521 hjkl_buffer::Buffer::new(),
4522 crate::types::DefaultHost::new(),
4523 crate::types::Options::default(),
4524 );
4525 e.set_content("hello");
4526 e.handle_key(shift_key(KeyCode::Char('O')));
4527 assert_eq!(e.vim_mode(), VimMode::Insert);
4528 assert_eq!(e.cursor(), (0, 0));
4529 assert_eq!(e.buffer().lines().len(), 2);
4530 }
4531
4532 #[test]
4533 fn vim_gg_goes_to_top() {
4534 let mut e = Editor::new(
4535 hjkl_buffer::Buffer::new(),
4536 crate::types::DefaultHost::new(),
4537 crate::types::Options::default(),
4538 );
4539 e.set_content("a\nb\nc");
4540 e.jump_cursor(2, 0);
4541 e.handle_key(key(KeyCode::Char('g')));
4542 e.handle_key(key(KeyCode::Char('g')));
4543 assert_eq!(e.cursor().0, 0);
4544 }
4545
4546 #[test]
4547 fn vim_shift_g_goes_to_bottom() {
4548 let mut e = Editor::new(
4549 hjkl_buffer::Buffer::new(),
4550 crate::types::DefaultHost::new(),
4551 crate::types::Options::default(),
4552 );
4553 e.set_content("a\nb\nc");
4554 e.handle_key(shift_key(KeyCode::Char('G')));
4555 assert_eq!(e.cursor().0, 2);
4556 }
4557
4558 #[test]
4559 fn vim_dd_deletes_line() {
4560 let mut e = Editor::new(
4561 hjkl_buffer::Buffer::new(),
4562 crate::types::DefaultHost::new(),
4563 crate::types::Options::default(),
4564 );
4565 e.set_content("first\nsecond");
4566 e.handle_key(key(KeyCode::Char('d')));
4567 e.handle_key(key(KeyCode::Char('d')));
4568 assert_eq!(e.buffer().lines().len(), 1);
4569 assert_eq!(e.buffer().lines()[0], "second");
4570 }
4571
4572 #[test]
4573 fn vim_dw_deletes_word() {
4574 let mut e = Editor::new(
4575 hjkl_buffer::Buffer::new(),
4576 crate::types::DefaultHost::new(),
4577 crate::types::Options::default(),
4578 );
4579 e.set_content("hello world");
4580 e.handle_key(key(KeyCode::Char('d')));
4581 e.handle_key(key(KeyCode::Char('w')));
4582 assert_eq!(e.vim_mode(), VimMode::Normal);
4583 assert!(!e.buffer().lines()[0].starts_with("hello"));
4584 }
4585
4586 #[test]
4587 fn vim_yy_yanks_line() {
4588 let mut e = Editor::new(
4589 hjkl_buffer::Buffer::new(),
4590 crate::types::DefaultHost::new(),
4591 crate::types::Options::default(),
4592 );
4593 e.set_content("hello\nworld");
4594 e.handle_key(key(KeyCode::Char('y')));
4595 e.handle_key(key(KeyCode::Char('y')));
4596 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
4597 }
4598
4599 #[test]
4600 fn vim_yy_does_not_move_cursor() {
4601 let mut e = Editor::new(
4602 hjkl_buffer::Buffer::new(),
4603 crate::types::DefaultHost::new(),
4604 crate::types::Options::default(),
4605 );
4606 e.set_content("first\nsecond\nthird");
4607 e.jump_cursor(1, 0);
4608 let before = e.cursor();
4609 e.handle_key(key(KeyCode::Char('y')));
4610 e.handle_key(key(KeyCode::Char('y')));
4611 assert_eq!(e.cursor(), before);
4612 assert_eq!(e.vim_mode(), VimMode::Normal);
4613 }
4614
4615 #[test]
4616 fn vim_yw_yanks_word() {
4617 let mut e = Editor::new(
4618 hjkl_buffer::Buffer::new(),
4619 crate::types::DefaultHost::new(),
4620 crate::types::Options::default(),
4621 );
4622 e.set_content("hello world");
4623 e.handle_key(key(KeyCode::Char('y')));
4624 e.handle_key(key(KeyCode::Char('w')));
4625 assert_eq!(e.vim_mode(), VimMode::Normal);
4626 assert!(e.last_yank.is_some());
4627 }
4628
4629 #[test]
4630 fn vim_cc_changes_line() {
4631 let mut e = Editor::new(
4632 hjkl_buffer::Buffer::new(),
4633 crate::types::DefaultHost::new(),
4634 crate::types::Options::default(),
4635 );
4636 e.set_content("hello\nworld");
4637 e.handle_key(key(KeyCode::Char('c')));
4638 e.handle_key(key(KeyCode::Char('c')));
4639 assert_eq!(e.vim_mode(), VimMode::Insert);
4640 }
4641
4642 #[test]
4643 fn vim_u_undoes_insert_session_as_chunk() {
4644 let mut e = Editor::new(
4645 hjkl_buffer::Buffer::new(),
4646 crate::types::DefaultHost::new(),
4647 crate::types::Options::default(),
4648 );
4649 e.set_content("hello");
4650 e.handle_key(key(KeyCode::Char('i')));
4651 e.handle_key(key(KeyCode::Enter));
4652 e.handle_key(key(KeyCode::Enter));
4653 e.handle_key(key(KeyCode::Esc));
4654 assert_eq!(e.buffer().lines().len(), 3);
4655 e.handle_key(key(KeyCode::Char('u')));
4656 assert_eq!(e.buffer().lines().len(), 1);
4657 assert_eq!(e.buffer().lines()[0], "hello");
4658 }
4659
4660 #[test]
4661 fn vim_undo_redo_roundtrip() {
4662 let mut e = Editor::new(
4663 hjkl_buffer::Buffer::new(),
4664 crate::types::DefaultHost::new(),
4665 crate::types::Options::default(),
4666 );
4667 e.set_content("hello");
4668 e.handle_key(key(KeyCode::Char('i')));
4669 for c in "world".chars() {
4670 e.handle_key(key(KeyCode::Char(c)));
4671 }
4672 e.handle_key(key(KeyCode::Esc));
4673 let after = e.buffer().lines()[0].clone();
4674 e.handle_key(key(KeyCode::Char('u')));
4675 assert_eq!(e.buffer().lines()[0], "hello");
4676 e.handle_key(ctrl_key(KeyCode::Char('r')));
4677 assert_eq!(e.buffer().lines()[0], after);
4678 }
4679
4680 #[test]
4681 fn vim_u_undoes_dd() {
4682 let mut e = Editor::new(
4683 hjkl_buffer::Buffer::new(),
4684 crate::types::DefaultHost::new(),
4685 crate::types::Options::default(),
4686 );
4687 e.set_content("first\nsecond");
4688 e.handle_key(key(KeyCode::Char('d')));
4689 e.handle_key(key(KeyCode::Char('d')));
4690 assert_eq!(e.buffer().lines().len(), 1);
4691 e.handle_key(key(KeyCode::Char('u')));
4692 assert_eq!(e.buffer().lines().len(), 2);
4693 assert_eq!(e.buffer().lines()[0], "first");
4694 }
4695
4696 #[test]
4697 fn vim_ctrl_r_redoes() {
4698 let mut e = Editor::new(
4699 hjkl_buffer::Buffer::new(),
4700 crate::types::DefaultHost::new(),
4701 crate::types::Options::default(),
4702 );
4703 e.set_content("hello");
4704 e.handle_key(ctrl_key(KeyCode::Char('r')));
4705 }
4706
4707 #[test]
4708 fn vim_r_replaces_char() {
4709 let mut e = Editor::new(
4710 hjkl_buffer::Buffer::new(),
4711 crate::types::DefaultHost::new(),
4712 crate::types::Options::default(),
4713 );
4714 e.set_content("hello");
4715 e.handle_key(key(KeyCode::Char('r')));
4716 e.handle_key(key(KeyCode::Char('x')));
4717 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
4718 }
4719
4720 #[test]
4721 fn vim_tilde_toggles_case() {
4722 let mut e = Editor::new(
4723 hjkl_buffer::Buffer::new(),
4724 crate::types::DefaultHost::new(),
4725 crate::types::Options::default(),
4726 );
4727 e.set_content("hello");
4728 e.handle_key(key(KeyCode::Char('~')));
4729 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
4730 }
4731
4732 #[test]
4733 fn vim_visual_d_cuts() {
4734 let mut e = Editor::new(
4735 hjkl_buffer::Buffer::new(),
4736 crate::types::DefaultHost::new(),
4737 crate::types::Options::default(),
4738 );
4739 e.set_content("hello");
4740 e.handle_key(key(KeyCode::Char('v')));
4741 e.handle_key(key(KeyCode::Char('l')));
4742 e.handle_key(key(KeyCode::Char('l')));
4743 e.handle_key(key(KeyCode::Char('d')));
4744 assert_eq!(e.vim_mode(), VimMode::Normal);
4745 assert!(e.last_yank.is_some());
4746 }
4747
4748 #[test]
4749 fn vim_visual_c_enters_insert() {
4750 let mut e = Editor::new(
4751 hjkl_buffer::Buffer::new(),
4752 crate::types::DefaultHost::new(),
4753 crate::types::Options::default(),
4754 );
4755 e.set_content("hello");
4756 e.handle_key(key(KeyCode::Char('v')));
4757 e.handle_key(key(KeyCode::Char('l')));
4758 e.handle_key(key(KeyCode::Char('c')));
4759 assert_eq!(e.vim_mode(), VimMode::Insert);
4760 }
4761
4762 #[test]
4763 fn vim_normal_unknown_key_consumed() {
4764 let mut e = Editor::new(
4765 hjkl_buffer::Buffer::new(),
4766 crate::types::DefaultHost::new(),
4767 crate::types::Options::default(),
4768 );
4769 let consumed = e.handle_key(key(KeyCode::Char('z')));
4771 assert!(consumed);
4772 }
4773
4774 #[test]
4775 fn force_normal_clears_operator() {
4776 let mut e = Editor::new(
4777 hjkl_buffer::Buffer::new(),
4778 crate::types::DefaultHost::new(),
4779 crate::types::Options::default(),
4780 );
4781 e.handle_key(key(KeyCode::Char('d')));
4782 e.force_normal();
4783 assert_eq!(e.vim_mode(), VimMode::Normal);
4784 }
4785
4786 fn many_lines(n: usize) -> String {
4787 (0..n)
4788 .map(|i| format!("line{i}"))
4789 .collect::<Vec<_>>()
4790 .join("\n")
4791 }
4792
4793 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
4794 e.set_viewport_height(height);
4795 }
4796
4797 #[test]
4798 fn zz_centers_cursor_in_viewport() {
4799 let mut e = Editor::new(
4800 hjkl_buffer::Buffer::new(),
4801 crate::types::DefaultHost::new(),
4802 crate::types::Options::default(),
4803 );
4804 e.set_content(&many_lines(100));
4805 prime_viewport(&mut e, 20);
4806 e.jump_cursor(50, 0);
4807 e.handle_key(key(KeyCode::Char('z')));
4808 e.handle_key(key(KeyCode::Char('z')));
4809 assert_eq!(e.host().viewport().top_row, 40);
4810 assert_eq!(e.cursor().0, 50);
4811 }
4812
4813 #[test]
4814 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
4815 let mut e = Editor::new(
4816 hjkl_buffer::Buffer::new(),
4817 crate::types::DefaultHost::new(),
4818 crate::types::Options::default(),
4819 );
4820 e.set_content(&many_lines(100));
4821 prime_viewport(&mut e, 20);
4822 e.jump_cursor(50, 0);
4823 e.handle_key(key(KeyCode::Char('z')));
4824 e.handle_key(key(KeyCode::Char('t')));
4825 assert_eq!(e.host().viewport().top_row, 45);
4828 assert_eq!(e.cursor().0, 50);
4829 }
4830
4831 #[test]
4832 fn ctrl_a_increments_number_at_cursor() {
4833 let mut e = Editor::new(
4834 hjkl_buffer::Buffer::new(),
4835 crate::types::DefaultHost::new(),
4836 crate::types::Options::default(),
4837 );
4838 e.set_content("x = 41");
4839 e.handle_key(ctrl_key(KeyCode::Char('a')));
4840 assert_eq!(e.buffer().lines()[0], "x = 42");
4841 assert_eq!(e.cursor(), (0, 5));
4842 }
4843
4844 #[test]
4845 fn ctrl_a_finds_number_to_right_of_cursor() {
4846 let mut e = Editor::new(
4847 hjkl_buffer::Buffer::new(),
4848 crate::types::DefaultHost::new(),
4849 crate::types::Options::default(),
4850 );
4851 e.set_content("foo 99 bar");
4852 e.handle_key(ctrl_key(KeyCode::Char('a')));
4853 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
4854 assert_eq!(e.cursor(), (0, 6));
4855 }
4856
4857 #[test]
4858 fn ctrl_a_with_count_adds_count() {
4859 let mut e = Editor::new(
4860 hjkl_buffer::Buffer::new(),
4861 crate::types::DefaultHost::new(),
4862 crate::types::Options::default(),
4863 );
4864 e.set_content("x = 10");
4865 for d in "5".chars() {
4866 e.handle_key(key(KeyCode::Char(d)));
4867 }
4868 e.handle_key(ctrl_key(KeyCode::Char('a')));
4869 assert_eq!(e.buffer().lines()[0], "x = 15");
4870 }
4871
4872 #[test]
4873 fn ctrl_x_decrements_number() {
4874 let mut e = Editor::new(
4875 hjkl_buffer::Buffer::new(),
4876 crate::types::DefaultHost::new(),
4877 crate::types::Options::default(),
4878 );
4879 e.set_content("n=5");
4880 e.handle_key(ctrl_key(KeyCode::Char('x')));
4881 assert_eq!(e.buffer().lines()[0], "n=4");
4882 }
4883
4884 #[test]
4885 fn ctrl_x_crosses_zero_into_negative() {
4886 let mut e = Editor::new(
4887 hjkl_buffer::Buffer::new(),
4888 crate::types::DefaultHost::new(),
4889 crate::types::Options::default(),
4890 );
4891 e.set_content("v=0");
4892 e.handle_key(ctrl_key(KeyCode::Char('x')));
4893 assert_eq!(e.buffer().lines()[0], "v=-1");
4894 }
4895
4896 #[test]
4897 fn ctrl_a_on_negative_number_increments_toward_zero() {
4898 let mut e = Editor::new(
4899 hjkl_buffer::Buffer::new(),
4900 crate::types::DefaultHost::new(),
4901 crate::types::Options::default(),
4902 );
4903 e.set_content("a = -5");
4904 e.handle_key(ctrl_key(KeyCode::Char('a')));
4905 assert_eq!(e.buffer().lines()[0], "a = -4");
4906 }
4907
4908 #[test]
4909 fn ctrl_a_noop_when_no_digit_on_line() {
4910 let mut e = Editor::new(
4911 hjkl_buffer::Buffer::new(),
4912 crate::types::DefaultHost::new(),
4913 crate::types::Options::default(),
4914 );
4915 e.set_content("no digits here");
4916 e.handle_key(ctrl_key(KeyCode::Char('a')));
4917 assert_eq!(e.buffer().lines()[0], "no digits here");
4918 }
4919
4920 #[test]
4921 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
4922 let mut e = Editor::new(
4923 hjkl_buffer::Buffer::new(),
4924 crate::types::DefaultHost::new(),
4925 crate::types::Options::default(),
4926 );
4927 e.set_content(&many_lines(100));
4928 prime_viewport(&mut e, 20);
4929 e.jump_cursor(50, 0);
4930 e.handle_key(key(KeyCode::Char('z')));
4931 e.handle_key(key(KeyCode::Char('b')));
4932 assert_eq!(e.host().viewport().top_row, 36);
4936 assert_eq!(e.cursor().0, 50);
4937 }
4938
4939 #[test]
4946 fn set_content_dirties_then_take_dirty_clears() {
4947 let mut e = Editor::new(
4948 hjkl_buffer::Buffer::new(),
4949 crate::types::DefaultHost::new(),
4950 crate::types::Options::default(),
4951 );
4952 e.set_content("hello");
4953 assert!(
4954 e.take_dirty(),
4955 "set_content should leave content_dirty=true"
4956 );
4957 assert!(!e.take_dirty(), "take_dirty should clear the flag");
4958 }
4959
4960 #[test]
4961 fn content_arc_returns_same_arc_until_mutation() {
4962 let mut e = Editor::new(
4963 hjkl_buffer::Buffer::new(),
4964 crate::types::DefaultHost::new(),
4965 crate::types::Options::default(),
4966 );
4967 e.set_content("hello");
4968 let a = e.content_arc();
4969 let b = e.content_arc();
4970 assert!(
4971 std::sync::Arc::ptr_eq(&a, &b),
4972 "repeated content_arc() should hit the cache"
4973 );
4974
4975 e.handle_key(key(KeyCode::Char('i')));
4977 e.handle_key(key(KeyCode::Char('!')));
4978 let c = e.content_arc();
4979 assert!(
4980 !std::sync::Arc::ptr_eq(&a, &c),
4981 "mutation should invalidate content_arc() cache"
4982 );
4983 assert!(c.contains('!'));
4984 }
4985
4986 #[test]
4987 fn content_arc_cache_invalidated_by_set_content() {
4988 let mut e = Editor::new(
4989 hjkl_buffer::Buffer::new(),
4990 crate::types::DefaultHost::new(),
4991 crate::types::Options::default(),
4992 );
4993 e.set_content("one");
4994 let a = e.content_arc();
4995 e.set_content("two");
4996 let b = e.content_arc();
4997 assert!(!std::sync::Arc::ptr_eq(&a, &b));
4998 assert!(b.starts_with("two"));
4999 }
5000
5001 #[test]
5007 fn mouse_click_past_eol_lands_on_last_char() {
5008 let mut e = Editor::new(
5009 hjkl_buffer::Buffer::new(),
5010 crate::types::DefaultHost::new(),
5011 crate::types::Options::default(),
5012 );
5013 e.set_content("hello");
5014 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
5018 e.mouse_click_in_rect(area, 78, 1);
5019 assert_eq!(e.cursor(), (0, 4));
5020 }
5021
5022 #[test]
5023 fn mouse_click_past_eol_handles_multibyte_line() {
5024 let mut e = Editor::new(
5025 hjkl_buffer::Buffer::new(),
5026 crate::types::DefaultHost::new(),
5027 crate::types::Options::default(),
5028 );
5029 e.set_content("héllo");
5032 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
5033 e.mouse_click_in_rect(area, 78, 1);
5034 assert_eq!(e.cursor(), (0, 4));
5035 }
5036
5037 #[test]
5038 fn mouse_click_inside_line_lands_on_clicked_char() {
5039 let mut e = Editor::new(
5040 hjkl_buffer::Buffer::new(),
5041 crate::types::DefaultHost::new(),
5042 crate::types::Options::default(),
5043 );
5044 e.set_content("hello world");
5045 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
5049 e.mouse_click_in_rect(area, 5, 1);
5050 assert_eq!(e.cursor(), (0, 0));
5051 e.mouse_click_in_rect(area, 7, 1);
5052 assert_eq!(e.cursor(), (0, 2));
5053 }
5054
5055 #[test]
5060 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
5061 let mut e = Editor::new(
5062 hjkl_buffer::Buffer::new(),
5063 crate::types::DefaultHost::new(),
5064 crate::types::Options::default(),
5065 );
5066 e.set_content("hello world");
5067 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
5068 assert!(e.settings().undo_break_on_motion);
5070 e.handle_key(key(KeyCode::Char('i')));
5072 e.handle_key(key(KeyCode::Char('A')));
5073 e.handle_key(key(KeyCode::Char('A')));
5074 e.handle_key(key(KeyCode::Char('A')));
5075 e.mouse_click_in_rect(area, 10, 1);
5077 e.handle_key(key(KeyCode::Char('B')));
5079 e.handle_key(key(KeyCode::Char('B')));
5080 e.handle_key(key(KeyCode::Char('B')));
5081 e.handle_key(key(KeyCode::Esc));
5083 e.handle_key(key(KeyCode::Char('u')));
5084 let line = e.buffer().line(0).unwrap_or("").to_string();
5085 assert!(
5086 line.contains("AAA"),
5087 "AAA must survive undo (separate group): {line:?}"
5088 );
5089 assert!(
5090 !line.contains("BBB"),
5091 "BBB must be undone (post-click group): {line:?}"
5092 );
5093 }
5094
5095 #[test]
5099 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
5100 let mut e = Editor::new(
5101 hjkl_buffer::Buffer::new(),
5102 crate::types::DefaultHost::new(),
5103 crate::types::Options::default(),
5104 );
5105 e.set_content("hello world");
5106 e.settings_mut().undo_break_on_motion = false;
5107 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
5108 e.handle_key(key(KeyCode::Char('i')));
5109 e.handle_key(key(KeyCode::Char('A')));
5110 e.handle_key(key(KeyCode::Char('A')));
5111 e.mouse_click_in_rect(area, 10, 1);
5112 e.handle_key(key(KeyCode::Char('B')));
5113 e.handle_key(key(KeyCode::Char('B')));
5114 e.handle_key(key(KeyCode::Esc));
5115 e.handle_key(key(KeyCode::Char('u')));
5116 let line = e.buffer().line(0).unwrap_or("").to_string();
5117 assert!(
5118 !line.contains("AA") && !line.contains("BB"),
5119 "with undobreak off, single `u` must reverse whole insert: {line:?}"
5120 );
5121 assert_eq!(line, "hello world");
5122 }
5123
5124 #[test]
5127 fn host_clipboard_round_trip_via_default_host() {
5128 let mut e = Editor::new(
5131 hjkl_buffer::Buffer::new(),
5132 crate::types::DefaultHost::new(),
5133 crate::types::Options::default(),
5134 );
5135 e.host_mut().write_clipboard("payload".to_string());
5136 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
5137 }
5138
5139 #[test]
5140 fn host_records_clipboard_on_yank() {
5141 let mut e = Editor::new(
5145 hjkl_buffer::Buffer::new(),
5146 crate::types::DefaultHost::new(),
5147 crate::types::Options::default(),
5148 );
5149 e.set_content("hello\n");
5150 e.handle_key(key(KeyCode::Char('y')));
5151 e.handle_key(key(KeyCode::Char('y')));
5152 let clip = e.host_mut().read_clipboard();
5154 assert!(
5155 clip.as_deref().unwrap_or("").starts_with("hello"),
5156 "host clipboard should carry the yank: {clip:?}"
5157 );
5158 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
5160 }
5161
5162 #[test]
5163 fn host_cursor_shape_via_shared_recorder() {
5164 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
5168 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
5169 struct LeakHost {
5170 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
5171 viewport: crate::types::Viewport,
5172 }
5173 impl crate::types::Host for LeakHost {
5174 type Intent = ();
5175 fn write_clipboard(&mut self, _: String) {}
5176 fn read_clipboard(&mut self) -> Option<String> {
5177 None
5178 }
5179 fn now(&self) -> core::time::Duration {
5180 core::time::Duration::ZERO
5181 }
5182 fn prompt_search(&mut self) -> Option<String> {
5183 None
5184 }
5185 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
5186 self.shapes.lock().unwrap().push(s);
5187 }
5188 fn viewport(&self) -> &crate::types::Viewport {
5189 &self.viewport
5190 }
5191 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
5192 &mut self.viewport
5193 }
5194 fn emit_intent(&mut self, _: Self::Intent) {}
5195 }
5196 let mut e = Editor::new(
5197 hjkl_buffer::Buffer::new(),
5198 LeakHost {
5199 shapes: shapes_ptr,
5200 viewport: crate::types::Viewport::default(),
5201 },
5202 crate::types::Options::default(),
5203 );
5204 e.set_content("abc");
5205 e.handle_key(key(KeyCode::Char('i')));
5207 e.handle_key(key(KeyCode::Esc));
5209 let shapes = shapes_ptr.lock().unwrap().clone();
5210 assert_eq!(
5211 shapes,
5212 vec![
5213 crate::types::CursorShape::Bar,
5214 crate::types::CursorShape::Block,
5215 ],
5216 "host should observe Insert(Bar) → Normal(Block) transitions"
5217 );
5218 }
5219
5220 #[test]
5221 fn host_now_drives_chord_timeout_deterministically() {
5222 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
5227 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
5228 struct ClockHost {
5229 now: &'static std::sync::Mutex<core::time::Duration>,
5230 viewport: crate::types::Viewport,
5231 }
5232 impl crate::types::Host for ClockHost {
5233 type Intent = ();
5234 fn write_clipboard(&mut self, _: String) {}
5235 fn read_clipboard(&mut self) -> Option<String> {
5236 None
5237 }
5238 fn now(&self) -> core::time::Duration {
5239 *self.now.lock().unwrap()
5240 }
5241 fn prompt_search(&mut self) -> Option<String> {
5242 None
5243 }
5244 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
5245 fn viewport(&self) -> &crate::types::Viewport {
5246 &self.viewport
5247 }
5248 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
5249 &mut self.viewport
5250 }
5251 fn emit_intent(&mut self, _: Self::Intent) {}
5252 }
5253 let mut e = Editor::new(
5254 hjkl_buffer::Buffer::new(),
5255 ClockHost {
5256 now: now_ptr,
5257 viewport: crate::types::Viewport::default(),
5258 },
5259 crate::types::Options::default(),
5260 );
5261 e.set_content("a\nb\nc\n");
5262 e.jump_cursor(2, 0);
5263 e.handle_key(key(KeyCode::Char('g')));
5265 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
5267 e.handle_key(key(KeyCode::Char('g')));
5270 assert_eq!(
5271 e.cursor().0,
5272 2,
5273 "Host::now() must drive `:set timeoutlen` deterministically"
5274 );
5275 }
5276
5277 fn fresh_editor(initial: &str) -> Editor {
5280 let buffer = hjkl_buffer::Buffer::from_str(initial);
5281 Editor::new(
5282 buffer,
5283 crate::types::DefaultHost::new(),
5284 crate::types::Options::default(),
5285 )
5286 }
5287
5288 #[test]
5289 fn content_edit_insert_char_at_origin() {
5290 let mut e = fresh_editor("");
5291 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
5292 at: hjkl_buffer::Position::new(0, 0),
5293 ch: 'a',
5294 });
5295 let edits = e.take_content_edits();
5296 assert_eq!(edits.len(), 1);
5297 let ce = &edits[0];
5298 assert_eq!(ce.start_byte, 0);
5299 assert_eq!(ce.old_end_byte, 0);
5300 assert_eq!(ce.new_end_byte, 1);
5301 assert_eq!(ce.start_position, (0, 0));
5302 assert_eq!(ce.old_end_position, (0, 0));
5303 assert_eq!(ce.new_end_position, (0, 1));
5304 }
5305
5306 #[test]
5307 fn content_edit_insert_str_multiline() {
5308 let mut e = fresh_editor("x\ny");
5310 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertStr {
5311 at: hjkl_buffer::Position::new(0, 1),
5312 text: "ab\ncd".into(),
5313 });
5314 let edits = e.take_content_edits();
5315 assert_eq!(edits.len(), 1);
5316 let ce = &edits[0];
5317 assert_eq!(ce.start_byte, 1);
5318 assert_eq!(ce.old_end_byte, 1);
5319 assert_eq!(ce.new_end_byte, 1 + 5);
5320 assert_eq!(ce.start_position, (0, 1));
5321 assert_eq!(ce.new_end_position, (1, 2));
5323 }
5324
5325 #[test]
5326 fn content_edit_delete_range_charwise() {
5327 let mut e = fresh_editor("abcdef");
5329 let _ = e.mutate_edit(hjkl_buffer::Edit::DeleteRange {
5330 start: hjkl_buffer::Position::new(0, 1),
5331 end: hjkl_buffer::Position::new(0, 4),
5332 kind: hjkl_buffer::MotionKind::Char,
5333 });
5334 let edits = e.take_content_edits();
5335 assert_eq!(edits.len(), 1);
5336 let ce = &edits[0];
5337 assert_eq!(ce.start_byte, 1);
5338 assert_eq!(ce.old_end_byte, 4);
5339 assert_eq!(ce.new_end_byte, 1);
5340 assert!(ce.old_end_byte > ce.new_end_byte);
5341 }
5342
5343 #[test]
5344 fn content_edit_set_content_resets() {
5345 let mut e = fresh_editor("foo");
5346 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
5347 at: hjkl_buffer::Position::new(0, 0),
5348 ch: 'X',
5349 });
5350 e.set_content("brand new");
5353 assert!(e.take_content_reset());
5354 assert!(!e.take_content_reset());
5356 assert!(e.take_content_edits().is_empty());
5358 }
5359
5360 #[test]
5361 fn content_edit_multiple_replaces_in_order() {
5362 let mut e = fresh_editor("xax xbx xcx");
5367 let _ = e.take_content_edits();
5368 let _ = e.take_content_reset();
5369 let positions = [(0usize, 0usize), (0, 4), (0, 8)];
5373 for (row, col) in positions {
5374 let _ = e.mutate_edit(hjkl_buffer::Edit::Replace {
5375 start: hjkl_buffer::Position::new(row, col),
5376 end: hjkl_buffer::Position::new(row, col + 1),
5377 with: "yy".into(),
5378 });
5379 }
5380 let edits = e.take_content_edits();
5381 assert_eq!(edits.len(), 3);
5382 for ce in &edits {
5383 assert!(ce.start_byte <= ce.old_end_byte);
5384 assert!(ce.start_byte <= ce.new_end_byte);
5385 }
5386 for w in edits.windows(2) {
5388 assert!(w[0].start_byte <= w[1].start_byte);
5389 }
5390 }
5391
5392 #[test]
5393 fn replace_char_at_replaces_single_char_under_cursor() {
5394 let mut e = fresh_editor("abc");
5396 e.jump_cursor(0, 1); e.replace_char_at('X', 1);
5398 let got = e.content();
5399 let got = got.trim_end_matches('\n');
5400 assert_eq!(
5401 got, "aXc",
5402 "replace_char_at(X, 1) must replace 'b' with 'X'"
5403 );
5404 assert_eq!(e.cursor(), (0, 1));
5406 }
5407
5408 #[test]
5409 fn replace_char_at_count_replaces_multiple_chars() {
5410 let mut e = fresh_editor("abcde");
5412 e.jump_cursor(0, 0);
5413 e.replace_char_at('Z', 3);
5414 let got = e.content();
5415 let got = got.trim_end_matches('\n');
5416 assert_eq!(
5417 got, "ZZZde",
5418 "replace_char_at(Z, 3) must replace first 3 chars"
5419 );
5420 }
5421
5422 #[test]
5423 fn find_char_method_moves_to_target() {
5424 let mut e = fresh_editor("abcabc");
5426 e.jump_cursor(0, 0);
5427 e.find_char('c', true, false, 1);
5428 assert_eq!(
5429 e.cursor(),
5430 (0, 2),
5431 "find_char('c', forward=true, till=false, count=1) must land on 'c' at col 2"
5432 );
5433 }
5434
5435 #[test]
5438 fn after_g_gg_jumps_to_top() {
5439 let content: String = (0..20).map(|i| format!("line {i}\n")).collect();
5440 let mut e = fresh_editor(&content);
5441 e.jump_cursor(15, 0);
5442 e.after_g('g', 1);
5443 assert_eq!(e.cursor().0, 0, "gg must move cursor to row 0");
5444 }
5445
5446 #[test]
5447 fn after_g_gg_with_count_jumps_line() {
5448 let content: String = (0..20).map(|i| format!("line {i}\n")).collect();
5450 let mut e = fresh_editor(&content);
5451 e.jump_cursor(0, 0);
5452 e.after_g('g', 5);
5453 assert_eq!(e.cursor().0, 4, "5gg must land on row 4");
5454 }
5455
5456 #[test]
5457 fn after_g_gv_restores_last_visual() {
5458 let mut e = fresh_editor("hello world\n");
5460 e.handle_key(key(KeyCode::Char('v')));
5462 e.handle_key(key(KeyCode::Char('l')));
5463 e.handle_key(key(KeyCode::Char('l')));
5464 e.handle_key(key(KeyCode::Char('l')));
5465 e.handle_key(key(KeyCode::Esc));
5466 assert_eq!(e.vim_mode(), VimMode::Normal, "should be Normal after Esc");
5467 e.after_g('v', 1);
5469 assert_eq!(
5470 e.vim_mode(),
5471 VimMode::Visual,
5472 "gv must re-enter Visual mode"
5473 );
5474 }
5475
5476 #[test]
5477 fn after_g_gj_moves_down() {
5478 let mut e = fresh_editor("line0\nline1\nline2\n");
5479 e.jump_cursor(0, 0);
5480 e.after_g('j', 1);
5481 assert_eq!(e.cursor().0, 1, "gj must move down one display row");
5482 }
5483
5484 #[test]
5485 fn after_g_gu_sets_operator_pending() {
5486 let mut e = fresh_editor("hello\n");
5488 e.after_g('U', 1);
5489 assert!(
5491 e.is_chord_pending(),
5492 "gU must set engine chord-pending (Pending::Op)"
5493 );
5494 }
5495
5496 #[test]
5497 fn after_g_g_star_searches_forward_non_whole_word() {
5498 let mut e = fresh_editor("foo foobar\n");
5500 e.jump_cursor(0, 0); e.after_g('*', 1);
5502 assert_eq!(e.vim_mode(), VimMode::Normal, "g* must stay in Normal mode");
5506 }
5507
5508 #[test]
5511 fn apply_motion_char_left_moves_cursor() {
5512 let mut e = fresh_editor("hello\n");
5513 e.jump_cursor(0, 3);
5514 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 1);
5515 assert_eq!(e.cursor(), (0, 2), "CharLeft moves one col left");
5516 }
5517
5518 #[test]
5519 fn apply_motion_char_left_clamps_at_col_zero() {
5520 let mut e = fresh_editor("hello\n");
5521 e.jump_cursor(0, 0);
5522 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 1);
5523 assert_eq!(e.cursor(), (0, 0), "CharLeft at col 0 must not wrap");
5524 }
5525
5526 #[test]
5527 fn apply_motion_char_left_with_count() {
5528 let mut e = fresh_editor("hello\n");
5529 e.jump_cursor(0, 4);
5530 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 3);
5531 assert_eq!(e.cursor(), (0, 1), "CharLeft count=3 moves three cols left");
5532 }
5533
5534 #[test]
5535 fn apply_motion_char_right_moves_cursor() {
5536 let mut e = fresh_editor("hello\n");
5537 e.jump_cursor(0, 0);
5538 e.apply_motion(hjkl_vim::MotionKind::CharRight, 1);
5539 assert_eq!(e.cursor(), (0, 1), "CharRight moves one col right");
5540 }
5541
5542 #[test]
5543 fn apply_motion_char_right_clamps_at_last_char() {
5544 let mut e = fresh_editor("hello\n");
5545 e.jump_cursor(0, 4);
5547 e.apply_motion(hjkl_vim::MotionKind::CharRight, 1);
5548 assert_eq!(
5549 e.cursor(),
5550 (0, 4),
5551 "CharRight at end must not go past last char"
5552 );
5553 }
5554
5555 #[test]
5556 fn apply_motion_line_down_moves_cursor() {
5557 let mut e = fresh_editor("line0\nline1\nline2\n");
5558 e.jump_cursor(0, 0);
5559 e.apply_motion(hjkl_vim::MotionKind::LineDown, 1);
5560 assert_eq!(e.cursor().0, 1, "LineDown moves one row down");
5561 }
5562
5563 #[test]
5564 fn apply_motion_line_down_with_count() {
5565 let mut e = fresh_editor("line0\nline1\nline2\n");
5566 e.jump_cursor(0, 0);
5567 e.apply_motion(hjkl_vim::MotionKind::LineDown, 2);
5568 assert_eq!(e.cursor().0, 2, "LineDown count=2 moves two rows down");
5569 }
5570
5571 #[test]
5572 fn apply_motion_line_up_moves_cursor() {
5573 let mut e = fresh_editor("line0\nline1\nline2\n");
5574 e.jump_cursor(2, 0);
5575 e.apply_motion(hjkl_vim::MotionKind::LineUp, 1);
5576 assert_eq!(e.cursor().0, 1, "LineUp moves one row up");
5577 }
5578
5579 #[test]
5580 fn apply_motion_line_up_clamps_at_top() {
5581 let mut e = fresh_editor("line0\nline1\n");
5582 e.jump_cursor(0, 0);
5583 e.apply_motion(hjkl_vim::MotionKind::LineUp, 1);
5584 assert_eq!(e.cursor().0, 0, "LineUp at top must not go negative");
5585 }
5586
5587 #[test]
5588 fn apply_motion_first_non_blank_down_moves_and_lands_on_non_blank() {
5589 let mut e = fresh_editor(" hello\n world\n");
5591 e.jump_cursor(0, 0);
5592 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlankDown, 1);
5593 assert_eq!(e.cursor().0, 1, "FirstNonBlankDown must move to next row");
5594 assert_eq!(
5595 e.cursor().1,
5596 2,
5597 "FirstNonBlankDown must land on first non-blank col"
5598 );
5599 }
5600
5601 #[test]
5602 fn apply_motion_first_non_blank_up_moves_and_lands_on_non_blank() {
5603 let mut e = fresh_editor(" hello\n world\n");
5604 e.jump_cursor(1, 4);
5605 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlankUp, 1);
5606 assert_eq!(e.cursor().0, 0, "FirstNonBlankUp must move to prev row");
5607 assert_eq!(
5608 e.cursor().1,
5609 2,
5610 "FirstNonBlankUp must land on first non-blank col"
5611 );
5612 }
5613
5614 #[test]
5615 fn apply_motion_count_zero_treated_as_one() {
5616 let mut e = fresh_editor("hello\n");
5618 e.jump_cursor(0, 3);
5619 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 0);
5620 assert_eq!(e.cursor(), (0, 2), "count=0 treated as 1 for CharLeft");
5621 }
5622
5623 #[test]
5626 fn apply_motion_word_forward_moves_to_next_word() {
5627 let mut e = fresh_editor("hello world\n");
5629 e.jump_cursor(0, 0);
5630 e.apply_motion(hjkl_vim::MotionKind::WordForward, 1);
5631 assert_eq!(
5632 e.cursor(),
5633 (0, 6),
5634 "WordForward moves to start of next word"
5635 );
5636 }
5637
5638 #[test]
5639 fn apply_motion_word_forward_with_count() {
5640 let mut e = fresh_editor("one two three\n");
5642 e.jump_cursor(0, 0);
5643 e.apply_motion(hjkl_vim::MotionKind::WordForward, 2);
5644 assert_eq!(e.cursor(), (0, 8), "WordForward count=2 skips two words");
5645 }
5646
5647 #[test]
5648 fn apply_motion_big_word_forward_moves_to_next_big_word() {
5649 let mut e = fresh_editor("foo.bar baz\n");
5651 e.jump_cursor(0, 0);
5652 e.apply_motion(hjkl_vim::MotionKind::BigWordForward, 1);
5653 assert_eq!(e.cursor(), (0, 8), "BigWordForward skips the whole WORD");
5654 }
5655
5656 #[test]
5657 fn apply_motion_big_word_forward_with_count() {
5658 let mut e = fresh_editor("aa bb cc\n");
5660 e.jump_cursor(0, 0);
5661 e.apply_motion(hjkl_vim::MotionKind::BigWordForward, 2);
5662 assert_eq!(e.cursor(), (0, 6), "BigWordForward count=2 skips two WORDs");
5663 }
5664
5665 #[test]
5666 fn apply_motion_word_backward_moves_to_prev_word() {
5667 let mut e = fresh_editor("hello world\n");
5669 e.jump_cursor(0, 6);
5670 e.apply_motion(hjkl_vim::MotionKind::WordBackward, 1);
5671 assert_eq!(
5672 e.cursor(),
5673 (0, 0),
5674 "WordBackward moves to start of prev word"
5675 );
5676 }
5677
5678 #[test]
5679 fn apply_motion_word_backward_with_count() {
5680 let mut e = fresh_editor("one two three\n");
5682 e.jump_cursor(0, 8);
5683 e.apply_motion(hjkl_vim::MotionKind::WordBackward, 2);
5684 assert_eq!(
5685 e.cursor(),
5686 (0, 0),
5687 "WordBackward count=2 skips two words back"
5688 );
5689 }
5690
5691 #[test]
5692 fn apply_motion_big_word_backward_moves_to_prev_big_word() {
5693 let mut e = fresh_editor("foo.bar baz\n");
5695 e.jump_cursor(0, 8);
5696 e.apply_motion(hjkl_vim::MotionKind::BigWordBackward, 1);
5697 assert_eq!(
5698 e.cursor(),
5699 (0, 0),
5700 "BigWordBackward jumps to start of prev WORD"
5701 );
5702 }
5703
5704 #[test]
5705 fn apply_motion_big_word_backward_with_count() {
5706 let mut e = fresh_editor("aa bb cc\n");
5708 e.jump_cursor(0, 6);
5709 e.apply_motion(hjkl_vim::MotionKind::BigWordBackward, 2);
5710 assert_eq!(
5711 e.cursor(),
5712 (0, 0),
5713 "BigWordBackward count=2 skips two WORDs back"
5714 );
5715 }
5716
5717 #[test]
5718 fn apply_motion_word_end_moves_to_end_of_word() {
5719 let mut e = fresh_editor("hello world\n");
5721 e.jump_cursor(0, 0);
5722 e.apply_motion(hjkl_vim::MotionKind::WordEnd, 1);
5723 assert_eq!(e.cursor(), (0, 4), "WordEnd moves to end of current word");
5724 }
5725
5726 #[test]
5727 fn apply_motion_word_end_with_count() {
5728 let mut e = fresh_editor("one two three\n");
5730 e.jump_cursor(0, 0);
5731 e.apply_motion(hjkl_vim::MotionKind::WordEnd, 2);
5732 assert_eq!(
5733 e.cursor(),
5734 (0, 6),
5735 "WordEnd count=2 lands on end of second word"
5736 );
5737 }
5738
5739 #[test]
5740 fn apply_motion_big_word_end_moves_to_end_of_big_word() {
5741 let mut e = fresh_editor("foo.bar baz\n");
5743 e.jump_cursor(0, 0);
5744 e.apply_motion(hjkl_vim::MotionKind::BigWordEnd, 1);
5745 assert_eq!(e.cursor(), (0, 6), "BigWordEnd lands on end of WORD");
5746 }
5747
5748 #[test]
5749 fn apply_motion_big_word_end_with_count() {
5750 let mut e = fresh_editor("aa bb cc\n");
5752 e.jump_cursor(0, 0);
5753 e.apply_motion(hjkl_vim::MotionKind::BigWordEnd, 2);
5754 assert_eq!(
5755 e.cursor(),
5756 (0, 4),
5757 "BigWordEnd count=2 lands on end of second WORD"
5758 );
5759 }
5760
5761 #[test]
5764 fn apply_motion_line_start_lands_at_col_zero() {
5765 let mut e = fresh_editor(" foo bar \n");
5767 e.jump_cursor(0, 5);
5768 e.apply_motion(hjkl_vim::MotionKind::LineStart, 1);
5769 assert_eq!(e.cursor(), (0, 0), "LineStart lands at col 0");
5770 }
5771
5772 #[test]
5773 fn apply_motion_line_start_from_beginning_stays_at_col_zero() {
5774 let mut e = fresh_editor(" foo bar \n");
5776 e.jump_cursor(0, 0);
5777 e.apply_motion(hjkl_vim::MotionKind::LineStart, 1);
5778 assert_eq!(e.cursor(), (0, 0), "LineStart from col 0 stays at col 0");
5779 }
5780
5781 #[test]
5782 fn apply_motion_first_non_blank_lands_on_first_non_blank() {
5783 let mut e = fresh_editor(" foo bar \n");
5785 e.jump_cursor(0, 0);
5786 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlank, 1);
5787 assert_eq!(
5788 e.cursor(),
5789 (0, 2),
5790 "FirstNonBlank lands on first non-blank char"
5791 );
5792 }
5793
5794 #[test]
5795 fn apply_motion_first_non_blank_on_blank_line_lands_at_zero() {
5796 let mut e = fresh_editor(" \n");
5798 e.jump_cursor(0, 2);
5799 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlank, 1);
5800 assert_eq!(
5801 e.cursor(),
5802 (0, 0),
5803 "FirstNonBlank on blank line stays at col 0"
5804 );
5805 }
5806
5807 #[test]
5808 fn apply_motion_line_end_lands_on_last_char() {
5809 let mut e = fresh_editor(" foo bar \n");
5811 e.jump_cursor(0, 0);
5812 e.apply_motion(hjkl_vim::MotionKind::LineEnd, 1);
5813 assert_eq!(e.cursor(), (0, 10), "LineEnd lands on last char of line");
5814 }
5815
5816 #[test]
5817 fn apply_motion_line_end_on_empty_line_stays_at_zero() {
5818 let mut e = fresh_editor("\n");
5820 e.jump_cursor(0, 0);
5821 e.apply_motion(hjkl_vim::MotionKind::LineEnd, 1);
5822 assert_eq!(e.cursor(), (0, 0), "LineEnd on empty line stays at col 0");
5823 }
5824
5825 #[test]
5828 fn goto_line_count_1_lands_on_last_line() {
5829 let mut e = fresh_editor("foo\nbar\nbaz\n");
5834 e.jump_cursor(0, 0);
5835 e.apply_motion(hjkl_vim::MotionKind::GotoLine, 1);
5836 assert_eq!(e.cursor(), (2, 0), "bare G lands on last content row");
5837 }
5838
5839 #[test]
5840 fn goto_line_count_5_lands_on_line_5() {
5841 let mut e = fresh_editor("a\nb\nc\nd\ne\nf\n");
5843 e.jump_cursor(0, 0);
5844 e.apply_motion(hjkl_vim::MotionKind::GotoLine, 5);
5845 assert_eq!(e.cursor(), (4, 0), "5G lands on row 4 (1-based line 5)");
5846 }
5847
5848 #[test]
5849 fn goto_line_count_past_buffer_clamps_to_last_line() {
5850 let mut e = fresh_editor("foo\nbar\nbaz\n");
5852 e.jump_cursor(0, 0);
5853 e.apply_motion(hjkl_vim::MotionKind::GotoLine, 100);
5854 assert_eq!(e.cursor(), (2, 0), "100G clamps to last content row");
5855 }
5856
5857 #[test]
5860 fn find_repeat_after_f_finds_next_occurrence() {
5861 let mut e = fresh_editor("abcabc");
5863 e.jump_cursor(0, 0);
5864 e.find_char('c', true, false, 1);
5865 assert_eq!(e.cursor(), (0, 2), "fc must land on first 'c'");
5866 e.apply_motion(hjkl_vim::MotionKind::FindRepeat, 1);
5867 assert_eq!(
5868 e.cursor(),
5869 (0, 5),
5870 "find_repeat (;) must advance to second 'c'"
5871 );
5872 }
5873
5874 #[test]
5875 fn find_repeat_reverse_after_f_finds_prev_occurrence() {
5876 let mut e = fresh_editor("abcabc");
5878 e.jump_cursor(0, 0);
5879 e.find_char('c', true, false, 1);
5880 assert_eq!(e.cursor(), (0, 2), "fc must land on first 'c'");
5881 e.apply_motion(hjkl_vim::MotionKind::FindRepeat, 1);
5882 assert_eq!(e.cursor(), (0, 5), "; must advance to second 'c'");
5883 e.apply_motion(hjkl_vim::MotionKind::FindRepeatReverse, 1);
5884 assert_eq!(
5885 e.cursor(),
5886 (0, 2),
5887 "find_repeat_reverse (,) must go back to first 'c'"
5888 );
5889 }
5890
5891 #[test]
5892 fn find_repeat_with_no_prior_find_is_noop() {
5893 let mut e = fresh_editor("abcabc");
5895 e.jump_cursor(0, 3);
5896 e.apply_motion(hjkl_vim::MotionKind::FindRepeat, 1);
5897 assert_eq!(
5898 e.cursor(),
5899 (0, 3),
5900 "find_repeat with no prior find must be a no-op"
5901 );
5902 }
5903
5904 #[test]
5905 fn find_repeat_with_count_advances_count_times() {
5906 let mut e = fresh_editor("aXaXaX");
5908 e.jump_cursor(0, 0);
5909 e.find_char('X', true, false, 1);
5910 assert_eq!(e.cursor(), (0, 1), "fX must land on first 'X' at col 1");
5911 e.apply_motion(hjkl_vim::MotionKind::FindRepeat, 3);
5912 assert_eq!(
5913 e.cursor(),
5914 (0, 5),
5915 "3; must advance 3 times from col 1 to col 5"
5916 );
5917 }
5918
5919 #[test]
5922 fn bracket_match_jumps_to_matching_close_paren() {
5923 let mut e = fresh_editor("(abc)");
5925 e.jump_cursor(0, 0);
5926 e.apply_motion(hjkl_vim::MotionKind::BracketMatch, 1);
5927 assert_eq!(
5928 e.cursor(),
5929 (0, 4),
5930 "% on '(' must land on matching ')' at col 4"
5931 );
5932 }
5933
5934 #[test]
5935 fn bracket_match_jumps_to_matching_open_paren() {
5936 let mut e = fresh_editor("(abc)");
5938 e.jump_cursor(0, 4);
5939 e.apply_motion(hjkl_vim::MotionKind::BracketMatch, 1);
5940 assert_eq!(
5941 e.cursor(),
5942 (0, 0),
5943 "% on ')' must land on matching '(' at col 0"
5944 );
5945 }
5946
5947 #[test]
5948 fn bracket_match_with_no_match_on_line_is_noop_or_engine_behaviour() {
5949 let mut e = fresh_editor("abcd");
5952 e.jump_cursor(0, 2);
5953 e.apply_motion(hjkl_vim::MotionKind::BracketMatch, 1);
5954 assert_eq!(
5955 e.cursor(),
5956 (0, 2),
5957 "% with no bracket under cursor must be a no-op"
5958 );
5959 }
5960
5961 fn fresh_viewport_editor() -> Editor {
5965 let content = many_lines(20);
5966 let mut e = Editor::new(
5967 hjkl_buffer::Buffer::from_str(&content),
5968 crate::types::DefaultHost::new(),
5969 crate::types::Options::default(),
5970 );
5971 e.set_viewport_height(10);
5975 e.sync_buffer_from_textarea();
5976 e.host_mut().viewport_mut().top_row = 5;
5977 e
5978 }
5979
5980 #[test]
5981 fn viewport_top_lands_on_first_visible_row() {
5982 let mut e = fresh_viewport_editor();
5985 e.jump_cursor(10, 0);
5986 e.apply_motion(hjkl_vim::MotionKind::ViewportTop, 1);
5987 assert_eq!(
5988 e.cursor().0,
5989 5,
5990 "H (count=1) must land on viewport top row (5)"
5991 );
5992 }
5993
5994 #[test]
5995 fn viewport_top_with_count_offsets_down() {
5996 let mut e = fresh_viewport_editor();
5998 e.jump_cursor(12, 0);
5999 e.apply_motion(hjkl_vim::MotionKind::ViewportTop, 3);
6000 assert_eq!(e.cursor().0, 7, "3H must land at viewport top + 2 = row 7");
6001 }
6002
6003 #[test]
6004 fn viewport_middle_lands_on_middle_visible_row() {
6005 let mut e = fresh_viewport_editor();
6007 e.jump_cursor(0, 0);
6008 e.apply_motion(hjkl_vim::MotionKind::ViewportMiddle, 1);
6009 assert_eq!(e.cursor().0, 9, "M must land on middle visible row (9)");
6010 }
6011
6012 #[test]
6013 fn viewport_bottom_lands_on_last_visible_row() {
6014 let mut e = fresh_viewport_editor();
6016 e.jump_cursor(5, 0);
6017 e.apply_motion(hjkl_vim::MotionKind::ViewportBottom, 1);
6018 assert_eq!(
6019 e.cursor().0,
6020 14,
6021 "L (count=1) must land on viewport bottom row (14)"
6022 );
6023 }
6024
6025 #[test]
6026 fn half_page_down_moves_cursor_by_half_window() {
6027 let mut e = Editor::new(
6029 hjkl_buffer::Buffer::from_str(&many_lines(30)),
6030 crate::types::DefaultHost::new(),
6031 crate::types::Options::default(),
6032 );
6033 e.set_viewport_height(10);
6034 e.jump_cursor(0, 0);
6035 e.apply_motion(hjkl_vim::MotionKind::HalfPageDown, 1);
6036 assert_eq!(
6037 e.cursor().0,
6038 5,
6039 "<C-d> from row 0 with viewport height=10 must land on row 5"
6040 );
6041 }
6042
6043 #[test]
6044 fn half_page_up_moves_cursor_by_half_window_reverse() {
6045 let mut e = Editor::new(
6047 hjkl_buffer::Buffer::from_str(&many_lines(30)),
6048 crate::types::DefaultHost::new(),
6049 crate::types::Options::default(),
6050 );
6051 e.set_viewport_height(10);
6052 e.jump_cursor(10, 0);
6053 e.apply_motion(hjkl_vim::MotionKind::HalfPageUp, 1);
6054 assert_eq!(
6055 e.cursor().0,
6056 5,
6057 "<C-u> from row 10 with viewport height=10 must land on row 5"
6058 );
6059 }
6060
6061 #[test]
6062 fn full_page_down_moves_cursor_by_full_window() {
6063 let mut e = Editor::new(
6065 hjkl_buffer::Buffer::from_str(&many_lines(30)),
6066 crate::types::DefaultHost::new(),
6067 crate::types::Options::default(),
6068 );
6069 e.set_viewport_height(10);
6070 e.jump_cursor(0, 0);
6071 e.apply_motion(hjkl_vim::MotionKind::FullPageDown, 1);
6072 assert_eq!(
6073 e.cursor().0,
6074 8,
6075 "<C-f> from row 0 with viewport height=10 must land on row 8"
6076 );
6077 }
6078
6079 #[test]
6080 fn full_page_up_moves_cursor_by_full_window_reverse() {
6081 let mut e = Editor::new(
6083 hjkl_buffer::Buffer::from_str(&many_lines(30)),
6084 crate::types::DefaultHost::new(),
6085 crate::types::Options::default(),
6086 );
6087 e.set_viewport_height(10);
6088 e.jump_cursor(10, 0);
6089 e.apply_motion(hjkl_vim::MotionKind::FullPageUp, 1);
6090 assert_eq!(
6091 e.cursor().0,
6092 2,
6093 "<C-b> from row 10 with viewport height=10 must land on row 2"
6094 );
6095 }
6096
6097 #[test]
6100 fn set_mark_at_cursor_alphabetic_records() {
6101 let mut e = fresh_editor("hello");
6103 e.jump_cursor(0, 2);
6104 e.set_mark_at_cursor('a');
6105 assert_eq!(
6106 e.mark('a'),
6107 Some((0, 2)),
6108 "mark 'a' must record current pos"
6109 );
6110 }
6111
6112 #[test]
6113 fn set_mark_at_cursor_invalid_char_no_op() {
6114 let mut e = fresh_editor("hello");
6116 e.jump_cursor(0, 1);
6117 e.set_mark_at_cursor('1'); assert_eq!(e.mark('1'), None, "digit mark must be a no-op");
6119 e.set_mark_at_cursor('['); assert_eq!(
6121 e.mark('['),
6122 None,
6123 "bracket char must be a no-op for set_mark"
6124 );
6125 }
6126
6127 #[test]
6128 fn set_mark_at_cursor_special_left_bracket() {
6129 let mut e = fresh_editor("hello");
6132 e.jump_cursor(0, 3);
6133 e.set_mark_at_cursor('[');
6134 assert_eq!(
6135 e.mark('['),
6136 None,
6137 "set_mark_at_cursor must reject '[' (vim: m[ is invalid)"
6138 );
6139 }
6140
6141 #[test]
6144 fn goto_mark_line_jumps_to_first_non_blank() {
6145 let mut e = fresh_editor("hello\n world\n");
6148 e.jump_cursor(1, 3);
6149 e.set_mark_at_cursor('a');
6150 e.jump_cursor(0, 0);
6151 e.goto_mark_line('a');
6152 assert_eq!(e.cursor().0, 1, "goto_mark_line must jump to mark row");
6153 assert_eq!(
6155 e.cursor().1,
6156 2,
6157 "goto_mark_line must land on first non-blank column"
6158 );
6159 }
6160
6161 #[test]
6162 fn goto_mark_line_unset_mark_no_op() {
6163 let mut e = fresh_editor("hello\nworld\n");
6165 e.jump_cursor(1, 2);
6166 e.goto_mark_line('z'); assert_eq!(e.cursor(), (1, 2), "unset mark jump must be a no-op");
6168 }
6169
6170 #[test]
6171 fn goto_mark_line_invalid_char_no_op() {
6172 let mut e = fresh_editor("hello\nworld\n");
6174 e.jump_cursor(0, 0);
6175 e.goto_mark_line('!');
6176 assert_eq!(e.cursor(), (0, 0), "invalid mark char must be a no-op");
6177 }
6178
6179 #[test]
6182 fn goto_mark_char_jumps_to_exact_pos() {
6183 let mut e = fresh_editor("hello\nworld\n");
6186 e.jump_cursor(1, 4);
6187 e.set_mark_at_cursor('b');
6188 e.jump_cursor(0, 0);
6189 e.goto_mark_char('b');
6190 assert_eq!(
6191 e.cursor(),
6192 (1, 4),
6193 "goto_mark_char must jump to exact mark position"
6194 );
6195 }
6196
6197 #[test]
6198 fn goto_mark_char_unset_mark_no_op() {
6199 let mut e = fresh_editor("hello\nworld\n");
6201 e.jump_cursor(1, 1);
6202 e.goto_mark_char('x'); assert_eq!(
6204 e.cursor(),
6205 (1, 1),
6206 "unset charwise mark jump must be a no-op"
6207 );
6208 }
6209
6210 #[test]
6211 fn goto_mark_char_invalid_char_no_op() {
6212 let mut e = fresh_editor("hello\nworld\n");
6214 e.jump_cursor(0, 2);
6215 e.goto_mark_char('#');
6216 assert_eq!(
6217 e.cursor(),
6218 (0, 2),
6219 "invalid charwise mark char must be a no-op"
6220 );
6221 }
6222
6223 #[test]
6226 fn start_macro_record_records_register() {
6227 let mut e = fresh_editor("hello");
6228 assert!(!e.is_recording_macro());
6229 e.start_macro_record('a');
6230 assert!(e.is_recording_macro());
6231 assert_eq!(e.recording_register(), Some('a'));
6232 }
6233
6234 #[test]
6235 fn start_macro_record_capital_seeds_existing() {
6236 let mut e = fresh_editor("hello");
6238 e.start_macro_record('a');
6239 e.record_input(crate::input::Input {
6240 key: crate::input::Key::Char('h'),
6241 ..Default::default()
6242 });
6243 e.stop_macro_record();
6244 e.start_macro_record('A');
6246 assert_eq!(
6248 e.vim.recording_keys.len(),
6249 1,
6250 "capital record must seed from existing lowercase reg"
6251 );
6252 }
6253
6254 #[test]
6255 fn stop_macro_record_writes_register() {
6256 let mut e = fresh_editor("hello");
6257 e.start_macro_record('a');
6258 e.record_input(crate::input::Input {
6259 key: crate::input::Key::Char('h'),
6260 ..Default::default()
6261 });
6262 e.record_input(crate::input::Input {
6263 key: crate::input::Key::Char('l'),
6264 ..Default::default()
6265 });
6266 e.stop_macro_record();
6267 assert!(!e.is_recording_macro());
6268 let text = e
6270 .registers()
6271 .read('a')
6272 .map(|s| s.text.clone())
6273 .unwrap_or_default();
6274 assert_eq!(
6275 text, "hl",
6276 "stop_macro_record must write encoded keys to register"
6277 );
6278 }
6279
6280 #[test]
6281 fn is_recording_macro_reflects_state() {
6282 let mut e = fresh_editor("hello");
6283 assert!(!e.is_recording_macro());
6284 e.start_macro_record('b');
6285 assert!(e.is_recording_macro());
6286 e.stop_macro_record();
6287 assert!(!e.is_recording_macro());
6288 }
6289
6290 #[test]
6291 fn play_macro_returns_decoded_inputs() {
6292 let mut e = fresh_editor("hello");
6293 e.set_named_register_text('a', "jj".to_string());
6295 let inputs = e.play_macro('a', 1);
6296 assert_eq!(inputs.len(), 2);
6297 assert_eq!(inputs[0].key, crate::input::Key::Char('j'));
6298 assert_eq!(inputs[1].key, crate::input::Key::Char('j'));
6299 assert!(e.is_replaying_macro(), "play_macro must set replaying flag");
6300 e.end_macro_replay();
6301 assert!(!e.is_replaying_macro());
6302 }
6303
6304 #[test]
6305 fn play_macro_at_uses_last_macro() {
6306 let mut e = fresh_editor("hello");
6307 e.set_named_register_text('a', "k".to_string());
6308 let _ = e.play_macro('a', 1);
6310 e.end_macro_replay();
6311 let inputs = e.play_macro('@', 1);
6313 assert_eq!(inputs.len(), 1);
6314 assert_eq!(inputs[0].key, crate::input::Key::Char('k'));
6315 e.end_macro_replay();
6316 }
6317
6318 #[test]
6319 fn play_macro_with_count_repeats() {
6320 let mut e = fresh_editor("hello");
6321 e.set_named_register_text('a', "j".to_string());
6322 let inputs = e.play_macro('a', 3);
6323 assert_eq!(inputs.len(), 3, "3@a must produce 3 inputs");
6324 e.end_macro_replay();
6325 }
6326
6327 #[test]
6328 fn record_input_appends_when_recording() {
6329 let mut e = fresh_editor("hello");
6330 e.record_input(crate::input::Input {
6332 key: crate::input::Key::Char('j'),
6333 ..Default::default()
6334 });
6335 assert_eq!(e.vim.recording_keys.len(), 0);
6336 e.start_macro_record('a');
6338 e.record_input(crate::input::Input {
6339 key: crate::input::Key::Char('j'),
6340 ..Default::default()
6341 });
6342 e.record_input(crate::input::Input {
6343 key: crate::input::Key::Char('k'),
6344 ..Default::default()
6345 });
6346 assert_eq!(e.vim.recording_keys.len(), 2);
6347 e.vim.replaying_macro = true;
6349 e.record_input(crate::input::Input {
6350 key: crate::input::Key::Char('l'),
6351 ..Default::default()
6352 });
6353 assert_eq!(
6354 e.vim.recording_keys.len(),
6355 2,
6356 "record_input must skip during replay"
6357 );
6358 e.vim.replaying_macro = false;
6359 e.stop_macro_record();
6360 }
6361
6362 fn enter_insert(e: &mut Editor) {
6366 e.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
6367 assert_eq!(e.vim_mode(), crate::VimMode::Insert);
6368 }
6369
6370 #[test]
6371 fn insert_char_basic() {
6372 let mut e = fresh_editor("hello");
6373 enter_insert(&mut e);
6374 e.insert_char('X');
6375 assert_eq!(e.buffer().lines()[0], "Xhello");
6376 assert!(e.take_dirty());
6377 }
6378
6379 #[test]
6380 fn insert_char_appends_in_replace_mode() {
6381 let mut e = fresh_editor("abc");
6384 e.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::SHIFT));
6385 assert_eq!(e.vim_mode(), crate::VimMode::Insert);
6386 e.insert_char('X');
6387 assert_eq!(e.buffer().lines()[0], "Xbc");
6389 e.insert_char('Y');
6390 assert_eq!(e.buffer().lines()[0], "XYc");
6392 }
6393
6394 #[test]
6395 fn insert_newline_splits_line() {
6396 let mut e = fresh_editor("hello");
6397 e.jump_cursor(0, 3);
6399 enter_insert(&mut e);
6400 e.insert_newline();
6401 let lines = e.buffer().lines().to_vec();
6402 assert_eq!(lines[0], "hel");
6403 assert_eq!(lines[1], "lo");
6404 }
6405
6406 #[test]
6407 fn insert_tab_expandtab_inserts_spaces() {
6408 let mut e = fresh_editor("");
6409 enter_insert(&mut e);
6411 e.insert_tab();
6412 assert_eq!(e.buffer().lines()[0], " ");
6414 }
6415
6416 #[test]
6417 fn insert_tab_real_tab_when_noexpandtab() {
6418 let opts = crate::types::Options {
6419 expandtab: false,
6420 ..crate::types::Options::default()
6421 };
6422 let mut e = Editor::new(
6423 hjkl_buffer::Buffer::new(),
6424 crate::types::DefaultHost::new(),
6425 opts,
6426 );
6427 e.set_content("");
6428 enter_insert(&mut e);
6429 e.insert_tab();
6430 assert_eq!(e.buffer().lines()[0], "\t");
6431 }
6432
6433 #[test]
6434 fn insert_backspace_single_char() {
6435 let mut e = fresh_editor("hello");
6437 e.jump_cursor(0, 3);
6438 enter_insert(&mut e);
6439 e.insert_backspace();
6440 assert_eq!(e.buffer().lines()[0], "helo");
6441 }
6442
6443 #[test]
6444 fn insert_backspace_softtabstop() {
6445 let mut e = fresh_editor(" hello");
6447 e.jump_cursor(0, 4);
6448 enter_insert(&mut e);
6449 e.insert_backspace();
6450 assert_eq!(e.buffer().lines()[0], "hello");
6451 }
6452
6453 #[test]
6454 fn insert_backspace_join_up() {
6455 let mut e = fresh_editor("foo\nbar");
6457 e.jump_cursor(1, 0);
6458 enter_insert(&mut e);
6459 e.insert_backspace();
6460 assert_eq!(e.buffer().lines().len(), 1);
6462 assert_eq!(e.buffer().lines()[0], "foobar");
6463 }
6464
6465 #[test]
6466 fn leave_insert_steps_back_col() {
6467 let mut e = fresh_editor("hello");
6469 e.jump_cursor(0, 3);
6470 enter_insert(&mut e);
6471 e.insert_char('X');
6473 let pre_col = e.cursor().1;
6475 e.leave_insert_to_normal();
6476 assert_eq!(e.vim_mode(), crate::VimMode::Normal);
6477 assert_eq!(e.cursor().1, pre_col - 1);
6479 }
6480
6481 #[test]
6482 fn insert_ctrl_w_word_back() {
6483 let mut e = fresh_editor("hello world");
6486 e.jump_cursor(0, 11);
6488 enter_insert(&mut e);
6489 e.insert_ctrl_w();
6490 assert_eq!(e.buffer().lines()[0], "hello ");
6492 }
6493
6494 #[test]
6495 fn insert_ctrl_u_deletes_to_line_start() {
6496 let mut e = fresh_editor("hello world");
6497 e.jump_cursor(0, 5);
6498 enter_insert(&mut e);
6499 e.insert_ctrl_u();
6500 assert_eq!(e.buffer().lines()[0], " world");
6501 }
6502
6503 #[test]
6504 fn insert_ctrl_h_single_backspace() {
6505 let mut e = fresh_editor("hello");
6507 e.jump_cursor(0, 3);
6508 enter_insert(&mut e);
6509 e.insert_ctrl_h();
6510 assert_eq!(e.buffer().lines()[0], "helo");
6511 }
6512
6513 #[test]
6514 fn insert_ctrl_h_join_up() {
6515 let mut e = fresh_editor("foo\nbar");
6516 e.jump_cursor(1, 0);
6517 enter_insert(&mut e);
6518 e.insert_ctrl_h();
6519 assert_eq!(e.buffer().lines().len(), 1);
6520 assert_eq!(e.buffer().lines()[0], "foobar");
6521 }
6522
6523 #[test]
6524 fn insert_ctrl_t_indents_current_line() {
6525 let mut e = Editor::new(
6526 hjkl_buffer::Buffer::new(),
6527 crate::types::DefaultHost::new(),
6528 crate::types::Options {
6529 shiftwidth: 4,
6530 ..crate::types::Options::default()
6531 },
6532 );
6533 e.set_content("hello");
6534 enter_insert(&mut e);
6535 e.insert_ctrl_t();
6536 assert_eq!(e.buffer().lines()[0], " hello");
6537 }
6538
6539 #[test]
6540 fn insert_ctrl_d_outdents_current_line() {
6541 let mut e = Editor::new(
6542 hjkl_buffer::Buffer::new(),
6543 crate::types::DefaultHost::new(),
6544 crate::types::Options {
6545 shiftwidth: 4,
6546 ..crate::types::Options::default()
6547 },
6548 );
6549 e.set_content(" hello");
6550 enter_insert(&mut e);
6551 e.insert_ctrl_d();
6552 assert_eq!(e.buffer().lines()[0], "hello");
6553 }
6554
6555 #[test]
6556 fn insert_ctrl_o_arm_sets_one_shot_normal() {
6557 let mut e = fresh_editor("hello");
6558 enter_insert(&mut e);
6559 e.insert_ctrl_o_arm();
6560 assert_eq!(e.vim_mode(), crate::VimMode::Normal);
6562 }
6563
6564 #[test]
6565 fn insert_ctrl_r_arm_sets_pending_register() {
6566 let mut e = fresh_editor("hello");
6567 enter_insert(&mut e);
6568 e.insert_ctrl_r_arm();
6569 assert_eq!(e.vim_mode(), crate::VimMode::Insert);
6571 assert!(e.vim.insert_pending_register);
6572 }
6573
6574 #[test]
6575 fn insert_delete_removes_char_under_cursor() {
6576 let mut e = fresh_editor("hello");
6577 e.jump_cursor(0, 2);
6578 enter_insert(&mut e);
6579 e.insert_delete();
6580 assert_eq!(e.buffer().lines()[0], "helo");
6581 }
6582
6583 #[test]
6584 fn insert_delete_joins_lines_at_eol() {
6585 let mut e = fresh_editor("foo\nbar");
6586 e.jump_cursor(0, 3);
6588 enter_insert(&mut e);
6589 e.insert_delete();
6590 assert_eq!(e.buffer().lines().len(), 1);
6591 assert_eq!(e.buffer().lines()[0], "foobar");
6592 }
6593
6594 #[test]
6595 fn insert_arrow_left_moves_cursor() {
6596 let mut e = fresh_editor("hello");
6597 e.jump_cursor(0, 3);
6598 enter_insert(&mut e);
6599 e.insert_arrow(crate::vim::InsertDir::Left);
6600 assert_eq!(e.cursor().1, 2);
6601 }
6602
6603 #[test]
6604 fn insert_arrow_right_moves_cursor() {
6605 let mut e = fresh_editor("hello");
6606 e.jump_cursor(0, 2);
6607 enter_insert(&mut e);
6608 e.insert_arrow(crate::vim::InsertDir::Right);
6609 assert_eq!(e.cursor().1, 3);
6610 }
6611
6612 #[test]
6613 fn insert_arrow_up_moves_cursor() {
6614 let mut e = fresh_editor("foo\nbar");
6615 e.jump_cursor(1, 0);
6616 enter_insert(&mut e);
6617 e.insert_arrow(crate::vim::InsertDir::Up);
6618 assert_eq!(e.cursor().0, 0);
6619 }
6620
6621 #[test]
6622 fn insert_arrow_down_moves_cursor() {
6623 let mut e = fresh_editor("foo\nbar");
6624 e.jump_cursor(0, 0);
6625 enter_insert(&mut e);
6626 e.insert_arrow(crate::vim::InsertDir::Down);
6627 assert_eq!(e.cursor().0, 1);
6628 }
6629
6630 #[test]
6631 fn insert_home_moves_to_line_start() {
6632 let mut e = fresh_editor("hello");
6633 e.jump_cursor(0, 4);
6634 enter_insert(&mut e);
6635 e.insert_home();
6636 assert_eq!(e.cursor().1, 0);
6637 }
6638
6639 #[test]
6640 fn insert_end_moves_to_line_end() {
6641 let mut e = fresh_editor("hello");
6642 e.jump_cursor(0, 0);
6643 enter_insert(&mut e);
6644 e.insert_end();
6645 assert_eq!(e.cursor().1, 4);
6647 }
6648
6649 #[test]
6650 fn insert_pageup_does_not_panic() {
6651 let mut e = fresh_editor("line1\nline2\nline3");
6652 e.jump_cursor(2, 0);
6653 enter_insert(&mut e);
6654 e.insert_pageup(24);
6656 }
6657
6658 #[test]
6659 fn insert_pagedown_does_not_panic() {
6660 let mut e = fresh_editor("line1\nline2\nline3");
6661 e.jump_cursor(0, 0);
6662 enter_insert(&mut e);
6663 e.insert_pagedown(24);
6664 }
6665
6666 #[test]
6667 fn insert_paste_register_inserts_text() {
6668 let mut e = fresh_editor("abc");
6669 e.handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
6671 e.handle_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
6672 enter_insert(&mut e);
6673 e.insert_paste_register('"');
6675 assert!(e.content().contains("abc"));
6677 }
6678
6679 #[test]
6680 fn leave_insert_to_normal_exits_mode() {
6681 let mut e = fresh_editor("hello");
6682 enter_insert(&mut e);
6683 e.leave_insert_to_normal();
6684 assert_eq!(e.vim_mode(), crate::VimMode::Normal);
6685 }
6686
6687 #[test]
6688 fn insert_backspace_at_buffer_start_is_noop() {
6689 let mut e = fresh_editor("hello");
6690 e.jump_cursor(0, 0);
6691 enter_insert(&mut e);
6692 e.insert_backspace();
6694 assert_eq!(e.buffer().lines()[0], "hello");
6695 }
6696
6697 #[test]
6698 fn insert_delete_at_buffer_end_is_noop() {
6699 let mut e = fresh_editor("hello");
6700 e.jump_cursor(0, 5);
6702 enter_insert(&mut e);
6703 e.insert_delete();
6705 assert_eq!(e.buffer().lines()[0], "hello");
6706 }
6707}