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}
782
783impl Default for Settings {
784 fn default() -> Self {
785 Self {
786 shiftwidth: 4,
787 tabstop: 4,
788 softtabstop: 4,
789 ignore_case: false,
790 smartcase: false,
791 wrapscan: true,
792 textwidth: 79,
793 expandtab: true,
794 wrap: hjkl_buffer::Wrap::None,
795 readonly: false,
796 autoindent: true,
797 smartindent: true,
798 undo_levels: 1000,
799 undo_break_on_motion: true,
800 iskeyword: "@,48-57,_,192-255".to_string(),
801 timeout_len: core::time::Duration::from_millis(1000),
802 }
803 }
804}
805
806fn settings_from_options(o: &crate::types::Options) -> Settings {
814 Settings {
815 shiftwidth: o.shiftwidth as usize,
816 tabstop: o.tabstop as usize,
817 softtabstop: o.softtabstop as usize,
818 ignore_case: o.ignorecase,
819 smartcase: o.smartcase,
820 wrapscan: o.wrapscan,
821 textwidth: o.textwidth as usize,
822 expandtab: o.expandtab,
823 wrap: match o.wrap {
824 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
825 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
826 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
827 },
828 readonly: o.readonly,
829 autoindent: o.autoindent,
830 smartindent: o.smartindent,
831 undo_levels: o.undo_levels,
832 undo_break_on_motion: o.undo_break_on_motion,
833 iskeyword: o.iskeyword.clone(),
834 timeout_len: o.timeout_len,
835 }
836}
837
838#[derive(Debug, Clone, Copy, PartialEq, Eq)]
842pub enum LspIntent {
843 GotoDefinition,
845}
846
847impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
848 pub fn new(buffer: hjkl_buffer::Buffer, host: H, options: crate::types::Options) -> Self {
858 let settings = settings_from_options(&options);
859 Self {
860 keybinding_mode: KeybindingMode::Vim,
861 last_yank: None,
862 vim: VimState::default(),
863 undo_stack: Vec::new(),
864 redo_stack: Vec::new(),
865 content_dirty: false,
866 cached_content: None,
867 viewport_height: AtomicU16::new(0),
868 pending_lsp: None,
869 pending_fold_ops: Vec::new(),
870 buffer,
871 #[cfg(feature = "ratatui")]
872 style_table: Vec::new(),
873 #[cfg(not(feature = "ratatui"))]
874 engine_style_table: Vec::new(),
875 registers: crate::registers::Registers::default(),
876 #[cfg(feature = "ratatui")]
877 styled_spans: Vec::new(),
878 settings,
879 marks: std::collections::BTreeMap::new(),
880 syntax_fold_ranges: Vec::new(),
881 change_log: Vec::new(),
882 sticky_col: None,
883 host,
884 last_emitted_mode: crate::VimMode::Normal,
885 search_state: crate::search::SearchState::new(),
886 buffer_spans: Vec::new(),
887 pending_content_edits: Vec::new(),
888 pending_content_reset: false,
889 }
890 }
891}
892
893impl<B: crate::types::Buffer, H: crate::types::Host> Editor<B, H> {
894 pub fn buffer(&self) -> &B {
897 &self.buffer
898 }
899
900 pub fn buffer_mut(&mut self) -> &mut B {
902 &mut self.buffer
903 }
904
905 pub fn host(&self) -> &H {
907 &self.host
908 }
909
910 pub fn host_mut(&mut self) -> &mut H {
912 &mut self.host
913 }
914}
915
916impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
917 pub fn set_iskeyword(&mut self, spec: impl Into<String>) {
924 self.settings.iskeyword = spec.into();
925 }
926
927 pub(crate) fn emit_cursor_shape_if_changed(&mut self) {
932 let mode = self.vim_mode();
933 if mode == self.last_emitted_mode {
934 return;
935 }
936 let shape = match mode {
937 crate::VimMode::Insert => crate::types::CursorShape::Bar,
938 _ => crate::types::CursorShape::Block,
939 };
940 self.host.emit_cursor_shape(shape);
941 self.last_emitted_mode = mode;
942 }
943
944 pub(crate) fn record_yank_to_host(&mut self, text: String) {
951 self.host.write_clipboard(text.clone());
952 self.last_yank = Some(text);
953 }
954
955 pub fn sticky_col(&self) -> Option<usize> {
960 self.sticky_col
961 }
962
963 pub fn set_sticky_col(&mut self, col: Option<usize>) {
967 self.sticky_col = col;
968 }
969
970 pub fn mark(&self, c: char) -> Option<(usize, usize)> {
978 self.marks.get(&c).copied()
979 }
980
981 pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
984 self.marks.insert(c, pos);
985 }
986
987 pub fn clear_mark(&mut self, c: char) {
989 self.marks.remove(&c);
990 }
991
992 #[deprecated(
997 since = "0.0.36",
998 note = "use Editor::mark — lowercase + uppercase marks now live in a single map"
999 )]
1000 pub fn buffer_mark(&self, c: char) -> Option<(usize, usize)> {
1001 self.mark(c)
1002 }
1003
1004 pub fn pop_last_undo(&mut self) -> bool {
1011 self.undo_stack.pop().is_some()
1012 }
1013
1014 pub fn marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1019 self.marks.iter().map(|(c, p)| (*c, *p))
1020 }
1021
1022 #[deprecated(
1027 since = "0.0.36",
1028 note = "use Editor::marks — lowercase + uppercase marks now live in a single map"
1029 )]
1030 pub fn buffer_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1031 self.marks
1032 .iter()
1033 .filter(|(c, _)| c.is_ascii_lowercase())
1034 .map(|(c, p)| (*c, *p))
1035 }
1036
1037 pub fn last_jump_back(&self) -> Option<(usize, usize)> {
1040 self.vim.jump_back.last().copied()
1041 }
1042
1043 pub fn last_edit_pos(&self) -> Option<(usize, usize)> {
1046 self.vim.last_edit_pos
1047 }
1048
1049 pub fn file_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1060 self.marks
1061 .iter()
1062 .filter(|(c, _)| c.is_ascii_uppercase())
1063 .map(|(c, p)| (*c, *p))
1064 }
1065
1066 pub fn syntax_fold_ranges(&self) -> &[(usize, usize)] {
1071 &self.syntax_fold_ranges
1072 }
1073
1074 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
1075 self.syntax_fold_ranges = ranges;
1076 }
1077
1078 pub fn settings(&self) -> &Settings {
1081 &self.settings
1082 }
1083
1084 pub fn settings_mut(&mut self) -> &mut Settings {
1089 &mut self.settings
1090 }
1091
1092 pub fn is_readonly(&self) -> bool {
1096 self.settings.readonly
1097 }
1098
1099 pub fn search_state(&self) -> &crate::search::SearchState {
1104 &self.search_state
1105 }
1106
1107 pub fn search_state_mut(&mut self) -> &mut crate::search::SearchState {
1111 &mut self.search_state
1112 }
1113
1114 pub fn set_search_pattern(&mut self, pattern: Option<regex::Regex>) {
1120 self.search_state.set_pattern(pattern);
1121 }
1122
1123 pub fn search_advance_forward(&mut self, skip_current: bool) -> bool {
1128 crate::search::search_forward(&mut self.buffer, &mut self.search_state, skip_current)
1129 }
1130
1131 pub fn search_advance_backward(&mut self, skip_current: bool) -> bool {
1133 crate::search::search_backward(&mut self.buffer, &mut self.search_state, skip_current)
1134 }
1135
1136 #[cfg(feature = "ratatui")]
1147 pub fn install_ratatui_syntax_spans(
1148 &mut self,
1149 spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
1150 ) {
1151 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1155 for (row, row_spans) in spans.iter().enumerate() {
1156 if row_spans.is_empty() {
1157 by_row.push(Vec::new());
1158 continue;
1159 }
1160 let line_len = buf_line(&self.buffer, row).map(str::len).unwrap_or(0);
1161 let mut translated = Vec::with_capacity(row_spans.len());
1162 for (start, end, style) in row_spans {
1163 let end_clamped = (*end).min(line_len);
1164 if end_clamped <= *start {
1165 continue;
1166 }
1167 let id = self.intern_ratatui_style(*style);
1168 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1169 }
1170 by_row.push(translated);
1171 }
1172 self.buffer_spans = by_row;
1173 self.styled_spans = spans;
1174 }
1175
1176 pub fn yank(&self) -> &str {
1178 &self.registers.unnamed.text
1179 }
1180
1181 pub fn registers(&self) -> &crate::registers::Registers {
1183 &self.registers
1184 }
1185
1186 pub fn registers_mut(&mut self) -> &mut crate::registers::Registers {
1190 &mut self.registers
1191 }
1192
1193 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
1198 self.registers.set_clipboard(text, linewise);
1199 }
1200
1201 pub fn pending_register_is_clipboard(&self) -> bool {
1205 matches!(self.vim.pending_register, Some('+') | Some('*'))
1206 }
1207
1208 pub fn recording_register(&self) -> Option<char> {
1212 self.vim.recording_macro
1213 }
1214
1215 pub fn pending_count(&self) -> Option<u32> {
1219 self.vim.pending_count_val()
1220 }
1221
1222 pub fn pending_op(&self) -> Option<char> {
1226 self.vim.pending_op_char()
1227 }
1228
1229 #[allow(clippy::type_complexity)]
1232 pub fn jump_list(&self) -> (&[(usize, usize)], &[(usize, usize)]) {
1233 (&self.vim.jump_back, &self.vim.jump_fwd)
1234 }
1235
1236 pub fn change_list(&self) -> (&[(usize, usize)], Option<usize>) {
1239 (&self.vim.change_list, self.vim.change_list_cursor)
1240 }
1241
1242 pub fn set_yank(&mut self, text: impl Into<String>) {
1246 let text = text.into();
1247 let linewise = self.vim.yank_linewise;
1248 self.registers.unnamed = crate::registers::Slot { text, linewise };
1249 }
1250
1251 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
1255 self.vim.yank_linewise = linewise;
1256 let target = self.vim.pending_register.take();
1257 self.registers.record_yank(text, linewise, target);
1258 }
1259
1260 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
1265 if let Some(slot) = match reg {
1266 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
1267 'A'..='Z' => {
1268 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
1269 }
1270 _ => None,
1271 } {
1272 slot.text = text;
1273 slot.linewise = false;
1274 }
1275 }
1276
1277 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
1280 self.vim.yank_linewise = linewise;
1281 let target = self.vim.pending_register.take();
1282 self.registers.record_delete(text, linewise, target);
1283 }
1284
1285 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
1294 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
1295 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
1296 .collect();
1297 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1298 #[cfg(feature = "ratatui")]
1299 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
1300 Vec::with_capacity(spans.len());
1301 for (row, row_spans) in spans.iter().enumerate() {
1302 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
1303 let mut translated = Vec::with_capacity(row_spans.len());
1304 #[cfg(feature = "ratatui")]
1305 let mut translated_r = Vec::with_capacity(row_spans.len());
1306 for (start, end, style) in row_spans {
1307 let end_clamped = (*end).min(line_len);
1308 if end_clamped <= *start {
1309 continue;
1310 }
1311 let id = self.intern_style(*style);
1312 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1313 #[cfg(feature = "ratatui")]
1314 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
1315 }
1316 by_row.push(translated);
1317 #[cfg(feature = "ratatui")]
1318 ratatui_spans.push(translated_r);
1319 }
1320 self.buffer_spans = by_row;
1321 #[cfg(feature = "ratatui")]
1322 {
1323 self.styled_spans = ratatui_spans;
1324 }
1325 }
1326
1327 #[cfg(feature = "ratatui")]
1336 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
1337 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
1338 return idx as u32;
1339 }
1340 self.style_table.push(style);
1341 (self.style_table.len() - 1) as u32
1342 }
1343
1344 #[cfg(feature = "ratatui")]
1348 pub fn style_table(&self) -> &[ratatui::style::Style] {
1349 &self.style_table
1350 }
1351
1352 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
1361 &self.buffer_spans
1362 }
1363
1364 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1379 #[cfg(feature = "ratatui")]
1380 {
1381 let r = engine_style_to_ratatui(style);
1382 self.intern_ratatui_style(r)
1383 }
1384 #[cfg(not(feature = "ratatui"))]
1385 {
1386 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1387 return idx as u32;
1388 }
1389 self.engine_style_table.push(style);
1390 (self.engine_style_table.len() - 1) as u32
1391 }
1392 }
1393
1394 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1398 #[cfg(feature = "ratatui")]
1399 {
1400 let r = self.style_table.get(id as usize).copied()?;
1401 Some(ratatui_style_to_engine(r))
1402 }
1403 #[cfg(not(feature = "ratatui"))]
1404 {
1405 self.engine_style_table.get(id as usize).copied()
1406 }
1407 }
1408
1409 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1413
1414 pub fn set_viewport_top(&mut self, row: usize) {
1422 let last = buf_row_count(&self.buffer).saturating_sub(1);
1423 let target = row.min(last);
1424 self.host.viewport_mut().top_row = target;
1425 }
1426
1427 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1431 buf_set_cursor_rc(&mut self.buffer, row, col);
1432 }
1433
1434 pub fn cursor(&self) -> (usize, usize) {
1442 buf_cursor_rc(&self.buffer)
1443 }
1444
1445 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1448 self.pending_lsp.take()
1449 }
1450
1451 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1465 std::mem::take(&mut self.pending_fold_ops)
1466 }
1467
1468 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1478 use crate::types::FoldProvider;
1479 self.pending_fold_ops.push(op);
1480 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1481 provider.apply(op);
1482 }
1483
1484 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1491 let height = self.viewport_height_value();
1492 self.host.viewport_mut().height = height;
1493 }
1494
1495 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1499 self.sync_buffer_from_textarea();
1500 }
1501
1502 pub fn record_jump(&mut self, pos: (usize, usize)) {
1507 const JUMPLIST_MAX: usize = 100;
1508 self.vim.jump_back.push(pos);
1509 if self.vim.jump_back.len() > JUMPLIST_MAX {
1510 self.vim.jump_back.remove(0);
1511 }
1512 self.vim.jump_fwd.clear();
1513 }
1514
1515 pub fn set_viewport_height(&self, height: u16) {
1518 self.viewport_height.store(height, Ordering::Relaxed);
1519 }
1520
1521 pub fn viewport_height_value(&self) -> u16 {
1523 self.viewport_height.load(Ordering::Relaxed)
1524 }
1525
1526 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1535 if self.settings.readonly {
1542 let _ = edit;
1543 return hjkl_buffer::Edit::InsertStr {
1544 at: buf_cursor_pos(&self.buffer),
1545 text: String::new(),
1546 };
1547 }
1548 let pre_row = buf_cursor_row(&self.buffer);
1549 let pre_rows = buf_row_count(&self.buffer);
1550 self.change_log.extend(edit_to_editops(&edit));
1554 let content_edits = content_edits_from_buffer_edit(&self.buffer, &edit);
1560 self.pending_content_edits.extend(content_edits);
1561 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1567 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1568 let lo = pre_row.min(pos_row);
1574 let hi = pre_row.max(pos_row);
1575 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1576 start_row: lo,
1577 end_row: hi,
1578 });
1579 self.vim.last_edit_pos = Some((pos_row, pos_col));
1580 let entry = (pos_row, pos_col);
1585 if self.vim.change_list.last() != Some(&entry) {
1586 if let Some(idx) = self.vim.change_list_cursor.take() {
1587 self.vim.change_list.truncate(idx + 1);
1588 }
1589 self.vim.change_list.push(entry);
1590 let len = self.vim.change_list.len();
1591 if len > crate::vim::CHANGE_LIST_MAX {
1592 self.vim
1593 .change_list
1594 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1595 }
1596 }
1597 self.vim.change_list_cursor = None;
1598 let post_rows = buf_row_count(&self.buffer);
1602 let delta = post_rows as isize - pre_rows as isize;
1603 if delta != 0 {
1604 self.shift_marks_after_edit(pre_row, delta);
1605 }
1606 self.push_buffer_content_to_textarea();
1607 self.mark_content_dirty();
1608 inverse
1609 }
1610
1611 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1616 if delta == 0 {
1617 return;
1618 }
1619 let drop_end = if delta < 0 {
1622 edit_start.saturating_add((-delta) as usize)
1623 } else {
1624 edit_start
1625 };
1626 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1627
1628 let mut to_drop: Vec<char> = Vec::new();
1631 for (c, (row, _col)) in self.marks.iter_mut() {
1632 if (edit_start..drop_end).contains(row) {
1633 to_drop.push(*c);
1634 } else if *row >= shift_threshold {
1635 *row = ((*row as isize) + delta).max(0) as usize;
1636 }
1637 }
1638 for c in to_drop {
1639 self.marks.remove(&c);
1640 }
1641
1642 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1643 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1644 for (row, _) in entries.iter_mut() {
1645 if *row >= shift_threshold {
1646 *row = ((*row as isize) + delta).max(0) as usize;
1647 }
1648 }
1649 };
1650 shift_jumps(&mut self.vim.jump_back);
1651 shift_jumps(&mut self.vim.jump_fwd);
1652 }
1653
1654 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1662
1663 pub fn mark_content_dirty(&mut self) {
1669 self.content_dirty = true;
1670 self.cached_content = None;
1671 }
1672
1673 pub fn take_dirty(&mut self) -> bool {
1675 let dirty = self.content_dirty;
1676 self.content_dirty = false;
1677 dirty
1678 }
1679
1680 pub fn take_content_edits(&mut self) -> Vec<crate::types::ContentEdit> {
1688 std::mem::take(&mut self.pending_content_edits)
1689 }
1690
1691 pub fn take_content_reset(&mut self) -> bool {
1697 let r = self.pending_content_reset;
1698 self.pending_content_reset = false;
1699 r
1700 }
1701
1702 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1712 if !self.content_dirty {
1713 return None;
1714 }
1715 let arc = self.content_arc();
1716 self.content_dirty = false;
1717 Some(arc)
1718 }
1719
1720 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1723 let cursor = buf_cursor_row(&self.buffer);
1724 let top = self.host.viewport().top_row;
1725 cursor.saturating_sub(top).min(height as usize - 1) as u16
1726 }
1727
1728 pub fn cursor_screen_pos(
1738 &self,
1739 area_x: u16,
1740 area_y: u16,
1741 area_width: u16,
1742 area_height: u16,
1743 ) -> Option<(u16, u16)> {
1744 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1745 let v = self.host.viewport();
1746 if pos_row < v.top_row || pos_col < v.top_col {
1747 return None;
1748 }
1749 let lnum_width = buf_row_count(&self.buffer).to_string().len() as u16 + 2;
1750 let dy = (pos_row - v.top_row) as u16;
1751 let line = self.buffer.line(pos_row).unwrap_or("");
1755 let tab_width = if v.tab_width == 0 {
1756 4
1757 } else {
1758 v.tab_width as usize
1759 };
1760 let visual_pos = visual_col_for_char(line, pos_col, tab_width);
1761 let visual_top = visual_col_for_char(line, v.top_col, tab_width);
1762 let dx = (visual_pos - visual_top) as u16;
1763 if dy >= area_height || dx + lnum_width >= area_width {
1764 return None;
1765 }
1766 Some((area_x + lnum_width + dx, area_y + dy))
1767 }
1768
1769 #[cfg(feature = "ratatui")]
1775 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1776 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1777 }
1778
1779 pub fn vim_mode(&self) -> VimMode {
1780 self.vim.public_mode()
1781 }
1782
1783 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1789 self.vim.search_prompt.as_ref()
1790 }
1791
1792 pub fn last_search(&self) -> Option<&str> {
1795 self.vim.last_search.as_deref()
1796 }
1797
1798 pub fn last_search_forward(&self) -> bool {
1802 self.vim.last_search_forward
1803 }
1804
1805 pub fn set_last_search(&mut self, text: Option<String>, forward: bool) {
1811 self.vim.last_search = text;
1812 self.vim.last_search_forward = forward;
1813 }
1814
1815 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1819 if self.vim_mode() != VimMode::Visual {
1820 return None;
1821 }
1822 let anchor = self.vim.visual_anchor;
1823 let cursor = self.cursor();
1824 let (start, end) = if anchor <= cursor {
1825 (anchor, cursor)
1826 } else {
1827 (cursor, anchor)
1828 };
1829 Some((start, end))
1830 }
1831
1832 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1835 if self.vim_mode() != VimMode::VisualLine {
1836 return None;
1837 }
1838 let anchor = self.vim.visual_line_anchor;
1839 let cursor = buf_cursor_row(&self.buffer);
1840 Some((anchor.min(cursor), anchor.max(cursor)))
1841 }
1842
1843 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1844 if self.vim_mode() != VimMode::VisualBlock {
1845 return None;
1846 }
1847 let (ar, ac) = self.vim.block_anchor;
1848 let cr = buf_cursor_row(&self.buffer);
1849 let cc = self.vim.block_vcol;
1850 let top = ar.min(cr);
1851 let bot = ar.max(cr);
1852 let left = ac.min(cc);
1853 let right = ac.max(cc);
1854 Some((top, bot, left, right))
1855 }
1856
1857 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1863 use hjkl_buffer::{Position, Selection};
1864 match self.vim_mode() {
1865 VimMode::Visual => {
1866 let (ar, ac) = self.vim.visual_anchor;
1867 let head = buf_cursor_pos(&self.buffer);
1868 Some(Selection::Char {
1869 anchor: Position::new(ar, ac),
1870 head,
1871 })
1872 }
1873 VimMode::VisualLine => {
1874 let anchor_row = self.vim.visual_line_anchor;
1875 let head_row = buf_cursor_row(&self.buffer);
1876 Some(Selection::Line {
1877 anchor_row,
1878 head_row,
1879 })
1880 }
1881 VimMode::VisualBlock => {
1882 let (ar, ac) = self.vim.block_anchor;
1883 let cr = buf_cursor_row(&self.buffer);
1884 let cc = self.vim.block_vcol;
1885 Some(Selection::Block {
1886 anchor: Position::new(ar, ac),
1887 head: Position::new(cr, cc),
1888 })
1889 }
1890 _ => None,
1891 }
1892 }
1893
1894 pub fn force_normal(&mut self) {
1896 self.vim.force_normal();
1897 }
1898
1899 pub fn content(&self) -> String {
1900 let n = buf_row_count(&self.buffer);
1901 let mut s = String::new();
1902 for r in 0..n {
1903 if r > 0 {
1904 s.push('\n');
1905 }
1906 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1907 }
1908 s.push('\n');
1909 s
1910 }
1911
1912 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
1917 if let Some(arc) = &self.cached_content {
1918 return std::sync::Arc::clone(arc);
1919 }
1920 let arc = std::sync::Arc::new(self.content());
1921 self.cached_content = Some(std::sync::Arc::clone(&arc));
1922 arc
1923 }
1924
1925 pub fn set_content(&mut self, text: &str) {
1926 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
1927 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
1928 lines.pop();
1929 }
1930 if lines.is_empty() {
1931 lines.push(String::new());
1932 }
1933 let _ = lines;
1934 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
1935 self.undo_stack.clear();
1936 self.redo_stack.clear();
1937 self.pending_content_edits.clear();
1939 self.pending_content_reset = true;
1940 self.mark_content_dirty();
1941 }
1942
1943 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
1959 use crate::{PlannedInput, SpecialKey};
1960 let (key, mods) = match input {
1961 PlannedInput::Char(c, m) => (Key::Char(c), m),
1962 PlannedInput::Key(k, m) => {
1963 let key = match k {
1964 SpecialKey::Esc => Key::Esc,
1965 SpecialKey::Enter => Key::Enter,
1966 SpecialKey::Backspace => Key::Backspace,
1967 SpecialKey::Tab => Key::Tab,
1968 SpecialKey::BackTab => Key::Tab,
1972 SpecialKey::Up => Key::Up,
1973 SpecialKey::Down => Key::Down,
1974 SpecialKey::Left => Key::Left,
1975 SpecialKey::Right => Key::Right,
1976 SpecialKey::Home => Key::Home,
1977 SpecialKey::End => Key::End,
1978 SpecialKey::PageUp => Key::PageUp,
1979 SpecialKey::PageDown => Key::PageDown,
1980 SpecialKey::Insert => Key::Null,
1984 SpecialKey::Delete => Key::Delete,
1985 SpecialKey::F(_) => Key::Null,
1986 };
1987 let m = if matches!(k, SpecialKey::BackTab) {
1988 crate::Modifiers { shift: true, ..m }
1989 } else {
1990 m
1991 };
1992 (key, m)
1993 }
1994 PlannedInput::Mouse(_)
1996 | PlannedInput::Paste(_)
1997 | PlannedInput::FocusGained
1998 | PlannedInput::FocusLost
1999 | PlannedInput::Resize(_, _) => return false,
2000 };
2001 if key == Key::Null {
2002 return false;
2003 }
2004 let event = Input {
2005 key,
2006 ctrl: mods.ctrl,
2007 alt: mods.alt,
2008 shift: mods.shift,
2009 };
2010 let consumed = vim::step(self, event);
2011 self.emit_cursor_shape_if_changed();
2012 consumed
2013 }
2014
2015 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
2032 std::mem::take(&mut self.change_log)
2033 }
2034
2035 pub fn current_options(&self) -> crate::types::Options {
2045 crate::types::Options {
2046 shiftwidth: self.settings.shiftwidth as u32,
2047 tabstop: self.settings.tabstop as u32,
2048 softtabstop: self.settings.softtabstop as u32,
2049 textwidth: self.settings.textwidth as u32,
2050 expandtab: self.settings.expandtab,
2051 ignorecase: self.settings.ignore_case,
2052 smartcase: self.settings.smartcase,
2053 wrapscan: self.settings.wrapscan,
2054 wrap: match self.settings.wrap {
2055 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
2056 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
2057 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
2058 },
2059 readonly: self.settings.readonly,
2060 autoindent: self.settings.autoindent,
2061 smartindent: self.settings.smartindent,
2062 undo_levels: self.settings.undo_levels,
2063 undo_break_on_motion: self.settings.undo_break_on_motion,
2064 iskeyword: self.settings.iskeyword.clone(),
2065 timeout_len: self.settings.timeout_len,
2066 ..crate::types::Options::default()
2067 }
2068 }
2069
2070 pub fn apply_options(&mut self, opts: &crate::types::Options) {
2075 self.settings.shiftwidth = opts.shiftwidth as usize;
2076 self.settings.tabstop = opts.tabstop as usize;
2077 self.settings.softtabstop = opts.softtabstop as usize;
2078 self.settings.textwidth = opts.textwidth as usize;
2079 self.settings.expandtab = opts.expandtab;
2080 self.settings.ignore_case = opts.ignorecase;
2081 self.settings.smartcase = opts.smartcase;
2082 self.settings.wrapscan = opts.wrapscan;
2083 self.settings.wrap = match opts.wrap {
2084 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
2085 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
2086 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
2087 };
2088 self.settings.readonly = opts.readonly;
2089 self.settings.autoindent = opts.autoindent;
2090 self.settings.smartindent = opts.smartindent;
2091 self.settings.undo_levels = opts.undo_levels;
2092 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
2093 self.set_iskeyword(opts.iskeyword.clone());
2094 self.settings.timeout_len = opts.timeout_len;
2095 }
2096
2097 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
2107 use crate::types::{Highlight, HighlightKind, Pos};
2108 let sel = self.buffer_selection()?;
2109 let (start, end) = match sel {
2110 hjkl_buffer::Selection::Char { anchor, head } => {
2111 let a = (anchor.row, anchor.col);
2112 let h = (head.row, head.col);
2113 if a <= h { (a, h) } else { (h, a) }
2114 }
2115 hjkl_buffer::Selection::Line {
2116 anchor_row,
2117 head_row,
2118 } => {
2119 let (top, bot) = if anchor_row <= head_row {
2120 (anchor_row, head_row)
2121 } else {
2122 (head_row, anchor_row)
2123 };
2124 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
2125 ((top, 0), (bot, last_col))
2126 }
2127 hjkl_buffer::Selection::Block { anchor, head } => {
2128 let (top, bot) = if anchor.row <= head.row {
2129 (anchor.row, head.row)
2130 } else {
2131 (head.row, anchor.row)
2132 };
2133 let (left, right) = if anchor.col <= head.col {
2134 (anchor.col, head.col)
2135 } else {
2136 (head.col, anchor.col)
2137 };
2138 ((top, left), (bot, right))
2139 }
2140 };
2141 Some(Highlight {
2142 range: Pos {
2143 line: start.0 as u32,
2144 col: start.1 as u32,
2145 }..Pos {
2146 line: end.0 as u32,
2147 col: end.1 as u32,
2148 },
2149 kind: HighlightKind::Selection,
2150 })
2151 }
2152
2153 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
2172 use crate::types::{Highlight, HighlightKind, Pos};
2173 let row = line as usize;
2174 if row >= buf_row_count(&self.buffer) {
2175 return Vec::new();
2176 }
2177
2178 if let Some(prompt) = self.search_prompt() {
2181 if prompt.text.is_empty() {
2182 return Vec::new();
2183 }
2184 let Ok(re) = regex::Regex::new(&prompt.text) else {
2185 return Vec::new();
2186 };
2187 let Some(haystack) = buf_line(&self.buffer, row) else {
2188 return Vec::new();
2189 };
2190 return re
2191 .find_iter(haystack)
2192 .map(|m| Highlight {
2193 range: Pos {
2194 line,
2195 col: m.start() as u32,
2196 }..Pos {
2197 line,
2198 col: m.end() as u32,
2199 },
2200 kind: HighlightKind::IncSearch,
2201 })
2202 .collect();
2203 }
2204
2205 if self.search_state.pattern.is_none() {
2206 return Vec::new();
2207 }
2208 let dgen = crate::types::Query::dirty_gen(&self.buffer);
2209 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
2210 .into_iter()
2211 .map(|(start, end)| Highlight {
2212 range: Pos {
2213 line,
2214 col: start as u32,
2215 }..Pos {
2216 line,
2217 col: end as u32,
2218 },
2219 kind: HighlightKind::SearchMatch,
2220 })
2221 .collect()
2222 }
2223
2224 pub fn render_frame(&self) -> crate::types::RenderFrame {
2234 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
2235 let (cursor_row, cursor_col) = self.cursor();
2236 let (mode, shape) = match self.vim_mode() {
2237 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
2238 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
2239 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
2240 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
2241 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
2242 };
2243 RenderFrame {
2244 mode,
2245 cursor_row: cursor_row as u32,
2246 cursor_col: cursor_col as u32,
2247 cursor_shape: shape,
2248 viewport_top: self.host.viewport().top_row as u32,
2249 line_count: crate::types::Query::line_count(&self.buffer),
2250 }
2251 }
2252
2253 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
2266 use crate::types::{EditorSnapshot, SnapshotMode};
2267 let mode = match self.vim_mode() {
2268 crate::VimMode::Normal => SnapshotMode::Normal,
2269 crate::VimMode::Insert => SnapshotMode::Insert,
2270 crate::VimMode::Visual => SnapshotMode::Visual,
2271 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
2272 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
2273 };
2274 let cursor = self.cursor();
2275 let cursor = (cursor.0 as u32, cursor.1 as u32);
2276 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
2277 let viewport_top = self.host.viewport().top_row as u32;
2278 let marks = self
2279 .marks
2280 .iter()
2281 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
2282 .collect();
2283 EditorSnapshot {
2284 version: EditorSnapshot::VERSION,
2285 mode,
2286 cursor,
2287 lines,
2288 viewport_top,
2289 registers: self.registers.clone(),
2290 marks,
2291 }
2292 }
2293
2294 pub fn restore_snapshot(
2302 &mut self,
2303 snap: crate::types::EditorSnapshot,
2304 ) -> Result<(), crate::EngineError> {
2305 use crate::types::EditorSnapshot;
2306 if snap.version != EditorSnapshot::VERSION {
2307 return Err(crate::EngineError::SnapshotVersion(
2308 snap.version,
2309 EditorSnapshot::VERSION,
2310 ));
2311 }
2312 let text = snap.lines.join("\n");
2313 self.set_content(&text);
2314 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
2315 self.host.viewport_mut().top_row = snap.viewport_top as usize;
2316 self.registers = snap.registers;
2317 self.marks = snap
2318 .marks
2319 .into_iter()
2320 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
2321 .collect();
2322 Ok(())
2323 }
2324
2325 pub fn seed_yank(&mut self, text: String) {
2329 let linewise = text.ends_with('\n');
2330 self.vim.yank_linewise = linewise;
2331 self.registers.unnamed = crate::registers::Slot { text, linewise };
2332 }
2333
2334 pub fn scroll_down(&mut self, rows: i16) {
2339 self.scroll_viewport(rows);
2340 }
2341
2342 pub fn scroll_up(&mut self, rows: i16) {
2346 self.scroll_viewport(-rows);
2347 }
2348
2349 const SCROLLOFF: usize = 5;
2353
2354 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
2359 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2360 if height == 0 {
2361 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2368 crate::viewport_math::ensure_cursor_visible(
2369 &self.buffer,
2370 &folds,
2371 self.host.viewport_mut(),
2372 );
2373 return;
2374 }
2375 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2379 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
2382 self.ensure_scrolloff_wrap(height, margin);
2383 return;
2384 }
2385 let cursor_row = buf_cursor_row(&self.buffer);
2386 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
2387 let v = self.host.viewport_mut();
2388 if cursor_row < v.top_row + margin {
2390 v.top_row = cursor_row.saturating_sub(margin);
2391 }
2392 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
2394 if cursor_row > v.top_row + max_bottom {
2395 v.top_row = cursor_row.saturating_sub(max_bottom);
2396 }
2397 let max_top = last_row.saturating_sub(height.saturating_sub(1));
2399 if v.top_row > max_top {
2400 v.top_row = max_top;
2401 }
2402 let cursor = buf_cursor_pos(&self.buffer);
2405 self.host.viewport_mut().ensure_visible(cursor);
2406 }
2407
2408 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
2413 let cursor_row = buf_cursor_row(&self.buffer);
2414 if cursor_row < self.host.viewport().top_row {
2417 let v = self.host.viewport_mut();
2418 v.top_row = cursor_row;
2419 v.top_col = 0;
2420 }
2421 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2430 loop {
2431 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2432 let csr =
2433 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2434 .unwrap_or(0);
2435 if csr <= max_csr {
2436 break;
2437 }
2438 let top = self.host.viewport().top_row;
2439 let row_count = buf_row_count(&self.buffer);
2440 let next = {
2441 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2442 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2443 };
2444 let Some(next) = next else {
2445 break;
2446 };
2447 if next > cursor_row {
2449 self.host.viewport_mut().top_row = cursor_row;
2450 break;
2451 }
2452 self.host.viewport_mut().top_row = next;
2453 }
2454 loop {
2457 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2458 let csr =
2459 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2460 .unwrap_or(0);
2461 if csr >= margin {
2462 break;
2463 }
2464 let top = self.host.viewport().top_row;
2465 let prev = {
2466 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2467 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2468 };
2469 let Some(prev) = prev else {
2470 break;
2471 };
2472 self.host.viewport_mut().top_row = prev;
2473 }
2474 let max_top = {
2479 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2480 crate::viewport_math::max_top_for_height(
2481 &self.buffer,
2482 &folds,
2483 self.host.viewport(),
2484 height,
2485 )
2486 };
2487 if self.host.viewport().top_row > max_top {
2488 self.host.viewport_mut().top_row = max_top;
2489 }
2490 self.host.viewport_mut().top_col = 0;
2491 }
2492
2493 fn scroll_viewport(&mut self, delta: i16) {
2494 if delta == 0 {
2495 return;
2496 }
2497 let total_rows = buf_row_count(&self.buffer) as isize;
2499 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2500 let cur_top = self.host.viewport().top_row as isize;
2501 let new_top = (cur_top + delta as isize)
2502 .max(0)
2503 .min((total_rows - 1).max(0)) as usize;
2504 self.host.viewport_mut().top_row = new_top;
2505 let _ = cur_top;
2508 if height == 0 {
2509 return;
2510 }
2511 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2514 let margin = Self::SCROLLOFF.min(height / 2);
2515 let min_row = new_top + margin;
2516 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2517 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2518 if target_row != cursor_row {
2519 let line_len = buf_line(&self.buffer, target_row)
2520 .map(|l| l.chars().count())
2521 .unwrap_or(0);
2522 let target_col = cursor_col.min(line_len.saturating_sub(1));
2523 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2524 }
2525 }
2526
2527 pub fn goto_line(&mut self, line: usize) {
2528 let row = line.saturating_sub(1);
2529 let max = buf_row_count(&self.buffer).saturating_sub(1);
2530 let target = row.min(max);
2531 buf_set_cursor_rc(&mut self.buffer, target, 0);
2532 self.ensure_cursor_in_scrolloff();
2536 }
2537
2538 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2542 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2543 if height == 0 {
2544 return;
2545 }
2546 let cur_row = buf_cursor_row(&self.buffer);
2547 let cur_top = self.host.viewport().top_row;
2548 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2554 let new_top = match pos {
2555 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2556 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2557 CursorScrollTarget::Bottom => {
2558 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2559 }
2560 };
2561 if new_top == cur_top {
2562 return;
2563 }
2564 self.host.viewport_mut().top_row = new_top;
2565 }
2566
2567 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2578 let n = buf_row_count(&self.buffer);
2579 let inner_top = area_y.saturating_add(1); let lnum_width = n.to_string().len() as u16 + 2;
2581 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2582 let rel_row = row.saturating_sub(inner_top) as usize;
2583 let top = self.host.viewport().top_row;
2584 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2585 let rel_col = col.saturating_sub(content_x) as usize;
2586 let line_chars = buf_line(&self.buffer, doc_row)
2587 .map(|l| l.chars().count())
2588 .unwrap_or(0);
2589 let last_col = line_chars.saturating_sub(1);
2590 (doc_row, rel_col.min(last_col))
2591 }
2592
2593 pub fn jump_to(&mut self, line: usize, col: usize) {
2595 let r = line.saturating_sub(1);
2596 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2597 let r = r.min(max_row);
2598 let line_len = buf_line(&self.buffer, r)
2599 .map(|l| l.chars().count())
2600 .unwrap_or(0);
2601 let c = col.saturating_sub(1).min(line_len);
2602 buf_set_cursor_rc(&mut self.buffer, r, c);
2603 }
2604
2605 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2613 if self.vim.is_visual() {
2614 self.vim.force_normal();
2615 }
2616 crate::vim::break_undo_group_in_insert(self);
2619 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2620 buf_set_cursor_rc(&mut self.buffer, r, c);
2621 }
2622
2623 #[cfg(feature = "ratatui")]
2629 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2630 self.mouse_click(area.x, area.y, col, row);
2631 }
2632
2633 pub fn mouse_begin_drag(&mut self) {
2635 if !self.vim.is_visual_char() {
2636 let cursor = self.cursor();
2637 self.vim.enter_visual(cursor);
2638 }
2639 }
2640
2641 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2647 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2648 buf_set_cursor_rc(&mut self.buffer, r, c);
2649 }
2650
2651 #[cfg(feature = "ratatui")]
2657 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2658 self.mouse_extend_drag(area.x, area.y, col, row);
2659 }
2660
2661 pub fn insert_str(&mut self, text: &str) {
2662 let pos = crate::types::Cursor::cursor(&self.buffer);
2663 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2664 self.push_buffer_content_to_textarea();
2665 self.mark_content_dirty();
2666 }
2667
2668 pub fn accept_completion(&mut self, completion: &str) {
2669 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2670 let cursor_pos = CursorTrait::cursor(&self.buffer);
2671 let cursor_row = cursor_pos.line as usize;
2672 let cursor_col = cursor_pos.col as usize;
2673 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2674 let chars: Vec<char> = line.chars().collect();
2675 let prefix_len = chars[..cursor_col.min(chars.len())]
2676 .iter()
2677 .rev()
2678 .take_while(|c| c.is_alphanumeric() || **c == '_')
2679 .count();
2680 if prefix_len > 0 {
2681 let start = Pos {
2682 line: cursor_row as u32,
2683 col: (cursor_col - prefix_len) as u32,
2684 };
2685 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2686 }
2687 let cursor = CursorTrait::cursor(&self.buffer);
2688 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2689 self.push_buffer_content_to_textarea();
2690 self.mark_content_dirty();
2691 }
2692
2693 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2694 let rc = buf_cursor_rc(&self.buffer);
2695 (buf_lines_to_vec(&self.buffer), rc)
2696 }
2697
2698 pub fn undo(&mut self) {
2702 crate::vim::do_undo(self);
2703 }
2704
2705 pub fn redo(&mut self) {
2708 crate::vim::do_redo(self);
2709 }
2710
2711 pub fn push_undo(&mut self) {
2716 let snap = self.snapshot();
2717 self.undo_stack.push(snap);
2718 self.cap_undo();
2719 self.redo_stack.clear();
2720 }
2721
2722 pub(crate) fn cap_undo(&mut self) {
2728 let cap = self.settings.undo_levels as usize;
2729 if cap > 0 && self.undo_stack.len() > cap {
2730 let diff = self.undo_stack.len() - cap;
2731 self.undo_stack.drain(..diff);
2732 }
2733 }
2734
2735 #[doc(hidden)]
2737 pub fn undo_stack_len(&self) -> usize {
2738 self.undo_stack.len()
2739 }
2740
2741 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2745 let text = lines.join("\n");
2746 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2747 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2748 self.pending_content_edits.clear();
2750 self.pending_content_reset = true;
2751 self.mark_content_dirty();
2752 }
2753
2754 #[cfg(feature = "crossterm")]
2756 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
2757 let input = crossterm_to_input(key);
2758 if input.key == Key::Null {
2759 return false;
2760 }
2761 let consumed = vim::step(self, input);
2762 self.emit_cursor_shape_if_changed();
2763 consumed
2764 }
2765}
2766
2767fn visual_col_for_char(line: &str, char_col: usize, tab_width: usize) -> usize {
2772 let mut visual = 0usize;
2773 for (i, ch) in line.chars().enumerate() {
2774 if i >= char_col {
2775 break;
2776 }
2777 if ch == '\t' {
2778 visual += tab_width - (visual % tab_width);
2779 } else {
2780 visual += 1;
2781 }
2782 }
2783 visual
2784}
2785
2786#[cfg(feature = "crossterm")]
2787impl From<KeyEvent> for Input {
2788 fn from(key: KeyEvent) -> Self {
2789 let k = match key.code {
2790 KeyCode::Char(c) => Key::Char(c),
2791 KeyCode::Backspace => Key::Backspace,
2792 KeyCode::Delete => Key::Delete,
2793 KeyCode::Enter => Key::Enter,
2794 KeyCode::Left => Key::Left,
2795 KeyCode::Right => Key::Right,
2796 KeyCode::Up => Key::Up,
2797 KeyCode::Down => Key::Down,
2798 KeyCode::Home => Key::Home,
2799 KeyCode::End => Key::End,
2800 KeyCode::Tab => Key::Tab,
2801 KeyCode::Esc => Key::Esc,
2802 _ => Key::Null,
2803 };
2804 Input {
2805 key: k,
2806 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
2807 alt: key.modifiers.contains(KeyModifiers::ALT),
2808 shift: key.modifiers.contains(KeyModifiers::SHIFT),
2809 }
2810 }
2811}
2812
2813#[cfg(feature = "crossterm")]
2817pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
2818 Input::from(key)
2819}
2820
2821#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
2822mod tests {
2823 use super::*;
2824 use crate::types::Host;
2825 use crossterm::event::KeyEvent;
2826
2827 fn key(code: KeyCode) -> KeyEvent {
2828 KeyEvent::new(code, KeyModifiers::NONE)
2829 }
2830 fn shift_key(code: KeyCode) -> KeyEvent {
2831 KeyEvent::new(code, KeyModifiers::SHIFT)
2832 }
2833 fn ctrl_key(code: KeyCode) -> KeyEvent {
2834 KeyEvent::new(code, KeyModifiers::CONTROL)
2835 }
2836
2837 #[test]
2838 fn vim_normal_to_insert() {
2839 let mut e = Editor::new(
2840 hjkl_buffer::Buffer::new(),
2841 crate::types::DefaultHost::new(),
2842 crate::types::Options::default(),
2843 );
2844 e.handle_key(key(KeyCode::Char('i')));
2845 assert_eq!(e.vim_mode(), VimMode::Insert);
2846 }
2847
2848 #[test]
2849 fn with_options_constructs_from_spec_options() {
2850 let opts = crate::types::Options {
2854 shiftwidth: 4,
2855 tabstop: 4,
2856 expandtab: true,
2857 iskeyword: "@,a-z".to_string(),
2858 wrap: crate::types::WrapMode::Word,
2859 ..crate::types::Options::default()
2860 };
2861 let mut e = Editor::new(
2862 hjkl_buffer::Buffer::new(),
2863 crate::types::DefaultHost::new(),
2864 opts,
2865 );
2866 assert_eq!(e.settings().shiftwidth, 4);
2867 assert_eq!(e.settings().tabstop, 4);
2868 assert!(e.settings().expandtab);
2869 assert_eq!(e.settings().iskeyword, "@,a-z");
2870 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
2871 e.handle_key(key(KeyCode::Char('i')));
2873 assert_eq!(e.vim_mode(), VimMode::Insert);
2874 }
2875
2876 #[test]
2877 fn feed_input_char_routes_through_handle_key() {
2878 use crate::{Modifiers, PlannedInput};
2879 let mut e = Editor::new(
2880 hjkl_buffer::Buffer::new(),
2881 crate::types::DefaultHost::new(),
2882 crate::types::Options::default(),
2883 );
2884 e.set_content("abc");
2885 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2887 assert_eq!(e.vim_mode(), VimMode::Insert);
2888 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
2890 assert!(e.content().contains('X'));
2891 }
2892
2893 #[test]
2894 fn feed_input_special_key_routes() {
2895 use crate::{Modifiers, PlannedInput, SpecialKey};
2896 let mut e = Editor::new(
2897 hjkl_buffer::Buffer::new(),
2898 crate::types::DefaultHost::new(),
2899 crate::types::Options::default(),
2900 );
2901 e.set_content("abc");
2902 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2903 assert_eq!(e.vim_mode(), VimMode::Insert);
2904 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
2905 assert_eq!(e.vim_mode(), VimMode::Normal);
2906 }
2907
2908 #[test]
2909 fn feed_input_mouse_paste_focus_resize_no_op() {
2910 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
2911 let mut e = Editor::new(
2912 hjkl_buffer::Buffer::new(),
2913 crate::types::DefaultHost::new(),
2914 crate::types::Options::default(),
2915 );
2916 e.set_content("abc");
2917 let mode_before = e.vim_mode();
2918 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
2919 kind: MouseKind::Press,
2920 pos: Pos::new(0, 0),
2921 mods: Default::default(),
2922 }));
2923 assert!(!consumed);
2924 assert_eq!(e.vim_mode(), mode_before);
2925 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
2926 assert!(!e.feed_input(PlannedInput::FocusGained));
2927 assert!(!e.feed_input(PlannedInput::FocusLost));
2928 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
2929 }
2930
2931 #[test]
2932 fn intern_style_dedups_engine_native_styles() {
2933 use crate::types::{Attrs, Color, Style};
2934 let mut e = Editor::new(
2935 hjkl_buffer::Buffer::new(),
2936 crate::types::DefaultHost::new(),
2937 crate::types::Options::default(),
2938 );
2939 let s = Style {
2940 fg: Some(Color(255, 0, 0)),
2941 bg: None,
2942 attrs: Attrs::BOLD,
2943 };
2944 let id_a = e.intern_style(s);
2945 let id_b = e.intern_style(s);
2947 assert_eq!(id_a, id_b);
2948 let back = e.engine_style_at(id_a).expect("interned");
2950 assert_eq!(back, s);
2951 }
2952
2953 #[test]
2954 fn engine_style_at_out_of_range_returns_none() {
2955 let e = Editor::new(
2956 hjkl_buffer::Buffer::new(),
2957 crate::types::DefaultHost::new(),
2958 crate::types::Options::default(),
2959 );
2960 assert!(e.engine_style_at(99).is_none());
2961 }
2962
2963 #[test]
2964 fn take_changes_emits_per_row_for_block_insert() {
2965 let mut e = Editor::new(
2970 hjkl_buffer::Buffer::new(),
2971 crate::types::DefaultHost::new(),
2972 crate::types::Options::default(),
2973 );
2974 e.set_content("aaa\nbbb\nccc\nddd");
2975 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
2977 e.handle_key(key(KeyCode::Char('j')));
2978 e.handle_key(key(KeyCode::Char('j')));
2979 e.handle_key(shift_key(KeyCode::Char('I')));
2981 e.handle_key(key(KeyCode::Char('X')));
2982 e.handle_key(key(KeyCode::Esc));
2983
2984 let changes = e.take_changes();
2985 assert!(
2989 changes.len() >= 3,
2990 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
2991 changes.len()
2992 );
2993 }
2994
2995 #[test]
2996 fn take_changes_drains_after_insert() {
2997 let mut e = Editor::new(
2998 hjkl_buffer::Buffer::new(),
2999 crate::types::DefaultHost::new(),
3000 crate::types::Options::default(),
3001 );
3002 e.set_content("abc");
3003 assert!(e.take_changes().is_empty());
3005 e.handle_key(key(KeyCode::Char('i')));
3007 e.handle_key(key(KeyCode::Char('X')));
3008 let changes = e.take_changes();
3009 assert!(
3010 !changes.is_empty(),
3011 "insert mode keystroke should produce a change"
3012 );
3013 assert!(e.take_changes().is_empty());
3015 }
3016
3017 #[test]
3018 fn options_bridge_roundtrip() {
3019 let mut e = Editor::new(
3020 hjkl_buffer::Buffer::new(),
3021 crate::types::DefaultHost::new(),
3022 crate::types::Options::default(),
3023 );
3024 let opts = e.current_options();
3025 assert_eq!(opts.shiftwidth, 4);
3027 assert_eq!(opts.tabstop, 4);
3028
3029 let new_opts = crate::types::Options {
3030 shiftwidth: 4,
3031 tabstop: 2,
3032 ignorecase: true,
3033 ..crate::types::Options::default()
3034 };
3035 e.apply_options(&new_opts);
3036
3037 let after = e.current_options();
3038 assert_eq!(after.shiftwidth, 4);
3039 assert_eq!(after.tabstop, 2);
3040 assert!(after.ignorecase);
3041 }
3042
3043 #[test]
3044 fn selection_highlight_none_in_normal() {
3045 let mut e = Editor::new(
3046 hjkl_buffer::Buffer::new(),
3047 crate::types::DefaultHost::new(),
3048 crate::types::Options::default(),
3049 );
3050 e.set_content("hello");
3051 assert!(e.selection_highlight().is_none());
3052 }
3053
3054 #[test]
3055 fn selection_highlight_some_in_visual() {
3056 use crate::types::HighlightKind;
3057 let mut e = Editor::new(
3058 hjkl_buffer::Buffer::new(),
3059 crate::types::DefaultHost::new(),
3060 crate::types::Options::default(),
3061 );
3062 e.set_content("hello world");
3063 e.handle_key(key(KeyCode::Char('v')));
3064 e.handle_key(key(KeyCode::Char('l')));
3065 e.handle_key(key(KeyCode::Char('l')));
3066 let h = e
3067 .selection_highlight()
3068 .expect("visual mode should produce a highlight");
3069 assert_eq!(h.kind, HighlightKind::Selection);
3070 assert_eq!(h.range.start.line, 0);
3071 assert_eq!(h.range.end.line, 0);
3072 }
3073
3074 #[test]
3075 fn highlights_emit_incsearch_during_active_prompt() {
3076 use crate::types::HighlightKind;
3077 let mut e = Editor::new(
3078 hjkl_buffer::Buffer::new(),
3079 crate::types::DefaultHost::new(),
3080 crate::types::Options::default(),
3081 );
3082 e.set_content("foo bar foo\nbaz\n");
3083 e.handle_key(key(KeyCode::Char('/')));
3085 e.handle_key(key(KeyCode::Char('f')));
3086 e.handle_key(key(KeyCode::Char('o')));
3087 e.handle_key(key(KeyCode::Char('o')));
3088 assert!(e.search_prompt().is_some());
3090 let hs = e.highlights_for_line(0);
3091 assert_eq!(hs.len(), 2);
3092 for h in &hs {
3093 assert_eq!(h.kind, HighlightKind::IncSearch);
3094 }
3095 }
3096
3097 #[test]
3098 fn highlights_empty_for_blank_prompt() {
3099 let mut e = Editor::new(
3100 hjkl_buffer::Buffer::new(),
3101 crate::types::DefaultHost::new(),
3102 crate::types::Options::default(),
3103 );
3104 e.set_content("foo");
3105 e.handle_key(key(KeyCode::Char('/')));
3106 assert!(e.search_prompt().is_some());
3108 assert!(e.highlights_for_line(0).is_empty());
3109 }
3110
3111 #[test]
3112 fn highlights_emit_search_matches() {
3113 use crate::types::HighlightKind;
3114 let mut e = Editor::new(
3115 hjkl_buffer::Buffer::new(),
3116 crate::types::DefaultHost::new(),
3117 crate::types::Options::default(),
3118 );
3119 e.set_content("foo bar foo\nbaz qux\n");
3120 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3124 let hs = e.highlights_for_line(0);
3125 assert_eq!(hs.len(), 2);
3126 for h in &hs {
3127 assert_eq!(h.kind, HighlightKind::SearchMatch);
3128 assert_eq!(h.range.start.line, 0);
3129 assert_eq!(h.range.end.line, 0);
3130 }
3131 }
3132
3133 #[test]
3134 fn highlights_empty_without_pattern() {
3135 let mut e = Editor::new(
3136 hjkl_buffer::Buffer::new(),
3137 crate::types::DefaultHost::new(),
3138 crate::types::Options::default(),
3139 );
3140 e.set_content("foo bar");
3141 assert!(e.highlights_for_line(0).is_empty());
3142 }
3143
3144 #[test]
3145 fn highlights_empty_for_out_of_range_line() {
3146 let mut e = Editor::new(
3147 hjkl_buffer::Buffer::new(),
3148 crate::types::DefaultHost::new(),
3149 crate::types::Options::default(),
3150 );
3151 e.set_content("foo");
3152 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3153 assert!(e.highlights_for_line(99).is_empty());
3154 }
3155
3156 #[test]
3157 fn render_frame_reflects_mode_and_cursor() {
3158 use crate::types::{CursorShape, SnapshotMode};
3159 let mut e = Editor::new(
3160 hjkl_buffer::Buffer::new(),
3161 crate::types::DefaultHost::new(),
3162 crate::types::Options::default(),
3163 );
3164 e.set_content("alpha\nbeta");
3165 let f = e.render_frame();
3166 assert_eq!(f.mode, SnapshotMode::Normal);
3167 assert_eq!(f.cursor_shape, CursorShape::Block);
3168 assert_eq!(f.line_count, 2);
3169
3170 e.handle_key(key(KeyCode::Char('i')));
3171 let f = e.render_frame();
3172 assert_eq!(f.mode, SnapshotMode::Insert);
3173 assert_eq!(f.cursor_shape, CursorShape::Bar);
3174 }
3175
3176 #[test]
3177 fn snapshot_roundtrips_through_restore() {
3178 use crate::types::SnapshotMode;
3179 let mut e = Editor::new(
3180 hjkl_buffer::Buffer::new(),
3181 crate::types::DefaultHost::new(),
3182 crate::types::Options::default(),
3183 );
3184 e.set_content("alpha\nbeta\ngamma");
3185 e.jump_cursor(2, 3);
3186 let snap = e.take_snapshot();
3187 assert_eq!(snap.mode, SnapshotMode::Normal);
3188 assert_eq!(snap.cursor, (2, 3));
3189 assert_eq!(snap.lines.len(), 3);
3190
3191 let mut other = Editor::new(
3192 hjkl_buffer::Buffer::new(),
3193 crate::types::DefaultHost::new(),
3194 crate::types::Options::default(),
3195 );
3196 other.restore_snapshot(snap).expect("restore");
3197 assert_eq!(other.cursor(), (2, 3));
3198 assert_eq!(other.buffer().lines().len(), 3);
3199 }
3200
3201 #[test]
3202 fn restore_snapshot_rejects_version_mismatch() {
3203 let mut e = Editor::new(
3204 hjkl_buffer::Buffer::new(),
3205 crate::types::DefaultHost::new(),
3206 crate::types::Options::default(),
3207 );
3208 let mut snap = e.take_snapshot();
3209 snap.version = 9999;
3210 match e.restore_snapshot(snap) {
3211 Err(crate::EngineError::SnapshotVersion(got, want)) => {
3212 assert_eq!(got, 9999);
3213 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
3214 }
3215 other => panic!("expected SnapshotVersion err, got {other:?}"),
3216 }
3217 }
3218
3219 #[test]
3220 fn take_content_change_returns_some_on_first_dirty() {
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("hello");
3227 let first = e.take_content_change();
3228 assert!(first.is_some());
3229 let second = e.take_content_change();
3230 assert!(second.is_none());
3231 }
3232
3233 #[test]
3234 fn take_content_change_none_until_mutation() {
3235 let mut e = Editor::new(
3236 hjkl_buffer::Buffer::new(),
3237 crate::types::DefaultHost::new(),
3238 crate::types::Options::default(),
3239 );
3240 e.set_content("hello");
3241 e.take_content_change();
3243 assert!(e.take_content_change().is_none());
3244 e.handle_key(key(KeyCode::Char('i')));
3246 e.handle_key(key(KeyCode::Char('x')));
3247 let after = e.take_content_change();
3248 assert!(after.is_some());
3249 assert!(after.unwrap().contains('x'));
3250 }
3251
3252 #[test]
3253 fn vim_insert_to_normal() {
3254 let mut e = Editor::new(
3255 hjkl_buffer::Buffer::new(),
3256 crate::types::DefaultHost::new(),
3257 crate::types::Options::default(),
3258 );
3259 e.handle_key(key(KeyCode::Char('i')));
3260 e.handle_key(key(KeyCode::Esc));
3261 assert_eq!(e.vim_mode(), VimMode::Normal);
3262 }
3263
3264 #[test]
3265 fn vim_normal_to_visual() {
3266 let mut e = Editor::new(
3267 hjkl_buffer::Buffer::new(),
3268 crate::types::DefaultHost::new(),
3269 crate::types::Options::default(),
3270 );
3271 e.handle_key(key(KeyCode::Char('v')));
3272 assert_eq!(e.vim_mode(), VimMode::Visual);
3273 }
3274
3275 #[test]
3276 fn vim_visual_to_normal() {
3277 let mut e = Editor::new(
3278 hjkl_buffer::Buffer::new(),
3279 crate::types::DefaultHost::new(),
3280 crate::types::Options::default(),
3281 );
3282 e.handle_key(key(KeyCode::Char('v')));
3283 e.handle_key(key(KeyCode::Esc));
3284 assert_eq!(e.vim_mode(), VimMode::Normal);
3285 }
3286
3287 #[test]
3288 fn vim_shift_i_moves_to_first_non_whitespace() {
3289 let mut e = Editor::new(
3290 hjkl_buffer::Buffer::new(),
3291 crate::types::DefaultHost::new(),
3292 crate::types::Options::default(),
3293 );
3294 e.set_content(" hello");
3295 e.jump_cursor(0, 8);
3296 e.handle_key(shift_key(KeyCode::Char('I')));
3297 assert_eq!(e.vim_mode(), VimMode::Insert);
3298 assert_eq!(e.cursor(), (0, 3));
3299 }
3300
3301 #[test]
3302 fn vim_shift_a_moves_to_end_and_insert() {
3303 let mut e = Editor::new(
3304 hjkl_buffer::Buffer::new(),
3305 crate::types::DefaultHost::new(),
3306 crate::types::Options::default(),
3307 );
3308 e.set_content("hello");
3309 e.handle_key(shift_key(KeyCode::Char('A')));
3310 assert_eq!(e.vim_mode(), VimMode::Insert);
3311 assert_eq!(e.cursor().1, 5);
3312 }
3313
3314 #[test]
3315 fn count_10j_moves_down_10() {
3316 let mut e = Editor::new(
3317 hjkl_buffer::Buffer::new(),
3318 crate::types::DefaultHost::new(),
3319 crate::types::Options::default(),
3320 );
3321 e.set_content(
3322 (0..20)
3323 .map(|i| format!("line{i}"))
3324 .collect::<Vec<_>>()
3325 .join("\n")
3326 .as_str(),
3327 );
3328 for d in "10".chars() {
3329 e.handle_key(key(KeyCode::Char(d)));
3330 }
3331 e.handle_key(key(KeyCode::Char('j')));
3332 assert_eq!(e.cursor().0, 10);
3333 }
3334
3335 #[test]
3336 fn count_o_repeats_insert_on_esc() {
3337 let mut e = Editor::new(
3338 hjkl_buffer::Buffer::new(),
3339 crate::types::DefaultHost::new(),
3340 crate::types::Options::default(),
3341 );
3342 e.set_content("hello");
3343 for d in "3".chars() {
3344 e.handle_key(key(KeyCode::Char(d)));
3345 }
3346 e.handle_key(key(KeyCode::Char('o')));
3347 assert_eq!(e.vim_mode(), VimMode::Insert);
3348 for c in "world".chars() {
3349 e.handle_key(key(KeyCode::Char(c)));
3350 }
3351 e.handle_key(key(KeyCode::Esc));
3352 assert_eq!(e.vim_mode(), VimMode::Normal);
3353 assert_eq!(e.buffer().lines().len(), 4);
3354 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
3355 }
3356
3357 #[test]
3358 fn count_i_repeats_text_on_esc() {
3359 let mut e = Editor::new(
3360 hjkl_buffer::Buffer::new(),
3361 crate::types::DefaultHost::new(),
3362 crate::types::Options::default(),
3363 );
3364 e.set_content("");
3365 for d in "3".chars() {
3366 e.handle_key(key(KeyCode::Char(d)));
3367 }
3368 e.handle_key(key(KeyCode::Char('i')));
3369 for c in "ab".chars() {
3370 e.handle_key(key(KeyCode::Char(c)));
3371 }
3372 e.handle_key(key(KeyCode::Esc));
3373 assert_eq!(e.vim_mode(), VimMode::Normal);
3374 assert_eq!(e.buffer().lines()[0], "ababab");
3375 }
3376
3377 #[test]
3378 fn vim_shift_o_opens_line_above() {
3379 let mut e = Editor::new(
3380 hjkl_buffer::Buffer::new(),
3381 crate::types::DefaultHost::new(),
3382 crate::types::Options::default(),
3383 );
3384 e.set_content("hello");
3385 e.handle_key(shift_key(KeyCode::Char('O')));
3386 assert_eq!(e.vim_mode(), VimMode::Insert);
3387 assert_eq!(e.cursor(), (0, 0));
3388 assert_eq!(e.buffer().lines().len(), 2);
3389 }
3390
3391 #[test]
3392 fn vim_gg_goes_to_top() {
3393 let mut e = Editor::new(
3394 hjkl_buffer::Buffer::new(),
3395 crate::types::DefaultHost::new(),
3396 crate::types::Options::default(),
3397 );
3398 e.set_content("a\nb\nc");
3399 e.jump_cursor(2, 0);
3400 e.handle_key(key(KeyCode::Char('g')));
3401 e.handle_key(key(KeyCode::Char('g')));
3402 assert_eq!(e.cursor().0, 0);
3403 }
3404
3405 #[test]
3406 fn vim_shift_g_goes_to_bottom() {
3407 let mut e = Editor::new(
3408 hjkl_buffer::Buffer::new(),
3409 crate::types::DefaultHost::new(),
3410 crate::types::Options::default(),
3411 );
3412 e.set_content("a\nb\nc");
3413 e.handle_key(shift_key(KeyCode::Char('G')));
3414 assert_eq!(e.cursor().0, 2);
3415 }
3416
3417 #[test]
3418 fn vim_dd_deletes_line() {
3419 let mut e = Editor::new(
3420 hjkl_buffer::Buffer::new(),
3421 crate::types::DefaultHost::new(),
3422 crate::types::Options::default(),
3423 );
3424 e.set_content("first\nsecond");
3425 e.handle_key(key(KeyCode::Char('d')));
3426 e.handle_key(key(KeyCode::Char('d')));
3427 assert_eq!(e.buffer().lines().len(), 1);
3428 assert_eq!(e.buffer().lines()[0], "second");
3429 }
3430
3431 #[test]
3432 fn vim_dw_deletes_word() {
3433 let mut e = Editor::new(
3434 hjkl_buffer::Buffer::new(),
3435 crate::types::DefaultHost::new(),
3436 crate::types::Options::default(),
3437 );
3438 e.set_content("hello world");
3439 e.handle_key(key(KeyCode::Char('d')));
3440 e.handle_key(key(KeyCode::Char('w')));
3441 assert_eq!(e.vim_mode(), VimMode::Normal);
3442 assert!(!e.buffer().lines()[0].starts_with("hello"));
3443 }
3444
3445 #[test]
3446 fn vim_yy_yanks_line() {
3447 let mut e = Editor::new(
3448 hjkl_buffer::Buffer::new(),
3449 crate::types::DefaultHost::new(),
3450 crate::types::Options::default(),
3451 );
3452 e.set_content("hello\nworld");
3453 e.handle_key(key(KeyCode::Char('y')));
3454 e.handle_key(key(KeyCode::Char('y')));
3455 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3456 }
3457
3458 #[test]
3459 fn vim_yy_does_not_move_cursor() {
3460 let mut e = Editor::new(
3461 hjkl_buffer::Buffer::new(),
3462 crate::types::DefaultHost::new(),
3463 crate::types::Options::default(),
3464 );
3465 e.set_content("first\nsecond\nthird");
3466 e.jump_cursor(1, 0);
3467 let before = e.cursor();
3468 e.handle_key(key(KeyCode::Char('y')));
3469 e.handle_key(key(KeyCode::Char('y')));
3470 assert_eq!(e.cursor(), before);
3471 assert_eq!(e.vim_mode(), VimMode::Normal);
3472 }
3473
3474 #[test]
3475 fn vim_yw_yanks_word() {
3476 let mut e = Editor::new(
3477 hjkl_buffer::Buffer::new(),
3478 crate::types::DefaultHost::new(),
3479 crate::types::Options::default(),
3480 );
3481 e.set_content("hello world");
3482 e.handle_key(key(KeyCode::Char('y')));
3483 e.handle_key(key(KeyCode::Char('w')));
3484 assert_eq!(e.vim_mode(), VimMode::Normal);
3485 assert!(e.last_yank.is_some());
3486 }
3487
3488 #[test]
3489 fn vim_cc_changes_line() {
3490 let mut e = Editor::new(
3491 hjkl_buffer::Buffer::new(),
3492 crate::types::DefaultHost::new(),
3493 crate::types::Options::default(),
3494 );
3495 e.set_content("hello\nworld");
3496 e.handle_key(key(KeyCode::Char('c')));
3497 e.handle_key(key(KeyCode::Char('c')));
3498 assert_eq!(e.vim_mode(), VimMode::Insert);
3499 }
3500
3501 #[test]
3502 fn vim_u_undoes_insert_session_as_chunk() {
3503 let mut e = Editor::new(
3504 hjkl_buffer::Buffer::new(),
3505 crate::types::DefaultHost::new(),
3506 crate::types::Options::default(),
3507 );
3508 e.set_content("hello");
3509 e.handle_key(key(KeyCode::Char('i')));
3510 e.handle_key(key(KeyCode::Enter));
3511 e.handle_key(key(KeyCode::Enter));
3512 e.handle_key(key(KeyCode::Esc));
3513 assert_eq!(e.buffer().lines().len(), 3);
3514 e.handle_key(key(KeyCode::Char('u')));
3515 assert_eq!(e.buffer().lines().len(), 1);
3516 assert_eq!(e.buffer().lines()[0], "hello");
3517 }
3518
3519 #[test]
3520 fn vim_undo_redo_roundtrip() {
3521 let mut e = Editor::new(
3522 hjkl_buffer::Buffer::new(),
3523 crate::types::DefaultHost::new(),
3524 crate::types::Options::default(),
3525 );
3526 e.set_content("hello");
3527 e.handle_key(key(KeyCode::Char('i')));
3528 for c in "world".chars() {
3529 e.handle_key(key(KeyCode::Char(c)));
3530 }
3531 e.handle_key(key(KeyCode::Esc));
3532 let after = e.buffer().lines()[0].clone();
3533 e.handle_key(key(KeyCode::Char('u')));
3534 assert_eq!(e.buffer().lines()[0], "hello");
3535 e.handle_key(ctrl_key(KeyCode::Char('r')));
3536 assert_eq!(e.buffer().lines()[0], after);
3537 }
3538
3539 #[test]
3540 fn vim_u_undoes_dd() {
3541 let mut e = Editor::new(
3542 hjkl_buffer::Buffer::new(),
3543 crate::types::DefaultHost::new(),
3544 crate::types::Options::default(),
3545 );
3546 e.set_content("first\nsecond");
3547 e.handle_key(key(KeyCode::Char('d')));
3548 e.handle_key(key(KeyCode::Char('d')));
3549 assert_eq!(e.buffer().lines().len(), 1);
3550 e.handle_key(key(KeyCode::Char('u')));
3551 assert_eq!(e.buffer().lines().len(), 2);
3552 assert_eq!(e.buffer().lines()[0], "first");
3553 }
3554
3555 #[test]
3556 fn vim_ctrl_r_redoes() {
3557 let mut e = Editor::new(
3558 hjkl_buffer::Buffer::new(),
3559 crate::types::DefaultHost::new(),
3560 crate::types::Options::default(),
3561 );
3562 e.set_content("hello");
3563 e.handle_key(ctrl_key(KeyCode::Char('r')));
3564 }
3565
3566 #[test]
3567 fn vim_r_replaces_char() {
3568 let mut e = Editor::new(
3569 hjkl_buffer::Buffer::new(),
3570 crate::types::DefaultHost::new(),
3571 crate::types::Options::default(),
3572 );
3573 e.set_content("hello");
3574 e.handle_key(key(KeyCode::Char('r')));
3575 e.handle_key(key(KeyCode::Char('x')));
3576 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
3577 }
3578
3579 #[test]
3580 fn vim_tilde_toggles_case() {
3581 let mut e = Editor::new(
3582 hjkl_buffer::Buffer::new(),
3583 crate::types::DefaultHost::new(),
3584 crate::types::Options::default(),
3585 );
3586 e.set_content("hello");
3587 e.handle_key(key(KeyCode::Char('~')));
3588 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
3589 }
3590
3591 #[test]
3592 fn vim_visual_d_cuts() {
3593 let mut e = Editor::new(
3594 hjkl_buffer::Buffer::new(),
3595 crate::types::DefaultHost::new(),
3596 crate::types::Options::default(),
3597 );
3598 e.set_content("hello");
3599 e.handle_key(key(KeyCode::Char('v')));
3600 e.handle_key(key(KeyCode::Char('l')));
3601 e.handle_key(key(KeyCode::Char('l')));
3602 e.handle_key(key(KeyCode::Char('d')));
3603 assert_eq!(e.vim_mode(), VimMode::Normal);
3604 assert!(e.last_yank.is_some());
3605 }
3606
3607 #[test]
3608 fn vim_visual_c_enters_insert() {
3609 let mut e = Editor::new(
3610 hjkl_buffer::Buffer::new(),
3611 crate::types::DefaultHost::new(),
3612 crate::types::Options::default(),
3613 );
3614 e.set_content("hello");
3615 e.handle_key(key(KeyCode::Char('v')));
3616 e.handle_key(key(KeyCode::Char('l')));
3617 e.handle_key(key(KeyCode::Char('c')));
3618 assert_eq!(e.vim_mode(), VimMode::Insert);
3619 }
3620
3621 #[test]
3622 fn vim_normal_unknown_key_consumed() {
3623 let mut e = Editor::new(
3624 hjkl_buffer::Buffer::new(),
3625 crate::types::DefaultHost::new(),
3626 crate::types::Options::default(),
3627 );
3628 let consumed = e.handle_key(key(KeyCode::Char('z')));
3630 assert!(consumed);
3631 }
3632
3633 #[test]
3634 fn force_normal_clears_operator() {
3635 let mut e = Editor::new(
3636 hjkl_buffer::Buffer::new(),
3637 crate::types::DefaultHost::new(),
3638 crate::types::Options::default(),
3639 );
3640 e.handle_key(key(KeyCode::Char('d')));
3641 e.force_normal();
3642 assert_eq!(e.vim_mode(), VimMode::Normal);
3643 }
3644
3645 fn many_lines(n: usize) -> String {
3646 (0..n)
3647 .map(|i| format!("line{i}"))
3648 .collect::<Vec<_>>()
3649 .join("\n")
3650 }
3651
3652 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
3653 e.set_viewport_height(height);
3654 }
3655
3656 #[test]
3657 fn zz_centers_cursor_in_viewport() {
3658 let mut e = Editor::new(
3659 hjkl_buffer::Buffer::new(),
3660 crate::types::DefaultHost::new(),
3661 crate::types::Options::default(),
3662 );
3663 e.set_content(&many_lines(100));
3664 prime_viewport(&mut e, 20);
3665 e.jump_cursor(50, 0);
3666 e.handle_key(key(KeyCode::Char('z')));
3667 e.handle_key(key(KeyCode::Char('z')));
3668 assert_eq!(e.host().viewport().top_row, 40);
3669 assert_eq!(e.cursor().0, 50);
3670 }
3671
3672 #[test]
3673 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
3674 let mut e = Editor::new(
3675 hjkl_buffer::Buffer::new(),
3676 crate::types::DefaultHost::new(),
3677 crate::types::Options::default(),
3678 );
3679 e.set_content(&many_lines(100));
3680 prime_viewport(&mut e, 20);
3681 e.jump_cursor(50, 0);
3682 e.handle_key(key(KeyCode::Char('z')));
3683 e.handle_key(key(KeyCode::Char('t')));
3684 assert_eq!(e.host().viewport().top_row, 45);
3687 assert_eq!(e.cursor().0, 50);
3688 }
3689
3690 #[test]
3691 fn ctrl_a_increments_number_at_cursor() {
3692 let mut e = Editor::new(
3693 hjkl_buffer::Buffer::new(),
3694 crate::types::DefaultHost::new(),
3695 crate::types::Options::default(),
3696 );
3697 e.set_content("x = 41");
3698 e.handle_key(ctrl_key(KeyCode::Char('a')));
3699 assert_eq!(e.buffer().lines()[0], "x = 42");
3700 assert_eq!(e.cursor(), (0, 5));
3701 }
3702
3703 #[test]
3704 fn ctrl_a_finds_number_to_right_of_cursor() {
3705 let mut e = Editor::new(
3706 hjkl_buffer::Buffer::new(),
3707 crate::types::DefaultHost::new(),
3708 crate::types::Options::default(),
3709 );
3710 e.set_content("foo 99 bar");
3711 e.handle_key(ctrl_key(KeyCode::Char('a')));
3712 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
3713 assert_eq!(e.cursor(), (0, 6));
3714 }
3715
3716 #[test]
3717 fn ctrl_a_with_count_adds_count() {
3718 let mut e = Editor::new(
3719 hjkl_buffer::Buffer::new(),
3720 crate::types::DefaultHost::new(),
3721 crate::types::Options::default(),
3722 );
3723 e.set_content("x = 10");
3724 for d in "5".chars() {
3725 e.handle_key(key(KeyCode::Char(d)));
3726 }
3727 e.handle_key(ctrl_key(KeyCode::Char('a')));
3728 assert_eq!(e.buffer().lines()[0], "x = 15");
3729 }
3730
3731 #[test]
3732 fn ctrl_x_decrements_number() {
3733 let mut e = Editor::new(
3734 hjkl_buffer::Buffer::new(),
3735 crate::types::DefaultHost::new(),
3736 crate::types::Options::default(),
3737 );
3738 e.set_content("n=5");
3739 e.handle_key(ctrl_key(KeyCode::Char('x')));
3740 assert_eq!(e.buffer().lines()[0], "n=4");
3741 }
3742
3743 #[test]
3744 fn ctrl_x_crosses_zero_into_negative() {
3745 let mut e = Editor::new(
3746 hjkl_buffer::Buffer::new(),
3747 crate::types::DefaultHost::new(),
3748 crate::types::Options::default(),
3749 );
3750 e.set_content("v=0");
3751 e.handle_key(ctrl_key(KeyCode::Char('x')));
3752 assert_eq!(e.buffer().lines()[0], "v=-1");
3753 }
3754
3755 #[test]
3756 fn ctrl_a_on_negative_number_increments_toward_zero() {
3757 let mut e = Editor::new(
3758 hjkl_buffer::Buffer::new(),
3759 crate::types::DefaultHost::new(),
3760 crate::types::Options::default(),
3761 );
3762 e.set_content("a = -5");
3763 e.handle_key(ctrl_key(KeyCode::Char('a')));
3764 assert_eq!(e.buffer().lines()[0], "a = -4");
3765 }
3766
3767 #[test]
3768 fn ctrl_a_noop_when_no_digit_on_line() {
3769 let mut e = Editor::new(
3770 hjkl_buffer::Buffer::new(),
3771 crate::types::DefaultHost::new(),
3772 crate::types::Options::default(),
3773 );
3774 e.set_content("no digits here");
3775 e.handle_key(ctrl_key(KeyCode::Char('a')));
3776 assert_eq!(e.buffer().lines()[0], "no digits here");
3777 }
3778
3779 #[test]
3780 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
3781 let mut e = Editor::new(
3782 hjkl_buffer::Buffer::new(),
3783 crate::types::DefaultHost::new(),
3784 crate::types::Options::default(),
3785 );
3786 e.set_content(&many_lines(100));
3787 prime_viewport(&mut e, 20);
3788 e.jump_cursor(50, 0);
3789 e.handle_key(key(KeyCode::Char('z')));
3790 e.handle_key(key(KeyCode::Char('b')));
3791 assert_eq!(e.host().viewport().top_row, 36);
3795 assert_eq!(e.cursor().0, 50);
3796 }
3797
3798 #[test]
3805 fn set_content_dirties_then_take_dirty_clears() {
3806 let mut e = Editor::new(
3807 hjkl_buffer::Buffer::new(),
3808 crate::types::DefaultHost::new(),
3809 crate::types::Options::default(),
3810 );
3811 e.set_content("hello");
3812 assert!(
3813 e.take_dirty(),
3814 "set_content should leave content_dirty=true"
3815 );
3816 assert!(!e.take_dirty(), "take_dirty should clear the flag");
3817 }
3818
3819 #[test]
3820 fn content_arc_returns_same_arc_until_mutation() {
3821 let mut e = Editor::new(
3822 hjkl_buffer::Buffer::new(),
3823 crate::types::DefaultHost::new(),
3824 crate::types::Options::default(),
3825 );
3826 e.set_content("hello");
3827 let a = e.content_arc();
3828 let b = e.content_arc();
3829 assert!(
3830 std::sync::Arc::ptr_eq(&a, &b),
3831 "repeated content_arc() should hit the cache"
3832 );
3833
3834 e.handle_key(key(KeyCode::Char('i')));
3836 e.handle_key(key(KeyCode::Char('!')));
3837 let c = e.content_arc();
3838 assert!(
3839 !std::sync::Arc::ptr_eq(&a, &c),
3840 "mutation should invalidate content_arc() cache"
3841 );
3842 assert!(c.contains('!'));
3843 }
3844
3845 #[test]
3846 fn content_arc_cache_invalidated_by_set_content() {
3847 let mut e = Editor::new(
3848 hjkl_buffer::Buffer::new(),
3849 crate::types::DefaultHost::new(),
3850 crate::types::Options::default(),
3851 );
3852 e.set_content("one");
3853 let a = e.content_arc();
3854 e.set_content("two");
3855 let b = e.content_arc();
3856 assert!(!std::sync::Arc::ptr_eq(&a, &b));
3857 assert!(b.starts_with("two"));
3858 }
3859
3860 #[test]
3866 fn mouse_click_past_eol_lands_on_last_char() {
3867 let mut e = Editor::new(
3868 hjkl_buffer::Buffer::new(),
3869 crate::types::DefaultHost::new(),
3870 crate::types::Options::default(),
3871 );
3872 e.set_content("hello");
3873 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3877 e.mouse_click_in_rect(area, 78, 1);
3878 assert_eq!(e.cursor(), (0, 4));
3879 }
3880
3881 #[test]
3882 fn mouse_click_past_eol_handles_multibyte_line() {
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("héllo");
3891 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3892 e.mouse_click_in_rect(area, 78, 1);
3893 assert_eq!(e.cursor(), (0, 4));
3894 }
3895
3896 #[test]
3897 fn mouse_click_inside_line_lands_on_clicked_char() {
3898 let mut e = Editor::new(
3899 hjkl_buffer::Buffer::new(),
3900 crate::types::DefaultHost::new(),
3901 crate::types::Options::default(),
3902 );
3903 e.set_content("hello world");
3904 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3907 e.mouse_click_in_rect(area, 4, 1);
3908 assert_eq!(e.cursor(), (0, 0));
3909 e.mouse_click_in_rect(area, 6, 1);
3910 assert_eq!(e.cursor(), (0, 2));
3911 }
3912
3913 #[test]
3918 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
3919 let mut e = Editor::new(
3920 hjkl_buffer::Buffer::new(),
3921 crate::types::DefaultHost::new(),
3922 crate::types::Options::default(),
3923 );
3924 e.set_content("hello world");
3925 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3926 assert!(e.settings().undo_break_on_motion);
3928 e.handle_key(key(KeyCode::Char('i')));
3930 e.handle_key(key(KeyCode::Char('A')));
3931 e.handle_key(key(KeyCode::Char('A')));
3932 e.handle_key(key(KeyCode::Char('A')));
3933 e.mouse_click_in_rect(area, 10, 1);
3935 e.handle_key(key(KeyCode::Char('B')));
3937 e.handle_key(key(KeyCode::Char('B')));
3938 e.handle_key(key(KeyCode::Char('B')));
3939 e.handle_key(key(KeyCode::Esc));
3941 e.handle_key(key(KeyCode::Char('u')));
3942 let line = e.buffer().line(0).unwrap_or("").to_string();
3943 assert!(
3944 line.contains("AAA"),
3945 "AAA must survive undo (separate group): {line:?}"
3946 );
3947 assert!(
3948 !line.contains("BBB"),
3949 "BBB must be undone (post-click group): {line:?}"
3950 );
3951 }
3952
3953 #[test]
3957 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
3958 let mut e = Editor::new(
3959 hjkl_buffer::Buffer::new(),
3960 crate::types::DefaultHost::new(),
3961 crate::types::Options::default(),
3962 );
3963 e.set_content("hello world");
3964 e.settings_mut().undo_break_on_motion = false;
3965 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3966 e.handle_key(key(KeyCode::Char('i')));
3967 e.handle_key(key(KeyCode::Char('A')));
3968 e.handle_key(key(KeyCode::Char('A')));
3969 e.mouse_click_in_rect(area, 10, 1);
3970 e.handle_key(key(KeyCode::Char('B')));
3971 e.handle_key(key(KeyCode::Char('B')));
3972 e.handle_key(key(KeyCode::Esc));
3973 e.handle_key(key(KeyCode::Char('u')));
3974 let line = e.buffer().line(0).unwrap_or("").to_string();
3975 assert!(
3976 !line.contains("AA") && !line.contains("BB"),
3977 "with undobreak off, single `u` must reverse whole insert: {line:?}"
3978 );
3979 assert_eq!(line, "hello world");
3980 }
3981
3982 #[test]
3985 fn host_clipboard_round_trip_via_default_host() {
3986 let mut e = Editor::new(
3989 hjkl_buffer::Buffer::new(),
3990 crate::types::DefaultHost::new(),
3991 crate::types::Options::default(),
3992 );
3993 e.host_mut().write_clipboard("payload".to_string());
3994 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
3995 }
3996
3997 #[test]
3998 fn host_records_clipboard_on_yank() {
3999 let mut e = Editor::new(
4003 hjkl_buffer::Buffer::new(),
4004 crate::types::DefaultHost::new(),
4005 crate::types::Options::default(),
4006 );
4007 e.set_content("hello\n");
4008 e.handle_key(key(KeyCode::Char('y')));
4009 e.handle_key(key(KeyCode::Char('y')));
4010 let clip = e.host_mut().read_clipboard();
4012 assert!(
4013 clip.as_deref().unwrap_or("").starts_with("hello"),
4014 "host clipboard should carry the yank: {clip:?}"
4015 );
4016 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
4018 }
4019
4020 #[test]
4021 fn host_cursor_shape_via_shared_recorder() {
4022 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
4026 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
4027 struct LeakHost {
4028 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
4029 viewport: crate::types::Viewport,
4030 }
4031 impl crate::types::Host for LeakHost {
4032 type Intent = ();
4033 fn write_clipboard(&mut self, _: String) {}
4034 fn read_clipboard(&mut self) -> Option<String> {
4035 None
4036 }
4037 fn now(&self) -> core::time::Duration {
4038 core::time::Duration::ZERO
4039 }
4040 fn prompt_search(&mut self) -> Option<String> {
4041 None
4042 }
4043 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
4044 self.shapes.lock().unwrap().push(s);
4045 }
4046 fn viewport(&self) -> &crate::types::Viewport {
4047 &self.viewport
4048 }
4049 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4050 &mut self.viewport
4051 }
4052 fn emit_intent(&mut self, _: Self::Intent) {}
4053 }
4054 let mut e = Editor::new(
4055 hjkl_buffer::Buffer::new(),
4056 LeakHost {
4057 shapes: shapes_ptr,
4058 viewport: crate::types::Viewport::default(),
4059 },
4060 crate::types::Options::default(),
4061 );
4062 e.set_content("abc");
4063 e.handle_key(key(KeyCode::Char('i')));
4065 e.handle_key(key(KeyCode::Esc));
4067 let shapes = shapes_ptr.lock().unwrap().clone();
4068 assert_eq!(
4069 shapes,
4070 vec![
4071 crate::types::CursorShape::Bar,
4072 crate::types::CursorShape::Block,
4073 ],
4074 "host should observe Insert(Bar) → Normal(Block) transitions"
4075 );
4076 }
4077
4078 #[test]
4079 fn host_now_drives_chord_timeout_deterministically() {
4080 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
4085 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
4086 struct ClockHost {
4087 now: &'static std::sync::Mutex<core::time::Duration>,
4088 viewport: crate::types::Viewport,
4089 }
4090 impl crate::types::Host for ClockHost {
4091 type Intent = ();
4092 fn write_clipboard(&mut self, _: String) {}
4093 fn read_clipboard(&mut self) -> Option<String> {
4094 None
4095 }
4096 fn now(&self) -> core::time::Duration {
4097 *self.now.lock().unwrap()
4098 }
4099 fn prompt_search(&mut self) -> Option<String> {
4100 None
4101 }
4102 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
4103 fn viewport(&self) -> &crate::types::Viewport {
4104 &self.viewport
4105 }
4106 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4107 &mut self.viewport
4108 }
4109 fn emit_intent(&mut self, _: Self::Intent) {}
4110 }
4111 let mut e = Editor::new(
4112 hjkl_buffer::Buffer::new(),
4113 ClockHost {
4114 now: now_ptr,
4115 viewport: crate::types::Viewport::default(),
4116 },
4117 crate::types::Options::default(),
4118 );
4119 e.set_content("a\nb\nc\n");
4120 e.jump_cursor(2, 0);
4121 e.handle_key(key(KeyCode::Char('g')));
4123 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
4125 e.handle_key(key(KeyCode::Char('g')));
4128 assert_eq!(
4129 e.cursor().0,
4130 2,
4131 "Host::now() must drive `:set timeoutlen` deterministically"
4132 );
4133 }
4134
4135 fn fresh_editor(initial: &str) -> Editor {
4138 let buffer = hjkl_buffer::Buffer::from_str(initial);
4139 Editor::new(
4140 buffer,
4141 crate::types::DefaultHost::new(),
4142 crate::types::Options::default(),
4143 )
4144 }
4145
4146 #[test]
4147 fn content_edit_insert_char_at_origin() {
4148 let mut e = fresh_editor("");
4149 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4150 at: hjkl_buffer::Position::new(0, 0),
4151 ch: 'a',
4152 });
4153 let edits = e.take_content_edits();
4154 assert_eq!(edits.len(), 1);
4155 let ce = &edits[0];
4156 assert_eq!(ce.start_byte, 0);
4157 assert_eq!(ce.old_end_byte, 0);
4158 assert_eq!(ce.new_end_byte, 1);
4159 assert_eq!(ce.start_position, (0, 0));
4160 assert_eq!(ce.old_end_position, (0, 0));
4161 assert_eq!(ce.new_end_position, (0, 1));
4162 }
4163
4164 #[test]
4165 fn content_edit_insert_str_multiline() {
4166 let mut e = fresh_editor("x\ny");
4168 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertStr {
4169 at: hjkl_buffer::Position::new(0, 1),
4170 text: "ab\ncd".into(),
4171 });
4172 let edits = e.take_content_edits();
4173 assert_eq!(edits.len(), 1);
4174 let ce = &edits[0];
4175 assert_eq!(ce.start_byte, 1);
4176 assert_eq!(ce.old_end_byte, 1);
4177 assert_eq!(ce.new_end_byte, 1 + 5);
4178 assert_eq!(ce.start_position, (0, 1));
4179 assert_eq!(ce.new_end_position, (1, 2));
4181 }
4182
4183 #[test]
4184 fn content_edit_delete_range_charwise() {
4185 let mut e = fresh_editor("abcdef");
4187 let _ = e.mutate_edit(hjkl_buffer::Edit::DeleteRange {
4188 start: hjkl_buffer::Position::new(0, 1),
4189 end: hjkl_buffer::Position::new(0, 4),
4190 kind: hjkl_buffer::MotionKind::Char,
4191 });
4192 let edits = e.take_content_edits();
4193 assert_eq!(edits.len(), 1);
4194 let ce = &edits[0];
4195 assert_eq!(ce.start_byte, 1);
4196 assert_eq!(ce.old_end_byte, 4);
4197 assert_eq!(ce.new_end_byte, 1);
4198 assert!(ce.old_end_byte > ce.new_end_byte);
4199 }
4200
4201 #[test]
4202 fn content_edit_set_content_resets() {
4203 let mut e = fresh_editor("foo");
4204 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4205 at: hjkl_buffer::Position::new(0, 0),
4206 ch: 'X',
4207 });
4208 e.set_content("brand new");
4211 assert!(e.take_content_reset());
4212 assert!(!e.take_content_reset());
4214 assert!(e.take_content_edits().is_empty());
4216 }
4217
4218 #[test]
4219 fn content_edit_multiple_replaces_in_order() {
4220 let mut e = fresh_editor("xax xbx xcx");
4225 let _ = e.take_content_edits();
4226 let _ = e.take_content_reset();
4227 let positions = [(0usize, 0usize), (0, 4), (0, 8)];
4231 for (row, col) in positions {
4232 let _ = e.mutate_edit(hjkl_buffer::Edit::Replace {
4233 start: hjkl_buffer::Position::new(row, col),
4234 end: hjkl_buffer::Position::new(row, col + 1),
4235 with: "yy".into(),
4236 });
4237 }
4238 let edits = e.take_content_edits();
4239 assert_eq!(edits.len(), 3);
4240 for ce in &edits {
4241 assert!(ce.start_byte <= ce.old_end_byte);
4242 assert!(ce.start_byte <= ce.new_end_byte);
4243 }
4244 for w in edits.windows(2) {
4246 assert!(w[0].start_byte <= w[1].start_byte);
4247 }
4248 }
4249}