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_is_clipboard(&self) -> bool {
1249 matches!(self.vim.pending_register, Some('+') | Some('*'))
1250 }
1251
1252 pub fn recording_register(&self) -> Option<char> {
1256 self.vim.recording_macro
1257 }
1258
1259 pub fn pending_count(&self) -> Option<u32> {
1263 self.vim.pending_count_val()
1264 }
1265
1266 pub fn pending_op(&self) -> Option<char> {
1270 self.vim.pending_op_char()
1271 }
1272
1273 pub fn is_chord_pending(&self) -> bool {
1283 self.vim.is_chord_pending()
1284 }
1285
1286 #[allow(clippy::type_complexity)]
1289 pub fn jump_list(&self) -> (&[(usize, usize)], &[(usize, usize)]) {
1290 (&self.vim.jump_back, &self.vim.jump_fwd)
1291 }
1292
1293 pub fn change_list(&self) -> (&[(usize, usize)], Option<usize>) {
1296 (&self.vim.change_list, self.vim.change_list_cursor)
1297 }
1298
1299 pub fn set_yank(&mut self, text: impl Into<String>) {
1303 let text = text.into();
1304 let linewise = self.vim.yank_linewise;
1305 self.registers.unnamed = crate::registers::Slot { text, linewise };
1306 }
1307
1308 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
1312 self.vim.yank_linewise = linewise;
1313 let target = self.vim.pending_register.take();
1314 self.registers.record_yank(text, linewise, target);
1315 }
1316
1317 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
1322 if let Some(slot) = match reg {
1323 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
1324 'A'..='Z' => {
1325 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
1326 }
1327 _ => None,
1328 } {
1329 slot.text = text;
1330 slot.linewise = false;
1331 }
1332 }
1333
1334 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
1337 self.vim.yank_linewise = linewise;
1338 let target = self.vim.pending_register.take();
1339 self.registers.record_delete(text, linewise, target);
1340 }
1341
1342 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
1351 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
1352 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
1353 .collect();
1354 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1355 #[cfg(feature = "ratatui")]
1356 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
1357 Vec::with_capacity(spans.len());
1358 for (row, row_spans) in spans.iter().enumerate() {
1359 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
1360 let mut translated = Vec::with_capacity(row_spans.len());
1361 #[cfg(feature = "ratatui")]
1362 let mut translated_r = Vec::with_capacity(row_spans.len());
1363 for (start, end, style) in row_spans {
1364 let end_clamped = (*end).min(line_len);
1365 if end_clamped <= *start {
1366 continue;
1367 }
1368 let id = self.intern_style(*style);
1369 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1370 #[cfg(feature = "ratatui")]
1371 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
1372 }
1373 by_row.push(translated);
1374 #[cfg(feature = "ratatui")]
1375 ratatui_spans.push(translated_r);
1376 }
1377 self.buffer_spans = by_row;
1378 #[cfg(feature = "ratatui")]
1379 {
1380 self.styled_spans = ratatui_spans;
1381 }
1382 }
1383
1384 #[cfg(feature = "ratatui")]
1393 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
1394 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
1395 return idx as u32;
1396 }
1397 self.style_table.push(style);
1398 (self.style_table.len() - 1) as u32
1399 }
1400
1401 #[cfg(feature = "ratatui")]
1405 pub fn style_table(&self) -> &[ratatui::style::Style] {
1406 &self.style_table
1407 }
1408
1409 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
1418 &self.buffer_spans
1419 }
1420
1421 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1436 #[cfg(feature = "ratatui")]
1437 {
1438 let r = engine_style_to_ratatui(style);
1439 self.intern_ratatui_style(r)
1440 }
1441 #[cfg(not(feature = "ratatui"))]
1442 {
1443 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1444 return idx as u32;
1445 }
1446 self.engine_style_table.push(style);
1447 (self.engine_style_table.len() - 1) as u32
1448 }
1449 }
1450
1451 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1455 #[cfg(feature = "ratatui")]
1456 {
1457 let r = self.style_table.get(id as usize).copied()?;
1458 Some(ratatui_style_to_engine(r))
1459 }
1460 #[cfg(not(feature = "ratatui"))]
1461 {
1462 self.engine_style_table.get(id as usize).copied()
1463 }
1464 }
1465
1466 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1470
1471 pub fn set_viewport_top(&mut self, row: usize) {
1479 let last = buf_row_count(&self.buffer).saturating_sub(1);
1480 let target = row.min(last);
1481 self.host.viewport_mut().top_row = target;
1482 }
1483
1484 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1488 buf_set_cursor_rc(&mut self.buffer, row, col);
1489 }
1490
1491 pub fn cursor(&self) -> (usize, usize) {
1499 buf_cursor_rc(&self.buffer)
1500 }
1501
1502 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1505 self.pending_lsp.take()
1506 }
1507
1508 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1522 std::mem::take(&mut self.pending_fold_ops)
1523 }
1524
1525 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1535 use crate::types::FoldProvider;
1536 self.pending_fold_ops.push(op);
1537 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1538 provider.apply(op);
1539 }
1540
1541 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1548 let height = self.viewport_height_value();
1549 self.host.viewport_mut().height = height;
1550 }
1551
1552 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1556 self.sync_buffer_from_textarea();
1557 }
1558
1559 pub fn record_jump(&mut self, pos: (usize, usize)) {
1564 const JUMPLIST_MAX: usize = 100;
1565 self.vim.jump_back.push(pos);
1566 if self.vim.jump_back.len() > JUMPLIST_MAX {
1567 self.vim.jump_back.remove(0);
1568 }
1569 self.vim.jump_fwd.clear();
1570 }
1571
1572 pub fn set_viewport_height(&self, height: u16) {
1575 self.viewport_height.store(height, Ordering::Relaxed);
1576 }
1577
1578 pub fn viewport_height_value(&self) -> u16 {
1580 self.viewport_height.load(Ordering::Relaxed)
1581 }
1582
1583 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1592 if self.settings.readonly {
1599 let _ = edit;
1600 return hjkl_buffer::Edit::InsertStr {
1601 at: buf_cursor_pos(&self.buffer),
1602 text: String::new(),
1603 };
1604 }
1605 let pre_row = buf_cursor_row(&self.buffer);
1606 let pre_rows = buf_row_count(&self.buffer);
1607 let (pre_edit_row, pre_edit_col) = buf_cursor_rc(&self.buffer);
1612 self.change_log.extend(edit_to_editops(&edit));
1616 let content_edits = content_edits_from_buffer_edit(&self.buffer, &edit);
1622 self.pending_content_edits.extend(content_edits);
1623 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1629 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1630 let lo = pre_row.min(pos_row);
1636 let hi = pre_row.max(pos_row);
1637 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1638 start_row: lo,
1639 end_row: hi,
1640 });
1641 self.vim.last_edit_pos = Some((pre_edit_row, pre_edit_col));
1645 let entry = (pos_row, pos_col);
1650 if self.vim.change_list.last() != Some(&entry) {
1651 if let Some(idx) = self.vim.change_list_cursor.take() {
1652 self.vim.change_list.truncate(idx + 1);
1653 }
1654 self.vim.change_list.push(entry);
1655 let len = self.vim.change_list.len();
1656 if len > crate::vim::CHANGE_LIST_MAX {
1657 self.vim
1658 .change_list
1659 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1660 }
1661 }
1662 self.vim.change_list_cursor = None;
1663 let post_rows = buf_row_count(&self.buffer);
1667 let delta = post_rows as isize - pre_rows as isize;
1668 if delta != 0 {
1669 self.shift_marks_after_edit(pre_row, delta);
1670 }
1671 self.push_buffer_content_to_textarea();
1672 self.mark_content_dirty();
1673 inverse
1674 }
1675
1676 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1681 if delta == 0 {
1682 return;
1683 }
1684 let drop_end = if delta < 0 {
1687 edit_start.saturating_add((-delta) as usize)
1688 } else {
1689 edit_start
1690 };
1691 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1692
1693 let mut to_drop: Vec<char> = Vec::new();
1696 for (c, (row, _col)) in self.marks.iter_mut() {
1697 if (edit_start..drop_end).contains(row) {
1698 to_drop.push(*c);
1699 } else if *row >= shift_threshold {
1700 *row = ((*row as isize) + delta).max(0) as usize;
1701 }
1702 }
1703 for c in to_drop {
1704 self.marks.remove(&c);
1705 }
1706
1707 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1708 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1709 for (row, _) in entries.iter_mut() {
1710 if *row >= shift_threshold {
1711 *row = ((*row as isize) + delta).max(0) as usize;
1712 }
1713 }
1714 };
1715 shift_jumps(&mut self.vim.jump_back);
1716 shift_jumps(&mut self.vim.jump_fwd);
1717 }
1718
1719 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1727
1728 pub fn mark_content_dirty(&mut self) {
1734 self.content_dirty = true;
1735 self.cached_content = None;
1736 }
1737
1738 pub fn take_dirty(&mut self) -> bool {
1740 let dirty = self.content_dirty;
1741 self.content_dirty = false;
1742 dirty
1743 }
1744
1745 pub fn take_content_edits(&mut self) -> Vec<crate::types::ContentEdit> {
1753 std::mem::take(&mut self.pending_content_edits)
1754 }
1755
1756 pub fn take_content_reset(&mut self) -> bool {
1762 let r = self.pending_content_reset;
1763 self.pending_content_reset = false;
1764 r
1765 }
1766
1767 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1777 if !self.content_dirty {
1778 return None;
1779 }
1780 let arc = self.content_arc();
1781 self.content_dirty = false;
1782 Some(arc)
1783 }
1784
1785 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1788 let cursor = buf_cursor_row(&self.buffer);
1789 let top = self.host.viewport().top_row;
1790 cursor.saturating_sub(top).min(height as usize - 1) as u16
1791 }
1792
1793 pub fn cursor_screen_pos(
1803 &self,
1804 area_x: u16,
1805 area_y: u16,
1806 area_width: u16,
1807 area_height: u16,
1808 ) -> Option<(u16, u16)> {
1809 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1810 let v = self.host.viewport();
1811 if pos_row < v.top_row || pos_col < v.top_col {
1812 return None;
1813 }
1814 let lnum_width = if self.settings.number || self.settings.relativenumber {
1815 let needed = buf_row_count(&self.buffer).to_string().len() + 1;
1816 needed.max(self.settings.numberwidth) as u16
1817 } else {
1818 0
1819 };
1820 let dy = (pos_row - v.top_row) as u16;
1821 let line = self.buffer.line(pos_row).unwrap_or("");
1825 let tab_width = if v.tab_width == 0 {
1826 4
1827 } else {
1828 v.tab_width as usize
1829 };
1830 let visual_pos = visual_col_for_char(line, pos_col, tab_width);
1831 let visual_top = visual_col_for_char(line, v.top_col, tab_width);
1832 let dx = (visual_pos - visual_top) as u16;
1833 if dy >= area_height || dx + lnum_width >= area_width {
1834 return None;
1835 }
1836 Some((area_x + lnum_width + dx, area_y + dy))
1837 }
1838
1839 #[cfg(feature = "ratatui")]
1845 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1846 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1847 }
1848
1849 pub fn vim_mode(&self) -> VimMode {
1850 self.vim.public_mode()
1851 }
1852
1853 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1859 self.vim.search_prompt.as_ref()
1860 }
1861
1862 pub fn last_search(&self) -> Option<&str> {
1865 self.vim.last_search.as_deref()
1866 }
1867
1868 pub fn last_search_forward(&self) -> bool {
1872 self.vim.last_search_forward
1873 }
1874
1875 pub fn set_last_search(&mut self, text: Option<String>, forward: bool) {
1881 self.vim.last_search = text;
1882 self.vim.last_search_forward = forward;
1883 }
1884
1885 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1889 if self.vim_mode() != VimMode::Visual {
1890 return None;
1891 }
1892 let anchor = self.vim.visual_anchor;
1893 let cursor = self.cursor();
1894 let (start, end) = if anchor <= cursor {
1895 (anchor, cursor)
1896 } else {
1897 (cursor, anchor)
1898 };
1899 Some((start, end))
1900 }
1901
1902 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1905 if self.vim_mode() != VimMode::VisualLine {
1906 return None;
1907 }
1908 let anchor = self.vim.visual_line_anchor;
1909 let cursor = buf_cursor_row(&self.buffer);
1910 Some((anchor.min(cursor), anchor.max(cursor)))
1911 }
1912
1913 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1914 if self.vim_mode() != VimMode::VisualBlock {
1915 return None;
1916 }
1917 let (ar, ac) = self.vim.block_anchor;
1918 let cr = buf_cursor_row(&self.buffer);
1919 let cc = self.vim.block_vcol;
1920 let top = ar.min(cr);
1921 let bot = ar.max(cr);
1922 let left = ac.min(cc);
1923 let right = ac.max(cc);
1924 Some((top, bot, left, right))
1925 }
1926
1927 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1933 use hjkl_buffer::{Position, Selection};
1934 match self.vim_mode() {
1935 VimMode::Visual => {
1936 let (ar, ac) = self.vim.visual_anchor;
1937 let head = buf_cursor_pos(&self.buffer);
1938 Some(Selection::Char {
1939 anchor: Position::new(ar, ac),
1940 head,
1941 })
1942 }
1943 VimMode::VisualLine => {
1944 let anchor_row = self.vim.visual_line_anchor;
1945 let head_row = buf_cursor_row(&self.buffer);
1946 Some(Selection::Line {
1947 anchor_row,
1948 head_row,
1949 })
1950 }
1951 VimMode::VisualBlock => {
1952 let (ar, ac) = self.vim.block_anchor;
1953 let cr = buf_cursor_row(&self.buffer);
1954 let cc = self.vim.block_vcol;
1955 Some(Selection::Block {
1956 anchor: Position::new(ar, ac),
1957 head: Position::new(cr, cc),
1958 })
1959 }
1960 _ => None,
1961 }
1962 }
1963
1964 pub fn force_normal(&mut self) {
1966 self.vim.force_normal();
1967 }
1968
1969 pub fn content(&self) -> String {
1970 let n = buf_row_count(&self.buffer);
1971 let mut s = String::new();
1972 for r in 0..n {
1973 if r > 0 {
1974 s.push('\n');
1975 }
1976 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1977 }
1978 s.push('\n');
1979 s
1980 }
1981
1982 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
1987 if let Some(arc) = &self.cached_content {
1988 return std::sync::Arc::clone(arc);
1989 }
1990 let arc = std::sync::Arc::new(self.content());
1991 self.cached_content = Some(std::sync::Arc::clone(&arc));
1992 arc
1993 }
1994
1995 pub fn set_content(&mut self, text: &str) {
1996 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
1997 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
1998 lines.pop();
1999 }
2000 if lines.is_empty() {
2001 lines.push(String::new());
2002 }
2003 let _ = lines;
2004 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
2005 self.undo_stack.clear();
2006 self.redo_stack.clear();
2007 self.pending_content_edits.clear();
2009 self.pending_content_reset = true;
2010 self.mark_content_dirty();
2011 }
2012
2013 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
2029 use crate::{PlannedInput, SpecialKey};
2030 let (key, mods) = match input {
2031 PlannedInput::Char(c, m) => (Key::Char(c), m),
2032 PlannedInput::Key(k, m) => {
2033 let key = match k {
2034 SpecialKey::Esc => Key::Esc,
2035 SpecialKey::Enter => Key::Enter,
2036 SpecialKey::Backspace => Key::Backspace,
2037 SpecialKey::Tab => Key::Tab,
2038 SpecialKey::BackTab => Key::Tab,
2042 SpecialKey::Up => Key::Up,
2043 SpecialKey::Down => Key::Down,
2044 SpecialKey::Left => Key::Left,
2045 SpecialKey::Right => Key::Right,
2046 SpecialKey::Home => Key::Home,
2047 SpecialKey::End => Key::End,
2048 SpecialKey::PageUp => Key::PageUp,
2049 SpecialKey::PageDown => Key::PageDown,
2050 SpecialKey::Insert => Key::Null,
2054 SpecialKey::Delete => Key::Delete,
2055 SpecialKey::F(_) => Key::Null,
2056 };
2057 let m = if matches!(k, SpecialKey::BackTab) {
2058 crate::Modifiers { shift: true, ..m }
2059 } else {
2060 m
2061 };
2062 (key, m)
2063 }
2064 PlannedInput::Mouse(_)
2066 | PlannedInput::Paste(_)
2067 | PlannedInput::FocusGained
2068 | PlannedInput::FocusLost
2069 | PlannedInput::Resize(_, _) => return false,
2070 };
2071 if key == Key::Null {
2072 return false;
2073 }
2074 let event = Input {
2075 key,
2076 ctrl: mods.ctrl,
2077 alt: mods.alt,
2078 shift: mods.shift,
2079 };
2080 let consumed = vim::step(self, event);
2081 self.emit_cursor_shape_if_changed();
2082 consumed
2083 }
2084
2085 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
2102 std::mem::take(&mut self.change_log)
2103 }
2104
2105 pub fn current_options(&self) -> crate::types::Options {
2115 crate::types::Options {
2116 shiftwidth: self.settings.shiftwidth as u32,
2117 tabstop: self.settings.tabstop as u32,
2118 softtabstop: self.settings.softtabstop as u32,
2119 textwidth: self.settings.textwidth as u32,
2120 expandtab: self.settings.expandtab,
2121 ignorecase: self.settings.ignore_case,
2122 smartcase: self.settings.smartcase,
2123 wrapscan: self.settings.wrapscan,
2124 wrap: match self.settings.wrap {
2125 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
2126 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
2127 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
2128 },
2129 readonly: self.settings.readonly,
2130 autoindent: self.settings.autoindent,
2131 smartindent: self.settings.smartindent,
2132 undo_levels: self.settings.undo_levels,
2133 undo_break_on_motion: self.settings.undo_break_on_motion,
2134 iskeyword: self.settings.iskeyword.clone(),
2135 timeout_len: self.settings.timeout_len,
2136 ..crate::types::Options::default()
2137 }
2138 }
2139
2140 pub fn apply_options(&mut self, opts: &crate::types::Options) {
2145 self.settings.shiftwidth = opts.shiftwidth as usize;
2146 self.settings.tabstop = opts.tabstop as usize;
2147 self.settings.softtabstop = opts.softtabstop as usize;
2148 self.settings.textwidth = opts.textwidth as usize;
2149 self.settings.expandtab = opts.expandtab;
2150 self.settings.ignore_case = opts.ignorecase;
2151 self.settings.smartcase = opts.smartcase;
2152 self.settings.wrapscan = opts.wrapscan;
2153 self.settings.wrap = match opts.wrap {
2154 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
2155 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
2156 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
2157 };
2158 self.settings.readonly = opts.readonly;
2159 self.settings.autoindent = opts.autoindent;
2160 self.settings.smartindent = opts.smartindent;
2161 self.settings.undo_levels = opts.undo_levels;
2162 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
2163 self.set_iskeyword(opts.iskeyword.clone());
2164 self.settings.timeout_len = opts.timeout_len;
2165 self.settings.number = opts.number;
2166 self.settings.relativenumber = opts.relativenumber;
2167 self.settings.numberwidth = opts.numberwidth;
2168 self.settings.cursorline = opts.cursorline;
2169 self.settings.cursorcolumn = opts.cursorcolumn;
2170 self.settings.signcolumn = opts.signcolumn;
2171 self.settings.foldcolumn = opts.foldcolumn;
2172 self.settings.colorcolumn = opts.colorcolumn.clone();
2173 }
2174
2175 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
2185 use crate::types::{Highlight, HighlightKind, Pos};
2186 let sel = self.buffer_selection()?;
2187 let (start, end) = match sel {
2188 hjkl_buffer::Selection::Char { anchor, head } => {
2189 let a = (anchor.row, anchor.col);
2190 let h = (head.row, head.col);
2191 if a <= h { (a, h) } else { (h, a) }
2192 }
2193 hjkl_buffer::Selection::Line {
2194 anchor_row,
2195 head_row,
2196 } => {
2197 let (top, bot) = if anchor_row <= head_row {
2198 (anchor_row, head_row)
2199 } else {
2200 (head_row, anchor_row)
2201 };
2202 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
2203 ((top, 0), (bot, last_col))
2204 }
2205 hjkl_buffer::Selection::Block { anchor, head } => {
2206 let (top, bot) = if anchor.row <= head.row {
2207 (anchor.row, head.row)
2208 } else {
2209 (head.row, anchor.row)
2210 };
2211 let (left, right) = if anchor.col <= head.col {
2212 (anchor.col, head.col)
2213 } else {
2214 (head.col, anchor.col)
2215 };
2216 ((top, left), (bot, right))
2217 }
2218 };
2219 Some(Highlight {
2220 range: Pos {
2221 line: start.0 as u32,
2222 col: start.1 as u32,
2223 }..Pos {
2224 line: end.0 as u32,
2225 col: end.1 as u32,
2226 },
2227 kind: HighlightKind::Selection,
2228 })
2229 }
2230
2231 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
2250 use crate::types::{Highlight, HighlightKind, Pos};
2251 let row = line as usize;
2252 if row >= buf_row_count(&self.buffer) {
2253 return Vec::new();
2254 }
2255
2256 if let Some(prompt) = self.search_prompt() {
2259 if prompt.text.is_empty() {
2260 return Vec::new();
2261 }
2262 let Ok(re) = regex::Regex::new(&prompt.text) else {
2263 return Vec::new();
2264 };
2265 let Some(haystack) = buf_line(&self.buffer, row) else {
2266 return Vec::new();
2267 };
2268 return re
2269 .find_iter(haystack)
2270 .map(|m| Highlight {
2271 range: Pos {
2272 line,
2273 col: m.start() as u32,
2274 }..Pos {
2275 line,
2276 col: m.end() as u32,
2277 },
2278 kind: HighlightKind::IncSearch,
2279 })
2280 .collect();
2281 }
2282
2283 if self.search_state.pattern.is_none() {
2284 return Vec::new();
2285 }
2286 let dgen = crate::types::Query::dirty_gen(&self.buffer);
2287 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
2288 .into_iter()
2289 .map(|(start, end)| Highlight {
2290 range: Pos {
2291 line,
2292 col: start as u32,
2293 }..Pos {
2294 line,
2295 col: end as u32,
2296 },
2297 kind: HighlightKind::SearchMatch,
2298 })
2299 .collect()
2300 }
2301
2302 pub fn render_frame(&self) -> crate::types::RenderFrame {
2312 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
2313 let (cursor_row, cursor_col) = self.cursor();
2314 let (mode, shape) = match self.vim_mode() {
2315 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
2316 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
2317 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
2318 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
2319 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
2320 };
2321 RenderFrame {
2322 mode,
2323 cursor_row: cursor_row as u32,
2324 cursor_col: cursor_col as u32,
2325 cursor_shape: shape,
2326 viewport_top: self.host.viewport().top_row as u32,
2327 line_count: crate::types::Query::line_count(&self.buffer),
2328 }
2329 }
2330
2331 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
2344 use crate::types::{EditorSnapshot, SnapshotMode};
2345 let mode = match self.vim_mode() {
2346 crate::VimMode::Normal => SnapshotMode::Normal,
2347 crate::VimMode::Insert => SnapshotMode::Insert,
2348 crate::VimMode::Visual => SnapshotMode::Visual,
2349 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
2350 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
2351 };
2352 let cursor = self.cursor();
2353 let cursor = (cursor.0 as u32, cursor.1 as u32);
2354 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
2355 let viewport_top = self.host.viewport().top_row as u32;
2356 let marks = self
2357 .marks
2358 .iter()
2359 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
2360 .collect();
2361 EditorSnapshot {
2362 version: EditorSnapshot::VERSION,
2363 mode,
2364 cursor,
2365 lines,
2366 viewport_top,
2367 registers: self.registers.clone(),
2368 marks,
2369 }
2370 }
2371
2372 pub fn restore_snapshot(
2380 &mut self,
2381 snap: crate::types::EditorSnapshot,
2382 ) -> Result<(), crate::EngineError> {
2383 use crate::types::EditorSnapshot;
2384 if snap.version != EditorSnapshot::VERSION {
2385 return Err(crate::EngineError::SnapshotVersion(
2386 snap.version,
2387 EditorSnapshot::VERSION,
2388 ));
2389 }
2390 let text = snap.lines.join("\n");
2391 self.set_content(&text);
2392 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
2393 self.host.viewport_mut().top_row = snap.viewport_top as usize;
2394 self.registers = snap.registers;
2395 self.marks = snap
2396 .marks
2397 .into_iter()
2398 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
2399 .collect();
2400 Ok(())
2401 }
2402
2403 pub fn seed_yank(&mut self, text: String) {
2407 let linewise = text.ends_with('\n');
2408 self.vim.yank_linewise = linewise;
2409 self.registers.unnamed = crate::registers::Slot { text, linewise };
2410 }
2411
2412 pub fn scroll_down(&mut self, rows: i16) {
2417 self.scroll_viewport(rows);
2418 }
2419
2420 pub fn scroll_up(&mut self, rows: i16) {
2424 self.scroll_viewport(-rows);
2425 }
2426
2427 const SCROLLOFF: usize = 5;
2431
2432 pub fn ensure_cursor_in_scrolloff(&mut self) {
2437 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2438 if height == 0 {
2439 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2446 crate::viewport_math::ensure_cursor_visible(
2447 &self.buffer,
2448 &folds,
2449 self.host.viewport_mut(),
2450 );
2451 return;
2452 }
2453 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2457 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
2460 self.ensure_scrolloff_wrap(height, margin);
2461 return;
2462 }
2463 let cursor_row = buf_cursor_row(&self.buffer);
2464 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
2465 let v = self.host.viewport_mut();
2466 if cursor_row < v.top_row + margin {
2468 v.top_row = cursor_row.saturating_sub(margin);
2469 }
2470 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
2472 if cursor_row > v.top_row + max_bottom {
2473 v.top_row = cursor_row.saturating_sub(max_bottom);
2474 }
2475 let max_top = last_row.saturating_sub(height.saturating_sub(1));
2477 if v.top_row > max_top {
2478 v.top_row = max_top;
2479 }
2480 let cursor = buf_cursor_pos(&self.buffer);
2483 self.host.viewport_mut().ensure_visible(cursor);
2484 }
2485
2486 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
2491 let cursor_row = buf_cursor_row(&self.buffer);
2492 if cursor_row < self.host.viewport().top_row {
2495 let v = self.host.viewport_mut();
2496 v.top_row = cursor_row;
2497 v.top_col = 0;
2498 }
2499 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2508 loop {
2509 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2510 let csr =
2511 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2512 .unwrap_or(0);
2513 if csr <= max_csr {
2514 break;
2515 }
2516 let top = self.host.viewport().top_row;
2517 let row_count = buf_row_count(&self.buffer);
2518 let next = {
2519 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2520 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2521 };
2522 let Some(next) = next else {
2523 break;
2524 };
2525 if next > cursor_row {
2527 self.host.viewport_mut().top_row = cursor_row;
2528 break;
2529 }
2530 self.host.viewport_mut().top_row = next;
2531 }
2532 loop {
2535 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2536 let csr =
2537 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2538 .unwrap_or(0);
2539 if csr >= margin {
2540 break;
2541 }
2542 let top = self.host.viewport().top_row;
2543 let prev = {
2544 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2545 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2546 };
2547 let Some(prev) = prev else {
2548 break;
2549 };
2550 self.host.viewport_mut().top_row = prev;
2551 }
2552 let max_top = {
2557 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2558 crate::viewport_math::max_top_for_height(
2559 &self.buffer,
2560 &folds,
2561 self.host.viewport(),
2562 height,
2563 )
2564 };
2565 if self.host.viewport().top_row > max_top {
2566 self.host.viewport_mut().top_row = max_top;
2567 }
2568 self.host.viewport_mut().top_col = 0;
2569 }
2570
2571 fn scroll_viewport(&mut self, delta: i16) {
2572 if delta == 0 {
2573 return;
2574 }
2575 let total_rows = buf_row_count(&self.buffer) as isize;
2577 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2578 let cur_top = self.host.viewport().top_row as isize;
2579 let new_top = (cur_top + delta as isize)
2580 .max(0)
2581 .min((total_rows - 1).max(0)) as usize;
2582 self.host.viewport_mut().top_row = new_top;
2583 let _ = cur_top;
2586 if height == 0 {
2587 return;
2588 }
2589 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2592 let margin = Self::SCROLLOFF.min(height / 2);
2593 let min_row = new_top + margin;
2594 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2595 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2596 if target_row != cursor_row {
2597 let line_len = buf_line(&self.buffer, target_row)
2598 .map(|l| l.chars().count())
2599 .unwrap_or(0);
2600 let target_col = cursor_col.min(line_len.saturating_sub(1));
2601 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2602 }
2603 }
2604
2605 pub fn goto_line(&mut self, line: usize) {
2606 let row = line.saturating_sub(1);
2607 let max = buf_row_count(&self.buffer).saturating_sub(1);
2608 let target = row.min(max);
2609 buf_set_cursor_rc(&mut self.buffer, target, 0);
2610 self.ensure_cursor_in_scrolloff();
2614 }
2615
2616 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2620 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2621 if height == 0 {
2622 return;
2623 }
2624 let cur_row = buf_cursor_row(&self.buffer);
2625 let cur_top = self.host.viewport().top_row;
2626 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2632 let new_top = match pos {
2633 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2634 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2635 CursorScrollTarget::Bottom => {
2636 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2637 }
2638 };
2639 if new_top == cur_top {
2640 return;
2641 }
2642 self.host.viewport_mut().top_row = new_top;
2643 }
2644
2645 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2656 let n = buf_row_count(&self.buffer);
2657 let inner_top = area_y.saturating_add(1); let lnum_width = if self.settings.number || self.settings.relativenumber {
2659 let needed = n.to_string().len() + 1;
2660 needed.max(self.settings.numberwidth) as u16
2661 } else {
2662 0
2663 };
2664 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2665 let rel_row = row.saturating_sub(inner_top) as usize;
2666 let top = self.host.viewport().top_row;
2667 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2668 let rel_col = col.saturating_sub(content_x) as usize;
2669 let line_chars = buf_line(&self.buffer, doc_row)
2670 .map(|l| l.chars().count())
2671 .unwrap_or(0);
2672 let last_col = line_chars.saturating_sub(1);
2673 (doc_row, rel_col.min(last_col))
2674 }
2675
2676 pub fn jump_to(&mut self, line: usize, col: usize) {
2678 let r = line.saturating_sub(1);
2679 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2680 let r = r.min(max_row);
2681 let line_len = buf_line(&self.buffer, r)
2682 .map(|l| l.chars().count())
2683 .unwrap_or(0);
2684 let c = col.saturating_sub(1).min(line_len);
2685 buf_set_cursor_rc(&mut self.buffer, r, c);
2686 }
2687
2688 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2696 if self.vim.is_visual() {
2697 self.vim.force_normal();
2698 }
2699 crate::vim::break_undo_group_in_insert(self);
2702 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2703 buf_set_cursor_rc(&mut self.buffer, r, c);
2704 }
2705
2706 #[cfg(feature = "ratatui")]
2712 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2713 self.mouse_click(area.x, area.y, col, row);
2714 }
2715
2716 pub fn mouse_begin_drag(&mut self) {
2718 if !self.vim.is_visual_char() {
2719 let cursor = self.cursor();
2720 self.vim.enter_visual(cursor);
2721 }
2722 }
2723
2724 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2730 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2731 buf_set_cursor_rc(&mut self.buffer, r, c);
2732 }
2733
2734 #[cfg(feature = "ratatui")]
2740 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2741 self.mouse_extend_drag(area.x, area.y, col, row);
2742 }
2743
2744 pub fn insert_str(&mut self, text: &str) {
2745 let pos = crate::types::Cursor::cursor(&self.buffer);
2746 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2747 self.push_buffer_content_to_textarea();
2748 self.mark_content_dirty();
2749 }
2750
2751 pub fn accept_completion(&mut self, completion: &str) {
2752 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2753 let cursor_pos = CursorTrait::cursor(&self.buffer);
2754 let cursor_row = cursor_pos.line as usize;
2755 let cursor_col = cursor_pos.col as usize;
2756 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2757 let chars: Vec<char> = line.chars().collect();
2758 let prefix_len = chars[..cursor_col.min(chars.len())]
2759 .iter()
2760 .rev()
2761 .take_while(|c| c.is_alphanumeric() || **c == '_')
2762 .count();
2763 if prefix_len > 0 {
2764 let start = Pos {
2765 line: cursor_row as u32,
2766 col: (cursor_col - prefix_len) as u32,
2767 };
2768 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2769 }
2770 let cursor = CursorTrait::cursor(&self.buffer);
2771 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2772 self.push_buffer_content_to_textarea();
2773 self.mark_content_dirty();
2774 }
2775
2776 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2777 let rc = buf_cursor_rc(&self.buffer);
2778 (buf_lines_to_vec(&self.buffer), rc)
2779 }
2780
2781 pub fn undo(&mut self) {
2785 crate::vim::do_undo(self);
2786 }
2787
2788 pub fn redo(&mut self) {
2791 crate::vim::do_redo(self);
2792 }
2793
2794 pub fn push_undo(&mut self) {
2799 let snap = self.snapshot();
2800 self.undo_stack.push(snap);
2801 self.cap_undo();
2802 self.redo_stack.clear();
2803 }
2804
2805 pub(crate) fn cap_undo(&mut self) {
2811 let cap = self.settings.undo_levels as usize;
2812 if cap > 0 && self.undo_stack.len() > cap {
2813 let diff = self.undo_stack.len() - cap;
2814 self.undo_stack.drain(..diff);
2815 }
2816 }
2817
2818 #[doc(hidden)]
2820 pub fn undo_stack_len(&self) -> usize {
2821 self.undo_stack.len()
2822 }
2823
2824 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2828 let text = lines.join("\n");
2829 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2830 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2831 self.pending_content_edits.clear();
2833 self.pending_content_reset = true;
2834 self.mark_content_dirty();
2835 }
2836
2837 pub fn replace_char_at(&mut self, ch: char, count: usize) {
2844 vim::replace_char(self, ch, count);
2845 }
2846
2847 pub fn find_char(&mut self, ch: char, forward: bool, till: bool, count: usize) {
2855 vim::apply_find_char(self, ch, forward, till, count.max(1));
2856 }
2857
2858 pub fn after_g(&mut self, ch: char, count: usize) {
2866 vim::apply_after_g(self, ch, count);
2867 }
2868
2869 pub fn after_z(&mut self, ch: char, count: usize) {
2878 vim::apply_after_z(self, ch, count);
2879 }
2880
2881 pub fn apply_op_motion(
2895 &mut self,
2896 op: crate::vim::Operator,
2897 motion_key: char,
2898 total_count: usize,
2899 ) {
2900 vim::apply_op_motion_key(self, op, motion_key, total_count);
2901 }
2902
2903 pub fn apply_op_double(&mut self, op: crate::vim::Operator, total_count: usize) {
2910 vim::apply_op_double(self, op, total_count);
2911 }
2912
2913 pub fn apply_op_find(
2926 &mut self,
2927 op: crate::vim::Operator,
2928 ch: char,
2929 forward: bool,
2930 till: bool,
2931 total_count: usize,
2932 ) {
2933 vim::apply_op_find_motion(self, op, ch, forward, till, total_count);
2934 }
2935
2936 pub fn apply_op_text_obj(
2952 &mut self,
2953 op: crate::vim::Operator,
2954 ch: char,
2955 inner: bool,
2956 total_count: usize,
2957 ) {
2958 vim::apply_op_text_obj_inner(self, op, ch, inner, total_count);
2959 }
2960
2961 pub fn apply_op_g(&mut self, op: crate::vim::Operator, ch: char, total_count: usize) {
2983 vim::apply_op_g_inner(self, op, ch, total_count);
2984 }
2985
2986 pub fn apply_motion(&mut self, kind: hjkl_vim::MotionKind, count: usize) {
3002 vim::apply_motion_kind(self, kind, count);
3003 }
3004
3005 pub fn set_pending_register(&mut self, reg: char) {
3016 if reg.is_ascii_alphanumeric() || matches!(reg, '"' | '+' | '*' | '_') {
3017 self.vim.pending_register = Some(reg);
3018 }
3019 }
3021
3022 #[cfg(feature = "crossterm")]
3023 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
3024 let input = crossterm_to_input(key);
3025 if input.key == Key::Null {
3026 return false;
3027 }
3028 let consumed = vim::step(self, input);
3029 self.emit_cursor_shape_if_changed();
3030 consumed
3031 }
3032}
3033
3034fn visual_col_for_char(line: &str, char_col: usize, tab_width: usize) -> usize {
3039 let mut visual = 0usize;
3040 for (i, ch) in line.chars().enumerate() {
3041 if i >= char_col {
3042 break;
3043 }
3044 if ch == '\t' {
3045 visual += tab_width - (visual % tab_width);
3046 } else {
3047 visual += 1;
3048 }
3049 }
3050 visual
3051}
3052
3053#[cfg(feature = "crossterm")]
3054impl From<KeyEvent> for Input {
3055 fn from(key: KeyEvent) -> Self {
3056 let k = match key.code {
3057 KeyCode::Char(c) => Key::Char(c),
3058 KeyCode::Backspace => Key::Backspace,
3059 KeyCode::Delete => Key::Delete,
3060 KeyCode::Enter => Key::Enter,
3061 KeyCode::Left => Key::Left,
3062 KeyCode::Right => Key::Right,
3063 KeyCode::Up => Key::Up,
3064 KeyCode::Down => Key::Down,
3065 KeyCode::Home => Key::Home,
3066 KeyCode::End => Key::End,
3067 KeyCode::Tab => Key::Tab,
3068 KeyCode::Esc => Key::Esc,
3069 _ => Key::Null,
3070 };
3071 Input {
3072 key: k,
3073 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
3074 alt: key.modifiers.contains(KeyModifiers::ALT),
3075 shift: key.modifiers.contains(KeyModifiers::SHIFT),
3076 }
3077 }
3078}
3079
3080#[cfg(feature = "crossterm")]
3084pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
3085 Input::from(key)
3086}
3087
3088#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
3089mod tests {
3090 use super::*;
3091 use crate::types::Host;
3092 use crossterm::event::KeyEvent;
3093
3094 fn key(code: KeyCode) -> KeyEvent {
3095 KeyEvent::new(code, KeyModifiers::NONE)
3096 }
3097 fn shift_key(code: KeyCode) -> KeyEvent {
3098 KeyEvent::new(code, KeyModifiers::SHIFT)
3099 }
3100 fn ctrl_key(code: KeyCode) -> KeyEvent {
3101 KeyEvent::new(code, KeyModifiers::CONTROL)
3102 }
3103
3104 #[test]
3105 fn vim_normal_to_insert() {
3106 let mut e = Editor::new(
3107 hjkl_buffer::Buffer::new(),
3108 crate::types::DefaultHost::new(),
3109 crate::types::Options::default(),
3110 );
3111 e.handle_key(key(KeyCode::Char('i')));
3112 assert_eq!(e.vim_mode(), VimMode::Insert);
3113 }
3114
3115 #[test]
3116 fn with_options_constructs_from_spec_options() {
3117 let opts = crate::types::Options {
3121 shiftwidth: 4,
3122 tabstop: 4,
3123 expandtab: true,
3124 iskeyword: "@,a-z".to_string(),
3125 wrap: crate::types::WrapMode::Word,
3126 ..crate::types::Options::default()
3127 };
3128 let mut e = Editor::new(
3129 hjkl_buffer::Buffer::new(),
3130 crate::types::DefaultHost::new(),
3131 opts,
3132 );
3133 assert_eq!(e.settings().shiftwidth, 4);
3134 assert_eq!(e.settings().tabstop, 4);
3135 assert!(e.settings().expandtab);
3136 assert_eq!(e.settings().iskeyword, "@,a-z");
3137 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
3138 e.handle_key(key(KeyCode::Char('i')));
3140 assert_eq!(e.vim_mode(), VimMode::Insert);
3141 }
3142
3143 #[test]
3144 fn feed_input_char_routes_through_handle_key() {
3145 use crate::{Modifiers, PlannedInput};
3146 let mut e = Editor::new(
3147 hjkl_buffer::Buffer::new(),
3148 crate::types::DefaultHost::new(),
3149 crate::types::Options::default(),
3150 );
3151 e.set_content("abc");
3152 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
3154 assert_eq!(e.vim_mode(), VimMode::Insert);
3155 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
3157 assert!(e.content().contains('X'));
3158 }
3159
3160 #[test]
3161 fn feed_input_special_key_routes() {
3162 use crate::{Modifiers, PlannedInput, SpecialKey};
3163 let mut e = Editor::new(
3164 hjkl_buffer::Buffer::new(),
3165 crate::types::DefaultHost::new(),
3166 crate::types::Options::default(),
3167 );
3168 e.set_content("abc");
3169 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
3170 assert_eq!(e.vim_mode(), VimMode::Insert);
3171 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
3172 assert_eq!(e.vim_mode(), VimMode::Normal);
3173 }
3174
3175 #[test]
3176 fn feed_input_mouse_paste_focus_resize_no_op() {
3177 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
3178 let mut e = Editor::new(
3179 hjkl_buffer::Buffer::new(),
3180 crate::types::DefaultHost::new(),
3181 crate::types::Options::default(),
3182 );
3183 e.set_content("abc");
3184 let mode_before = e.vim_mode();
3185 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
3186 kind: MouseKind::Press,
3187 pos: Pos::new(0, 0),
3188 mods: Default::default(),
3189 }));
3190 assert!(!consumed);
3191 assert_eq!(e.vim_mode(), mode_before);
3192 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
3193 assert!(!e.feed_input(PlannedInput::FocusGained));
3194 assert!(!e.feed_input(PlannedInput::FocusLost));
3195 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
3196 }
3197
3198 #[test]
3199 fn intern_style_dedups_engine_native_styles() {
3200 use crate::types::{Attrs, Color, Style};
3201 let mut e = Editor::new(
3202 hjkl_buffer::Buffer::new(),
3203 crate::types::DefaultHost::new(),
3204 crate::types::Options::default(),
3205 );
3206 let s = Style {
3207 fg: Some(Color(255, 0, 0)),
3208 bg: None,
3209 attrs: Attrs::BOLD,
3210 };
3211 let id_a = e.intern_style(s);
3212 let id_b = e.intern_style(s);
3214 assert_eq!(id_a, id_b);
3215 let back = e.engine_style_at(id_a).expect("interned");
3217 assert_eq!(back, s);
3218 }
3219
3220 #[test]
3221 fn engine_style_at_out_of_range_returns_none() {
3222 let e = Editor::new(
3223 hjkl_buffer::Buffer::new(),
3224 crate::types::DefaultHost::new(),
3225 crate::types::Options::default(),
3226 );
3227 assert!(e.engine_style_at(99).is_none());
3228 }
3229
3230 #[test]
3231 fn take_changes_emits_per_row_for_block_insert() {
3232 let mut e = Editor::new(
3237 hjkl_buffer::Buffer::new(),
3238 crate::types::DefaultHost::new(),
3239 crate::types::Options::default(),
3240 );
3241 e.set_content("aaa\nbbb\nccc\nddd");
3242 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
3244 e.handle_key(key(KeyCode::Char('j')));
3245 e.handle_key(key(KeyCode::Char('j')));
3246 e.handle_key(shift_key(KeyCode::Char('I')));
3248 e.handle_key(key(KeyCode::Char('X')));
3249 e.handle_key(key(KeyCode::Esc));
3250
3251 let changes = e.take_changes();
3252 assert!(
3256 changes.len() >= 3,
3257 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
3258 changes.len()
3259 );
3260 }
3261
3262 #[test]
3263 fn take_changes_drains_after_insert() {
3264 let mut e = Editor::new(
3265 hjkl_buffer::Buffer::new(),
3266 crate::types::DefaultHost::new(),
3267 crate::types::Options::default(),
3268 );
3269 e.set_content("abc");
3270 assert!(e.take_changes().is_empty());
3272 e.handle_key(key(KeyCode::Char('i')));
3274 e.handle_key(key(KeyCode::Char('X')));
3275 let changes = e.take_changes();
3276 assert!(
3277 !changes.is_empty(),
3278 "insert mode keystroke should produce a change"
3279 );
3280 assert!(e.take_changes().is_empty());
3282 }
3283
3284 #[test]
3285 fn options_bridge_roundtrip() {
3286 let mut e = Editor::new(
3287 hjkl_buffer::Buffer::new(),
3288 crate::types::DefaultHost::new(),
3289 crate::types::Options::default(),
3290 );
3291 let opts = e.current_options();
3292 assert_eq!(opts.shiftwidth, 4);
3294 assert_eq!(opts.tabstop, 4);
3295
3296 let new_opts = crate::types::Options {
3297 shiftwidth: 4,
3298 tabstop: 2,
3299 ignorecase: true,
3300 ..crate::types::Options::default()
3301 };
3302 e.apply_options(&new_opts);
3303
3304 let after = e.current_options();
3305 assert_eq!(after.shiftwidth, 4);
3306 assert_eq!(after.tabstop, 2);
3307 assert!(after.ignorecase);
3308 }
3309
3310 #[test]
3311 fn selection_highlight_none_in_normal() {
3312 let mut e = Editor::new(
3313 hjkl_buffer::Buffer::new(),
3314 crate::types::DefaultHost::new(),
3315 crate::types::Options::default(),
3316 );
3317 e.set_content("hello");
3318 assert!(e.selection_highlight().is_none());
3319 }
3320
3321 #[test]
3322 fn selection_highlight_some_in_visual() {
3323 use crate::types::HighlightKind;
3324 let mut e = Editor::new(
3325 hjkl_buffer::Buffer::new(),
3326 crate::types::DefaultHost::new(),
3327 crate::types::Options::default(),
3328 );
3329 e.set_content("hello world");
3330 e.handle_key(key(KeyCode::Char('v')));
3331 e.handle_key(key(KeyCode::Char('l')));
3332 e.handle_key(key(KeyCode::Char('l')));
3333 let h = e
3334 .selection_highlight()
3335 .expect("visual mode should produce a highlight");
3336 assert_eq!(h.kind, HighlightKind::Selection);
3337 assert_eq!(h.range.start.line, 0);
3338 assert_eq!(h.range.end.line, 0);
3339 }
3340
3341 #[test]
3342 fn highlights_emit_incsearch_during_active_prompt() {
3343 use crate::types::HighlightKind;
3344 let mut e = Editor::new(
3345 hjkl_buffer::Buffer::new(),
3346 crate::types::DefaultHost::new(),
3347 crate::types::Options::default(),
3348 );
3349 e.set_content("foo bar foo\nbaz\n");
3350 e.handle_key(key(KeyCode::Char('/')));
3352 e.handle_key(key(KeyCode::Char('f')));
3353 e.handle_key(key(KeyCode::Char('o')));
3354 e.handle_key(key(KeyCode::Char('o')));
3355 assert!(e.search_prompt().is_some());
3357 let hs = e.highlights_for_line(0);
3358 assert_eq!(hs.len(), 2);
3359 for h in &hs {
3360 assert_eq!(h.kind, HighlightKind::IncSearch);
3361 }
3362 }
3363
3364 #[test]
3365 fn highlights_empty_for_blank_prompt() {
3366 let mut e = Editor::new(
3367 hjkl_buffer::Buffer::new(),
3368 crate::types::DefaultHost::new(),
3369 crate::types::Options::default(),
3370 );
3371 e.set_content("foo");
3372 e.handle_key(key(KeyCode::Char('/')));
3373 assert!(e.search_prompt().is_some());
3375 assert!(e.highlights_for_line(0).is_empty());
3376 }
3377
3378 #[test]
3379 fn highlights_emit_search_matches() {
3380 use crate::types::HighlightKind;
3381 let mut e = Editor::new(
3382 hjkl_buffer::Buffer::new(),
3383 crate::types::DefaultHost::new(),
3384 crate::types::Options::default(),
3385 );
3386 e.set_content("foo bar foo\nbaz qux\n");
3387 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3391 let hs = e.highlights_for_line(0);
3392 assert_eq!(hs.len(), 2);
3393 for h in &hs {
3394 assert_eq!(h.kind, HighlightKind::SearchMatch);
3395 assert_eq!(h.range.start.line, 0);
3396 assert_eq!(h.range.end.line, 0);
3397 }
3398 }
3399
3400 #[test]
3401 fn highlights_empty_without_pattern() {
3402 let mut e = Editor::new(
3403 hjkl_buffer::Buffer::new(),
3404 crate::types::DefaultHost::new(),
3405 crate::types::Options::default(),
3406 );
3407 e.set_content("foo bar");
3408 assert!(e.highlights_for_line(0).is_empty());
3409 }
3410
3411 #[test]
3412 fn highlights_empty_for_out_of_range_line() {
3413 let mut e = Editor::new(
3414 hjkl_buffer::Buffer::new(),
3415 crate::types::DefaultHost::new(),
3416 crate::types::Options::default(),
3417 );
3418 e.set_content("foo");
3419 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3420 assert!(e.highlights_for_line(99).is_empty());
3421 }
3422
3423 #[test]
3424 fn render_frame_reflects_mode_and_cursor() {
3425 use crate::types::{CursorShape, SnapshotMode};
3426 let mut e = Editor::new(
3427 hjkl_buffer::Buffer::new(),
3428 crate::types::DefaultHost::new(),
3429 crate::types::Options::default(),
3430 );
3431 e.set_content("alpha\nbeta");
3432 let f = e.render_frame();
3433 assert_eq!(f.mode, SnapshotMode::Normal);
3434 assert_eq!(f.cursor_shape, CursorShape::Block);
3435 assert_eq!(f.line_count, 2);
3436
3437 e.handle_key(key(KeyCode::Char('i')));
3438 let f = e.render_frame();
3439 assert_eq!(f.mode, SnapshotMode::Insert);
3440 assert_eq!(f.cursor_shape, CursorShape::Bar);
3441 }
3442
3443 #[test]
3444 fn snapshot_roundtrips_through_restore() {
3445 use crate::types::SnapshotMode;
3446 let mut e = Editor::new(
3447 hjkl_buffer::Buffer::new(),
3448 crate::types::DefaultHost::new(),
3449 crate::types::Options::default(),
3450 );
3451 e.set_content("alpha\nbeta\ngamma");
3452 e.jump_cursor(2, 3);
3453 let snap = e.take_snapshot();
3454 assert_eq!(snap.mode, SnapshotMode::Normal);
3455 assert_eq!(snap.cursor, (2, 3));
3456 assert_eq!(snap.lines.len(), 3);
3457
3458 let mut other = Editor::new(
3459 hjkl_buffer::Buffer::new(),
3460 crate::types::DefaultHost::new(),
3461 crate::types::Options::default(),
3462 );
3463 other.restore_snapshot(snap).expect("restore");
3464 assert_eq!(other.cursor(), (2, 3));
3465 assert_eq!(other.buffer().lines().len(), 3);
3466 }
3467
3468 #[test]
3469 fn restore_snapshot_rejects_version_mismatch() {
3470 let mut e = Editor::new(
3471 hjkl_buffer::Buffer::new(),
3472 crate::types::DefaultHost::new(),
3473 crate::types::Options::default(),
3474 );
3475 let mut snap = e.take_snapshot();
3476 snap.version = 9999;
3477 match e.restore_snapshot(snap) {
3478 Err(crate::EngineError::SnapshotVersion(got, want)) => {
3479 assert_eq!(got, 9999);
3480 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
3481 }
3482 other => panic!("expected SnapshotVersion err, got {other:?}"),
3483 }
3484 }
3485
3486 #[test]
3487 fn take_content_change_returns_some_on_first_dirty() {
3488 let mut e = Editor::new(
3489 hjkl_buffer::Buffer::new(),
3490 crate::types::DefaultHost::new(),
3491 crate::types::Options::default(),
3492 );
3493 e.set_content("hello");
3494 let first = e.take_content_change();
3495 assert!(first.is_some());
3496 let second = e.take_content_change();
3497 assert!(second.is_none());
3498 }
3499
3500 #[test]
3501 fn take_content_change_none_until_mutation() {
3502 let mut e = Editor::new(
3503 hjkl_buffer::Buffer::new(),
3504 crate::types::DefaultHost::new(),
3505 crate::types::Options::default(),
3506 );
3507 e.set_content("hello");
3508 e.take_content_change();
3510 assert!(e.take_content_change().is_none());
3511 e.handle_key(key(KeyCode::Char('i')));
3513 e.handle_key(key(KeyCode::Char('x')));
3514 let after = e.take_content_change();
3515 assert!(after.is_some());
3516 assert!(after.unwrap().contains('x'));
3517 }
3518
3519 #[test]
3520 fn vim_insert_to_normal() {
3521 let mut e = Editor::new(
3522 hjkl_buffer::Buffer::new(),
3523 crate::types::DefaultHost::new(),
3524 crate::types::Options::default(),
3525 );
3526 e.handle_key(key(KeyCode::Char('i')));
3527 e.handle_key(key(KeyCode::Esc));
3528 assert_eq!(e.vim_mode(), VimMode::Normal);
3529 }
3530
3531 #[test]
3532 fn vim_normal_to_visual() {
3533 let mut e = Editor::new(
3534 hjkl_buffer::Buffer::new(),
3535 crate::types::DefaultHost::new(),
3536 crate::types::Options::default(),
3537 );
3538 e.handle_key(key(KeyCode::Char('v')));
3539 assert_eq!(e.vim_mode(), VimMode::Visual);
3540 }
3541
3542 #[test]
3543 fn vim_visual_to_normal() {
3544 let mut e = Editor::new(
3545 hjkl_buffer::Buffer::new(),
3546 crate::types::DefaultHost::new(),
3547 crate::types::Options::default(),
3548 );
3549 e.handle_key(key(KeyCode::Char('v')));
3550 e.handle_key(key(KeyCode::Esc));
3551 assert_eq!(e.vim_mode(), VimMode::Normal);
3552 }
3553
3554 #[test]
3555 fn vim_shift_i_moves_to_first_non_whitespace() {
3556 let mut e = Editor::new(
3557 hjkl_buffer::Buffer::new(),
3558 crate::types::DefaultHost::new(),
3559 crate::types::Options::default(),
3560 );
3561 e.set_content(" hello");
3562 e.jump_cursor(0, 8);
3563 e.handle_key(shift_key(KeyCode::Char('I')));
3564 assert_eq!(e.vim_mode(), VimMode::Insert);
3565 assert_eq!(e.cursor(), (0, 3));
3566 }
3567
3568 #[test]
3569 fn vim_shift_a_moves_to_end_and_insert() {
3570 let mut e = Editor::new(
3571 hjkl_buffer::Buffer::new(),
3572 crate::types::DefaultHost::new(),
3573 crate::types::Options::default(),
3574 );
3575 e.set_content("hello");
3576 e.handle_key(shift_key(KeyCode::Char('A')));
3577 assert_eq!(e.vim_mode(), VimMode::Insert);
3578 assert_eq!(e.cursor().1, 5);
3579 }
3580
3581 #[test]
3582 fn count_10j_moves_down_10() {
3583 let mut e = Editor::new(
3584 hjkl_buffer::Buffer::new(),
3585 crate::types::DefaultHost::new(),
3586 crate::types::Options::default(),
3587 );
3588 e.set_content(
3589 (0..20)
3590 .map(|i| format!("line{i}"))
3591 .collect::<Vec<_>>()
3592 .join("\n")
3593 .as_str(),
3594 );
3595 for d in "10".chars() {
3596 e.handle_key(key(KeyCode::Char(d)));
3597 }
3598 e.handle_key(key(KeyCode::Char('j')));
3599 assert_eq!(e.cursor().0, 10);
3600 }
3601
3602 #[test]
3603 fn count_o_repeats_insert_on_esc() {
3604 let mut e = Editor::new(
3605 hjkl_buffer::Buffer::new(),
3606 crate::types::DefaultHost::new(),
3607 crate::types::Options::default(),
3608 );
3609 e.set_content("hello");
3610 for d in "3".chars() {
3611 e.handle_key(key(KeyCode::Char(d)));
3612 }
3613 e.handle_key(key(KeyCode::Char('o')));
3614 assert_eq!(e.vim_mode(), VimMode::Insert);
3615 for c in "world".chars() {
3616 e.handle_key(key(KeyCode::Char(c)));
3617 }
3618 e.handle_key(key(KeyCode::Esc));
3619 assert_eq!(e.vim_mode(), VimMode::Normal);
3620 assert_eq!(e.buffer().lines().len(), 4);
3621 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
3622 }
3623
3624 #[test]
3625 fn count_i_repeats_text_on_esc() {
3626 let mut e = Editor::new(
3627 hjkl_buffer::Buffer::new(),
3628 crate::types::DefaultHost::new(),
3629 crate::types::Options::default(),
3630 );
3631 e.set_content("");
3632 for d in "3".chars() {
3633 e.handle_key(key(KeyCode::Char(d)));
3634 }
3635 e.handle_key(key(KeyCode::Char('i')));
3636 for c in "ab".chars() {
3637 e.handle_key(key(KeyCode::Char(c)));
3638 }
3639 e.handle_key(key(KeyCode::Esc));
3640 assert_eq!(e.vim_mode(), VimMode::Normal);
3641 assert_eq!(e.buffer().lines()[0], "ababab");
3642 }
3643
3644 #[test]
3645 fn vim_shift_o_opens_line_above() {
3646 let mut e = Editor::new(
3647 hjkl_buffer::Buffer::new(),
3648 crate::types::DefaultHost::new(),
3649 crate::types::Options::default(),
3650 );
3651 e.set_content("hello");
3652 e.handle_key(shift_key(KeyCode::Char('O')));
3653 assert_eq!(e.vim_mode(), VimMode::Insert);
3654 assert_eq!(e.cursor(), (0, 0));
3655 assert_eq!(e.buffer().lines().len(), 2);
3656 }
3657
3658 #[test]
3659 fn vim_gg_goes_to_top() {
3660 let mut e = Editor::new(
3661 hjkl_buffer::Buffer::new(),
3662 crate::types::DefaultHost::new(),
3663 crate::types::Options::default(),
3664 );
3665 e.set_content("a\nb\nc");
3666 e.jump_cursor(2, 0);
3667 e.handle_key(key(KeyCode::Char('g')));
3668 e.handle_key(key(KeyCode::Char('g')));
3669 assert_eq!(e.cursor().0, 0);
3670 }
3671
3672 #[test]
3673 fn vim_shift_g_goes_to_bottom() {
3674 let mut e = Editor::new(
3675 hjkl_buffer::Buffer::new(),
3676 crate::types::DefaultHost::new(),
3677 crate::types::Options::default(),
3678 );
3679 e.set_content("a\nb\nc");
3680 e.handle_key(shift_key(KeyCode::Char('G')));
3681 assert_eq!(e.cursor().0, 2);
3682 }
3683
3684 #[test]
3685 fn vim_dd_deletes_line() {
3686 let mut e = Editor::new(
3687 hjkl_buffer::Buffer::new(),
3688 crate::types::DefaultHost::new(),
3689 crate::types::Options::default(),
3690 );
3691 e.set_content("first\nsecond");
3692 e.handle_key(key(KeyCode::Char('d')));
3693 e.handle_key(key(KeyCode::Char('d')));
3694 assert_eq!(e.buffer().lines().len(), 1);
3695 assert_eq!(e.buffer().lines()[0], "second");
3696 }
3697
3698 #[test]
3699 fn vim_dw_deletes_word() {
3700 let mut e = Editor::new(
3701 hjkl_buffer::Buffer::new(),
3702 crate::types::DefaultHost::new(),
3703 crate::types::Options::default(),
3704 );
3705 e.set_content("hello world");
3706 e.handle_key(key(KeyCode::Char('d')));
3707 e.handle_key(key(KeyCode::Char('w')));
3708 assert_eq!(e.vim_mode(), VimMode::Normal);
3709 assert!(!e.buffer().lines()[0].starts_with("hello"));
3710 }
3711
3712 #[test]
3713 fn vim_yy_yanks_line() {
3714 let mut e = Editor::new(
3715 hjkl_buffer::Buffer::new(),
3716 crate::types::DefaultHost::new(),
3717 crate::types::Options::default(),
3718 );
3719 e.set_content("hello\nworld");
3720 e.handle_key(key(KeyCode::Char('y')));
3721 e.handle_key(key(KeyCode::Char('y')));
3722 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3723 }
3724
3725 #[test]
3726 fn vim_yy_does_not_move_cursor() {
3727 let mut e = Editor::new(
3728 hjkl_buffer::Buffer::new(),
3729 crate::types::DefaultHost::new(),
3730 crate::types::Options::default(),
3731 );
3732 e.set_content("first\nsecond\nthird");
3733 e.jump_cursor(1, 0);
3734 let before = e.cursor();
3735 e.handle_key(key(KeyCode::Char('y')));
3736 e.handle_key(key(KeyCode::Char('y')));
3737 assert_eq!(e.cursor(), before);
3738 assert_eq!(e.vim_mode(), VimMode::Normal);
3739 }
3740
3741 #[test]
3742 fn vim_yw_yanks_word() {
3743 let mut e = Editor::new(
3744 hjkl_buffer::Buffer::new(),
3745 crate::types::DefaultHost::new(),
3746 crate::types::Options::default(),
3747 );
3748 e.set_content("hello world");
3749 e.handle_key(key(KeyCode::Char('y')));
3750 e.handle_key(key(KeyCode::Char('w')));
3751 assert_eq!(e.vim_mode(), VimMode::Normal);
3752 assert!(e.last_yank.is_some());
3753 }
3754
3755 #[test]
3756 fn vim_cc_changes_line() {
3757 let mut e = Editor::new(
3758 hjkl_buffer::Buffer::new(),
3759 crate::types::DefaultHost::new(),
3760 crate::types::Options::default(),
3761 );
3762 e.set_content("hello\nworld");
3763 e.handle_key(key(KeyCode::Char('c')));
3764 e.handle_key(key(KeyCode::Char('c')));
3765 assert_eq!(e.vim_mode(), VimMode::Insert);
3766 }
3767
3768 #[test]
3769 fn vim_u_undoes_insert_session_as_chunk() {
3770 let mut e = Editor::new(
3771 hjkl_buffer::Buffer::new(),
3772 crate::types::DefaultHost::new(),
3773 crate::types::Options::default(),
3774 );
3775 e.set_content("hello");
3776 e.handle_key(key(KeyCode::Char('i')));
3777 e.handle_key(key(KeyCode::Enter));
3778 e.handle_key(key(KeyCode::Enter));
3779 e.handle_key(key(KeyCode::Esc));
3780 assert_eq!(e.buffer().lines().len(), 3);
3781 e.handle_key(key(KeyCode::Char('u')));
3782 assert_eq!(e.buffer().lines().len(), 1);
3783 assert_eq!(e.buffer().lines()[0], "hello");
3784 }
3785
3786 #[test]
3787 fn vim_undo_redo_roundtrip() {
3788 let mut e = Editor::new(
3789 hjkl_buffer::Buffer::new(),
3790 crate::types::DefaultHost::new(),
3791 crate::types::Options::default(),
3792 );
3793 e.set_content("hello");
3794 e.handle_key(key(KeyCode::Char('i')));
3795 for c in "world".chars() {
3796 e.handle_key(key(KeyCode::Char(c)));
3797 }
3798 e.handle_key(key(KeyCode::Esc));
3799 let after = e.buffer().lines()[0].clone();
3800 e.handle_key(key(KeyCode::Char('u')));
3801 assert_eq!(e.buffer().lines()[0], "hello");
3802 e.handle_key(ctrl_key(KeyCode::Char('r')));
3803 assert_eq!(e.buffer().lines()[0], after);
3804 }
3805
3806 #[test]
3807 fn vim_u_undoes_dd() {
3808 let mut e = Editor::new(
3809 hjkl_buffer::Buffer::new(),
3810 crate::types::DefaultHost::new(),
3811 crate::types::Options::default(),
3812 );
3813 e.set_content("first\nsecond");
3814 e.handle_key(key(KeyCode::Char('d')));
3815 e.handle_key(key(KeyCode::Char('d')));
3816 assert_eq!(e.buffer().lines().len(), 1);
3817 e.handle_key(key(KeyCode::Char('u')));
3818 assert_eq!(e.buffer().lines().len(), 2);
3819 assert_eq!(e.buffer().lines()[0], "first");
3820 }
3821
3822 #[test]
3823 fn vim_ctrl_r_redoes() {
3824 let mut e = Editor::new(
3825 hjkl_buffer::Buffer::new(),
3826 crate::types::DefaultHost::new(),
3827 crate::types::Options::default(),
3828 );
3829 e.set_content("hello");
3830 e.handle_key(ctrl_key(KeyCode::Char('r')));
3831 }
3832
3833 #[test]
3834 fn vim_r_replaces_char() {
3835 let mut e = Editor::new(
3836 hjkl_buffer::Buffer::new(),
3837 crate::types::DefaultHost::new(),
3838 crate::types::Options::default(),
3839 );
3840 e.set_content("hello");
3841 e.handle_key(key(KeyCode::Char('r')));
3842 e.handle_key(key(KeyCode::Char('x')));
3843 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
3844 }
3845
3846 #[test]
3847 fn vim_tilde_toggles_case() {
3848 let mut e = Editor::new(
3849 hjkl_buffer::Buffer::new(),
3850 crate::types::DefaultHost::new(),
3851 crate::types::Options::default(),
3852 );
3853 e.set_content("hello");
3854 e.handle_key(key(KeyCode::Char('~')));
3855 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
3856 }
3857
3858 #[test]
3859 fn vim_visual_d_cuts() {
3860 let mut e = Editor::new(
3861 hjkl_buffer::Buffer::new(),
3862 crate::types::DefaultHost::new(),
3863 crate::types::Options::default(),
3864 );
3865 e.set_content("hello");
3866 e.handle_key(key(KeyCode::Char('v')));
3867 e.handle_key(key(KeyCode::Char('l')));
3868 e.handle_key(key(KeyCode::Char('l')));
3869 e.handle_key(key(KeyCode::Char('d')));
3870 assert_eq!(e.vim_mode(), VimMode::Normal);
3871 assert!(e.last_yank.is_some());
3872 }
3873
3874 #[test]
3875 fn vim_visual_c_enters_insert() {
3876 let mut e = Editor::new(
3877 hjkl_buffer::Buffer::new(),
3878 crate::types::DefaultHost::new(),
3879 crate::types::Options::default(),
3880 );
3881 e.set_content("hello");
3882 e.handle_key(key(KeyCode::Char('v')));
3883 e.handle_key(key(KeyCode::Char('l')));
3884 e.handle_key(key(KeyCode::Char('c')));
3885 assert_eq!(e.vim_mode(), VimMode::Insert);
3886 }
3887
3888 #[test]
3889 fn vim_normal_unknown_key_consumed() {
3890 let mut e = Editor::new(
3891 hjkl_buffer::Buffer::new(),
3892 crate::types::DefaultHost::new(),
3893 crate::types::Options::default(),
3894 );
3895 let consumed = e.handle_key(key(KeyCode::Char('z')));
3897 assert!(consumed);
3898 }
3899
3900 #[test]
3901 fn force_normal_clears_operator() {
3902 let mut e = Editor::new(
3903 hjkl_buffer::Buffer::new(),
3904 crate::types::DefaultHost::new(),
3905 crate::types::Options::default(),
3906 );
3907 e.handle_key(key(KeyCode::Char('d')));
3908 e.force_normal();
3909 assert_eq!(e.vim_mode(), VimMode::Normal);
3910 }
3911
3912 fn many_lines(n: usize) -> String {
3913 (0..n)
3914 .map(|i| format!("line{i}"))
3915 .collect::<Vec<_>>()
3916 .join("\n")
3917 }
3918
3919 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
3920 e.set_viewport_height(height);
3921 }
3922
3923 #[test]
3924 fn zz_centers_cursor_in_viewport() {
3925 let mut e = Editor::new(
3926 hjkl_buffer::Buffer::new(),
3927 crate::types::DefaultHost::new(),
3928 crate::types::Options::default(),
3929 );
3930 e.set_content(&many_lines(100));
3931 prime_viewport(&mut e, 20);
3932 e.jump_cursor(50, 0);
3933 e.handle_key(key(KeyCode::Char('z')));
3934 e.handle_key(key(KeyCode::Char('z')));
3935 assert_eq!(e.host().viewport().top_row, 40);
3936 assert_eq!(e.cursor().0, 50);
3937 }
3938
3939 #[test]
3940 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
3941 let mut e = Editor::new(
3942 hjkl_buffer::Buffer::new(),
3943 crate::types::DefaultHost::new(),
3944 crate::types::Options::default(),
3945 );
3946 e.set_content(&many_lines(100));
3947 prime_viewport(&mut e, 20);
3948 e.jump_cursor(50, 0);
3949 e.handle_key(key(KeyCode::Char('z')));
3950 e.handle_key(key(KeyCode::Char('t')));
3951 assert_eq!(e.host().viewport().top_row, 45);
3954 assert_eq!(e.cursor().0, 50);
3955 }
3956
3957 #[test]
3958 fn ctrl_a_increments_number_at_cursor() {
3959 let mut e = Editor::new(
3960 hjkl_buffer::Buffer::new(),
3961 crate::types::DefaultHost::new(),
3962 crate::types::Options::default(),
3963 );
3964 e.set_content("x = 41");
3965 e.handle_key(ctrl_key(KeyCode::Char('a')));
3966 assert_eq!(e.buffer().lines()[0], "x = 42");
3967 assert_eq!(e.cursor(), (0, 5));
3968 }
3969
3970 #[test]
3971 fn ctrl_a_finds_number_to_right_of_cursor() {
3972 let mut e = Editor::new(
3973 hjkl_buffer::Buffer::new(),
3974 crate::types::DefaultHost::new(),
3975 crate::types::Options::default(),
3976 );
3977 e.set_content("foo 99 bar");
3978 e.handle_key(ctrl_key(KeyCode::Char('a')));
3979 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
3980 assert_eq!(e.cursor(), (0, 6));
3981 }
3982
3983 #[test]
3984 fn ctrl_a_with_count_adds_count() {
3985 let mut e = Editor::new(
3986 hjkl_buffer::Buffer::new(),
3987 crate::types::DefaultHost::new(),
3988 crate::types::Options::default(),
3989 );
3990 e.set_content("x = 10");
3991 for d in "5".chars() {
3992 e.handle_key(key(KeyCode::Char(d)));
3993 }
3994 e.handle_key(ctrl_key(KeyCode::Char('a')));
3995 assert_eq!(e.buffer().lines()[0], "x = 15");
3996 }
3997
3998 #[test]
3999 fn ctrl_x_decrements_number() {
4000 let mut e = Editor::new(
4001 hjkl_buffer::Buffer::new(),
4002 crate::types::DefaultHost::new(),
4003 crate::types::Options::default(),
4004 );
4005 e.set_content("n=5");
4006 e.handle_key(ctrl_key(KeyCode::Char('x')));
4007 assert_eq!(e.buffer().lines()[0], "n=4");
4008 }
4009
4010 #[test]
4011 fn ctrl_x_crosses_zero_into_negative() {
4012 let mut e = Editor::new(
4013 hjkl_buffer::Buffer::new(),
4014 crate::types::DefaultHost::new(),
4015 crate::types::Options::default(),
4016 );
4017 e.set_content("v=0");
4018 e.handle_key(ctrl_key(KeyCode::Char('x')));
4019 assert_eq!(e.buffer().lines()[0], "v=-1");
4020 }
4021
4022 #[test]
4023 fn ctrl_a_on_negative_number_increments_toward_zero() {
4024 let mut e = Editor::new(
4025 hjkl_buffer::Buffer::new(),
4026 crate::types::DefaultHost::new(),
4027 crate::types::Options::default(),
4028 );
4029 e.set_content("a = -5");
4030 e.handle_key(ctrl_key(KeyCode::Char('a')));
4031 assert_eq!(e.buffer().lines()[0], "a = -4");
4032 }
4033
4034 #[test]
4035 fn ctrl_a_noop_when_no_digit_on_line() {
4036 let mut e = Editor::new(
4037 hjkl_buffer::Buffer::new(),
4038 crate::types::DefaultHost::new(),
4039 crate::types::Options::default(),
4040 );
4041 e.set_content("no digits here");
4042 e.handle_key(ctrl_key(KeyCode::Char('a')));
4043 assert_eq!(e.buffer().lines()[0], "no digits here");
4044 }
4045
4046 #[test]
4047 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
4048 let mut e = Editor::new(
4049 hjkl_buffer::Buffer::new(),
4050 crate::types::DefaultHost::new(),
4051 crate::types::Options::default(),
4052 );
4053 e.set_content(&many_lines(100));
4054 prime_viewport(&mut e, 20);
4055 e.jump_cursor(50, 0);
4056 e.handle_key(key(KeyCode::Char('z')));
4057 e.handle_key(key(KeyCode::Char('b')));
4058 assert_eq!(e.host().viewport().top_row, 36);
4062 assert_eq!(e.cursor().0, 50);
4063 }
4064
4065 #[test]
4072 fn set_content_dirties_then_take_dirty_clears() {
4073 let mut e = Editor::new(
4074 hjkl_buffer::Buffer::new(),
4075 crate::types::DefaultHost::new(),
4076 crate::types::Options::default(),
4077 );
4078 e.set_content("hello");
4079 assert!(
4080 e.take_dirty(),
4081 "set_content should leave content_dirty=true"
4082 );
4083 assert!(!e.take_dirty(), "take_dirty should clear the flag");
4084 }
4085
4086 #[test]
4087 fn content_arc_returns_same_arc_until_mutation() {
4088 let mut e = Editor::new(
4089 hjkl_buffer::Buffer::new(),
4090 crate::types::DefaultHost::new(),
4091 crate::types::Options::default(),
4092 );
4093 e.set_content("hello");
4094 let a = e.content_arc();
4095 let b = e.content_arc();
4096 assert!(
4097 std::sync::Arc::ptr_eq(&a, &b),
4098 "repeated content_arc() should hit the cache"
4099 );
4100
4101 e.handle_key(key(KeyCode::Char('i')));
4103 e.handle_key(key(KeyCode::Char('!')));
4104 let c = e.content_arc();
4105 assert!(
4106 !std::sync::Arc::ptr_eq(&a, &c),
4107 "mutation should invalidate content_arc() cache"
4108 );
4109 assert!(c.contains('!'));
4110 }
4111
4112 #[test]
4113 fn content_arc_cache_invalidated_by_set_content() {
4114 let mut e = Editor::new(
4115 hjkl_buffer::Buffer::new(),
4116 crate::types::DefaultHost::new(),
4117 crate::types::Options::default(),
4118 );
4119 e.set_content("one");
4120 let a = e.content_arc();
4121 e.set_content("two");
4122 let b = e.content_arc();
4123 assert!(!std::sync::Arc::ptr_eq(&a, &b));
4124 assert!(b.starts_with("two"));
4125 }
4126
4127 #[test]
4133 fn mouse_click_past_eol_lands_on_last_char() {
4134 let mut e = Editor::new(
4135 hjkl_buffer::Buffer::new(),
4136 crate::types::DefaultHost::new(),
4137 crate::types::Options::default(),
4138 );
4139 e.set_content("hello");
4140 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4144 e.mouse_click_in_rect(area, 78, 1);
4145 assert_eq!(e.cursor(), (0, 4));
4146 }
4147
4148 #[test]
4149 fn mouse_click_past_eol_handles_multibyte_line() {
4150 let mut e = Editor::new(
4151 hjkl_buffer::Buffer::new(),
4152 crate::types::DefaultHost::new(),
4153 crate::types::Options::default(),
4154 );
4155 e.set_content("héllo");
4158 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4159 e.mouse_click_in_rect(area, 78, 1);
4160 assert_eq!(e.cursor(), (0, 4));
4161 }
4162
4163 #[test]
4164 fn mouse_click_inside_line_lands_on_clicked_char() {
4165 let mut e = Editor::new(
4166 hjkl_buffer::Buffer::new(),
4167 crate::types::DefaultHost::new(),
4168 crate::types::Options::default(),
4169 );
4170 e.set_content("hello world");
4171 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4175 e.mouse_click_in_rect(area, 5, 1);
4176 assert_eq!(e.cursor(), (0, 0));
4177 e.mouse_click_in_rect(area, 7, 1);
4178 assert_eq!(e.cursor(), (0, 2));
4179 }
4180
4181 #[test]
4186 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
4187 let mut e = Editor::new(
4188 hjkl_buffer::Buffer::new(),
4189 crate::types::DefaultHost::new(),
4190 crate::types::Options::default(),
4191 );
4192 e.set_content("hello world");
4193 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4194 assert!(e.settings().undo_break_on_motion);
4196 e.handle_key(key(KeyCode::Char('i')));
4198 e.handle_key(key(KeyCode::Char('A')));
4199 e.handle_key(key(KeyCode::Char('A')));
4200 e.handle_key(key(KeyCode::Char('A')));
4201 e.mouse_click_in_rect(area, 10, 1);
4203 e.handle_key(key(KeyCode::Char('B')));
4205 e.handle_key(key(KeyCode::Char('B')));
4206 e.handle_key(key(KeyCode::Char('B')));
4207 e.handle_key(key(KeyCode::Esc));
4209 e.handle_key(key(KeyCode::Char('u')));
4210 let line = e.buffer().line(0).unwrap_or("").to_string();
4211 assert!(
4212 line.contains("AAA"),
4213 "AAA must survive undo (separate group): {line:?}"
4214 );
4215 assert!(
4216 !line.contains("BBB"),
4217 "BBB must be undone (post-click group): {line:?}"
4218 );
4219 }
4220
4221 #[test]
4225 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
4226 let mut e = Editor::new(
4227 hjkl_buffer::Buffer::new(),
4228 crate::types::DefaultHost::new(),
4229 crate::types::Options::default(),
4230 );
4231 e.set_content("hello world");
4232 e.settings_mut().undo_break_on_motion = false;
4233 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4234 e.handle_key(key(KeyCode::Char('i')));
4235 e.handle_key(key(KeyCode::Char('A')));
4236 e.handle_key(key(KeyCode::Char('A')));
4237 e.mouse_click_in_rect(area, 10, 1);
4238 e.handle_key(key(KeyCode::Char('B')));
4239 e.handle_key(key(KeyCode::Char('B')));
4240 e.handle_key(key(KeyCode::Esc));
4241 e.handle_key(key(KeyCode::Char('u')));
4242 let line = e.buffer().line(0).unwrap_or("").to_string();
4243 assert!(
4244 !line.contains("AA") && !line.contains("BB"),
4245 "with undobreak off, single `u` must reverse whole insert: {line:?}"
4246 );
4247 assert_eq!(line, "hello world");
4248 }
4249
4250 #[test]
4253 fn host_clipboard_round_trip_via_default_host() {
4254 let mut e = Editor::new(
4257 hjkl_buffer::Buffer::new(),
4258 crate::types::DefaultHost::new(),
4259 crate::types::Options::default(),
4260 );
4261 e.host_mut().write_clipboard("payload".to_string());
4262 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
4263 }
4264
4265 #[test]
4266 fn host_records_clipboard_on_yank() {
4267 let mut e = Editor::new(
4271 hjkl_buffer::Buffer::new(),
4272 crate::types::DefaultHost::new(),
4273 crate::types::Options::default(),
4274 );
4275 e.set_content("hello\n");
4276 e.handle_key(key(KeyCode::Char('y')));
4277 e.handle_key(key(KeyCode::Char('y')));
4278 let clip = e.host_mut().read_clipboard();
4280 assert!(
4281 clip.as_deref().unwrap_or("").starts_with("hello"),
4282 "host clipboard should carry the yank: {clip:?}"
4283 );
4284 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
4286 }
4287
4288 #[test]
4289 fn host_cursor_shape_via_shared_recorder() {
4290 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
4294 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
4295 struct LeakHost {
4296 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
4297 viewport: crate::types::Viewport,
4298 }
4299 impl crate::types::Host for LeakHost {
4300 type Intent = ();
4301 fn write_clipboard(&mut self, _: String) {}
4302 fn read_clipboard(&mut self) -> Option<String> {
4303 None
4304 }
4305 fn now(&self) -> core::time::Duration {
4306 core::time::Duration::ZERO
4307 }
4308 fn prompt_search(&mut self) -> Option<String> {
4309 None
4310 }
4311 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
4312 self.shapes.lock().unwrap().push(s);
4313 }
4314 fn viewport(&self) -> &crate::types::Viewport {
4315 &self.viewport
4316 }
4317 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4318 &mut self.viewport
4319 }
4320 fn emit_intent(&mut self, _: Self::Intent) {}
4321 }
4322 let mut e = Editor::new(
4323 hjkl_buffer::Buffer::new(),
4324 LeakHost {
4325 shapes: shapes_ptr,
4326 viewport: crate::types::Viewport::default(),
4327 },
4328 crate::types::Options::default(),
4329 );
4330 e.set_content("abc");
4331 e.handle_key(key(KeyCode::Char('i')));
4333 e.handle_key(key(KeyCode::Esc));
4335 let shapes = shapes_ptr.lock().unwrap().clone();
4336 assert_eq!(
4337 shapes,
4338 vec![
4339 crate::types::CursorShape::Bar,
4340 crate::types::CursorShape::Block,
4341 ],
4342 "host should observe Insert(Bar) → Normal(Block) transitions"
4343 );
4344 }
4345
4346 #[test]
4347 fn host_now_drives_chord_timeout_deterministically() {
4348 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
4353 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
4354 struct ClockHost {
4355 now: &'static std::sync::Mutex<core::time::Duration>,
4356 viewport: crate::types::Viewport,
4357 }
4358 impl crate::types::Host for ClockHost {
4359 type Intent = ();
4360 fn write_clipboard(&mut self, _: String) {}
4361 fn read_clipboard(&mut self) -> Option<String> {
4362 None
4363 }
4364 fn now(&self) -> core::time::Duration {
4365 *self.now.lock().unwrap()
4366 }
4367 fn prompt_search(&mut self) -> Option<String> {
4368 None
4369 }
4370 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
4371 fn viewport(&self) -> &crate::types::Viewport {
4372 &self.viewport
4373 }
4374 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4375 &mut self.viewport
4376 }
4377 fn emit_intent(&mut self, _: Self::Intent) {}
4378 }
4379 let mut e = Editor::new(
4380 hjkl_buffer::Buffer::new(),
4381 ClockHost {
4382 now: now_ptr,
4383 viewport: crate::types::Viewport::default(),
4384 },
4385 crate::types::Options::default(),
4386 );
4387 e.set_content("a\nb\nc\n");
4388 e.jump_cursor(2, 0);
4389 e.handle_key(key(KeyCode::Char('g')));
4391 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
4393 e.handle_key(key(KeyCode::Char('g')));
4396 assert_eq!(
4397 e.cursor().0,
4398 2,
4399 "Host::now() must drive `:set timeoutlen` deterministically"
4400 );
4401 }
4402
4403 fn fresh_editor(initial: &str) -> Editor {
4406 let buffer = hjkl_buffer::Buffer::from_str(initial);
4407 Editor::new(
4408 buffer,
4409 crate::types::DefaultHost::new(),
4410 crate::types::Options::default(),
4411 )
4412 }
4413
4414 #[test]
4415 fn content_edit_insert_char_at_origin() {
4416 let mut e = fresh_editor("");
4417 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4418 at: hjkl_buffer::Position::new(0, 0),
4419 ch: 'a',
4420 });
4421 let edits = e.take_content_edits();
4422 assert_eq!(edits.len(), 1);
4423 let ce = &edits[0];
4424 assert_eq!(ce.start_byte, 0);
4425 assert_eq!(ce.old_end_byte, 0);
4426 assert_eq!(ce.new_end_byte, 1);
4427 assert_eq!(ce.start_position, (0, 0));
4428 assert_eq!(ce.old_end_position, (0, 0));
4429 assert_eq!(ce.new_end_position, (0, 1));
4430 }
4431
4432 #[test]
4433 fn content_edit_insert_str_multiline() {
4434 let mut e = fresh_editor("x\ny");
4436 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertStr {
4437 at: hjkl_buffer::Position::new(0, 1),
4438 text: "ab\ncd".into(),
4439 });
4440 let edits = e.take_content_edits();
4441 assert_eq!(edits.len(), 1);
4442 let ce = &edits[0];
4443 assert_eq!(ce.start_byte, 1);
4444 assert_eq!(ce.old_end_byte, 1);
4445 assert_eq!(ce.new_end_byte, 1 + 5);
4446 assert_eq!(ce.start_position, (0, 1));
4447 assert_eq!(ce.new_end_position, (1, 2));
4449 }
4450
4451 #[test]
4452 fn content_edit_delete_range_charwise() {
4453 let mut e = fresh_editor("abcdef");
4455 let _ = e.mutate_edit(hjkl_buffer::Edit::DeleteRange {
4456 start: hjkl_buffer::Position::new(0, 1),
4457 end: hjkl_buffer::Position::new(0, 4),
4458 kind: hjkl_buffer::MotionKind::Char,
4459 });
4460 let edits = e.take_content_edits();
4461 assert_eq!(edits.len(), 1);
4462 let ce = &edits[0];
4463 assert_eq!(ce.start_byte, 1);
4464 assert_eq!(ce.old_end_byte, 4);
4465 assert_eq!(ce.new_end_byte, 1);
4466 assert!(ce.old_end_byte > ce.new_end_byte);
4467 }
4468
4469 #[test]
4470 fn content_edit_set_content_resets() {
4471 let mut e = fresh_editor("foo");
4472 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4473 at: hjkl_buffer::Position::new(0, 0),
4474 ch: 'X',
4475 });
4476 e.set_content("brand new");
4479 assert!(e.take_content_reset());
4480 assert!(!e.take_content_reset());
4482 assert!(e.take_content_edits().is_empty());
4484 }
4485
4486 #[test]
4487 fn content_edit_multiple_replaces_in_order() {
4488 let mut e = fresh_editor("xax xbx xcx");
4493 let _ = e.take_content_edits();
4494 let _ = e.take_content_reset();
4495 let positions = [(0usize, 0usize), (0, 4), (0, 8)];
4499 for (row, col) in positions {
4500 let _ = e.mutate_edit(hjkl_buffer::Edit::Replace {
4501 start: hjkl_buffer::Position::new(row, col),
4502 end: hjkl_buffer::Position::new(row, col + 1),
4503 with: "yy".into(),
4504 });
4505 }
4506 let edits = e.take_content_edits();
4507 assert_eq!(edits.len(), 3);
4508 for ce in &edits {
4509 assert!(ce.start_byte <= ce.old_end_byte);
4510 assert!(ce.start_byte <= ce.new_end_byte);
4511 }
4512 for w in edits.windows(2) {
4514 assert!(w[0].start_byte <= w[1].start_byte);
4515 }
4516 }
4517
4518 #[test]
4519 fn replace_char_at_replaces_single_char_under_cursor() {
4520 let mut e = fresh_editor("abc");
4522 e.jump_cursor(0, 1); e.replace_char_at('X', 1);
4524 let got = e.content();
4525 let got = got.trim_end_matches('\n');
4526 assert_eq!(
4527 got, "aXc",
4528 "replace_char_at(X, 1) must replace 'b' with 'X'"
4529 );
4530 assert_eq!(e.cursor(), (0, 1));
4532 }
4533
4534 #[test]
4535 fn replace_char_at_count_replaces_multiple_chars() {
4536 let mut e = fresh_editor("abcde");
4538 e.jump_cursor(0, 0);
4539 e.replace_char_at('Z', 3);
4540 let got = e.content();
4541 let got = got.trim_end_matches('\n');
4542 assert_eq!(
4543 got, "ZZZde",
4544 "replace_char_at(Z, 3) must replace first 3 chars"
4545 );
4546 }
4547
4548 #[test]
4549 fn find_char_method_moves_to_target() {
4550 let mut e = fresh_editor("abcabc");
4552 e.jump_cursor(0, 0);
4553 e.find_char('c', true, false, 1);
4554 assert_eq!(
4555 e.cursor(),
4556 (0, 2),
4557 "find_char('c', forward=true, till=false, count=1) must land on 'c' at col 2"
4558 );
4559 }
4560
4561 #[test]
4564 fn after_g_gg_jumps_to_top() {
4565 let content: String = (0..20).map(|i| format!("line {i}\n")).collect();
4566 let mut e = fresh_editor(&content);
4567 e.jump_cursor(15, 0);
4568 e.after_g('g', 1);
4569 assert_eq!(e.cursor().0, 0, "gg must move cursor to row 0");
4570 }
4571
4572 #[test]
4573 fn after_g_gg_with_count_jumps_line() {
4574 let content: String = (0..20).map(|i| format!("line {i}\n")).collect();
4576 let mut e = fresh_editor(&content);
4577 e.jump_cursor(0, 0);
4578 e.after_g('g', 5);
4579 assert_eq!(e.cursor().0, 4, "5gg must land on row 4");
4580 }
4581
4582 #[test]
4583 fn after_g_gv_restores_last_visual() {
4584 let mut e = fresh_editor("hello world\n");
4586 e.handle_key(key(KeyCode::Char('v')));
4588 e.handle_key(key(KeyCode::Char('l')));
4589 e.handle_key(key(KeyCode::Char('l')));
4590 e.handle_key(key(KeyCode::Char('l')));
4591 e.handle_key(key(KeyCode::Esc));
4592 assert_eq!(e.vim_mode(), VimMode::Normal, "should be Normal after Esc");
4593 e.after_g('v', 1);
4595 assert_eq!(
4596 e.vim_mode(),
4597 VimMode::Visual,
4598 "gv must re-enter Visual mode"
4599 );
4600 }
4601
4602 #[test]
4603 fn after_g_gj_moves_down() {
4604 let mut e = fresh_editor("line0\nline1\nline2\n");
4605 e.jump_cursor(0, 0);
4606 e.after_g('j', 1);
4607 assert_eq!(e.cursor().0, 1, "gj must move down one display row");
4608 }
4609
4610 #[test]
4611 fn after_g_gu_sets_operator_pending() {
4612 let mut e = fresh_editor("hello\n");
4614 e.after_g('U', 1);
4615 assert!(
4617 e.is_chord_pending(),
4618 "gU must set engine chord-pending (Pending::Op)"
4619 );
4620 }
4621
4622 #[test]
4623 fn after_g_g_star_searches_forward_non_whole_word() {
4624 let mut e = fresh_editor("foo foobar\n");
4626 e.jump_cursor(0, 0); e.after_g('*', 1);
4628 assert_eq!(e.vim_mode(), VimMode::Normal, "g* must stay in Normal mode");
4632 }
4633
4634 #[test]
4637 fn apply_motion_char_left_moves_cursor() {
4638 let mut e = fresh_editor("hello\n");
4639 e.jump_cursor(0, 3);
4640 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 1);
4641 assert_eq!(e.cursor(), (0, 2), "CharLeft moves one col left");
4642 }
4643
4644 #[test]
4645 fn apply_motion_char_left_clamps_at_col_zero() {
4646 let mut e = fresh_editor("hello\n");
4647 e.jump_cursor(0, 0);
4648 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 1);
4649 assert_eq!(e.cursor(), (0, 0), "CharLeft at col 0 must not wrap");
4650 }
4651
4652 #[test]
4653 fn apply_motion_char_left_with_count() {
4654 let mut e = fresh_editor("hello\n");
4655 e.jump_cursor(0, 4);
4656 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 3);
4657 assert_eq!(e.cursor(), (0, 1), "CharLeft count=3 moves three cols left");
4658 }
4659
4660 #[test]
4661 fn apply_motion_char_right_moves_cursor() {
4662 let mut e = fresh_editor("hello\n");
4663 e.jump_cursor(0, 0);
4664 e.apply_motion(hjkl_vim::MotionKind::CharRight, 1);
4665 assert_eq!(e.cursor(), (0, 1), "CharRight moves one col right");
4666 }
4667
4668 #[test]
4669 fn apply_motion_char_right_clamps_at_last_char() {
4670 let mut e = fresh_editor("hello\n");
4671 e.jump_cursor(0, 4);
4673 e.apply_motion(hjkl_vim::MotionKind::CharRight, 1);
4674 assert_eq!(
4675 e.cursor(),
4676 (0, 4),
4677 "CharRight at end must not go past last char"
4678 );
4679 }
4680
4681 #[test]
4682 fn apply_motion_line_down_moves_cursor() {
4683 let mut e = fresh_editor("line0\nline1\nline2\n");
4684 e.jump_cursor(0, 0);
4685 e.apply_motion(hjkl_vim::MotionKind::LineDown, 1);
4686 assert_eq!(e.cursor().0, 1, "LineDown moves one row down");
4687 }
4688
4689 #[test]
4690 fn apply_motion_line_down_with_count() {
4691 let mut e = fresh_editor("line0\nline1\nline2\n");
4692 e.jump_cursor(0, 0);
4693 e.apply_motion(hjkl_vim::MotionKind::LineDown, 2);
4694 assert_eq!(e.cursor().0, 2, "LineDown count=2 moves two rows down");
4695 }
4696
4697 #[test]
4698 fn apply_motion_line_up_moves_cursor() {
4699 let mut e = fresh_editor("line0\nline1\nline2\n");
4700 e.jump_cursor(2, 0);
4701 e.apply_motion(hjkl_vim::MotionKind::LineUp, 1);
4702 assert_eq!(e.cursor().0, 1, "LineUp moves one row up");
4703 }
4704
4705 #[test]
4706 fn apply_motion_line_up_clamps_at_top() {
4707 let mut e = fresh_editor("line0\nline1\n");
4708 e.jump_cursor(0, 0);
4709 e.apply_motion(hjkl_vim::MotionKind::LineUp, 1);
4710 assert_eq!(e.cursor().0, 0, "LineUp at top must not go negative");
4711 }
4712
4713 #[test]
4714 fn apply_motion_first_non_blank_down_moves_and_lands_on_non_blank() {
4715 let mut e = fresh_editor(" hello\n world\n");
4717 e.jump_cursor(0, 0);
4718 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlankDown, 1);
4719 assert_eq!(e.cursor().0, 1, "FirstNonBlankDown must move to next row");
4720 assert_eq!(
4721 e.cursor().1,
4722 2,
4723 "FirstNonBlankDown must land on first non-blank col"
4724 );
4725 }
4726
4727 #[test]
4728 fn apply_motion_first_non_blank_up_moves_and_lands_on_non_blank() {
4729 let mut e = fresh_editor(" hello\n world\n");
4730 e.jump_cursor(1, 4);
4731 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlankUp, 1);
4732 assert_eq!(e.cursor().0, 0, "FirstNonBlankUp must move to prev row");
4733 assert_eq!(
4734 e.cursor().1,
4735 2,
4736 "FirstNonBlankUp must land on first non-blank col"
4737 );
4738 }
4739
4740 #[test]
4741 fn apply_motion_count_zero_treated_as_one() {
4742 let mut e = fresh_editor("hello\n");
4744 e.jump_cursor(0, 3);
4745 e.apply_motion(hjkl_vim::MotionKind::CharLeft, 0);
4746 assert_eq!(e.cursor(), (0, 2), "count=0 treated as 1 for CharLeft");
4747 }
4748
4749 #[test]
4752 fn apply_motion_word_forward_moves_to_next_word() {
4753 let mut e = fresh_editor("hello world\n");
4755 e.jump_cursor(0, 0);
4756 e.apply_motion(hjkl_vim::MotionKind::WordForward, 1);
4757 assert_eq!(
4758 e.cursor(),
4759 (0, 6),
4760 "WordForward moves to start of next word"
4761 );
4762 }
4763
4764 #[test]
4765 fn apply_motion_word_forward_with_count() {
4766 let mut e = fresh_editor("one two three\n");
4768 e.jump_cursor(0, 0);
4769 e.apply_motion(hjkl_vim::MotionKind::WordForward, 2);
4770 assert_eq!(e.cursor(), (0, 8), "WordForward count=2 skips two words");
4771 }
4772
4773 #[test]
4774 fn apply_motion_big_word_forward_moves_to_next_big_word() {
4775 let mut e = fresh_editor("foo.bar baz\n");
4777 e.jump_cursor(0, 0);
4778 e.apply_motion(hjkl_vim::MotionKind::BigWordForward, 1);
4779 assert_eq!(e.cursor(), (0, 8), "BigWordForward skips the whole WORD");
4780 }
4781
4782 #[test]
4783 fn apply_motion_big_word_forward_with_count() {
4784 let mut e = fresh_editor("aa bb cc\n");
4786 e.jump_cursor(0, 0);
4787 e.apply_motion(hjkl_vim::MotionKind::BigWordForward, 2);
4788 assert_eq!(e.cursor(), (0, 6), "BigWordForward count=2 skips two WORDs");
4789 }
4790
4791 #[test]
4792 fn apply_motion_word_backward_moves_to_prev_word() {
4793 let mut e = fresh_editor("hello world\n");
4795 e.jump_cursor(0, 6);
4796 e.apply_motion(hjkl_vim::MotionKind::WordBackward, 1);
4797 assert_eq!(
4798 e.cursor(),
4799 (0, 0),
4800 "WordBackward moves to start of prev word"
4801 );
4802 }
4803
4804 #[test]
4805 fn apply_motion_word_backward_with_count() {
4806 let mut e = fresh_editor("one two three\n");
4808 e.jump_cursor(0, 8);
4809 e.apply_motion(hjkl_vim::MotionKind::WordBackward, 2);
4810 assert_eq!(
4811 e.cursor(),
4812 (0, 0),
4813 "WordBackward count=2 skips two words back"
4814 );
4815 }
4816
4817 #[test]
4818 fn apply_motion_big_word_backward_moves_to_prev_big_word() {
4819 let mut e = fresh_editor("foo.bar baz\n");
4821 e.jump_cursor(0, 8);
4822 e.apply_motion(hjkl_vim::MotionKind::BigWordBackward, 1);
4823 assert_eq!(
4824 e.cursor(),
4825 (0, 0),
4826 "BigWordBackward jumps to start of prev WORD"
4827 );
4828 }
4829
4830 #[test]
4831 fn apply_motion_big_word_backward_with_count() {
4832 let mut e = fresh_editor("aa bb cc\n");
4834 e.jump_cursor(0, 6);
4835 e.apply_motion(hjkl_vim::MotionKind::BigWordBackward, 2);
4836 assert_eq!(
4837 e.cursor(),
4838 (0, 0),
4839 "BigWordBackward count=2 skips two WORDs back"
4840 );
4841 }
4842
4843 #[test]
4844 fn apply_motion_word_end_moves_to_end_of_word() {
4845 let mut e = fresh_editor("hello world\n");
4847 e.jump_cursor(0, 0);
4848 e.apply_motion(hjkl_vim::MotionKind::WordEnd, 1);
4849 assert_eq!(e.cursor(), (0, 4), "WordEnd moves to end of current word");
4850 }
4851
4852 #[test]
4853 fn apply_motion_word_end_with_count() {
4854 let mut e = fresh_editor("one two three\n");
4856 e.jump_cursor(0, 0);
4857 e.apply_motion(hjkl_vim::MotionKind::WordEnd, 2);
4858 assert_eq!(
4859 e.cursor(),
4860 (0, 6),
4861 "WordEnd count=2 lands on end of second word"
4862 );
4863 }
4864
4865 #[test]
4866 fn apply_motion_big_word_end_moves_to_end_of_big_word() {
4867 let mut e = fresh_editor("foo.bar baz\n");
4869 e.jump_cursor(0, 0);
4870 e.apply_motion(hjkl_vim::MotionKind::BigWordEnd, 1);
4871 assert_eq!(e.cursor(), (0, 6), "BigWordEnd lands on end of WORD");
4872 }
4873
4874 #[test]
4875 fn apply_motion_big_word_end_with_count() {
4876 let mut e = fresh_editor("aa bb cc\n");
4878 e.jump_cursor(0, 0);
4879 e.apply_motion(hjkl_vim::MotionKind::BigWordEnd, 2);
4880 assert_eq!(
4881 e.cursor(),
4882 (0, 4),
4883 "BigWordEnd count=2 lands on end of second WORD"
4884 );
4885 }
4886
4887 #[test]
4890 fn apply_motion_line_start_lands_at_col_zero() {
4891 let mut e = fresh_editor(" foo bar \n");
4893 e.jump_cursor(0, 5);
4894 e.apply_motion(hjkl_vim::MotionKind::LineStart, 1);
4895 assert_eq!(e.cursor(), (0, 0), "LineStart lands at col 0");
4896 }
4897
4898 #[test]
4899 fn apply_motion_line_start_from_beginning_stays_at_col_zero() {
4900 let mut e = fresh_editor(" foo bar \n");
4902 e.jump_cursor(0, 0);
4903 e.apply_motion(hjkl_vim::MotionKind::LineStart, 1);
4904 assert_eq!(e.cursor(), (0, 0), "LineStart from col 0 stays at col 0");
4905 }
4906
4907 #[test]
4908 fn apply_motion_first_non_blank_lands_on_first_non_blank() {
4909 let mut e = fresh_editor(" foo bar \n");
4911 e.jump_cursor(0, 0);
4912 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlank, 1);
4913 assert_eq!(
4914 e.cursor(),
4915 (0, 2),
4916 "FirstNonBlank lands on first non-blank char"
4917 );
4918 }
4919
4920 #[test]
4921 fn apply_motion_first_non_blank_on_blank_line_lands_at_zero() {
4922 let mut e = fresh_editor(" \n");
4924 e.jump_cursor(0, 2);
4925 e.apply_motion(hjkl_vim::MotionKind::FirstNonBlank, 1);
4926 assert_eq!(
4927 e.cursor(),
4928 (0, 0),
4929 "FirstNonBlank on blank line stays at col 0"
4930 );
4931 }
4932
4933 #[test]
4934 fn apply_motion_line_end_lands_on_last_char() {
4935 let mut e = fresh_editor(" foo bar \n");
4937 e.jump_cursor(0, 0);
4938 e.apply_motion(hjkl_vim::MotionKind::LineEnd, 1);
4939 assert_eq!(e.cursor(), (0, 10), "LineEnd lands on last char of line");
4940 }
4941
4942 #[test]
4943 fn apply_motion_line_end_on_empty_line_stays_at_zero() {
4944 let mut e = fresh_editor("\n");
4946 e.jump_cursor(0, 0);
4947 e.apply_motion(hjkl_vim::MotionKind::LineEnd, 1);
4948 assert_eq!(e.cursor(), (0, 0), "LineEnd on empty line stays at col 0");
4949 }
4950}