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 #[allow(clippy::type_complexity)]
1276 pub fn jump_list(&self) -> (&[(usize, usize)], &[(usize, usize)]) {
1277 (&self.vim.jump_back, &self.vim.jump_fwd)
1278 }
1279
1280 pub fn change_list(&self) -> (&[(usize, usize)], Option<usize>) {
1283 (&self.vim.change_list, self.vim.change_list_cursor)
1284 }
1285
1286 pub fn set_yank(&mut self, text: impl Into<String>) {
1290 let text = text.into();
1291 let linewise = self.vim.yank_linewise;
1292 self.registers.unnamed = crate::registers::Slot { text, linewise };
1293 }
1294
1295 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
1299 self.vim.yank_linewise = linewise;
1300 let target = self.vim.pending_register.take();
1301 self.registers.record_yank(text, linewise, target);
1302 }
1303
1304 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
1309 if let Some(slot) = match reg {
1310 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
1311 'A'..='Z' => {
1312 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
1313 }
1314 _ => None,
1315 } {
1316 slot.text = text;
1317 slot.linewise = false;
1318 }
1319 }
1320
1321 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
1324 self.vim.yank_linewise = linewise;
1325 let target = self.vim.pending_register.take();
1326 self.registers.record_delete(text, linewise, target);
1327 }
1328
1329 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
1338 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
1339 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
1340 .collect();
1341 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1342 #[cfg(feature = "ratatui")]
1343 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
1344 Vec::with_capacity(spans.len());
1345 for (row, row_spans) in spans.iter().enumerate() {
1346 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
1347 let mut translated = Vec::with_capacity(row_spans.len());
1348 #[cfg(feature = "ratatui")]
1349 let mut translated_r = Vec::with_capacity(row_spans.len());
1350 for (start, end, style) in row_spans {
1351 let end_clamped = (*end).min(line_len);
1352 if end_clamped <= *start {
1353 continue;
1354 }
1355 let id = self.intern_style(*style);
1356 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1357 #[cfg(feature = "ratatui")]
1358 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
1359 }
1360 by_row.push(translated);
1361 #[cfg(feature = "ratatui")]
1362 ratatui_spans.push(translated_r);
1363 }
1364 self.buffer_spans = by_row;
1365 #[cfg(feature = "ratatui")]
1366 {
1367 self.styled_spans = ratatui_spans;
1368 }
1369 }
1370
1371 #[cfg(feature = "ratatui")]
1380 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
1381 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
1382 return idx as u32;
1383 }
1384 self.style_table.push(style);
1385 (self.style_table.len() - 1) as u32
1386 }
1387
1388 #[cfg(feature = "ratatui")]
1392 pub fn style_table(&self) -> &[ratatui::style::Style] {
1393 &self.style_table
1394 }
1395
1396 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
1405 &self.buffer_spans
1406 }
1407
1408 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1423 #[cfg(feature = "ratatui")]
1424 {
1425 let r = engine_style_to_ratatui(style);
1426 self.intern_ratatui_style(r)
1427 }
1428 #[cfg(not(feature = "ratatui"))]
1429 {
1430 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1431 return idx as u32;
1432 }
1433 self.engine_style_table.push(style);
1434 (self.engine_style_table.len() - 1) as u32
1435 }
1436 }
1437
1438 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1442 #[cfg(feature = "ratatui")]
1443 {
1444 let r = self.style_table.get(id as usize).copied()?;
1445 Some(ratatui_style_to_engine(r))
1446 }
1447 #[cfg(not(feature = "ratatui"))]
1448 {
1449 self.engine_style_table.get(id as usize).copied()
1450 }
1451 }
1452
1453 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1457
1458 pub fn set_viewport_top(&mut self, row: usize) {
1466 let last = buf_row_count(&self.buffer).saturating_sub(1);
1467 let target = row.min(last);
1468 self.host.viewport_mut().top_row = target;
1469 }
1470
1471 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1475 buf_set_cursor_rc(&mut self.buffer, row, col);
1476 }
1477
1478 pub fn cursor(&self) -> (usize, usize) {
1486 buf_cursor_rc(&self.buffer)
1487 }
1488
1489 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1492 self.pending_lsp.take()
1493 }
1494
1495 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1509 std::mem::take(&mut self.pending_fold_ops)
1510 }
1511
1512 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1522 use crate::types::FoldProvider;
1523 self.pending_fold_ops.push(op);
1524 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1525 provider.apply(op);
1526 }
1527
1528 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1535 let height = self.viewport_height_value();
1536 self.host.viewport_mut().height = height;
1537 }
1538
1539 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1543 self.sync_buffer_from_textarea();
1544 }
1545
1546 pub fn record_jump(&mut self, pos: (usize, usize)) {
1551 const JUMPLIST_MAX: usize = 100;
1552 self.vim.jump_back.push(pos);
1553 if self.vim.jump_back.len() > JUMPLIST_MAX {
1554 self.vim.jump_back.remove(0);
1555 }
1556 self.vim.jump_fwd.clear();
1557 }
1558
1559 pub fn set_viewport_height(&self, height: u16) {
1562 self.viewport_height.store(height, Ordering::Relaxed);
1563 }
1564
1565 pub fn viewport_height_value(&self) -> u16 {
1567 self.viewport_height.load(Ordering::Relaxed)
1568 }
1569
1570 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1579 if self.settings.readonly {
1586 let _ = edit;
1587 return hjkl_buffer::Edit::InsertStr {
1588 at: buf_cursor_pos(&self.buffer),
1589 text: String::new(),
1590 };
1591 }
1592 let pre_row = buf_cursor_row(&self.buffer);
1593 let pre_rows = buf_row_count(&self.buffer);
1594 self.change_log.extend(edit_to_editops(&edit));
1598 let content_edits = content_edits_from_buffer_edit(&self.buffer, &edit);
1604 self.pending_content_edits.extend(content_edits);
1605 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1611 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1612 let lo = pre_row.min(pos_row);
1618 let hi = pre_row.max(pos_row);
1619 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1620 start_row: lo,
1621 end_row: hi,
1622 });
1623 self.vim.last_edit_pos = Some((pos_row, pos_col));
1624 let entry = (pos_row, pos_col);
1629 if self.vim.change_list.last() != Some(&entry) {
1630 if let Some(idx) = self.vim.change_list_cursor.take() {
1631 self.vim.change_list.truncate(idx + 1);
1632 }
1633 self.vim.change_list.push(entry);
1634 let len = self.vim.change_list.len();
1635 if len > crate::vim::CHANGE_LIST_MAX {
1636 self.vim
1637 .change_list
1638 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1639 }
1640 }
1641 self.vim.change_list_cursor = None;
1642 let post_rows = buf_row_count(&self.buffer);
1646 let delta = post_rows as isize - pre_rows as isize;
1647 if delta != 0 {
1648 self.shift_marks_after_edit(pre_row, delta);
1649 }
1650 self.push_buffer_content_to_textarea();
1651 self.mark_content_dirty();
1652 inverse
1653 }
1654
1655 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1660 if delta == 0 {
1661 return;
1662 }
1663 let drop_end = if delta < 0 {
1666 edit_start.saturating_add((-delta) as usize)
1667 } else {
1668 edit_start
1669 };
1670 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1671
1672 let mut to_drop: Vec<char> = Vec::new();
1675 for (c, (row, _col)) in self.marks.iter_mut() {
1676 if (edit_start..drop_end).contains(row) {
1677 to_drop.push(*c);
1678 } else if *row >= shift_threshold {
1679 *row = ((*row as isize) + delta).max(0) as usize;
1680 }
1681 }
1682 for c in to_drop {
1683 self.marks.remove(&c);
1684 }
1685
1686 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1687 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1688 for (row, _) in entries.iter_mut() {
1689 if *row >= shift_threshold {
1690 *row = ((*row as isize) + delta).max(0) as usize;
1691 }
1692 }
1693 };
1694 shift_jumps(&mut self.vim.jump_back);
1695 shift_jumps(&mut self.vim.jump_fwd);
1696 }
1697
1698 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1706
1707 pub fn mark_content_dirty(&mut self) {
1713 self.content_dirty = true;
1714 self.cached_content = None;
1715 }
1716
1717 pub fn take_dirty(&mut self) -> bool {
1719 let dirty = self.content_dirty;
1720 self.content_dirty = false;
1721 dirty
1722 }
1723
1724 pub fn take_content_edits(&mut self) -> Vec<crate::types::ContentEdit> {
1732 std::mem::take(&mut self.pending_content_edits)
1733 }
1734
1735 pub fn take_content_reset(&mut self) -> bool {
1741 let r = self.pending_content_reset;
1742 self.pending_content_reset = false;
1743 r
1744 }
1745
1746 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1756 if !self.content_dirty {
1757 return None;
1758 }
1759 let arc = self.content_arc();
1760 self.content_dirty = false;
1761 Some(arc)
1762 }
1763
1764 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1767 let cursor = buf_cursor_row(&self.buffer);
1768 let top = self.host.viewport().top_row;
1769 cursor.saturating_sub(top).min(height as usize - 1) as u16
1770 }
1771
1772 pub fn cursor_screen_pos(
1782 &self,
1783 area_x: u16,
1784 area_y: u16,
1785 area_width: u16,
1786 area_height: u16,
1787 ) -> Option<(u16, u16)> {
1788 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1789 let v = self.host.viewport();
1790 if pos_row < v.top_row || pos_col < v.top_col {
1791 return None;
1792 }
1793 let lnum_width = if self.settings.number || self.settings.relativenumber {
1794 let needed = buf_row_count(&self.buffer).to_string().len() + 1;
1795 needed.max(self.settings.numberwidth) as u16
1796 } else {
1797 0
1798 };
1799 let dy = (pos_row - v.top_row) as u16;
1800 let line = self.buffer.line(pos_row).unwrap_or("");
1804 let tab_width = if v.tab_width == 0 {
1805 4
1806 } else {
1807 v.tab_width as usize
1808 };
1809 let visual_pos = visual_col_for_char(line, pos_col, tab_width);
1810 let visual_top = visual_col_for_char(line, v.top_col, tab_width);
1811 let dx = (visual_pos - visual_top) as u16;
1812 if dy >= area_height || dx + lnum_width >= area_width {
1813 return None;
1814 }
1815 Some((area_x + lnum_width + dx, area_y + dy))
1816 }
1817
1818 #[cfg(feature = "ratatui")]
1824 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1825 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1826 }
1827
1828 pub fn vim_mode(&self) -> VimMode {
1829 self.vim.public_mode()
1830 }
1831
1832 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1838 self.vim.search_prompt.as_ref()
1839 }
1840
1841 pub fn last_search(&self) -> Option<&str> {
1844 self.vim.last_search.as_deref()
1845 }
1846
1847 pub fn last_search_forward(&self) -> bool {
1851 self.vim.last_search_forward
1852 }
1853
1854 pub fn set_last_search(&mut self, text: Option<String>, forward: bool) {
1860 self.vim.last_search = text;
1861 self.vim.last_search_forward = forward;
1862 }
1863
1864 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1868 if self.vim_mode() != VimMode::Visual {
1869 return None;
1870 }
1871 let anchor = self.vim.visual_anchor;
1872 let cursor = self.cursor();
1873 let (start, end) = if anchor <= cursor {
1874 (anchor, cursor)
1875 } else {
1876 (cursor, anchor)
1877 };
1878 Some((start, end))
1879 }
1880
1881 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1884 if self.vim_mode() != VimMode::VisualLine {
1885 return None;
1886 }
1887 let anchor = self.vim.visual_line_anchor;
1888 let cursor = buf_cursor_row(&self.buffer);
1889 Some((anchor.min(cursor), anchor.max(cursor)))
1890 }
1891
1892 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1893 if self.vim_mode() != VimMode::VisualBlock {
1894 return None;
1895 }
1896 let (ar, ac) = self.vim.block_anchor;
1897 let cr = buf_cursor_row(&self.buffer);
1898 let cc = self.vim.block_vcol;
1899 let top = ar.min(cr);
1900 let bot = ar.max(cr);
1901 let left = ac.min(cc);
1902 let right = ac.max(cc);
1903 Some((top, bot, left, right))
1904 }
1905
1906 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1912 use hjkl_buffer::{Position, Selection};
1913 match self.vim_mode() {
1914 VimMode::Visual => {
1915 let (ar, ac) = self.vim.visual_anchor;
1916 let head = buf_cursor_pos(&self.buffer);
1917 Some(Selection::Char {
1918 anchor: Position::new(ar, ac),
1919 head,
1920 })
1921 }
1922 VimMode::VisualLine => {
1923 let anchor_row = self.vim.visual_line_anchor;
1924 let head_row = buf_cursor_row(&self.buffer);
1925 Some(Selection::Line {
1926 anchor_row,
1927 head_row,
1928 })
1929 }
1930 VimMode::VisualBlock => {
1931 let (ar, ac) = self.vim.block_anchor;
1932 let cr = buf_cursor_row(&self.buffer);
1933 let cc = self.vim.block_vcol;
1934 Some(Selection::Block {
1935 anchor: Position::new(ar, ac),
1936 head: Position::new(cr, cc),
1937 })
1938 }
1939 _ => None,
1940 }
1941 }
1942
1943 pub fn force_normal(&mut self) {
1945 self.vim.force_normal();
1946 }
1947
1948 pub fn content(&self) -> String {
1949 let n = buf_row_count(&self.buffer);
1950 let mut s = String::new();
1951 for r in 0..n {
1952 if r > 0 {
1953 s.push('\n');
1954 }
1955 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1956 }
1957 s.push('\n');
1958 s
1959 }
1960
1961 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
1966 if let Some(arc) = &self.cached_content {
1967 return std::sync::Arc::clone(arc);
1968 }
1969 let arc = std::sync::Arc::new(self.content());
1970 self.cached_content = Some(std::sync::Arc::clone(&arc));
1971 arc
1972 }
1973
1974 pub fn set_content(&mut self, text: &str) {
1975 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
1976 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
1977 lines.pop();
1978 }
1979 if lines.is_empty() {
1980 lines.push(String::new());
1981 }
1982 let _ = lines;
1983 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
1984 self.undo_stack.clear();
1985 self.redo_stack.clear();
1986 self.pending_content_edits.clear();
1988 self.pending_content_reset = true;
1989 self.mark_content_dirty();
1990 }
1991
1992 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
2008 use crate::{PlannedInput, SpecialKey};
2009 let (key, mods) = match input {
2010 PlannedInput::Char(c, m) => (Key::Char(c), m),
2011 PlannedInput::Key(k, m) => {
2012 let key = match k {
2013 SpecialKey::Esc => Key::Esc,
2014 SpecialKey::Enter => Key::Enter,
2015 SpecialKey::Backspace => Key::Backspace,
2016 SpecialKey::Tab => Key::Tab,
2017 SpecialKey::BackTab => Key::Tab,
2021 SpecialKey::Up => Key::Up,
2022 SpecialKey::Down => Key::Down,
2023 SpecialKey::Left => Key::Left,
2024 SpecialKey::Right => Key::Right,
2025 SpecialKey::Home => Key::Home,
2026 SpecialKey::End => Key::End,
2027 SpecialKey::PageUp => Key::PageUp,
2028 SpecialKey::PageDown => Key::PageDown,
2029 SpecialKey::Insert => Key::Null,
2033 SpecialKey::Delete => Key::Delete,
2034 SpecialKey::F(_) => Key::Null,
2035 };
2036 let m = if matches!(k, SpecialKey::BackTab) {
2037 crate::Modifiers { shift: true, ..m }
2038 } else {
2039 m
2040 };
2041 (key, m)
2042 }
2043 PlannedInput::Mouse(_)
2045 | PlannedInput::Paste(_)
2046 | PlannedInput::FocusGained
2047 | PlannedInput::FocusLost
2048 | PlannedInput::Resize(_, _) => return false,
2049 };
2050 if key == Key::Null {
2051 return false;
2052 }
2053 let event = Input {
2054 key,
2055 ctrl: mods.ctrl,
2056 alt: mods.alt,
2057 shift: mods.shift,
2058 };
2059 let consumed = vim::step(self, event);
2060 self.emit_cursor_shape_if_changed();
2061 consumed
2062 }
2063
2064 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
2081 std::mem::take(&mut self.change_log)
2082 }
2083
2084 pub fn current_options(&self) -> crate::types::Options {
2094 crate::types::Options {
2095 shiftwidth: self.settings.shiftwidth as u32,
2096 tabstop: self.settings.tabstop as u32,
2097 softtabstop: self.settings.softtabstop as u32,
2098 textwidth: self.settings.textwidth as u32,
2099 expandtab: self.settings.expandtab,
2100 ignorecase: self.settings.ignore_case,
2101 smartcase: self.settings.smartcase,
2102 wrapscan: self.settings.wrapscan,
2103 wrap: match self.settings.wrap {
2104 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
2105 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
2106 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
2107 },
2108 readonly: self.settings.readonly,
2109 autoindent: self.settings.autoindent,
2110 smartindent: self.settings.smartindent,
2111 undo_levels: self.settings.undo_levels,
2112 undo_break_on_motion: self.settings.undo_break_on_motion,
2113 iskeyword: self.settings.iskeyword.clone(),
2114 timeout_len: self.settings.timeout_len,
2115 ..crate::types::Options::default()
2116 }
2117 }
2118
2119 pub fn apply_options(&mut self, opts: &crate::types::Options) {
2124 self.settings.shiftwidth = opts.shiftwidth as usize;
2125 self.settings.tabstop = opts.tabstop as usize;
2126 self.settings.softtabstop = opts.softtabstop as usize;
2127 self.settings.textwidth = opts.textwidth as usize;
2128 self.settings.expandtab = opts.expandtab;
2129 self.settings.ignore_case = opts.ignorecase;
2130 self.settings.smartcase = opts.smartcase;
2131 self.settings.wrapscan = opts.wrapscan;
2132 self.settings.wrap = match opts.wrap {
2133 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
2134 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
2135 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
2136 };
2137 self.settings.readonly = opts.readonly;
2138 self.settings.autoindent = opts.autoindent;
2139 self.settings.smartindent = opts.smartindent;
2140 self.settings.undo_levels = opts.undo_levels;
2141 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
2142 self.set_iskeyword(opts.iskeyword.clone());
2143 self.settings.timeout_len = opts.timeout_len;
2144 self.settings.number = opts.number;
2145 self.settings.relativenumber = opts.relativenumber;
2146 self.settings.numberwidth = opts.numberwidth;
2147 self.settings.cursorline = opts.cursorline;
2148 self.settings.cursorcolumn = opts.cursorcolumn;
2149 self.settings.signcolumn = opts.signcolumn;
2150 self.settings.foldcolumn = opts.foldcolumn;
2151 self.settings.colorcolumn = opts.colorcolumn.clone();
2152 }
2153
2154 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
2164 use crate::types::{Highlight, HighlightKind, Pos};
2165 let sel = self.buffer_selection()?;
2166 let (start, end) = match sel {
2167 hjkl_buffer::Selection::Char { anchor, head } => {
2168 let a = (anchor.row, anchor.col);
2169 let h = (head.row, head.col);
2170 if a <= h { (a, h) } else { (h, a) }
2171 }
2172 hjkl_buffer::Selection::Line {
2173 anchor_row,
2174 head_row,
2175 } => {
2176 let (top, bot) = if anchor_row <= head_row {
2177 (anchor_row, head_row)
2178 } else {
2179 (head_row, anchor_row)
2180 };
2181 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
2182 ((top, 0), (bot, last_col))
2183 }
2184 hjkl_buffer::Selection::Block { anchor, head } => {
2185 let (top, bot) = if anchor.row <= head.row {
2186 (anchor.row, head.row)
2187 } else {
2188 (head.row, anchor.row)
2189 };
2190 let (left, right) = if anchor.col <= head.col {
2191 (anchor.col, head.col)
2192 } else {
2193 (head.col, anchor.col)
2194 };
2195 ((top, left), (bot, right))
2196 }
2197 };
2198 Some(Highlight {
2199 range: Pos {
2200 line: start.0 as u32,
2201 col: start.1 as u32,
2202 }..Pos {
2203 line: end.0 as u32,
2204 col: end.1 as u32,
2205 },
2206 kind: HighlightKind::Selection,
2207 })
2208 }
2209
2210 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
2229 use crate::types::{Highlight, HighlightKind, Pos};
2230 let row = line as usize;
2231 if row >= buf_row_count(&self.buffer) {
2232 return Vec::new();
2233 }
2234
2235 if let Some(prompt) = self.search_prompt() {
2238 if prompt.text.is_empty() {
2239 return Vec::new();
2240 }
2241 let Ok(re) = regex::Regex::new(&prompt.text) else {
2242 return Vec::new();
2243 };
2244 let Some(haystack) = buf_line(&self.buffer, row) else {
2245 return Vec::new();
2246 };
2247 return re
2248 .find_iter(haystack)
2249 .map(|m| Highlight {
2250 range: Pos {
2251 line,
2252 col: m.start() as u32,
2253 }..Pos {
2254 line,
2255 col: m.end() as u32,
2256 },
2257 kind: HighlightKind::IncSearch,
2258 })
2259 .collect();
2260 }
2261
2262 if self.search_state.pattern.is_none() {
2263 return Vec::new();
2264 }
2265 let dgen = crate::types::Query::dirty_gen(&self.buffer);
2266 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
2267 .into_iter()
2268 .map(|(start, end)| Highlight {
2269 range: Pos {
2270 line,
2271 col: start as u32,
2272 }..Pos {
2273 line,
2274 col: end as u32,
2275 },
2276 kind: HighlightKind::SearchMatch,
2277 })
2278 .collect()
2279 }
2280
2281 pub fn render_frame(&self) -> crate::types::RenderFrame {
2291 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
2292 let (cursor_row, cursor_col) = self.cursor();
2293 let (mode, shape) = match self.vim_mode() {
2294 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
2295 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
2296 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
2297 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
2298 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
2299 };
2300 RenderFrame {
2301 mode,
2302 cursor_row: cursor_row as u32,
2303 cursor_col: cursor_col as u32,
2304 cursor_shape: shape,
2305 viewport_top: self.host.viewport().top_row as u32,
2306 line_count: crate::types::Query::line_count(&self.buffer),
2307 }
2308 }
2309
2310 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
2323 use crate::types::{EditorSnapshot, SnapshotMode};
2324 let mode = match self.vim_mode() {
2325 crate::VimMode::Normal => SnapshotMode::Normal,
2326 crate::VimMode::Insert => SnapshotMode::Insert,
2327 crate::VimMode::Visual => SnapshotMode::Visual,
2328 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
2329 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
2330 };
2331 let cursor = self.cursor();
2332 let cursor = (cursor.0 as u32, cursor.1 as u32);
2333 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
2334 let viewport_top = self.host.viewport().top_row as u32;
2335 let marks = self
2336 .marks
2337 .iter()
2338 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
2339 .collect();
2340 EditorSnapshot {
2341 version: EditorSnapshot::VERSION,
2342 mode,
2343 cursor,
2344 lines,
2345 viewport_top,
2346 registers: self.registers.clone(),
2347 marks,
2348 }
2349 }
2350
2351 pub fn restore_snapshot(
2359 &mut self,
2360 snap: crate::types::EditorSnapshot,
2361 ) -> Result<(), crate::EngineError> {
2362 use crate::types::EditorSnapshot;
2363 if snap.version != EditorSnapshot::VERSION {
2364 return Err(crate::EngineError::SnapshotVersion(
2365 snap.version,
2366 EditorSnapshot::VERSION,
2367 ));
2368 }
2369 let text = snap.lines.join("\n");
2370 self.set_content(&text);
2371 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
2372 self.host.viewport_mut().top_row = snap.viewport_top as usize;
2373 self.registers = snap.registers;
2374 self.marks = snap
2375 .marks
2376 .into_iter()
2377 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
2378 .collect();
2379 Ok(())
2380 }
2381
2382 pub fn seed_yank(&mut self, text: String) {
2386 let linewise = text.ends_with('\n');
2387 self.vim.yank_linewise = linewise;
2388 self.registers.unnamed = crate::registers::Slot { text, linewise };
2389 }
2390
2391 pub fn scroll_down(&mut self, rows: i16) {
2396 self.scroll_viewport(rows);
2397 }
2398
2399 pub fn scroll_up(&mut self, rows: i16) {
2403 self.scroll_viewport(-rows);
2404 }
2405
2406 const SCROLLOFF: usize = 5;
2410
2411 pub fn ensure_cursor_in_scrolloff(&mut self) {
2416 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2417 if height == 0 {
2418 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2425 crate::viewport_math::ensure_cursor_visible(
2426 &self.buffer,
2427 &folds,
2428 self.host.viewport_mut(),
2429 );
2430 return;
2431 }
2432 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2436 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
2439 self.ensure_scrolloff_wrap(height, margin);
2440 return;
2441 }
2442 let cursor_row = buf_cursor_row(&self.buffer);
2443 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
2444 let v = self.host.viewport_mut();
2445 if cursor_row < v.top_row + margin {
2447 v.top_row = cursor_row.saturating_sub(margin);
2448 }
2449 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
2451 if cursor_row > v.top_row + max_bottom {
2452 v.top_row = cursor_row.saturating_sub(max_bottom);
2453 }
2454 let max_top = last_row.saturating_sub(height.saturating_sub(1));
2456 if v.top_row > max_top {
2457 v.top_row = max_top;
2458 }
2459 let cursor = buf_cursor_pos(&self.buffer);
2462 self.host.viewport_mut().ensure_visible(cursor);
2463 }
2464
2465 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
2470 let cursor_row = buf_cursor_row(&self.buffer);
2471 if cursor_row < self.host.viewport().top_row {
2474 let v = self.host.viewport_mut();
2475 v.top_row = cursor_row;
2476 v.top_col = 0;
2477 }
2478 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2487 loop {
2488 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2489 let csr =
2490 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2491 .unwrap_or(0);
2492 if csr <= max_csr {
2493 break;
2494 }
2495 let top = self.host.viewport().top_row;
2496 let row_count = buf_row_count(&self.buffer);
2497 let next = {
2498 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2499 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2500 };
2501 let Some(next) = next else {
2502 break;
2503 };
2504 if next > cursor_row {
2506 self.host.viewport_mut().top_row = cursor_row;
2507 break;
2508 }
2509 self.host.viewport_mut().top_row = next;
2510 }
2511 loop {
2514 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2515 let csr =
2516 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2517 .unwrap_or(0);
2518 if csr >= margin {
2519 break;
2520 }
2521 let top = self.host.viewport().top_row;
2522 let prev = {
2523 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2524 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2525 };
2526 let Some(prev) = prev else {
2527 break;
2528 };
2529 self.host.viewport_mut().top_row = prev;
2530 }
2531 let max_top = {
2536 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2537 crate::viewport_math::max_top_for_height(
2538 &self.buffer,
2539 &folds,
2540 self.host.viewport(),
2541 height,
2542 )
2543 };
2544 if self.host.viewport().top_row > max_top {
2545 self.host.viewport_mut().top_row = max_top;
2546 }
2547 self.host.viewport_mut().top_col = 0;
2548 }
2549
2550 fn scroll_viewport(&mut self, delta: i16) {
2551 if delta == 0 {
2552 return;
2553 }
2554 let total_rows = buf_row_count(&self.buffer) as isize;
2556 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2557 let cur_top = self.host.viewport().top_row as isize;
2558 let new_top = (cur_top + delta as isize)
2559 .max(0)
2560 .min((total_rows - 1).max(0)) as usize;
2561 self.host.viewport_mut().top_row = new_top;
2562 let _ = cur_top;
2565 if height == 0 {
2566 return;
2567 }
2568 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2571 let margin = Self::SCROLLOFF.min(height / 2);
2572 let min_row = new_top + margin;
2573 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2574 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2575 if target_row != cursor_row {
2576 let line_len = buf_line(&self.buffer, target_row)
2577 .map(|l| l.chars().count())
2578 .unwrap_or(0);
2579 let target_col = cursor_col.min(line_len.saturating_sub(1));
2580 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2581 }
2582 }
2583
2584 pub fn goto_line(&mut self, line: usize) {
2585 let row = line.saturating_sub(1);
2586 let max = buf_row_count(&self.buffer).saturating_sub(1);
2587 let target = row.min(max);
2588 buf_set_cursor_rc(&mut self.buffer, target, 0);
2589 self.ensure_cursor_in_scrolloff();
2593 }
2594
2595 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2599 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2600 if height == 0 {
2601 return;
2602 }
2603 let cur_row = buf_cursor_row(&self.buffer);
2604 let cur_top = self.host.viewport().top_row;
2605 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2611 let new_top = match pos {
2612 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2613 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2614 CursorScrollTarget::Bottom => {
2615 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2616 }
2617 };
2618 if new_top == cur_top {
2619 return;
2620 }
2621 self.host.viewport_mut().top_row = new_top;
2622 }
2623
2624 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2635 let n = buf_row_count(&self.buffer);
2636 let inner_top = area_y.saturating_add(1); let lnum_width = if self.settings.number || self.settings.relativenumber {
2638 let needed = n.to_string().len() + 1;
2639 needed.max(self.settings.numberwidth) as u16
2640 } else {
2641 0
2642 };
2643 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2644 let rel_row = row.saturating_sub(inner_top) as usize;
2645 let top = self.host.viewport().top_row;
2646 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2647 let rel_col = col.saturating_sub(content_x) as usize;
2648 let line_chars = buf_line(&self.buffer, doc_row)
2649 .map(|l| l.chars().count())
2650 .unwrap_or(0);
2651 let last_col = line_chars.saturating_sub(1);
2652 (doc_row, rel_col.min(last_col))
2653 }
2654
2655 pub fn jump_to(&mut self, line: usize, col: usize) {
2657 let r = line.saturating_sub(1);
2658 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2659 let r = r.min(max_row);
2660 let line_len = buf_line(&self.buffer, r)
2661 .map(|l| l.chars().count())
2662 .unwrap_or(0);
2663 let c = col.saturating_sub(1).min(line_len);
2664 buf_set_cursor_rc(&mut self.buffer, r, c);
2665 }
2666
2667 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2675 if self.vim.is_visual() {
2676 self.vim.force_normal();
2677 }
2678 crate::vim::break_undo_group_in_insert(self);
2681 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2682 buf_set_cursor_rc(&mut self.buffer, r, c);
2683 }
2684
2685 #[cfg(feature = "ratatui")]
2691 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2692 self.mouse_click(area.x, area.y, col, row);
2693 }
2694
2695 pub fn mouse_begin_drag(&mut self) {
2697 if !self.vim.is_visual_char() {
2698 let cursor = self.cursor();
2699 self.vim.enter_visual(cursor);
2700 }
2701 }
2702
2703 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2709 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2710 buf_set_cursor_rc(&mut self.buffer, r, c);
2711 }
2712
2713 #[cfg(feature = "ratatui")]
2719 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2720 self.mouse_extend_drag(area.x, area.y, col, row);
2721 }
2722
2723 pub fn insert_str(&mut self, text: &str) {
2724 let pos = crate::types::Cursor::cursor(&self.buffer);
2725 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2726 self.push_buffer_content_to_textarea();
2727 self.mark_content_dirty();
2728 }
2729
2730 pub fn accept_completion(&mut self, completion: &str) {
2731 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2732 let cursor_pos = CursorTrait::cursor(&self.buffer);
2733 let cursor_row = cursor_pos.line as usize;
2734 let cursor_col = cursor_pos.col as usize;
2735 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2736 let chars: Vec<char> = line.chars().collect();
2737 let prefix_len = chars[..cursor_col.min(chars.len())]
2738 .iter()
2739 .rev()
2740 .take_while(|c| c.is_alphanumeric() || **c == '_')
2741 .count();
2742 if prefix_len > 0 {
2743 let start = Pos {
2744 line: cursor_row as u32,
2745 col: (cursor_col - prefix_len) as u32,
2746 };
2747 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2748 }
2749 let cursor = CursorTrait::cursor(&self.buffer);
2750 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2751 self.push_buffer_content_to_textarea();
2752 self.mark_content_dirty();
2753 }
2754
2755 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2756 let rc = buf_cursor_rc(&self.buffer);
2757 (buf_lines_to_vec(&self.buffer), rc)
2758 }
2759
2760 pub fn undo(&mut self) {
2764 crate::vim::do_undo(self);
2765 }
2766
2767 pub fn redo(&mut self) {
2770 crate::vim::do_redo(self);
2771 }
2772
2773 pub fn push_undo(&mut self) {
2778 let snap = self.snapshot();
2779 self.undo_stack.push(snap);
2780 self.cap_undo();
2781 self.redo_stack.clear();
2782 }
2783
2784 pub(crate) fn cap_undo(&mut self) {
2790 let cap = self.settings.undo_levels as usize;
2791 if cap > 0 && self.undo_stack.len() > cap {
2792 let diff = self.undo_stack.len() - cap;
2793 self.undo_stack.drain(..diff);
2794 }
2795 }
2796
2797 #[doc(hidden)]
2799 pub fn undo_stack_len(&self) -> usize {
2800 self.undo_stack.len()
2801 }
2802
2803 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2807 let text = lines.join("\n");
2808 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2809 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2810 self.pending_content_edits.clear();
2812 self.pending_content_reset = true;
2813 self.mark_content_dirty();
2814 }
2815
2816 #[cfg(feature = "crossterm")]
2818 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
2819 let input = crossterm_to_input(key);
2820 if input.key == Key::Null {
2821 return false;
2822 }
2823 let consumed = vim::step(self, input);
2824 self.emit_cursor_shape_if_changed();
2825 consumed
2826 }
2827}
2828
2829fn visual_col_for_char(line: &str, char_col: usize, tab_width: usize) -> usize {
2834 let mut visual = 0usize;
2835 for (i, ch) in line.chars().enumerate() {
2836 if i >= char_col {
2837 break;
2838 }
2839 if ch == '\t' {
2840 visual += tab_width - (visual % tab_width);
2841 } else {
2842 visual += 1;
2843 }
2844 }
2845 visual
2846}
2847
2848#[cfg(feature = "crossterm")]
2849impl From<KeyEvent> for Input {
2850 fn from(key: KeyEvent) -> Self {
2851 let k = match key.code {
2852 KeyCode::Char(c) => Key::Char(c),
2853 KeyCode::Backspace => Key::Backspace,
2854 KeyCode::Delete => Key::Delete,
2855 KeyCode::Enter => Key::Enter,
2856 KeyCode::Left => Key::Left,
2857 KeyCode::Right => Key::Right,
2858 KeyCode::Up => Key::Up,
2859 KeyCode::Down => Key::Down,
2860 KeyCode::Home => Key::Home,
2861 KeyCode::End => Key::End,
2862 KeyCode::Tab => Key::Tab,
2863 KeyCode::Esc => Key::Esc,
2864 _ => Key::Null,
2865 };
2866 Input {
2867 key: k,
2868 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
2869 alt: key.modifiers.contains(KeyModifiers::ALT),
2870 shift: key.modifiers.contains(KeyModifiers::SHIFT),
2871 }
2872 }
2873}
2874
2875#[cfg(feature = "crossterm")]
2879pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
2880 Input::from(key)
2881}
2882
2883#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
2884mod tests {
2885 use super::*;
2886 use crate::types::Host;
2887 use crossterm::event::KeyEvent;
2888
2889 fn key(code: KeyCode) -> KeyEvent {
2890 KeyEvent::new(code, KeyModifiers::NONE)
2891 }
2892 fn shift_key(code: KeyCode) -> KeyEvent {
2893 KeyEvent::new(code, KeyModifiers::SHIFT)
2894 }
2895 fn ctrl_key(code: KeyCode) -> KeyEvent {
2896 KeyEvent::new(code, KeyModifiers::CONTROL)
2897 }
2898
2899 #[test]
2900 fn vim_normal_to_insert() {
2901 let mut e = Editor::new(
2902 hjkl_buffer::Buffer::new(),
2903 crate::types::DefaultHost::new(),
2904 crate::types::Options::default(),
2905 );
2906 e.handle_key(key(KeyCode::Char('i')));
2907 assert_eq!(e.vim_mode(), VimMode::Insert);
2908 }
2909
2910 #[test]
2911 fn with_options_constructs_from_spec_options() {
2912 let opts = crate::types::Options {
2916 shiftwidth: 4,
2917 tabstop: 4,
2918 expandtab: true,
2919 iskeyword: "@,a-z".to_string(),
2920 wrap: crate::types::WrapMode::Word,
2921 ..crate::types::Options::default()
2922 };
2923 let mut e = Editor::new(
2924 hjkl_buffer::Buffer::new(),
2925 crate::types::DefaultHost::new(),
2926 opts,
2927 );
2928 assert_eq!(e.settings().shiftwidth, 4);
2929 assert_eq!(e.settings().tabstop, 4);
2930 assert!(e.settings().expandtab);
2931 assert_eq!(e.settings().iskeyword, "@,a-z");
2932 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
2933 e.handle_key(key(KeyCode::Char('i')));
2935 assert_eq!(e.vim_mode(), VimMode::Insert);
2936 }
2937
2938 #[test]
2939 fn feed_input_char_routes_through_handle_key() {
2940 use crate::{Modifiers, PlannedInput};
2941 let mut e = Editor::new(
2942 hjkl_buffer::Buffer::new(),
2943 crate::types::DefaultHost::new(),
2944 crate::types::Options::default(),
2945 );
2946 e.set_content("abc");
2947 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2949 assert_eq!(e.vim_mode(), VimMode::Insert);
2950 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
2952 assert!(e.content().contains('X'));
2953 }
2954
2955 #[test]
2956 fn feed_input_special_key_routes() {
2957 use crate::{Modifiers, PlannedInput, SpecialKey};
2958 let mut e = Editor::new(
2959 hjkl_buffer::Buffer::new(),
2960 crate::types::DefaultHost::new(),
2961 crate::types::Options::default(),
2962 );
2963 e.set_content("abc");
2964 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2965 assert_eq!(e.vim_mode(), VimMode::Insert);
2966 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
2967 assert_eq!(e.vim_mode(), VimMode::Normal);
2968 }
2969
2970 #[test]
2971 fn feed_input_mouse_paste_focus_resize_no_op() {
2972 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
2973 let mut e = Editor::new(
2974 hjkl_buffer::Buffer::new(),
2975 crate::types::DefaultHost::new(),
2976 crate::types::Options::default(),
2977 );
2978 e.set_content("abc");
2979 let mode_before = e.vim_mode();
2980 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
2981 kind: MouseKind::Press,
2982 pos: Pos::new(0, 0),
2983 mods: Default::default(),
2984 }));
2985 assert!(!consumed);
2986 assert_eq!(e.vim_mode(), mode_before);
2987 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
2988 assert!(!e.feed_input(PlannedInput::FocusGained));
2989 assert!(!e.feed_input(PlannedInput::FocusLost));
2990 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
2991 }
2992
2993 #[test]
2994 fn intern_style_dedups_engine_native_styles() {
2995 use crate::types::{Attrs, Color, Style};
2996 let mut e = Editor::new(
2997 hjkl_buffer::Buffer::new(),
2998 crate::types::DefaultHost::new(),
2999 crate::types::Options::default(),
3000 );
3001 let s = Style {
3002 fg: Some(Color(255, 0, 0)),
3003 bg: None,
3004 attrs: Attrs::BOLD,
3005 };
3006 let id_a = e.intern_style(s);
3007 let id_b = e.intern_style(s);
3009 assert_eq!(id_a, id_b);
3010 let back = e.engine_style_at(id_a).expect("interned");
3012 assert_eq!(back, s);
3013 }
3014
3015 #[test]
3016 fn engine_style_at_out_of_range_returns_none() {
3017 let e = Editor::new(
3018 hjkl_buffer::Buffer::new(),
3019 crate::types::DefaultHost::new(),
3020 crate::types::Options::default(),
3021 );
3022 assert!(e.engine_style_at(99).is_none());
3023 }
3024
3025 #[test]
3026 fn take_changes_emits_per_row_for_block_insert() {
3027 let mut e = Editor::new(
3032 hjkl_buffer::Buffer::new(),
3033 crate::types::DefaultHost::new(),
3034 crate::types::Options::default(),
3035 );
3036 e.set_content("aaa\nbbb\nccc\nddd");
3037 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
3039 e.handle_key(key(KeyCode::Char('j')));
3040 e.handle_key(key(KeyCode::Char('j')));
3041 e.handle_key(shift_key(KeyCode::Char('I')));
3043 e.handle_key(key(KeyCode::Char('X')));
3044 e.handle_key(key(KeyCode::Esc));
3045
3046 let changes = e.take_changes();
3047 assert!(
3051 changes.len() >= 3,
3052 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
3053 changes.len()
3054 );
3055 }
3056
3057 #[test]
3058 fn take_changes_drains_after_insert() {
3059 let mut e = Editor::new(
3060 hjkl_buffer::Buffer::new(),
3061 crate::types::DefaultHost::new(),
3062 crate::types::Options::default(),
3063 );
3064 e.set_content("abc");
3065 assert!(e.take_changes().is_empty());
3067 e.handle_key(key(KeyCode::Char('i')));
3069 e.handle_key(key(KeyCode::Char('X')));
3070 let changes = e.take_changes();
3071 assert!(
3072 !changes.is_empty(),
3073 "insert mode keystroke should produce a change"
3074 );
3075 assert!(e.take_changes().is_empty());
3077 }
3078
3079 #[test]
3080 fn options_bridge_roundtrip() {
3081 let mut e = Editor::new(
3082 hjkl_buffer::Buffer::new(),
3083 crate::types::DefaultHost::new(),
3084 crate::types::Options::default(),
3085 );
3086 let opts = e.current_options();
3087 assert_eq!(opts.shiftwidth, 4);
3089 assert_eq!(opts.tabstop, 4);
3090
3091 let new_opts = crate::types::Options {
3092 shiftwidth: 4,
3093 tabstop: 2,
3094 ignorecase: true,
3095 ..crate::types::Options::default()
3096 };
3097 e.apply_options(&new_opts);
3098
3099 let after = e.current_options();
3100 assert_eq!(after.shiftwidth, 4);
3101 assert_eq!(after.tabstop, 2);
3102 assert!(after.ignorecase);
3103 }
3104
3105 #[test]
3106 fn selection_highlight_none_in_normal() {
3107 let mut e = Editor::new(
3108 hjkl_buffer::Buffer::new(),
3109 crate::types::DefaultHost::new(),
3110 crate::types::Options::default(),
3111 );
3112 e.set_content("hello");
3113 assert!(e.selection_highlight().is_none());
3114 }
3115
3116 #[test]
3117 fn selection_highlight_some_in_visual() {
3118 use crate::types::HighlightKind;
3119 let mut e = Editor::new(
3120 hjkl_buffer::Buffer::new(),
3121 crate::types::DefaultHost::new(),
3122 crate::types::Options::default(),
3123 );
3124 e.set_content("hello world");
3125 e.handle_key(key(KeyCode::Char('v')));
3126 e.handle_key(key(KeyCode::Char('l')));
3127 e.handle_key(key(KeyCode::Char('l')));
3128 let h = e
3129 .selection_highlight()
3130 .expect("visual mode should produce a highlight");
3131 assert_eq!(h.kind, HighlightKind::Selection);
3132 assert_eq!(h.range.start.line, 0);
3133 assert_eq!(h.range.end.line, 0);
3134 }
3135
3136 #[test]
3137 fn highlights_emit_incsearch_during_active_prompt() {
3138 use crate::types::HighlightKind;
3139 let mut e = Editor::new(
3140 hjkl_buffer::Buffer::new(),
3141 crate::types::DefaultHost::new(),
3142 crate::types::Options::default(),
3143 );
3144 e.set_content("foo bar foo\nbaz\n");
3145 e.handle_key(key(KeyCode::Char('/')));
3147 e.handle_key(key(KeyCode::Char('f')));
3148 e.handle_key(key(KeyCode::Char('o')));
3149 e.handle_key(key(KeyCode::Char('o')));
3150 assert!(e.search_prompt().is_some());
3152 let hs = e.highlights_for_line(0);
3153 assert_eq!(hs.len(), 2);
3154 for h in &hs {
3155 assert_eq!(h.kind, HighlightKind::IncSearch);
3156 }
3157 }
3158
3159 #[test]
3160 fn highlights_empty_for_blank_prompt() {
3161 let mut e = Editor::new(
3162 hjkl_buffer::Buffer::new(),
3163 crate::types::DefaultHost::new(),
3164 crate::types::Options::default(),
3165 );
3166 e.set_content("foo");
3167 e.handle_key(key(KeyCode::Char('/')));
3168 assert!(e.search_prompt().is_some());
3170 assert!(e.highlights_for_line(0).is_empty());
3171 }
3172
3173 #[test]
3174 fn highlights_emit_search_matches() {
3175 use crate::types::HighlightKind;
3176 let mut e = Editor::new(
3177 hjkl_buffer::Buffer::new(),
3178 crate::types::DefaultHost::new(),
3179 crate::types::Options::default(),
3180 );
3181 e.set_content("foo bar foo\nbaz qux\n");
3182 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3186 let hs = e.highlights_for_line(0);
3187 assert_eq!(hs.len(), 2);
3188 for h in &hs {
3189 assert_eq!(h.kind, HighlightKind::SearchMatch);
3190 assert_eq!(h.range.start.line, 0);
3191 assert_eq!(h.range.end.line, 0);
3192 }
3193 }
3194
3195 #[test]
3196 fn highlights_empty_without_pattern() {
3197 let mut e = Editor::new(
3198 hjkl_buffer::Buffer::new(),
3199 crate::types::DefaultHost::new(),
3200 crate::types::Options::default(),
3201 );
3202 e.set_content("foo bar");
3203 assert!(e.highlights_for_line(0).is_empty());
3204 }
3205
3206 #[test]
3207 fn highlights_empty_for_out_of_range_line() {
3208 let mut e = Editor::new(
3209 hjkl_buffer::Buffer::new(),
3210 crate::types::DefaultHost::new(),
3211 crate::types::Options::default(),
3212 );
3213 e.set_content("foo");
3214 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3215 assert!(e.highlights_for_line(99).is_empty());
3216 }
3217
3218 #[test]
3219 fn render_frame_reflects_mode_and_cursor() {
3220 use crate::types::{CursorShape, SnapshotMode};
3221 let mut e = Editor::new(
3222 hjkl_buffer::Buffer::new(),
3223 crate::types::DefaultHost::new(),
3224 crate::types::Options::default(),
3225 );
3226 e.set_content("alpha\nbeta");
3227 let f = e.render_frame();
3228 assert_eq!(f.mode, SnapshotMode::Normal);
3229 assert_eq!(f.cursor_shape, CursorShape::Block);
3230 assert_eq!(f.line_count, 2);
3231
3232 e.handle_key(key(KeyCode::Char('i')));
3233 let f = e.render_frame();
3234 assert_eq!(f.mode, SnapshotMode::Insert);
3235 assert_eq!(f.cursor_shape, CursorShape::Bar);
3236 }
3237
3238 #[test]
3239 fn snapshot_roundtrips_through_restore() {
3240 use crate::types::SnapshotMode;
3241 let mut e = Editor::new(
3242 hjkl_buffer::Buffer::new(),
3243 crate::types::DefaultHost::new(),
3244 crate::types::Options::default(),
3245 );
3246 e.set_content("alpha\nbeta\ngamma");
3247 e.jump_cursor(2, 3);
3248 let snap = e.take_snapshot();
3249 assert_eq!(snap.mode, SnapshotMode::Normal);
3250 assert_eq!(snap.cursor, (2, 3));
3251 assert_eq!(snap.lines.len(), 3);
3252
3253 let mut other = Editor::new(
3254 hjkl_buffer::Buffer::new(),
3255 crate::types::DefaultHost::new(),
3256 crate::types::Options::default(),
3257 );
3258 other.restore_snapshot(snap).expect("restore");
3259 assert_eq!(other.cursor(), (2, 3));
3260 assert_eq!(other.buffer().lines().len(), 3);
3261 }
3262
3263 #[test]
3264 fn restore_snapshot_rejects_version_mismatch() {
3265 let mut e = Editor::new(
3266 hjkl_buffer::Buffer::new(),
3267 crate::types::DefaultHost::new(),
3268 crate::types::Options::default(),
3269 );
3270 let mut snap = e.take_snapshot();
3271 snap.version = 9999;
3272 match e.restore_snapshot(snap) {
3273 Err(crate::EngineError::SnapshotVersion(got, want)) => {
3274 assert_eq!(got, 9999);
3275 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
3276 }
3277 other => panic!("expected SnapshotVersion err, got {other:?}"),
3278 }
3279 }
3280
3281 #[test]
3282 fn take_content_change_returns_some_on_first_dirty() {
3283 let mut e = Editor::new(
3284 hjkl_buffer::Buffer::new(),
3285 crate::types::DefaultHost::new(),
3286 crate::types::Options::default(),
3287 );
3288 e.set_content("hello");
3289 let first = e.take_content_change();
3290 assert!(first.is_some());
3291 let second = e.take_content_change();
3292 assert!(second.is_none());
3293 }
3294
3295 #[test]
3296 fn take_content_change_none_until_mutation() {
3297 let mut e = Editor::new(
3298 hjkl_buffer::Buffer::new(),
3299 crate::types::DefaultHost::new(),
3300 crate::types::Options::default(),
3301 );
3302 e.set_content("hello");
3303 e.take_content_change();
3305 assert!(e.take_content_change().is_none());
3306 e.handle_key(key(KeyCode::Char('i')));
3308 e.handle_key(key(KeyCode::Char('x')));
3309 let after = e.take_content_change();
3310 assert!(after.is_some());
3311 assert!(after.unwrap().contains('x'));
3312 }
3313
3314 #[test]
3315 fn vim_insert_to_normal() {
3316 let mut e = Editor::new(
3317 hjkl_buffer::Buffer::new(),
3318 crate::types::DefaultHost::new(),
3319 crate::types::Options::default(),
3320 );
3321 e.handle_key(key(KeyCode::Char('i')));
3322 e.handle_key(key(KeyCode::Esc));
3323 assert_eq!(e.vim_mode(), VimMode::Normal);
3324 }
3325
3326 #[test]
3327 fn vim_normal_to_visual() {
3328 let mut e = Editor::new(
3329 hjkl_buffer::Buffer::new(),
3330 crate::types::DefaultHost::new(),
3331 crate::types::Options::default(),
3332 );
3333 e.handle_key(key(KeyCode::Char('v')));
3334 assert_eq!(e.vim_mode(), VimMode::Visual);
3335 }
3336
3337 #[test]
3338 fn vim_visual_to_normal() {
3339 let mut e = Editor::new(
3340 hjkl_buffer::Buffer::new(),
3341 crate::types::DefaultHost::new(),
3342 crate::types::Options::default(),
3343 );
3344 e.handle_key(key(KeyCode::Char('v')));
3345 e.handle_key(key(KeyCode::Esc));
3346 assert_eq!(e.vim_mode(), VimMode::Normal);
3347 }
3348
3349 #[test]
3350 fn vim_shift_i_moves_to_first_non_whitespace() {
3351 let mut e = Editor::new(
3352 hjkl_buffer::Buffer::new(),
3353 crate::types::DefaultHost::new(),
3354 crate::types::Options::default(),
3355 );
3356 e.set_content(" hello");
3357 e.jump_cursor(0, 8);
3358 e.handle_key(shift_key(KeyCode::Char('I')));
3359 assert_eq!(e.vim_mode(), VimMode::Insert);
3360 assert_eq!(e.cursor(), (0, 3));
3361 }
3362
3363 #[test]
3364 fn vim_shift_a_moves_to_end_and_insert() {
3365 let mut e = Editor::new(
3366 hjkl_buffer::Buffer::new(),
3367 crate::types::DefaultHost::new(),
3368 crate::types::Options::default(),
3369 );
3370 e.set_content("hello");
3371 e.handle_key(shift_key(KeyCode::Char('A')));
3372 assert_eq!(e.vim_mode(), VimMode::Insert);
3373 assert_eq!(e.cursor().1, 5);
3374 }
3375
3376 #[test]
3377 fn count_10j_moves_down_10() {
3378 let mut e = Editor::new(
3379 hjkl_buffer::Buffer::new(),
3380 crate::types::DefaultHost::new(),
3381 crate::types::Options::default(),
3382 );
3383 e.set_content(
3384 (0..20)
3385 .map(|i| format!("line{i}"))
3386 .collect::<Vec<_>>()
3387 .join("\n")
3388 .as_str(),
3389 );
3390 for d in "10".chars() {
3391 e.handle_key(key(KeyCode::Char(d)));
3392 }
3393 e.handle_key(key(KeyCode::Char('j')));
3394 assert_eq!(e.cursor().0, 10);
3395 }
3396
3397 #[test]
3398 fn count_o_repeats_insert_on_esc() {
3399 let mut e = Editor::new(
3400 hjkl_buffer::Buffer::new(),
3401 crate::types::DefaultHost::new(),
3402 crate::types::Options::default(),
3403 );
3404 e.set_content("hello");
3405 for d in "3".chars() {
3406 e.handle_key(key(KeyCode::Char(d)));
3407 }
3408 e.handle_key(key(KeyCode::Char('o')));
3409 assert_eq!(e.vim_mode(), VimMode::Insert);
3410 for c in "world".chars() {
3411 e.handle_key(key(KeyCode::Char(c)));
3412 }
3413 e.handle_key(key(KeyCode::Esc));
3414 assert_eq!(e.vim_mode(), VimMode::Normal);
3415 assert_eq!(e.buffer().lines().len(), 4);
3416 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
3417 }
3418
3419 #[test]
3420 fn count_i_repeats_text_on_esc() {
3421 let mut e = Editor::new(
3422 hjkl_buffer::Buffer::new(),
3423 crate::types::DefaultHost::new(),
3424 crate::types::Options::default(),
3425 );
3426 e.set_content("");
3427 for d in "3".chars() {
3428 e.handle_key(key(KeyCode::Char(d)));
3429 }
3430 e.handle_key(key(KeyCode::Char('i')));
3431 for c in "ab".chars() {
3432 e.handle_key(key(KeyCode::Char(c)));
3433 }
3434 e.handle_key(key(KeyCode::Esc));
3435 assert_eq!(e.vim_mode(), VimMode::Normal);
3436 assert_eq!(e.buffer().lines()[0], "ababab");
3437 }
3438
3439 #[test]
3440 fn vim_shift_o_opens_line_above() {
3441 let mut e = Editor::new(
3442 hjkl_buffer::Buffer::new(),
3443 crate::types::DefaultHost::new(),
3444 crate::types::Options::default(),
3445 );
3446 e.set_content("hello");
3447 e.handle_key(shift_key(KeyCode::Char('O')));
3448 assert_eq!(e.vim_mode(), VimMode::Insert);
3449 assert_eq!(e.cursor(), (0, 0));
3450 assert_eq!(e.buffer().lines().len(), 2);
3451 }
3452
3453 #[test]
3454 fn vim_gg_goes_to_top() {
3455 let mut e = Editor::new(
3456 hjkl_buffer::Buffer::new(),
3457 crate::types::DefaultHost::new(),
3458 crate::types::Options::default(),
3459 );
3460 e.set_content("a\nb\nc");
3461 e.jump_cursor(2, 0);
3462 e.handle_key(key(KeyCode::Char('g')));
3463 e.handle_key(key(KeyCode::Char('g')));
3464 assert_eq!(e.cursor().0, 0);
3465 }
3466
3467 #[test]
3468 fn vim_shift_g_goes_to_bottom() {
3469 let mut e = Editor::new(
3470 hjkl_buffer::Buffer::new(),
3471 crate::types::DefaultHost::new(),
3472 crate::types::Options::default(),
3473 );
3474 e.set_content("a\nb\nc");
3475 e.handle_key(shift_key(KeyCode::Char('G')));
3476 assert_eq!(e.cursor().0, 2);
3477 }
3478
3479 #[test]
3480 fn vim_dd_deletes_line() {
3481 let mut e = Editor::new(
3482 hjkl_buffer::Buffer::new(),
3483 crate::types::DefaultHost::new(),
3484 crate::types::Options::default(),
3485 );
3486 e.set_content("first\nsecond");
3487 e.handle_key(key(KeyCode::Char('d')));
3488 e.handle_key(key(KeyCode::Char('d')));
3489 assert_eq!(e.buffer().lines().len(), 1);
3490 assert_eq!(e.buffer().lines()[0], "second");
3491 }
3492
3493 #[test]
3494 fn vim_dw_deletes_word() {
3495 let mut e = Editor::new(
3496 hjkl_buffer::Buffer::new(),
3497 crate::types::DefaultHost::new(),
3498 crate::types::Options::default(),
3499 );
3500 e.set_content("hello world");
3501 e.handle_key(key(KeyCode::Char('d')));
3502 e.handle_key(key(KeyCode::Char('w')));
3503 assert_eq!(e.vim_mode(), VimMode::Normal);
3504 assert!(!e.buffer().lines()[0].starts_with("hello"));
3505 }
3506
3507 #[test]
3508 fn vim_yy_yanks_line() {
3509 let mut e = Editor::new(
3510 hjkl_buffer::Buffer::new(),
3511 crate::types::DefaultHost::new(),
3512 crate::types::Options::default(),
3513 );
3514 e.set_content("hello\nworld");
3515 e.handle_key(key(KeyCode::Char('y')));
3516 e.handle_key(key(KeyCode::Char('y')));
3517 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3518 }
3519
3520 #[test]
3521 fn vim_yy_does_not_move_cursor() {
3522 let mut e = Editor::new(
3523 hjkl_buffer::Buffer::new(),
3524 crate::types::DefaultHost::new(),
3525 crate::types::Options::default(),
3526 );
3527 e.set_content("first\nsecond\nthird");
3528 e.jump_cursor(1, 0);
3529 let before = e.cursor();
3530 e.handle_key(key(KeyCode::Char('y')));
3531 e.handle_key(key(KeyCode::Char('y')));
3532 assert_eq!(e.cursor(), before);
3533 assert_eq!(e.vim_mode(), VimMode::Normal);
3534 }
3535
3536 #[test]
3537 fn vim_yw_yanks_word() {
3538 let mut e = Editor::new(
3539 hjkl_buffer::Buffer::new(),
3540 crate::types::DefaultHost::new(),
3541 crate::types::Options::default(),
3542 );
3543 e.set_content("hello world");
3544 e.handle_key(key(KeyCode::Char('y')));
3545 e.handle_key(key(KeyCode::Char('w')));
3546 assert_eq!(e.vim_mode(), VimMode::Normal);
3547 assert!(e.last_yank.is_some());
3548 }
3549
3550 #[test]
3551 fn vim_cc_changes_line() {
3552 let mut e = Editor::new(
3553 hjkl_buffer::Buffer::new(),
3554 crate::types::DefaultHost::new(),
3555 crate::types::Options::default(),
3556 );
3557 e.set_content("hello\nworld");
3558 e.handle_key(key(KeyCode::Char('c')));
3559 e.handle_key(key(KeyCode::Char('c')));
3560 assert_eq!(e.vim_mode(), VimMode::Insert);
3561 }
3562
3563 #[test]
3564 fn vim_u_undoes_insert_session_as_chunk() {
3565 let mut e = Editor::new(
3566 hjkl_buffer::Buffer::new(),
3567 crate::types::DefaultHost::new(),
3568 crate::types::Options::default(),
3569 );
3570 e.set_content("hello");
3571 e.handle_key(key(KeyCode::Char('i')));
3572 e.handle_key(key(KeyCode::Enter));
3573 e.handle_key(key(KeyCode::Enter));
3574 e.handle_key(key(KeyCode::Esc));
3575 assert_eq!(e.buffer().lines().len(), 3);
3576 e.handle_key(key(KeyCode::Char('u')));
3577 assert_eq!(e.buffer().lines().len(), 1);
3578 assert_eq!(e.buffer().lines()[0], "hello");
3579 }
3580
3581 #[test]
3582 fn vim_undo_redo_roundtrip() {
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("hello");
3589 e.handle_key(key(KeyCode::Char('i')));
3590 for c in "world".chars() {
3591 e.handle_key(key(KeyCode::Char(c)));
3592 }
3593 e.handle_key(key(KeyCode::Esc));
3594 let after = e.buffer().lines()[0].clone();
3595 e.handle_key(key(KeyCode::Char('u')));
3596 assert_eq!(e.buffer().lines()[0], "hello");
3597 e.handle_key(ctrl_key(KeyCode::Char('r')));
3598 assert_eq!(e.buffer().lines()[0], after);
3599 }
3600
3601 #[test]
3602 fn vim_u_undoes_dd() {
3603 let mut e = Editor::new(
3604 hjkl_buffer::Buffer::new(),
3605 crate::types::DefaultHost::new(),
3606 crate::types::Options::default(),
3607 );
3608 e.set_content("first\nsecond");
3609 e.handle_key(key(KeyCode::Char('d')));
3610 e.handle_key(key(KeyCode::Char('d')));
3611 assert_eq!(e.buffer().lines().len(), 1);
3612 e.handle_key(key(KeyCode::Char('u')));
3613 assert_eq!(e.buffer().lines().len(), 2);
3614 assert_eq!(e.buffer().lines()[0], "first");
3615 }
3616
3617 #[test]
3618 fn vim_ctrl_r_redoes() {
3619 let mut e = Editor::new(
3620 hjkl_buffer::Buffer::new(),
3621 crate::types::DefaultHost::new(),
3622 crate::types::Options::default(),
3623 );
3624 e.set_content("hello");
3625 e.handle_key(ctrl_key(KeyCode::Char('r')));
3626 }
3627
3628 #[test]
3629 fn vim_r_replaces_char() {
3630 let mut e = Editor::new(
3631 hjkl_buffer::Buffer::new(),
3632 crate::types::DefaultHost::new(),
3633 crate::types::Options::default(),
3634 );
3635 e.set_content("hello");
3636 e.handle_key(key(KeyCode::Char('r')));
3637 e.handle_key(key(KeyCode::Char('x')));
3638 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
3639 }
3640
3641 #[test]
3642 fn vim_tilde_toggles_case() {
3643 let mut e = Editor::new(
3644 hjkl_buffer::Buffer::new(),
3645 crate::types::DefaultHost::new(),
3646 crate::types::Options::default(),
3647 );
3648 e.set_content("hello");
3649 e.handle_key(key(KeyCode::Char('~')));
3650 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
3651 }
3652
3653 #[test]
3654 fn vim_visual_d_cuts() {
3655 let mut e = Editor::new(
3656 hjkl_buffer::Buffer::new(),
3657 crate::types::DefaultHost::new(),
3658 crate::types::Options::default(),
3659 );
3660 e.set_content("hello");
3661 e.handle_key(key(KeyCode::Char('v')));
3662 e.handle_key(key(KeyCode::Char('l')));
3663 e.handle_key(key(KeyCode::Char('l')));
3664 e.handle_key(key(KeyCode::Char('d')));
3665 assert_eq!(e.vim_mode(), VimMode::Normal);
3666 assert!(e.last_yank.is_some());
3667 }
3668
3669 #[test]
3670 fn vim_visual_c_enters_insert() {
3671 let mut e = Editor::new(
3672 hjkl_buffer::Buffer::new(),
3673 crate::types::DefaultHost::new(),
3674 crate::types::Options::default(),
3675 );
3676 e.set_content("hello");
3677 e.handle_key(key(KeyCode::Char('v')));
3678 e.handle_key(key(KeyCode::Char('l')));
3679 e.handle_key(key(KeyCode::Char('c')));
3680 assert_eq!(e.vim_mode(), VimMode::Insert);
3681 }
3682
3683 #[test]
3684 fn vim_normal_unknown_key_consumed() {
3685 let mut e = Editor::new(
3686 hjkl_buffer::Buffer::new(),
3687 crate::types::DefaultHost::new(),
3688 crate::types::Options::default(),
3689 );
3690 let consumed = e.handle_key(key(KeyCode::Char('z')));
3692 assert!(consumed);
3693 }
3694
3695 #[test]
3696 fn force_normal_clears_operator() {
3697 let mut e = Editor::new(
3698 hjkl_buffer::Buffer::new(),
3699 crate::types::DefaultHost::new(),
3700 crate::types::Options::default(),
3701 );
3702 e.handle_key(key(KeyCode::Char('d')));
3703 e.force_normal();
3704 assert_eq!(e.vim_mode(), VimMode::Normal);
3705 }
3706
3707 fn many_lines(n: usize) -> String {
3708 (0..n)
3709 .map(|i| format!("line{i}"))
3710 .collect::<Vec<_>>()
3711 .join("\n")
3712 }
3713
3714 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
3715 e.set_viewport_height(height);
3716 }
3717
3718 #[test]
3719 fn zz_centers_cursor_in_viewport() {
3720 let mut e = Editor::new(
3721 hjkl_buffer::Buffer::new(),
3722 crate::types::DefaultHost::new(),
3723 crate::types::Options::default(),
3724 );
3725 e.set_content(&many_lines(100));
3726 prime_viewport(&mut e, 20);
3727 e.jump_cursor(50, 0);
3728 e.handle_key(key(KeyCode::Char('z')));
3729 e.handle_key(key(KeyCode::Char('z')));
3730 assert_eq!(e.host().viewport().top_row, 40);
3731 assert_eq!(e.cursor().0, 50);
3732 }
3733
3734 #[test]
3735 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
3736 let mut e = Editor::new(
3737 hjkl_buffer::Buffer::new(),
3738 crate::types::DefaultHost::new(),
3739 crate::types::Options::default(),
3740 );
3741 e.set_content(&many_lines(100));
3742 prime_viewport(&mut e, 20);
3743 e.jump_cursor(50, 0);
3744 e.handle_key(key(KeyCode::Char('z')));
3745 e.handle_key(key(KeyCode::Char('t')));
3746 assert_eq!(e.host().viewport().top_row, 45);
3749 assert_eq!(e.cursor().0, 50);
3750 }
3751
3752 #[test]
3753 fn ctrl_a_increments_number_at_cursor() {
3754 let mut e = Editor::new(
3755 hjkl_buffer::Buffer::new(),
3756 crate::types::DefaultHost::new(),
3757 crate::types::Options::default(),
3758 );
3759 e.set_content("x = 41");
3760 e.handle_key(ctrl_key(KeyCode::Char('a')));
3761 assert_eq!(e.buffer().lines()[0], "x = 42");
3762 assert_eq!(e.cursor(), (0, 5));
3763 }
3764
3765 #[test]
3766 fn ctrl_a_finds_number_to_right_of_cursor() {
3767 let mut e = Editor::new(
3768 hjkl_buffer::Buffer::new(),
3769 crate::types::DefaultHost::new(),
3770 crate::types::Options::default(),
3771 );
3772 e.set_content("foo 99 bar");
3773 e.handle_key(ctrl_key(KeyCode::Char('a')));
3774 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
3775 assert_eq!(e.cursor(), (0, 6));
3776 }
3777
3778 #[test]
3779 fn ctrl_a_with_count_adds_count() {
3780 let mut e = Editor::new(
3781 hjkl_buffer::Buffer::new(),
3782 crate::types::DefaultHost::new(),
3783 crate::types::Options::default(),
3784 );
3785 e.set_content("x = 10");
3786 for d in "5".chars() {
3787 e.handle_key(key(KeyCode::Char(d)));
3788 }
3789 e.handle_key(ctrl_key(KeyCode::Char('a')));
3790 assert_eq!(e.buffer().lines()[0], "x = 15");
3791 }
3792
3793 #[test]
3794 fn ctrl_x_decrements_number() {
3795 let mut e = Editor::new(
3796 hjkl_buffer::Buffer::new(),
3797 crate::types::DefaultHost::new(),
3798 crate::types::Options::default(),
3799 );
3800 e.set_content("n=5");
3801 e.handle_key(ctrl_key(KeyCode::Char('x')));
3802 assert_eq!(e.buffer().lines()[0], "n=4");
3803 }
3804
3805 #[test]
3806 fn ctrl_x_crosses_zero_into_negative() {
3807 let mut e = Editor::new(
3808 hjkl_buffer::Buffer::new(),
3809 crate::types::DefaultHost::new(),
3810 crate::types::Options::default(),
3811 );
3812 e.set_content("v=0");
3813 e.handle_key(ctrl_key(KeyCode::Char('x')));
3814 assert_eq!(e.buffer().lines()[0], "v=-1");
3815 }
3816
3817 #[test]
3818 fn ctrl_a_on_negative_number_increments_toward_zero() {
3819 let mut e = Editor::new(
3820 hjkl_buffer::Buffer::new(),
3821 crate::types::DefaultHost::new(),
3822 crate::types::Options::default(),
3823 );
3824 e.set_content("a = -5");
3825 e.handle_key(ctrl_key(KeyCode::Char('a')));
3826 assert_eq!(e.buffer().lines()[0], "a = -4");
3827 }
3828
3829 #[test]
3830 fn ctrl_a_noop_when_no_digit_on_line() {
3831 let mut e = Editor::new(
3832 hjkl_buffer::Buffer::new(),
3833 crate::types::DefaultHost::new(),
3834 crate::types::Options::default(),
3835 );
3836 e.set_content("no digits here");
3837 e.handle_key(ctrl_key(KeyCode::Char('a')));
3838 assert_eq!(e.buffer().lines()[0], "no digits here");
3839 }
3840
3841 #[test]
3842 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
3843 let mut e = Editor::new(
3844 hjkl_buffer::Buffer::new(),
3845 crate::types::DefaultHost::new(),
3846 crate::types::Options::default(),
3847 );
3848 e.set_content(&many_lines(100));
3849 prime_viewport(&mut e, 20);
3850 e.jump_cursor(50, 0);
3851 e.handle_key(key(KeyCode::Char('z')));
3852 e.handle_key(key(KeyCode::Char('b')));
3853 assert_eq!(e.host().viewport().top_row, 36);
3857 assert_eq!(e.cursor().0, 50);
3858 }
3859
3860 #[test]
3867 fn set_content_dirties_then_take_dirty_clears() {
3868 let mut e = Editor::new(
3869 hjkl_buffer::Buffer::new(),
3870 crate::types::DefaultHost::new(),
3871 crate::types::Options::default(),
3872 );
3873 e.set_content("hello");
3874 assert!(
3875 e.take_dirty(),
3876 "set_content should leave content_dirty=true"
3877 );
3878 assert!(!e.take_dirty(), "take_dirty should clear the flag");
3879 }
3880
3881 #[test]
3882 fn content_arc_returns_same_arc_until_mutation() {
3883 let mut e = Editor::new(
3884 hjkl_buffer::Buffer::new(),
3885 crate::types::DefaultHost::new(),
3886 crate::types::Options::default(),
3887 );
3888 e.set_content("hello");
3889 let a = e.content_arc();
3890 let b = e.content_arc();
3891 assert!(
3892 std::sync::Arc::ptr_eq(&a, &b),
3893 "repeated content_arc() should hit the cache"
3894 );
3895
3896 e.handle_key(key(KeyCode::Char('i')));
3898 e.handle_key(key(KeyCode::Char('!')));
3899 let c = e.content_arc();
3900 assert!(
3901 !std::sync::Arc::ptr_eq(&a, &c),
3902 "mutation should invalidate content_arc() cache"
3903 );
3904 assert!(c.contains('!'));
3905 }
3906
3907 #[test]
3908 fn content_arc_cache_invalidated_by_set_content() {
3909 let mut e = Editor::new(
3910 hjkl_buffer::Buffer::new(),
3911 crate::types::DefaultHost::new(),
3912 crate::types::Options::default(),
3913 );
3914 e.set_content("one");
3915 let a = e.content_arc();
3916 e.set_content("two");
3917 let b = e.content_arc();
3918 assert!(!std::sync::Arc::ptr_eq(&a, &b));
3919 assert!(b.starts_with("two"));
3920 }
3921
3922 #[test]
3928 fn mouse_click_past_eol_lands_on_last_char() {
3929 let mut e = Editor::new(
3930 hjkl_buffer::Buffer::new(),
3931 crate::types::DefaultHost::new(),
3932 crate::types::Options::default(),
3933 );
3934 e.set_content("hello");
3935 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3939 e.mouse_click_in_rect(area, 78, 1);
3940 assert_eq!(e.cursor(), (0, 4));
3941 }
3942
3943 #[test]
3944 fn mouse_click_past_eol_handles_multibyte_line() {
3945 let mut e = Editor::new(
3946 hjkl_buffer::Buffer::new(),
3947 crate::types::DefaultHost::new(),
3948 crate::types::Options::default(),
3949 );
3950 e.set_content("héllo");
3953 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3954 e.mouse_click_in_rect(area, 78, 1);
3955 assert_eq!(e.cursor(), (0, 4));
3956 }
3957
3958 #[test]
3959 fn mouse_click_inside_line_lands_on_clicked_char() {
3960 let mut e = Editor::new(
3961 hjkl_buffer::Buffer::new(),
3962 crate::types::DefaultHost::new(),
3963 crate::types::Options::default(),
3964 );
3965 e.set_content("hello world");
3966 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3970 e.mouse_click_in_rect(area, 5, 1);
3971 assert_eq!(e.cursor(), (0, 0));
3972 e.mouse_click_in_rect(area, 7, 1);
3973 assert_eq!(e.cursor(), (0, 2));
3974 }
3975
3976 #[test]
3981 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
3982 let mut e = Editor::new(
3983 hjkl_buffer::Buffer::new(),
3984 crate::types::DefaultHost::new(),
3985 crate::types::Options::default(),
3986 );
3987 e.set_content("hello world");
3988 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3989 assert!(e.settings().undo_break_on_motion);
3991 e.handle_key(key(KeyCode::Char('i')));
3993 e.handle_key(key(KeyCode::Char('A')));
3994 e.handle_key(key(KeyCode::Char('A')));
3995 e.handle_key(key(KeyCode::Char('A')));
3996 e.mouse_click_in_rect(area, 10, 1);
3998 e.handle_key(key(KeyCode::Char('B')));
4000 e.handle_key(key(KeyCode::Char('B')));
4001 e.handle_key(key(KeyCode::Char('B')));
4002 e.handle_key(key(KeyCode::Esc));
4004 e.handle_key(key(KeyCode::Char('u')));
4005 let line = e.buffer().line(0).unwrap_or("").to_string();
4006 assert!(
4007 line.contains("AAA"),
4008 "AAA must survive undo (separate group): {line:?}"
4009 );
4010 assert!(
4011 !line.contains("BBB"),
4012 "BBB must be undone (post-click group): {line:?}"
4013 );
4014 }
4015
4016 #[test]
4020 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
4021 let mut e = Editor::new(
4022 hjkl_buffer::Buffer::new(),
4023 crate::types::DefaultHost::new(),
4024 crate::types::Options::default(),
4025 );
4026 e.set_content("hello world");
4027 e.settings_mut().undo_break_on_motion = false;
4028 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
4029 e.handle_key(key(KeyCode::Char('i')));
4030 e.handle_key(key(KeyCode::Char('A')));
4031 e.handle_key(key(KeyCode::Char('A')));
4032 e.mouse_click_in_rect(area, 10, 1);
4033 e.handle_key(key(KeyCode::Char('B')));
4034 e.handle_key(key(KeyCode::Char('B')));
4035 e.handle_key(key(KeyCode::Esc));
4036 e.handle_key(key(KeyCode::Char('u')));
4037 let line = e.buffer().line(0).unwrap_or("").to_string();
4038 assert!(
4039 !line.contains("AA") && !line.contains("BB"),
4040 "with undobreak off, single `u` must reverse whole insert: {line:?}"
4041 );
4042 assert_eq!(line, "hello world");
4043 }
4044
4045 #[test]
4048 fn host_clipboard_round_trip_via_default_host() {
4049 let mut e = Editor::new(
4052 hjkl_buffer::Buffer::new(),
4053 crate::types::DefaultHost::new(),
4054 crate::types::Options::default(),
4055 );
4056 e.host_mut().write_clipboard("payload".to_string());
4057 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
4058 }
4059
4060 #[test]
4061 fn host_records_clipboard_on_yank() {
4062 let mut e = Editor::new(
4066 hjkl_buffer::Buffer::new(),
4067 crate::types::DefaultHost::new(),
4068 crate::types::Options::default(),
4069 );
4070 e.set_content("hello\n");
4071 e.handle_key(key(KeyCode::Char('y')));
4072 e.handle_key(key(KeyCode::Char('y')));
4073 let clip = e.host_mut().read_clipboard();
4075 assert!(
4076 clip.as_deref().unwrap_or("").starts_with("hello"),
4077 "host clipboard should carry the yank: {clip:?}"
4078 );
4079 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
4081 }
4082
4083 #[test]
4084 fn host_cursor_shape_via_shared_recorder() {
4085 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
4089 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
4090 struct LeakHost {
4091 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
4092 viewport: crate::types::Viewport,
4093 }
4094 impl crate::types::Host for LeakHost {
4095 type Intent = ();
4096 fn write_clipboard(&mut self, _: String) {}
4097 fn read_clipboard(&mut self) -> Option<String> {
4098 None
4099 }
4100 fn now(&self) -> core::time::Duration {
4101 core::time::Duration::ZERO
4102 }
4103 fn prompt_search(&mut self) -> Option<String> {
4104 None
4105 }
4106 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
4107 self.shapes.lock().unwrap().push(s);
4108 }
4109 fn viewport(&self) -> &crate::types::Viewport {
4110 &self.viewport
4111 }
4112 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4113 &mut self.viewport
4114 }
4115 fn emit_intent(&mut self, _: Self::Intent) {}
4116 }
4117 let mut e = Editor::new(
4118 hjkl_buffer::Buffer::new(),
4119 LeakHost {
4120 shapes: shapes_ptr,
4121 viewport: crate::types::Viewport::default(),
4122 },
4123 crate::types::Options::default(),
4124 );
4125 e.set_content("abc");
4126 e.handle_key(key(KeyCode::Char('i')));
4128 e.handle_key(key(KeyCode::Esc));
4130 let shapes = shapes_ptr.lock().unwrap().clone();
4131 assert_eq!(
4132 shapes,
4133 vec![
4134 crate::types::CursorShape::Bar,
4135 crate::types::CursorShape::Block,
4136 ],
4137 "host should observe Insert(Bar) → Normal(Block) transitions"
4138 );
4139 }
4140
4141 #[test]
4142 fn host_now_drives_chord_timeout_deterministically() {
4143 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
4148 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
4149 struct ClockHost {
4150 now: &'static std::sync::Mutex<core::time::Duration>,
4151 viewport: crate::types::Viewport,
4152 }
4153 impl crate::types::Host for ClockHost {
4154 type Intent = ();
4155 fn write_clipboard(&mut self, _: String) {}
4156 fn read_clipboard(&mut self) -> Option<String> {
4157 None
4158 }
4159 fn now(&self) -> core::time::Duration {
4160 *self.now.lock().unwrap()
4161 }
4162 fn prompt_search(&mut self) -> Option<String> {
4163 None
4164 }
4165 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
4166 fn viewport(&self) -> &crate::types::Viewport {
4167 &self.viewport
4168 }
4169 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4170 &mut self.viewport
4171 }
4172 fn emit_intent(&mut self, _: Self::Intent) {}
4173 }
4174 let mut e = Editor::new(
4175 hjkl_buffer::Buffer::new(),
4176 ClockHost {
4177 now: now_ptr,
4178 viewport: crate::types::Viewport::default(),
4179 },
4180 crate::types::Options::default(),
4181 );
4182 e.set_content("a\nb\nc\n");
4183 e.jump_cursor(2, 0);
4184 e.handle_key(key(KeyCode::Char('g')));
4186 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
4188 e.handle_key(key(KeyCode::Char('g')));
4191 assert_eq!(
4192 e.cursor().0,
4193 2,
4194 "Host::now() must drive `:set timeoutlen` deterministically"
4195 );
4196 }
4197
4198 fn fresh_editor(initial: &str) -> Editor {
4201 let buffer = hjkl_buffer::Buffer::from_str(initial);
4202 Editor::new(
4203 buffer,
4204 crate::types::DefaultHost::new(),
4205 crate::types::Options::default(),
4206 )
4207 }
4208
4209 #[test]
4210 fn content_edit_insert_char_at_origin() {
4211 let mut e = fresh_editor("");
4212 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4213 at: hjkl_buffer::Position::new(0, 0),
4214 ch: 'a',
4215 });
4216 let edits = e.take_content_edits();
4217 assert_eq!(edits.len(), 1);
4218 let ce = &edits[0];
4219 assert_eq!(ce.start_byte, 0);
4220 assert_eq!(ce.old_end_byte, 0);
4221 assert_eq!(ce.new_end_byte, 1);
4222 assert_eq!(ce.start_position, (0, 0));
4223 assert_eq!(ce.old_end_position, (0, 0));
4224 assert_eq!(ce.new_end_position, (0, 1));
4225 }
4226
4227 #[test]
4228 fn content_edit_insert_str_multiline() {
4229 let mut e = fresh_editor("x\ny");
4231 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertStr {
4232 at: hjkl_buffer::Position::new(0, 1),
4233 text: "ab\ncd".into(),
4234 });
4235 let edits = e.take_content_edits();
4236 assert_eq!(edits.len(), 1);
4237 let ce = &edits[0];
4238 assert_eq!(ce.start_byte, 1);
4239 assert_eq!(ce.old_end_byte, 1);
4240 assert_eq!(ce.new_end_byte, 1 + 5);
4241 assert_eq!(ce.start_position, (0, 1));
4242 assert_eq!(ce.new_end_position, (1, 2));
4244 }
4245
4246 #[test]
4247 fn content_edit_delete_range_charwise() {
4248 let mut e = fresh_editor("abcdef");
4250 let _ = e.mutate_edit(hjkl_buffer::Edit::DeleteRange {
4251 start: hjkl_buffer::Position::new(0, 1),
4252 end: hjkl_buffer::Position::new(0, 4),
4253 kind: hjkl_buffer::MotionKind::Char,
4254 });
4255 let edits = e.take_content_edits();
4256 assert_eq!(edits.len(), 1);
4257 let ce = &edits[0];
4258 assert_eq!(ce.start_byte, 1);
4259 assert_eq!(ce.old_end_byte, 4);
4260 assert_eq!(ce.new_end_byte, 1);
4261 assert!(ce.old_end_byte > ce.new_end_byte);
4262 }
4263
4264 #[test]
4265 fn content_edit_set_content_resets() {
4266 let mut e = fresh_editor("foo");
4267 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4268 at: hjkl_buffer::Position::new(0, 0),
4269 ch: 'X',
4270 });
4271 e.set_content("brand new");
4274 assert!(e.take_content_reset());
4275 assert!(!e.take_content_reset());
4277 assert!(e.take_content_edits().is_empty());
4279 }
4280
4281 #[test]
4282 fn content_edit_multiple_replaces_in_order() {
4283 let mut e = fresh_editor("xax xbx xcx");
4288 let _ = e.take_content_edits();
4289 let _ = e.take_content_reset();
4290 let positions = [(0usize, 0usize), (0, 4), (0, 8)];
4294 for (row, col) in positions {
4295 let _ = e.mutate_edit(hjkl_buffer::Edit::Replace {
4296 start: hjkl_buffer::Position::new(row, col),
4297 end: hjkl_buffer::Position::new(row, col + 1),
4298 with: "yy".into(),
4299 });
4300 }
4301 let edits = e.take_content_edits();
4302 assert_eq!(edits.len(), 3);
4303 for ce in &edits {
4304 assert!(ce.start_byte <= ce.old_end_byte);
4305 assert!(ce.start_byte <= ce.new_end_byte);
4306 }
4307 for w in edits.windows(2) {
4309 assert!(w[0].start_byte <= w[1].start_byte);
4310 }
4311 }
4312}