1use crate::input::{Input, Key};
10use crate::vim::{self, VimState};
11use crate::{KeybindingMode, VimMode};
12#[cfg(feature = "crossterm")]
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14#[cfg(feature = "ratatui")]
15use ratatui::layout::Rect;
16use std::sync::atomic::{AtomicU16, Ordering};
17
18#[cfg(feature = "ratatui")]
25pub(crate) fn engine_style_to_ratatui(s: crate::types::Style) -> ratatui::style::Style {
26 use crate::types::Attrs;
27 use ratatui::style::{Color as RColor, Modifier as RMod, Style as RStyle};
28 let mut out = RStyle::default();
29 if let Some(c) = s.fg {
30 out = out.fg(RColor::Rgb(c.0, c.1, c.2));
31 }
32 if let Some(c) = s.bg {
33 out = out.bg(RColor::Rgb(c.0, c.1, c.2));
34 }
35 let mut m = RMod::empty();
36 if s.attrs.contains(Attrs::BOLD) {
37 m |= RMod::BOLD;
38 }
39 if s.attrs.contains(Attrs::ITALIC) {
40 m |= RMod::ITALIC;
41 }
42 if s.attrs.contains(Attrs::UNDERLINE) {
43 m |= RMod::UNDERLINED;
44 }
45 if s.attrs.contains(Attrs::REVERSE) {
46 m |= RMod::REVERSED;
47 }
48 if s.attrs.contains(Attrs::DIM) {
49 m |= RMod::DIM;
50 }
51 if s.attrs.contains(Attrs::STRIKE) {
52 m |= RMod::CROSSED_OUT;
53 }
54 out.add_modifier(m)
55}
56
57#[cfg(feature = "ratatui")]
61pub(crate) fn ratatui_style_to_engine(s: ratatui::style::Style) -> crate::types::Style {
62 use crate::types::{Attrs, Color, Style};
63 use ratatui::style::{Color as RColor, Modifier as RMod};
64 fn c(rc: RColor) -> Color {
65 match rc {
66 RColor::Rgb(r, g, b) => Color(r, g, b),
67 RColor::Black => Color(0, 0, 0),
68 RColor::Red => Color(205, 49, 49),
69 RColor::Green => Color(13, 188, 121),
70 RColor::Yellow => Color(229, 229, 16),
71 RColor::Blue => Color(36, 114, 200),
72 RColor::Magenta => Color(188, 63, 188),
73 RColor::Cyan => Color(17, 168, 205),
74 RColor::Gray => Color(229, 229, 229),
75 RColor::DarkGray => Color(102, 102, 102),
76 RColor::LightRed => Color(241, 76, 76),
77 RColor::LightGreen => Color(35, 209, 139),
78 RColor::LightYellow => Color(245, 245, 67),
79 RColor::LightBlue => Color(59, 142, 234),
80 RColor::LightMagenta => Color(214, 112, 214),
81 RColor::LightCyan => Color(41, 184, 219),
82 RColor::White => Color(255, 255, 255),
83 _ => Color(0, 0, 0),
84 }
85 }
86 let mut attrs = Attrs::empty();
87 if s.add_modifier.contains(RMod::BOLD) {
88 attrs |= Attrs::BOLD;
89 }
90 if s.add_modifier.contains(RMod::ITALIC) {
91 attrs |= Attrs::ITALIC;
92 }
93 if s.add_modifier.contains(RMod::UNDERLINED) {
94 attrs |= Attrs::UNDERLINE;
95 }
96 if s.add_modifier.contains(RMod::REVERSED) {
97 attrs |= Attrs::REVERSE;
98 }
99 if s.add_modifier.contains(RMod::DIM) {
100 attrs |= Attrs::DIM;
101 }
102 if s.add_modifier.contains(RMod::CROSSED_OUT) {
103 attrs |= Attrs::STRIKE;
104 }
105 Style {
106 fg: s.fg.map(c),
107 bg: s.bg.map(c),
108 attrs,
109 }
110}
111
112fn edit_to_editops(edit: &hjkl_buffer::Edit) -> Vec<crate::types::Edit> {
124 use crate::types::{Edit as Op, Pos};
125 use hjkl_buffer::Edit as B;
126 let to_pos = |p: hjkl_buffer::Position| Pos {
127 line: p.row as u32,
128 col: p.col as u32,
129 };
130 match edit {
131 B::InsertChar { at, ch } => vec![Op {
132 range: to_pos(*at)..to_pos(*at),
133 replacement: ch.to_string(),
134 }],
135 B::InsertStr { at, text } => vec![Op {
136 range: to_pos(*at)..to_pos(*at),
137 replacement: text.clone(),
138 }],
139 B::DeleteRange { start, end, .. } => vec![Op {
140 range: to_pos(*start)..to_pos(*end),
141 replacement: String::new(),
142 }],
143 B::Replace { start, end, with } => vec![Op {
144 range: to_pos(*start)..to_pos(*end),
145 replacement: with.clone(),
146 }],
147 B::JoinLines {
148 row,
149 count,
150 with_space,
151 } => {
152 let start = Pos {
157 line: *row as u32 + 1,
158 col: 0,
159 };
160 let end = Pos {
161 line: (*row + *count) as u32,
162 col: u32::MAX, };
164 vec![Op {
165 range: start..end,
166 replacement: if *with_space {
167 " ".into()
168 } else {
169 String::new()
170 },
171 }]
172 }
173 B::SplitLines {
174 row,
175 cols,
176 inserted_space: _,
177 } => {
178 cols.iter()
181 .map(|c| {
182 let p = Pos {
183 line: *row as u32,
184 col: *c as u32,
185 };
186 Op {
187 range: p..p,
188 replacement: "\n".into(),
189 }
190 })
191 .collect()
192 }
193 B::InsertBlock { at, chunks } => {
194 chunks
196 .iter()
197 .enumerate()
198 .map(|(i, chunk)| {
199 let p = Pos {
200 line: at.row as u32 + i as u32,
201 col: at.col as u32,
202 };
203 Op {
204 range: p..p,
205 replacement: chunk.clone(),
206 }
207 })
208 .collect()
209 }
210 B::DeleteBlockChunks { at, widths } => {
211 widths
214 .iter()
215 .enumerate()
216 .map(|(i, w)| {
217 let start = Pos {
218 line: at.row as u32 + i as u32,
219 col: at.col as u32,
220 };
221 let end = Pos {
222 line: at.row as u32 + i as u32,
223 col: at.col as u32 + *w as u32,
224 };
225 Op {
226 range: start..end,
227 replacement: String::new(),
228 }
229 })
230 .collect()
231 }
232 }
233}
234
235#[inline]
239fn buffer_byte_of_row(buf: &hjkl_buffer::Buffer, row: usize) -> usize {
240 let n = buf.row_count();
241 let row = row.min(n);
242 let mut acc = 0usize;
243 for r in 0..row {
244 acc += buf.line(r).map(str::len).unwrap_or(0);
245 if r + 1 < n {
246 acc += 1; }
248 }
249 acc
250}
251
252fn position_to_byte_coords(
256 buf: &hjkl_buffer::Buffer,
257 pos: hjkl_buffer::Position,
258) -> (usize, (u32, u32)) {
259 let row = pos.row.min(buf.row_count().saturating_sub(1));
260 let line = buf.line(row).unwrap_or("");
261 let col_byte = pos.byte_offset(line);
262 let byte = buffer_byte_of_row(buf, row) + col_byte;
263 (byte, (row as u32, col_byte as u32))
264}
265
266fn advance_by_text(text: &str, start_byte: usize, start_pos: (u32, u32)) -> (usize, (u32, u32)) {
269 let new_end_byte = start_byte + text.len();
270 let newlines = text.bytes().filter(|&b| b == b'\n').count();
271 let end_pos = if newlines == 0 {
272 (start_pos.0, start_pos.1 + text.len() as u32)
273 } else {
274 let last_nl = text.rfind('\n').unwrap();
276 let tail_bytes = (text.len() - last_nl - 1) as u32;
277 (start_pos.0 + newlines as u32, tail_bytes)
278 };
279 (new_end_byte, end_pos)
280}
281
282fn content_edits_from_buffer_edit(
287 buf: &hjkl_buffer::Buffer,
288 edit: &hjkl_buffer::Edit,
289) -> Vec<crate::types::ContentEdit> {
290 use hjkl_buffer::Edit as B;
291 use hjkl_buffer::Position;
292
293 let mut out: Vec<crate::types::ContentEdit> = Vec::new();
294
295 match edit {
296 B::InsertChar { at, ch } => {
297 let (start_byte, start_pos) = position_to_byte_coords(buf, *at);
298 let new_end_byte = start_byte + ch.len_utf8();
299 let new_end_pos = (start_pos.0, start_pos.1 + ch.len_utf8() as u32);
300 out.push(crate::types::ContentEdit {
301 start_byte,
302 old_end_byte: start_byte,
303 new_end_byte,
304 start_position: start_pos,
305 old_end_position: start_pos,
306 new_end_position: new_end_pos,
307 });
308 }
309 B::InsertStr { at, text } => {
310 let (start_byte, start_pos) = position_to_byte_coords(buf, *at);
311 let (new_end_byte, new_end_pos) = advance_by_text(text, start_byte, start_pos);
312 out.push(crate::types::ContentEdit {
313 start_byte,
314 old_end_byte: start_byte,
315 new_end_byte,
316 start_position: start_pos,
317 old_end_position: start_pos,
318 new_end_position: new_end_pos,
319 });
320 }
321 B::DeleteRange { start, end, kind } => {
322 let (start, end) = if start <= end {
323 (*start, *end)
324 } else {
325 (*end, *start)
326 };
327 match kind {
328 hjkl_buffer::MotionKind::Char => {
329 let (start_byte, start_pos) = position_to_byte_coords(buf, start);
330 let (old_end_byte, old_end_pos) = position_to_byte_coords(buf, end);
331 out.push(crate::types::ContentEdit {
332 start_byte,
333 old_end_byte,
334 new_end_byte: start_byte,
335 start_position: start_pos,
336 old_end_position: old_end_pos,
337 new_end_position: start_pos,
338 });
339 }
340 hjkl_buffer::MotionKind::Line => {
341 let lo = start.row;
346 let hi = end.row.min(buf.row_count().saturating_sub(1));
347 let start_byte = buffer_byte_of_row(buf, lo);
348 let next_row_byte = if hi + 1 < buf.row_count() {
349 buffer_byte_of_row(buf, hi + 1)
350 } else {
351 buffer_byte_of_row(buf, buf.row_count())
353 + buf
354 .line(buf.row_count().saturating_sub(1))
355 .map(str::len)
356 .unwrap_or(0)
357 };
358 out.push(crate::types::ContentEdit {
359 start_byte,
360 old_end_byte: next_row_byte,
361 new_end_byte: start_byte,
362 start_position: (lo as u32, 0),
363 old_end_position: ((hi + 1) as u32, 0),
364 new_end_position: (lo as u32, 0),
365 });
366 }
367 hjkl_buffer::MotionKind::Block => {
368 let (left_col, right_col) = (start.col.min(end.col), start.col.max(end.col));
371 for row in start.row..=end.row {
372 let row_start_pos = Position::new(row, left_col);
373 let row_end_pos = Position::new(row, right_col + 1);
374 let (sb, sp) = position_to_byte_coords(buf, row_start_pos);
375 let (eb, ep) = position_to_byte_coords(buf, row_end_pos);
376 if eb <= sb {
377 continue;
378 }
379 out.push(crate::types::ContentEdit {
380 start_byte: sb,
381 old_end_byte: eb,
382 new_end_byte: sb,
383 start_position: sp,
384 old_end_position: ep,
385 new_end_position: sp,
386 });
387 }
388 }
389 }
390 }
391 B::Replace { start, end, with } => {
392 let (start, end) = if start <= end {
393 (*start, *end)
394 } else {
395 (*end, *start)
396 };
397 let (start_byte, start_pos) = position_to_byte_coords(buf, start);
398 let (old_end_byte, old_end_pos) = position_to_byte_coords(buf, end);
399 let (new_end_byte, new_end_pos) = advance_by_text(with, start_byte, start_pos);
400 out.push(crate::types::ContentEdit {
401 start_byte,
402 old_end_byte,
403 new_end_byte,
404 start_position: start_pos,
405 old_end_position: old_end_pos,
406 new_end_position: new_end_pos,
407 });
408 }
409 B::JoinLines {
410 row,
411 count,
412 with_space,
413 } => {
414 let row = (*row).min(buf.row_count().saturating_sub(1));
420 let last_join_row = (row + count).min(buf.row_count().saturating_sub(1));
421 let line = buf.line(row).unwrap_or("");
422 let row_eol_byte = buffer_byte_of_row(buf, row) + line.len();
423 let row_eol_col = line.len() as u32;
424 let next_row_after = last_join_row + 1;
425 let old_end_byte = if next_row_after < buf.row_count() {
426 buffer_byte_of_row(buf, next_row_after).saturating_sub(1)
427 } else {
428 buffer_byte_of_row(buf, buf.row_count())
429 + buf
430 .line(buf.row_count().saturating_sub(1))
431 .map(str::len)
432 .unwrap_or(0)
433 };
434 let last_line = buf.line(last_join_row).unwrap_or("");
435 let old_end_pos = (last_join_row as u32, last_line.len() as u32);
436 let replacement_len = if *with_space { 1 } else { 0 };
437 let new_end_byte = row_eol_byte + replacement_len;
438 let new_end_pos = (row as u32, row_eol_col + replacement_len as u32);
439 out.push(crate::types::ContentEdit {
440 start_byte: row_eol_byte,
441 old_end_byte,
442 new_end_byte,
443 start_position: (row as u32, row_eol_col),
444 old_end_position: old_end_pos,
445 new_end_position: new_end_pos,
446 });
447 }
448 B::SplitLines {
449 row,
450 cols,
451 inserted_space,
452 } => {
453 let row = (*row).min(buf.row_count().saturating_sub(1));
460 let line = buf.line(row).unwrap_or("");
461 let row_byte = buffer_byte_of_row(buf, row);
462 let insert = if *inserted_space { "\n " } else { "\n" };
463 for &c in cols {
464 let pos = Position::new(row, c);
465 let col_byte = pos.byte_offset(line);
466 let start_byte = row_byte + col_byte;
467 let start_pos = (row as u32, col_byte as u32);
468 let (new_end_byte, new_end_pos) = advance_by_text(insert, start_byte, start_pos);
469 out.push(crate::types::ContentEdit {
470 start_byte,
471 old_end_byte: start_byte,
472 new_end_byte,
473 start_position: start_pos,
474 old_end_position: start_pos,
475 new_end_position: new_end_pos,
476 });
477 }
478 }
479 B::InsertBlock { at, chunks } => {
480 for (i, chunk) in chunks.iter().enumerate() {
483 let pos = Position::new(at.row + i, at.col);
484 let (start_byte, start_pos) = position_to_byte_coords(buf, pos);
485 let (new_end_byte, new_end_pos) = advance_by_text(chunk, start_byte, start_pos);
486 out.push(crate::types::ContentEdit {
487 start_byte,
488 old_end_byte: start_byte,
489 new_end_byte,
490 start_position: start_pos,
491 old_end_position: start_pos,
492 new_end_position: new_end_pos,
493 });
494 }
495 }
496 B::DeleteBlockChunks { at, widths } => {
497 for (i, w) in widths.iter().enumerate() {
498 let row = at.row + i;
499 let start_pos = Position::new(row, at.col);
500 let end_pos = Position::new(row, at.col + *w);
501 let (sb, sp) = position_to_byte_coords(buf, start_pos);
502 let (eb, ep) = position_to_byte_coords(buf, end_pos);
503 if eb <= sb {
504 continue;
505 }
506 out.push(crate::types::ContentEdit {
507 start_byte: sb,
508 old_end_byte: eb,
509 new_end_byte: sb,
510 start_position: sp,
511 old_end_position: ep,
512 new_end_position: sp,
513 });
514 }
515 }
516 }
517
518 out
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub(super) enum CursorScrollTarget {
525 Center,
526 Top,
527 Bottom,
528}
529
530use crate::buf_helpers::{
538 apply_buffer_edit, buf_cursor_pos, buf_cursor_rc, buf_cursor_row, buf_line, buf_lines_to_vec,
539 buf_row_count, buf_set_cursor_rc,
540};
541
542pub struct Editor<
543 B: crate::types::Buffer = hjkl_buffer::Buffer,
544 H: crate::types::Host = crate::types::DefaultHost,
545> {
546 pub keybinding_mode: KeybindingMode,
547 pub last_yank: Option<String>,
549 pub(crate) vim: VimState,
554 pub(crate) undo_stack: Vec<(Vec<String>, (usize, usize))>,
558 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
560 pub(super) content_dirty: bool,
562 pub(super) cached_content: Option<std::sync::Arc<String>>,
567 pub(super) viewport_height: AtomicU16,
572 pub(super) pending_lsp: Option<LspIntent>,
576 pub(super) pending_fold_ops: Vec<crate::types::FoldOp>,
584 pub(super) buffer: B,
591 #[cfg(feature = "ratatui")]
603 pub(super) style_table: Vec<ratatui::style::Style>,
604 #[cfg(not(feature = "ratatui"))]
609 pub(super) engine_style_table: Vec<crate::types::Style>,
610 pub(crate) registers: crate::registers::Registers,
615 #[cfg(feature = "ratatui")]
622 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
623 pub(crate) settings: Settings,
628 pub(crate) marks: std::collections::BTreeMap<char, (usize, usize)>,
643 pub(crate) syntax_fold_ranges: Vec<(usize, usize)>,
649 pub(crate) change_log: Vec<crate::types::Edit>,
658 pub(crate) sticky_col: Option<usize>,
667 pub(crate) host: H,
675 pub(crate) last_emitted_mode: crate::VimMode,
680 pub(crate) search_state: crate::search::SearchState,
687 pub(crate) buffer_spans: Vec<Vec<hjkl_buffer::Span>>,
698 pub(crate) pending_content_edits: Vec<crate::types::ContentEdit>,
703 pub(crate) pending_content_reset: bool,
708}
709
710#[derive(Debug, Clone)]
713pub struct Settings {
714 pub shiftwidth: usize,
716 pub tabstop: usize,
719 pub ignore_case: bool,
722 pub smartcase: bool,
726 pub wrapscan: bool,
729 pub textwidth: usize,
731 pub expandtab: bool,
735 pub softtabstop: usize,
740 pub wrap: hjkl_buffer::Wrap,
746 pub readonly: bool,
750 pub autoindent: bool,
754 pub smartindent: bool,
759 pub undo_levels: u32,
763 pub undo_break_on_motion: bool,
770 pub iskeyword: String,
776 pub timeout_len: core::time::Duration,
781 pub number: bool,
784 pub relativenumber: bool,
789 pub numberwidth: usize,
794}
795
796impl Default for Settings {
797 fn default() -> Self {
798 Self {
799 shiftwidth: 4,
800 tabstop: 4,
801 softtabstop: 4,
802 ignore_case: false,
803 smartcase: false,
804 wrapscan: true,
805 textwidth: 79,
806 expandtab: true,
807 wrap: hjkl_buffer::Wrap::None,
808 readonly: false,
809 autoindent: true,
810 smartindent: true,
811 undo_levels: 1000,
812 undo_break_on_motion: true,
813 iskeyword: "@,48-57,_,192-255".to_string(),
814 timeout_len: core::time::Duration::from_millis(1000),
815 number: true,
816 relativenumber: false,
817 numberwidth: 4,
818 }
819 }
820}
821
822fn settings_from_options(o: &crate::types::Options) -> Settings {
830 Settings {
831 shiftwidth: o.shiftwidth as usize,
832 tabstop: o.tabstop as usize,
833 softtabstop: o.softtabstop as usize,
834 ignore_case: o.ignorecase,
835 smartcase: o.smartcase,
836 wrapscan: o.wrapscan,
837 textwidth: o.textwidth as usize,
838 expandtab: o.expandtab,
839 wrap: match o.wrap {
840 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
841 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
842 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
843 },
844 readonly: o.readonly,
845 autoindent: o.autoindent,
846 smartindent: o.smartindent,
847 undo_levels: o.undo_levels,
848 undo_break_on_motion: o.undo_break_on_motion,
849 iskeyword: o.iskeyword.clone(),
850 timeout_len: o.timeout_len,
851 number: o.number,
852 relativenumber: o.relativenumber,
853 numberwidth: o.numberwidth,
854 }
855}
856
857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861pub enum LspIntent {
862 GotoDefinition,
864}
865
866impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
867 pub fn new(buffer: hjkl_buffer::Buffer, host: H, options: crate::types::Options) -> Self {
877 let settings = settings_from_options(&options);
878 Self {
879 keybinding_mode: KeybindingMode::Vim,
880 last_yank: None,
881 vim: VimState::default(),
882 undo_stack: Vec::new(),
883 redo_stack: Vec::new(),
884 content_dirty: false,
885 cached_content: None,
886 viewport_height: AtomicU16::new(0),
887 pending_lsp: None,
888 pending_fold_ops: Vec::new(),
889 buffer,
890 #[cfg(feature = "ratatui")]
891 style_table: Vec::new(),
892 #[cfg(not(feature = "ratatui"))]
893 engine_style_table: Vec::new(),
894 registers: crate::registers::Registers::default(),
895 #[cfg(feature = "ratatui")]
896 styled_spans: Vec::new(),
897 settings,
898 marks: std::collections::BTreeMap::new(),
899 syntax_fold_ranges: Vec::new(),
900 change_log: Vec::new(),
901 sticky_col: None,
902 host,
903 last_emitted_mode: crate::VimMode::Normal,
904 search_state: crate::search::SearchState::new(),
905 buffer_spans: Vec::new(),
906 pending_content_edits: Vec::new(),
907 pending_content_reset: false,
908 }
909 }
910}
911
912impl<B: crate::types::Buffer, H: crate::types::Host> Editor<B, H> {
913 pub fn buffer(&self) -> &B {
916 &self.buffer
917 }
918
919 pub fn buffer_mut(&mut self) -> &mut B {
921 &mut self.buffer
922 }
923
924 pub fn host(&self) -> &H {
926 &self.host
927 }
928
929 pub fn host_mut(&mut self) -> &mut H {
931 &mut self.host
932 }
933}
934
935impl<H: crate::types::Host> Editor<hjkl_buffer::Buffer, H> {
936 pub fn set_iskeyword(&mut self, spec: impl Into<String>) {
943 self.settings.iskeyword = spec.into();
944 }
945
946 pub(crate) fn emit_cursor_shape_if_changed(&mut self) {
951 let mode = self.vim_mode();
952 if mode == self.last_emitted_mode {
953 return;
954 }
955 let shape = match mode {
956 crate::VimMode::Insert => crate::types::CursorShape::Bar,
957 _ => crate::types::CursorShape::Block,
958 };
959 self.host.emit_cursor_shape(shape);
960 self.last_emitted_mode = mode;
961 }
962
963 pub(crate) fn record_yank_to_host(&mut self, text: String) {
970 self.host.write_clipboard(text.clone());
971 self.last_yank = Some(text);
972 }
973
974 pub fn sticky_col(&self) -> Option<usize> {
979 self.sticky_col
980 }
981
982 pub fn set_sticky_col(&mut self, col: Option<usize>) {
986 self.sticky_col = col;
987 }
988
989 pub fn mark(&self, c: char) -> Option<(usize, usize)> {
997 self.marks.get(&c).copied()
998 }
999
1000 pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
1003 self.marks.insert(c, pos);
1004 }
1005
1006 pub fn clear_mark(&mut self, c: char) {
1008 self.marks.remove(&c);
1009 }
1010
1011 #[deprecated(
1016 since = "0.0.36",
1017 note = "use Editor::mark — lowercase + uppercase marks now live in a single map"
1018 )]
1019 pub fn buffer_mark(&self, c: char) -> Option<(usize, usize)> {
1020 self.mark(c)
1021 }
1022
1023 pub fn pop_last_undo(&mut self) -> bool {
1030 self.undo_stack.pop().is_some()
1031 }
1032
1033 pub fn marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1038 self.marks.iter().map(|(c, p)| (*c, *p))
1039 }
1040
1041 #[deprecated(
1046 since = "0.0.36",
1047 note = "use Editor::marks — lowercase + uppercase marks now live in a single map"
1048 )]
1049 pub fn buffer_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1050 self.marks
1051 .iter()
1052 .filter(|(c, _)| c.is_ascii_lowercase())
1053 .map(|(c, p)| (*c, *p))
1054 }
1055
1056 pub fn last_jump_back(&self) -> Option<(usize, usize)> {
1059 self.vim.jump_back.last().copied()
1060 }
1061
1062 pub fn last_edit_pos(&self) -> Option<(usize, usize)> {
1065 self.vim.last_edit_pos
1066 }
1067
1068 pub fn file_marks(&self) -> impl Iterator<Item = (char, (usize, usize))> + '_ {
1079 self.marks
1080 .iter()
1081 .filter(|(c, _)| c.is_ascii_uppercase())
1082 .map(|(c, p)| (*c, *p))
1083 }
1084
1085 pub fn syntax_fold_ranges(&self) -> &[(usize, usize)] {
1090 &self.syntax_fold_ranges
1091 }
1092
1093 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
1094 self.syntax_fold_ranges = ranges;
1095 }
1096
1097 pub fn settings(&self) -> &Settings {
1100 &self.settings
1101 }
1102
1103 pub fn settings_mut(&mut self) -> &mut Settings {
1108 &mut self.settings
1109 }
1110
1111 pub fn is_readonly(&self) -> bool {
1115 self.settings.readonly
1116 }
1117
1118 pub fn search_state(&self) -> &crate::search::SearchState {
1123 &self.search_state
1124 }
1125
1126 pub fn search_state_mut(&mut self) -> &mut crate::search::SearchState {
1130 &mut self.search_state
1131 }
1132
1133 pub fn set_search_pattern(&mut self, pattern: Option<regex::Regex>) {
1139 self.search_state.set_pattern(pattern);
1140 }
1141
1142 pub fn search_advance_forward(&mut self, skip_current: bool) -> bool {
1147 crate::search::search_forward(&mut self.buffer, &mut self.search_state, skip_current)
1148 }
1149
1150 pub fn search_advance_backward(&mut self, skip_current: bool) -> bool {
1152 crate::search::search_backward(&mut self.buffer, &mut self.search_state, skip_current)
1153 }
1154
1155 #[cfg(feature = "ratatui")]
1166 pub fn install_ratatui_syntax_spans(
1167 &mut self,
1168 spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
1169 ) {
1170 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1174 for (row, row_spans) in spans.iter().enumerate() {
1175 if row_spans.is_empty() {
1176 by_row.push(Vec::new());
1177 continue;
1178 }
1179 let line_len = buf_line(&self.buffer, row).map(str::len).unwrap_or(0);
1180 let mut translated = Vec::with_capacity(row_spans.len());
1181 for (start, end, style) in row_spans {
1182 let end_clamped = (*end).min(line_len);
1183 if end_clamped <= *start {
1184 continue;
1185 }
1186 let id = self.intern_ratatui_style(*style);
1187 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1188 }
1189 by_row.push(translated);
1190 }
1191 self.buffer_spans = by_row;
1192 self.styled_spans = spans;
1193 }
1194
1195 pub fn yank(&self) -> &str {
1197 &self.registers.unnamed.text
1198 }
1199
1200 pub fn registers(&self) -> &crate::registers::Registers {
1202 &self.registers
1203 }
1204
1205 pub fn registers_mut(&mut self) -> &mut crate::registers::Registers {
1209 &mut self.registers
1210 }
1211
1212 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
1217 self.registers.set_clipboard(text, linewise);
1218 }
1219
1220 pub fn pending_register_is_clipboard(&self) -> bool {
1224 matches!(self.vim.pending_register, Some('+') | Some('*'))
1225 }
1226
1227 pub fn recording_register(&self) -> Option<char> {
1231 self.vim.recording_macro
1232 }
1233
1234 pub fn pending_count(&self) -> Option<u32> {
1238 self.vim.pending_count_val()
1239 }
1240
1241 pub fn pending_op(&self) -> Option<char> {
1245 self.vim.pending_op_char()
1246 }
1247
1248 #[allow(clippy::type_complexity)]
1251 pub fn jump_list(&self) -> (&[(usize, usize)], &[(usize, usize)]) {
1252 (&self.vim.jump_back, &self.vim.jump_fwd)
1253 }
1254
1255 pub fn change_list(&self) -> (&[(usize, usize)], Option<usize>) {
1258 (&self.vim.change_list, self.vim.change_list_cursor)
1259 }
1260
1261 pub fn set_yank(&mut self, text: impl Into<String>) {
1265 let text = text.into();
1266 let linewise = self.vim.yank_linewise;
1267 self.registers.unnamed = crate::registers::Slot { text, linewise };
1268 }
1269
1270 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
1274 self.vim.yank_linewise = linewise;
1275 let target = self.vim.pending_register.take();
1276 self.registers.record_yank(text, linewise, target);
1277 }
1278
1279 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
1284 if let Some(slot) = match reg {
1285 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
1286 'A'..='Z' => {
1287 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
1288 }
1289 _ => None,
1290 } {
1291 slot.text = text;
1292 slot.linewise = false;
1293 }
1294 }
1295
1296 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
1299 self.vim.yank_linewise = linewise;
1300 let target = self.vim.pending_register.take();
1301 self.registers.record_delete(text, linewise, target);
1302 }
1303
1304 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, crate::types::Style)>>) {
1313 let line_byte_lens: Vec<usize> = (0..buf_row_count(&self.buffer))
1314 .map(|r| buf_line(&self.buffer, r).map(str::len).unwrap_or(0))
1315 .collect();
1316 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
1317 #[cfg(feature = "ratatui")]
1318 let mut ratatui_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> =
1319 Vec::with_capacity(spans.len());
1320 for (row, row_spans) in spans.iter().enumerate() {
1321 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
1322 let mut translated = Vec::with_capacity(row_spans.len());
1323 #[cfg(feature = "ratatui")]
1324 let mut translated_r = Vec::with_capacity(row_spans.len());
1325 for (start, end, style) in row_spans {
1326 let end_clamped = (*end).min(line_len);
1327 if end_clamped <= *start {
1328 continue;
1329 }
1330 let id = self.intern_style(*style);
1331 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
1332 #[cfg(feature = "ratatui")]
1333 translated_r.push((*start, end_clamped, engine_style_to_ratatui(*style)));
1334 }
1335 by_row.push(translated);
1336 #[cfg(feature = "ratatui")]
1337 ratatui_spans.push(translated_r);
1338 }
1339 self.buffer_spans = by_row;
1340 #[cfg(feature = "ratatui")]
1341 {
1342 self.styled_spans = ratatui_spans;
1343 }
1344 }
1345
1346 #[cfg(feature = "ratatui")]
1355 pub fn intern_ratatui_style(&mut self, style: ratatui::style::Style) -> u32 {
1356 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
1357 return idx as u32;
1358 }
1359 self.style_table.push(style);
1360 (self.style_table.len() - 1) as u32
1361 }
1362
1363 #[cfg(feature = "ratatui")]
1367 pub fn style_table(&self) -> &[ratatui::style::Style] {
1368 &self.style_table
1369 }
1370
1371 pub fn buffer_spans(&self) -> &[Vec<hjkl_buffer::Span>] {
1380 &self.buffer_spans
1381 }
1382
1383 pub fn intern_style(&mut self, style: crate::types::Style) -> u32 {
1398 #[cfg(feature = "ratatui")]
1399 {
1400 let r = engine_style_to_ratatui(style);
1401 self.intern_ratatui_style(r)
1402 }
1403 #[cfg(not(feature = "ratatui"))]
1404 {
1405 if let Some(idx) = self.engine_style_table.iter().position(|s| *s == style) {
1406 return idx as u32;
1407 }
1408 self.engine_style_table.push(style);
1409 (self.engine_style_table.len() - 1) as u32
1410 }
1411 }
1412
1413 pub fn engine_style_at(&self, id: u32) -> Option<crate::types::Style> {
1417 #[cfg(feature = "ratatui")]
1418 {
1419 let r = self.style_table.get(id as usize).copied()?;
1420 Some(ratatui_style_to_engine(r))
1421 }
1422 #[cfg(not(feature = "ratatui"))]
1423 {
1424 self.engine_style_table.get(id as usize).copied()
1425 }
1426 }
1427
1428 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
1432
1433 pub fn set_viewport_top(&mut self, row: usize) {
1441 let last = buf_row_count(&self.buffer).saturating_sub(1);
1442 let target = row.min(last);
1443 self.host.viewport_mut().top_row = target;
1444 }
1445
1446 pub fn jump_cursor(&mut self, row: usize, col: usize) {
1450 buf_set_cursor_rc(&mut self.buffer, row, col);
1451 }
1452
1453 pub fn cursor(&self) -> (usize, usize) {
1461 buf_cursor_rc(&self.buffer)
1462 }
1463
1464 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
1467 self.pending_lsp.take()
1468 }
1469
1470 pub fn take_fold_ops(&mut self) -> Vec<crate::types::FoldOp> {
1484 std::mem::take(&mut self.pending_fold_ops)
1485 }
1486
1487 pub fn apply_fold_op(&mut self, op: crate::types::FoldOp) {
1497 use crate::types::FoldProvider;
1498 self.pending_fold_ops.push(op);
1499 let mut provider = crate::buffer_impl::BufferFoldProviderMut::new(&mut self.buffer);
1500 provider.apply(op);
1501 }
1502
1503 pub(crate) fn sync_buffer_from_textarea(&mut self) {
1510 let height = self.viewport_height_value();
1511 self.host.viewport_mut().height = height;
1512 }
1513
1514 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
1518 self.sync_buffer_from_textarea();
1519 }
1520
1521 pub fn record_jump(&mut self, pos: (usize, usize)) {
1526 const JUMPLIST_MAX: usize = 100;
1527 self.vim.jump_back.push(pos);
1528 if self.vim.jump_back.len() > JUMPLIST_MAX {
1529 self.vim.jump_back.remove(0);
1530 }
1531 self.vim.jump_fwd.clear();
1532 }
1533
1534 pub fn set_viewport_height(&self, height: u16) {
1537 self.viewport_height.store(height, Ordering::Relaxed);
1538 }
1539
1540 pub fn viewport_height_value(&self) -> u16 {
1542 self.viewport_height.load(Ordering::Relaxed)
1543 }
1544
1545 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
1554 if self.settings.readonly {
1561 let _ = edit;
1562 return hjkl_buffer::Edit::InsertStr {
1563 at: buf_cursor_pos(&self.buffer),
1564 text: String::new(),
1565 };
1566 }
1567 let pre_row = buf_cursor_row(&self.buffer);
1568 let pre_rows = buf_row_count(&self.buffer);
1569 self.change_log.extend(edit_to_editops(&edit));
1573 let content_edits = content_edits_from_buffer_edit(&self.buffer, &edit);
1579 self.pending_content_edits.extend(content_edits);
1580 let inverse = apply_buffer_edit(&mut self.buffer, edit);
1586 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1587 let lo = pre_row.min(pos_row);
1593 let hi = pre_row.max(pos_row);
1594 self.apply_fold_op(crate::types::FoldOp::Invalidate {
1595 start_row: lo,
1596 end_row: hi,
1597 });
1598 self.vim.last_edit_pos = Some((pos_row, pos_col));
1599 let entry = (pos_row, pos_col);
1604 if self.vim.change_list.last() != Some(&entry) {
1605 if let Some(idx) = self.vim.change_list_cursor.take() {
1606 self.vim.change_list.truncate(idx + 1);
1607 }
1608 self.vim.change_list.push(entry);
1609 let len = self.vim.change_list.len();
1610 if len > crate::vim::CHANGE_LIST_MAX {
1611 self.vim
1612 .change_list
1613 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
1614 }
1615 }
1616 self.vim.change_list_cursor = None;
1617 let post_rows = buf_row_count(&self.buffer);
1621 let delta = post_rows as isize - pre_rows as isize;
1622 if delta != 0 {
1623 self.shift_marks_after_edit(pre_row, delta);
1624 }
1625 self.push_buffer_content_to_textarea();
1626 self.mark_content_dirty();
1627 inverse
1628 }
1629
1630 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
1635 if delta == 0 {
1636 return;
1637 }
1638 let drop_end = if delta < 0 {
1641 edit_start.saturating_add((-delta) as usize)
1642 } else {
1643 edit_start
1644 };
1645 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
1646
1647 let mut to_drop: Vec<char> = Vec::new();
1650 for (c, (row, _col)) in self.marks.iter_mut() {
1651 if (edit_start..drop_end).contains(row) {
1652 to_drop.push(*c);
1653 } else if *row >= shift_threshold {
1654 *row = ((*row as isize) + delta).max(0) as usize;
1655 }
1656 }
1657 for c in to_drop {
1658 self.marks.remove(&c);
1659 }
1660
1661 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
1662 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
1663 for (row, _) in entries.iter_mut() {
1664 if *row >= shift_threshold {
1665 *row = ((*row as isize) + delta).max(0) as usize;
1666 }
1667 }
1668 };
1669 shift_jumps(&mut self.vim.jump_back);
1670 shift_jumps(&mut self.vim.jump_fwd);
1671 }
1672
1673 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
1681
1682 pub fn mark_content_dirty(&mut self) {
1688 self.content_dirty = true;
1689 self.cached_content = None;
1690 }
1691
1692 pub fn take_dirty(&mut self) -> bool {
1694 let dirty = self.content_dirty;
1695 self.content_dirty = false;
1696 dirty
1697 }
1698
1699 pub fn take_content_edits(&mut self) -> Vec<crate::types::ContentEdit> {
1707 std::mem::take(&mut self.pending_content_edits)
1708 }
1709
1710 pub fn take_content_reset(&mut self) -> bool {
1716 let r = self.pending_content_reset;
1717 self.pending_content_reset = false;
1718 r
1719 }
1720
1721 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
1731 if !self.content_dirty {
1732 return None;
1733 }
1734 let arc = self.content_arc();
1735 self.content_dirty = false;
1736 Some(arc)
1737 }
1738
1739 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
1742 let cursor = buf_cursor_row(&self.buffer);
1743 let top = self.host.viewport().top_row;
1744 cursor.saturating_sub(top).min(height as usize - 1) as u16
1745 }
1746
1747 pub fn cursor_screen_pos(
1757 &self,
1758 area_x: u16,
1759 area_y: u16,
1760 area_width: u16,
1761 area_height: u16,
1762 ) -> Option<(u16, u16)> {
1763 let (pos_row, pos_col) = buf_cursor_rc(&self.buffer);
1764 let v = self.host.viewport();
1765 if pos_row < v.top_row || pos_col < v.top_col {
1766 return None;
1767 }
1768 let lnum_width = if self.settings.number || self.settings.relativenumber {
1769 let needed = buf_row_count(&self.buffer).to_string().len() + 1;
1770 needed.max(self.settings.numberwidth) as u16
1771 } else {
1772 0
1773 };
1774 let dy = (pos_row - v.top_row) as u16;
1775 let line = self.buffer.line(pos_row).unwrap_or("");
1779 let tab_width = if v.tab_width == 0 {
1780 4
1781 } else {
1782 v.tab_width as usize
1783 };
1784 let visual_pos = visual_col_for_char(line, pos_col, tab_width);
1785 let visual_top = visual_col_for_char(line, v.top_col, tab_width);
1786 let dx = (visual_pos - visual_top) as u16;
1787 if dy >= area_height || dx + lnum_width >= area_width {
1788 return None;
1789 }
1790 Some((area_x + lnum_width + dx, area_y + dy))
1791 }
1792
1793 #[cfg(feature = "ratatui")]
1799 pub fn cursor_screen_pos_in_rect(&self, area: Rect) -> Option<(u16, u16)> {
1800 self.cursor_screen_pos(area.x, area.y, area.width, area.height)
1801 }
1802
1803 pub fn vim_mode(&self) -> VimMode {
1804 self.vim.public_mode()
1805 }
1806
1807 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
1813 self.vim.search_prompt.as_ref()
1814 }
1815
1816 pub fn last_search(&self) -> Option<&str> {
1819 self.vim.last_search.as_deref()
1820 }
1821
1822 pub fn last_search_forward(&self) -> bool {
1826 self.vim.last_search_forward
1827 }
1828
1829 pub fn set_last_search(&mut self, text: Option<String>, forward: bool) {
1835 self.vim.last_search = text;
1836 self.vim.last_search_forward = forward;
1837 }
1838
1839 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1843 if self.vim_mode() != VimMode::Visual {
1844 return None;
1845 }
1846 let anchor = self.vim.visual_anchor;
1847 let cursor = self.cursor();
1848 let (start, end) = if anchor <= cursor {
1849 (anchor, cursor)
1850 } else {
1851 (cursor, anchor)
1852 };
1853 Some((start, end))
1854 }
1855
1856 pub fn line_highlight(&self) -> Option<(usize, usize)> {
1859 if self.vim_mode() != VimMode::VisualLine {
1860 return None;
1861 }
1862 let anchor = self.vim.visual_line_anchor;
1863 let cursor = buf_cursor_row(&self.buffer);
1864 Some((anchor.min(cursor), anchor.max(cursor)))
1865 }
1866
1867 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1868 if self.vim_mode() != VimMode::VisualBlock {
1869 return None;
1870 }
1871 let (ar, ac) = self.vim.block_anchor;
1872 let cr = buf_cursor_row(&self.buffer);
1873 let cc = self.vim.block_vcol;
1874 let top = ar.min(cr);
1875 let bot = ar.max(cr);
1876 let left = ac.min(cc);
1877 let right = ac.max(cc);
1878 Some((top, bot, left, right))
1879 }
1880
1881 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1887 use hjkl_buffer::{Position, Selection};
1888 match self.vim_mode() {
1889 VimMode::Visual => {
1890 let (ar, ac) = self.vim.visual_anchor;
1891 let head = buf_cursor_pos(&self.buffer);
1892 Some(Selection::Char {
1893 anchor: Position::new(ar, ac),
1894 head,
1895 })
1896 }
1897 VimMode::VisualLine => {
1898 let anchor_row = self.vim.visual_line_anchor;
1899 let head_row = buf_cursor_row(&self.buffer);
1900 Some(Selection::Line {
1901 anchor_row,
1902 head_row,
1903 })
1904 }
1905 VimMode::VisualBlock => {
1906 let (ar, ac) = self.vim.block_anchor;
1907 let cr = buf_cursor_row(&self.buffer);
1908 let cc = self.vim.block_vcol;
1909 Some(Selection::Block {
1910 anchor: Position::new(ar, ac),
1911 head: Position::new(cr, cc),
1912 })
1913 }
1914 _ => None,
1915 }
1916 }
1917
1918 pub fn force_normal(&mut self) {
1920 self.vim.force_normal();
1921 }
1922
1923 pub fn content(&self) -> String {
1924 let n = buf_row_count(&self.buffer);
1925 let mut s = String::new();
1926 for r in 0..n {
1927 if r > 0 {
1928 s.push('\n');
1929 }
1930 s.push_str(crate::types::Query::line(&self.buffer, r as u32));
1931 }
1932 s.push('\n');
1933 s
1934 }
1935
1936 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
1941 if let Some(arc) = &self.cached_content {
1942 return std::sync::Arc::clone(arc);
1943 }
1944 let arc = std::sync::Arc::new(self.content());
1945 self.cached_content = Some(std::sync::Arc::clone(&arc));
1946 arc
1947 }
1948
1949 pub fn set_content(&mut self, text: &str) {
1950 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
1951 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
1952 lines.pop();
1953 }
1954 if lines.is_empty() {
1955 lines.push(String::new());
1956 }
1957 let _ = lines;
1958 crate::types::BufferEdit::replace_all(&mut self.buffer, text);
1959 self.undo_stack.clear();
1960 self.redo_stack.clear();
1961 self.pending_content_edits.clear();
1963 self.pending_content_reset = true;
1964 self.mark_content_dirty();
1965 }
1966
1967 pub fn feed_input(&mut self, input: crate::PlannedInput) -> bool {
1983 use crate::{PlannedInput, SpecialKey};
1984 let (key, mods) = match input {
1985 PlannedInput::Char(c, m) => (Key::Char(c), m),
1986 PlannedInput::Key(k, m) => {
1987 let key = match k {
1988 SpecialKey::Esc => Key::Esc,
1989 SpecialKey::Enter => Key::Enter,
1990 SpecialKey::Backspace => Key::Backspace,
1991 SpecialKey::Tab => Key::Tab,
1992 SpecialKey::BackTab => Key::Tab,
1996 SpecialKey::Up => Key::Up,
1997 SpecialKey::Down => Key::Down,
1998 SpecialKey::Left => Key::Left,
1999 SpecialKey::Right => Key::Right,
2000 SpecialKey::Home => Key::Home,
2001 SpecialKey::End => Key::End,
2002 SpecialKey::PageUp => Key::PageUp,
2003 SpecialKey::PageDown => Key::PageDown,
2004 SpecialKey::Insert => Key::Null,
2008 SpecialKey::Delete => Key::Delete,
2009 SpecialKey::F(_) => Key::Null,
2010 };
2011 let m = if matches!(k, SpecialKey::BackTab) {
2012 crate::Modifiers { shift: true, ..m }
2013 } else {
2014 m
2015 };
2016 (key, m)
2017 }
2018 PlannedInput::Mouse(_)
2020 | PlannedInput::Paste(_)
2021 | PlannedInput::FocusGained
2022 | PlannedInput::FocusLost
2023 | PlannedInput::Resize(_, _) => return false,
2024 };
2025 if key == Key::Null {
2026 return false;
2027 }
2028 let event = Input {
2029 key,
2030 ctrl: mods.ctrl,
2031 alt: mods.alt,
2032 shift: mods.shift,
2033 };
2034 let consumed = vim::step(self, event);
2035 self.emit_cursor_shape_if_changed();
2036 consumed
2037 }
2038
2039 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
2056 std::mem::take(&mut self.change_log)
2057 }
2058
2059 pub fn current_options(&self) -> crate::types::Options {
2069 crate::types::Options {
2070 shiftwidth: self.settings.shiftwidth as u32,
2071 tabstop: self.settings.tabstop as u32,
2072 softtabstop: self.settings.softtabstop as u32,
2073 textwidth: self.settings.textwidth as u32,
2074 expandtab: self.settings.expandtab,
2075 ignorecase: self.settings.ignore_case,
2076 smartcase: self.settings.smartcase,
2077 wrapscan: self.settings.wrapscan,
2078 wrap: match self.settings.wrap {
2079 hjkl_buffer::Wrap::None => crate::types::WrapMode::None,
2080 hjkl_buffer::Wrap::Char => crate::types::WrapMode::Char,
2081 hjkl_buffer::Wrap::Word => crate::types::WrapMode::Word,
2082 },
2083 readonly: self.settings.readonly,
2084 autoindent: self.settings.autoindent,
2085 smartindent: self.settings.smartindent,
2086 undo_levels: self.settings.undo_levels,
2087 undo_break_on_motion: self.settings.undo_break_on_motion,
2088 iskeyword: self.settings.iskeyword.clone(),
2089 timeout_len: self.settings.timeout_len,
2090 ..crate::types::Options::default()
2091 }
2092 }
2093
2094 pub fn apply_options(&mut self, opts: &crate::types::Options) {
2099 self.settings.shiftwidth = opts.shiftwidth as usize;
2100 self.settings.tabstop = opts.tabstop as usize;
2101 self.settings.softtabstop = opts.softtabstop as usize;
2102 self.settings.textwidth = opts.textwidth as usize;
2103 self.settings.expandtab = opts.expandtab;
2104 self.settings.ignore_case = opts.ignorecase;
2105 self.settings.smartcase = opts.smartcase;
2106 self.settings.wrapscan = opts.wrapscan;
2107 self.settings.wrap = match opts.wrap {
2108 crate::types::WrapMode::None => hjkl_buffer::Wrap::None,
2109 crate::types::WrapMode::Char => hjkl_buffer::Wrap::Char,
2110 crate::types::WrapMode::Word => hjkl_buffer::Wrap::Word,
2111 };
2112 self.settings.readonly = opts.readonly;
2113 self.settings.autoindent = opts.autoindent;
2114 self.settings.smartindent = opts.smartindent;
2115 self.settings.undo_levels = opts.undo_levels;
2116 self.settings.undo_break_on_motion = opts.undo_break_on_motion;
2117 self.set_iskeyword(opts.iskeyword.clone());
2118 self.settings.timeout_len = opts.timeout_len;
2119 }
2120
2121 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
2131 use crate::types::{Highlight, HighlightKind, Pos};
2132 let sel = self.buffer_selection()?;
2133 let (start, end) = match sel {
2134 hjkl_buffer::Selection::Char { anchor, head } => {
2135 let a = (anchor.row, anchor.col);
2136 let h = (head.row, head.col);
2137 if a <= h { (a, h) } else { (h, a) }
2138 }
2139 hjkl_buffer::Selection::Line {
2140 anchor_row,
2141 head_row,
2142 } => {
2143 let (top, bot) = if anchor_row <= head_row {
2144 (anchor_row, head_row)
2145 } else {
2146 (head_row, anchor_row)
2147 };
2148 let last_col = buf_line(&self.buffer, bot).map(|l| l.len()).unwrap_or(0);
2149 ((top, 0), (bot, last_col))
2150 }
2151 hjkl_buffer::Selection::Block { anchor, head } => {
2152 let (top, bot) = if anchor.row <= head.row {
2153 (anchor.row, head.row)
2154 } else {
2155 (head.row, anchor.row)
2156 };
2157 let (left, right) = if anchor.col <= head.col {
2158 (anchor.col, head.col)
2159 } else {
2160 (head.col, anchor.col)
2161 };
2162 ((top, left), (bot, right))
2163 }
2164 };
2165 Some(Highlight {
2166 range: Pos {
2167 line: start.0 as u32,
2168 col: start.1 as u32,
2169 }..Pos {
2170 line: end.0 as u32,
2171 col: end.1 as u32,
2172 },
2173 kind: HighlightKind::Selection,
2174 })
2175 }
2176
2177 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
2196 use crate::types::{Highlight, HighlightKind, Pos};
2197 let row = line as usize;
2198 if row >= buf_row_count(&self.buffer) {
2199 return Vec::new();
2200 }
2201
2202 if let Some(prompt) = self.search_prompt() {
2205 if prompt.text.is_empty() {
2206 return Vec::new();
2207 }
2208 let Ok(re) = regex::Regex::new(&prompt.text) else {
2209 return Vec::new();
2210 };
2211 let Some(haystack) = buf_line(&self.buffer, row) else {
2212 return Vec::new();
2213 };
2214 return re
2215 .find_iter(haystack)
2216 .map(|m| Highlight {
2217 range: Pos {
2218 line,
2219 col: m.start() as u32,
2220 }..Pos {
2221 line,
2222 col: m.end() as u32,
2223 },
2224 kind: HighlightKind::IncSearch,
2225 })
2226 .collect();
2227 }
2228
2229 if self.search_state.pattern.is_none() {
2230 return Vec::new();
2231 }
2232 let dgen = crate::types::Query::dirty_gen(&self.buffer);
2233 crate::search::search_matches(&self.buffer, &mut self.search_state, dgen, row)
2234 .into_iter()
2235 .map(|(start, end)| Highlight {
2236 range: Pos {
2237 line,
2238 col: start as u32,
2239 }..Pos {
2240 line,
2241 col: end as u32,
2242 },
2243 kind: HighlightKind::SearchMatch,
2244 })
2245 .collect()
2246 }
2247
2248 pub fn render_frame(&self) -> crate::types::RenderFrame {
2258 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
2259 let (cursor_row, cursor_col) = self.cursor();
2260 let (mode, shape) = match self.vim_mode() {
2261 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
2262 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
2263 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
2264 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
2265 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
2266 };
2267 RenderFrame {
2268 mode,
2269 cursor_row: cursor_row as u32,
2270 cursor_col: cursor_col as u32,
2271 cursor_shape: shape,
2272 viewport_top: self.host.viewport().top_row as u32,
2273 line_count: crate::types::Query::line_count(&self.buffer),
2274 }
2275 }
2276
2277 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
2290 use crate::types::{EditorSnapshot, SnapshotMode};
2291 let mode = match self.vim_mode() {
2292 crate::VimMode::Normal => SnapshotMode::Normal,
2293 crate::VimMode::Insert => SnapshotMode::Insert,
2294 crate::VimMode::Visual => SnapshotMode::Visual,
2295 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
2296 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
2297 };
2298 let cursor = self.cursor();
2299 let cursor = (cursor.0 as u32, cursor.1 as u32);
2300 let lines: Vec<String> = buf_lines_to_vec(&self.buffer);
2301 let viewport_top = self.host.viewport().top_row as u32;
2302 let marks = self
2303 .marks
2304 .iter()
2305 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
2306 .collect();
2307 EditorSnapshot {
2308 version: EditorSnapshot::VERSION,
2309 mode,
2310 cursor,
2311 lines,
2312 viewport_top,
2313 registers: self.registers.clone(),
2314 marks,
2315 }
2316 }
2317
2318 pub fn restore_snapshot(
2326 &mut self,
2327 snap: crate::types::EditorSnapshot,
2328 ) -> Result<(), crate::EngineError> {
2329 use crate::types::EditorSnapshot;
2330 if snap.version != EditorSnapshot::VERSION {
2331 return Err(crate::EngineError::SnapshotVersion(
2332 snap.version,
2333 EditorSnapshot::VERSION,
2334 ));
2335 }
2336 let text = snap.lines.join("\n");
2337 self.set_content(&text);
2338 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
2339 self.host.viewport_mut().top_row = snap.viewport_top as usize;
2340 self.registers = snap.registers;
2341 self.marks = snap
2342 .marks
2343 .into_iter()
2344 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
2345 .collect();
2346 Ok(())
2347 }
2348
2349 pub fn seed_yank(&mut self, text: String) {
2353 let linewise = text.ends_with('\n');
2354 self.vim.yank_linewise = linewise;
2355 self.registers.unnamed = crate::registers::Slot { text, linewise };
2356 }
2357
2358 pub fn scroll_down(&mut self, rows: i16) {
2363 self.scroll_viewport(rows);
2364 }
2365
2366 pub fn scroll_up(&mut self, rows: i16) {
2370 self.scroll_viewport(-rows);
2371 }
2372
2373 const SCROLLOFF: usize = 5;
2377
2378 pub fn ensure_cursor_in_scrolloff(&mut self) {
2383 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2384 if height == 0 {
2385 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2392 crate::viewport_math::ensure_cursor_visible(
2393 &self.buffer,
2394 &folds,
2395 self.host.viewport_mut(),
2396 );
2397 return;
2398 }
2399 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2403 if !matches!(self.host.viewport().wrap, hjkl_buffer::Wrap::None) {
2406 self.ensure_scrolloff_wrap(height, margin);
2407 return;
2408 }
2409 let cursor_row = buf_cursor_row(&self.buffer);
2410 let last_row = buf_row_count(&self.buffer).saturating_sub(1);
2411 let v = self.host.viewport_mut();
2412 if cursor_row < v.top_row + margin {
2414 v.top_row = cursor_row.saturating_sub(margin);
2415 }
2416 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
2418 if cursor_row > v.top_row + max_bottom {
2419 v.top_row = cursor_row.saturating_sub(max_bottom);
2420 }
2421 let max_top = last_row.saturating_sub(height.saturating_sub(1));
2423 if v.top_row > max_top {
2424 v.top_row = max_top;
2425 }
2426 let cursor = buf_cursor_pos(&self.buffer);
2429 self.host.viewport_mut().ensure_visible(cursor);
2430 }
2431
2432 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
2437 let cursor_row = buf_cursor_row(&self.buffer);
2438 if cursor_row < self.host.viewport().top_row {
2441 let v = self.host.viewport_mut();
2442 v.top_row = cursor_row;
2443 v.top_col = 0;
2444 }
2445 let max_csr = height.saturating_sub(1).saturating_sub(margin);
2454 loop {
2455 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2456 let csr =
2457 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2458 .unwrap_or(0);
2459 if csr <= max_csr {
2460 break;
2461 }
2462 let top = self.host.viewport().top_row;
2463 let row_count = buf_row_count(&self.buffer);
2464 let next = {
2465 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2466 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::next_visible_row(&folds, top, row_count)
2467 };
2468 let Some(next) = next else {
2469 break;
2470 };
2471 if next > cursor_row {
2473 self.host.viewport_mut().top_row = cursor_row;
2474 break;
2475 }
2476 self.host.viewport_mut().top_row = next;
2477 }
2478 loop {
2481 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2482 let csr =
2483 crate::viewport_math::cursor_screen_row(&self.buffer, &folds, self.host.viewport())
2484 .unwrap_or(0);
2485 if csr >= margin {
2486 break;
2487 }
2488 let top = self.host.viewport().top_row;
2489 let prev = {
2490 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2491 <crate::buffer_impl::BufferFoldProvider<'_> as crate::types::FoldProvider>::prev_visible_row(&folds, top)
2492 };
2493 let Some(prev) = prev else {
2494 break;
2495 };
2496 self.host.viewport_mut().top_row = prev;
2497 }
2498 let max_top = {
2503 let folds = crate::buffer_impl::BufferFoldProvider::new(&self.buffer);
2504 crate::viewport_math::max_top_for_height(
2505 &self.buffer,
2506 &folds,
2507 self.host.viewport(),
2508 height,
2509 )
2510 };
2511 if self.host.viewport().top_row > max_top {
2512 self.host.viewport_mut().top_row = max_top;
2513 }
2514 self.host.viewport_mut().top_col = 0;
2515 }
2516
2517 fn scroll_viewport(&mut self, delta: i16) {
2518 if delta == 0 {
2519 return;
2520 }
2521 let total_rows = buf_row_count(&self.buffer) as isize;
2523 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2524 let cur_top = self.host.viewport().top_row as isize;
2525 let new_top = (cur_top + delta as isize)
2526 .max(0)
2527 .min((total_rows - 1).max(0)) as usize;
2528 self.host.viewport_mut().top_row = new_top;
2529 let _ = cur_top;
2532 if height == 0 {
2533 return;
2534 }
2535 let (cursor_row, cursor_col) = buf_cursor_rc(&self.buffer);
2538 let margin = Self::SCROLLOFF.min(height / 2);
2539 let min_row = new_top + margin;
2540 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
2541 let target_row = cursor_row.clamp(min_row, max_row.max(min_row));
2542 if target_row != cursor_row {
2543 let line_len = buf_line(&self.buffer, target_row)
2544 .map(|l| l.chars().count())
2545 .unwrap_or(0);
2546 let target_col = cursor_col.min(line_len.saturating_sub(1));
2547 buf_set_cursor_rc(&mut self.buffer, target_row, target_col);
2548 }
2549 }
2550
2551 pub fn goto_line(&mut self, line: usize) {
2552 let row = line.saturating_sub(1);
2553 let max = buf_row_count(&self.buffer).saturating_sub(1);
2554 let target = row.min(max);
2555 buf_set_cursor_rc(&mut self.buffer, target, 0);
2556 self.ensure_cursor_in_scrolloff();
2560 }
2561
2562 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
2566 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
2567 if height == 0 {
2568 return;
2569 }
2570 let cur_row = buf_cursor_row(&self.buffer);
2571 let cur_top = self.host.viewport().top_row;
2572 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
2578 let new_top = match pos {
2579 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
2580 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
2581 CursorScrollTarget::Bottom => {
2582 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
2583 }
2584 };
2585 if new_top == cur_top {
2586 return;
2587 }
2588 self.host.viewport_mut().top_row = new_top;
2589 }
2590
2591 fn mouse_to_doc_pos_xy(&self, area_x: u16, area_y: u16, col: u16, row: u16) -> (usize, usize) {
2602 let n = buf_row_count(&self.buffer);
2603 let inner_top = area_y.saturating_add(1); let lnum_width = if self.settings.number || self.settings.relativenumber {
2605 let needed = n.to_string().len() + 1;
2606 needed.max(self.settings.numberwidth) as u16
2607 } else {
2608 0
2609 };
2610 let content_x = area_x.saturating_add(1).saturating_add(lnum_width);
2611 let rel_row = row.saturating_sub(inner_top) as usize;
2612 let top = self.host.viewport().top_row;
2613 let doc_row = (top + rel_row).min(n.saturating_sub(1));
2614 let rel_col = col.saturating_sub(content_x) as usize;
2615 let line_chars = buf_line(&self.buffer, doc_row)
2616 .map(|l| l.chars().count())
2617 .unwrap_or(0);
2618 let last_col = line_chars.saturating_sub(1);
2619 (doc_row, rel_col.min(last_col))
2620 }
2621
2622 pub fn jump_to(&mut self, line: usize, col: usize) {
2624 let r = line.saturating_sub(1);
2625 let max_row = buf_row_count(&self.buffer).saturating_sub(1);
2626 let r = r.min(max_row);
2627 let line_len = buf_line(&self.buffer, r)
2628 .map(|l| l.chars().count())
2629 .unwrap_or(0);
2630 let c = col.saturating_sub(1).min(line_len);
2631 buf_set_cursor_rc(&mut self.buffer, r, c);
2632 }
2633
2634 pub fn mouse_click(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2642 if self.vim.is_visual() {
2643 self.vim.force_normal();
2644 }
2645 crate::vim::break_undo_group_in_insert(self);
2648 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2649 buf_set_cursor_rc(&mut self.buffer, r, c);
2650 }
2651
2652 #[cfg(feature = "ratatui")]
2658 pub fn mouse_click_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2659 self.mouse_click(area.x, area.y, col, row);
2660 }
2661
2662 pub fn mouse_begin_drag(&mut self) {
2664 if !self.vim.is_visual_char() {
2665 let cursor = self.cursor();
2666 self.vim.enter_visual(cursor);
2667 }
2668 }
2669
2670 pub fn mouse_extend_drag(&mut self, area_x: u16, area_y: u16, col: u16, row: u16) {
2676 let (r, c) = self.mouse_to_doc_pos_xy(area_x, area_y, col, row);
2677 buf_set_cursor_rc(&mut self.buffer, r, c);
2678 }
2679
2680 #[cfg(feature = "ratatui")]
2686 pub fn mouse_extend_drag_in_rect(&mut self, area: Rect, col: u16, row: u16) {
2687 self.mouse_extend_drag(area.x, area.y, col, row);
2688 }
2689
2690 pub fn insert_str(&mut self, text: &str) {
2691 let pos = crate::types::Cursor::cursor(&self.buffer);
2692 crate::types::BufferEdit::insert_at(&mut self.buffer, pos, text);
2693 self.push_buffer_content_to_textarea();
2694 self.mark_content_dirty();
2695 }
2696
2697 pub fn accept_completion(&mut self, completion: &str) {
2698 use crate::types::{BufferEdit, Cursor as CursorTrait, Pos};
2699 let cursor_pos = CursorTrait::cursor(&self.buffer);
2700 let cursor_row = cursor_pos.line as usize;
2701 let cursor_col = cursor_pos.col as usize;
2702 let line = buf_line(&self.buffer, cursor_row).unwrap_or("").to_string();
2703 let chars: Vec<char> = line.chars().collect();
2704 let prefix_len = chars[..cursor_col.min(chars.len())]
2705 .iter()
2706 .rev()
2707 .take_while(|c| c.is_alphanumeric() || **c == '_')
2708 .count();
2709 if prefix_len > 0 {
2710 let start = Pos {
2711 line: cursor_row as u32,
2712 col: (cursor_col - prefix_len) as u32,
2713 };
2714 BufferEdit::delete_range(&mut self.buffer, start..cursor_pos);
2715 }
2716 let cursor = CursorTrait::cursor(&self.buffer);
2717 BufferEdit::insert_at(&mut self.buffer, cursor, completion);
2718 self.push_buffer_content_to_textarea();
2719 self.mark_content_dirty();
2720 }
2721
2722 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
2723 let rc = buf_cursor_rc(&self.buffer);
2724 (buf_lines_to_vec(&self.buffer), rc)
2725 }
2726
2727 pub fn undo(&mut self) {
2731 crate::vim::do_undo(self);
2732 }
2733
2734 pub fn redo(&mut self) {
2737 crate::vim::do_redo(self);
2738 }
2739
2740 pub fn push_undo(&mut self) {
2745 let snap = self.snapshot();
2746 self.undo_stack.push(snap);
2747 self.cap_undo();
2748 self.redo_stack.clear();
2749 }
2750
2751 pub(crate) fn cap_undo(&mut self) {
2757 let cap = self.settings.undo_levels as usize;
2758 if cap > 0 && self.undo_stack.len() > cap {
2759 let diff = self.undo_stack.len() - cap;
2760 self.undo_stack.drain(..diff);
2761 }
2762 }
2763
2764 #[doc(hidden)]
2766 pub fn undo_stack_len(&self) -> usize {
2767 self.undo_stack.len()
2768 }
2769
2770 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
2774 let text = lines.join("\n");
2775 crate::types::BufferEdit::replace_all(&mut self.buffer, &text);
2776 buf_set_cursor_rc(&mut self.buffer, cursor.0, cursor.1);
2777 self.pending_content_edits.clear();
2779 self.pending_content_reset = true;
2780 self.mark_content_dirty();
2781 }
2782
2783 #[cfg(feature = "crossterm")]
2785 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
2786 let input = crossterm_to_input(key);
2787 if input.key == Key::Null {
2788 return false;
2789 }
2790 let consumed = vim::step(self, input);
2791 self.emit_cursor_shape_if_changed();
2792 consumed
2793 }
2794}
2795
2796fn visual_col_for_char(line: &str, char_col: usize, tab_width: usize) -> usize {
2801 let mut visual = 0usize;
2802 for (i, ch) in line.chars().enumerate() {
2803 if i >= char_col {
2804 break;
2805 }
2806 if ch == '\t' {
2807 visual += tab_width - (visual % tab_width);
2808 } else {
2809 visual += 1;
2810 }
2811 }
2812 visual
2813}
2814
2815#[cfg(feature = "crossterm")]
2816impl From<KeyEvent> for Input {
2817 fn from(key: KeyEvent) -> Self {
2818 let k = match key.code {
2819 KeyCode::Char(c) => Key::Char(c),
2820 KeyCode::Backspace => Key::Backspace,
2821 KeyCode::Delete => Key::Delete,
2822 KeyCode::Enter => Key::Enter,
2823 KeyCode::Left => Key::Left,
2824 KeyCode::Right => Key::Right,
2825 KeyCode::Up => Key::Up,
2826 KeyCode::Down => Key::Down,
2827 KeyCode::Home => Key::Home,
2828 KeyCode::End => Key::End,
2829 KeyCode::Tab => Key::Tab,
2830 KeyCode::Esc => Key::Esc,
2831 _ => Key::Null,
2832 };
2833 Input {
2834 key: k,
2835 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
2836 alt: key.modifiers.contains(KeyModifiers::ALT),
2837 shift: key.modifiers.contains(KeyModifiers::SHIFT),
2838 }
2839 }
2840}
2841
2842#[cfg(feature = "crossterm")]
2846pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
2847 Input::from(key)
2848}
2849
2850#[cfg(all(test, feature = "crossterm", feature = "ratatui"))]
2851mod tests {
2852 use super::*;
2853 use crate::types::Host;
2854 use crossterm::event::KeyEvent;
2855
2856 fn key(code: KeyCode) -> KeyEvent {
2857 KeyEvent::new(code, KeyModifiers::NONE)
2858 }
2859 fn shift_key(code: KeyCode) -> KeyEvent {
2860 KeyEvent::new(code, KeyModifiers::SHIFT)
2861 }
2862 fn ctrl_key(code: KeyCode) -> KeyEvent {
2863 KeyEvent::new(code, KeyModifiers::CONTROL)
2864 }
2865
2866 #[test]
2867 fn vim_normal_to_insert() {
2868 let mut e = Editor::new(
2869 hjkl_buffer::Buffer::new(),
2870 crate::types::DefaultHost::new(),
2871 crate::types::Options::default(),
2872 );
2873 e.handle_key(key(KeyCode::Char('i')));
2874 assert_eq!(e.vim_mode(), VimMode::Insert);
2875 }
2876
2877 #[test]
2878 fn with_options_constructs_from_spec_options() {
2879 let opts = crate::types::Options {
2883 shiftwidth: 4,
2884 tabstop: 4,
2885 expandtab: true,
2886 iskeyword: "@,a-z".to_string(),
2887 wrap: crate::types::WrapMode::Word,
2888 ..crate::types::Options::default()
2889 };
2890 let mut e = Editor::new(
2891 hjkl_buffer::Buffer::new(),
2892 crate::types::DefaultHost::new(),
2893 opts,
2894 );
2895 assert_eq!(e.settings().shiftwidth, 4);
2896 assert_eq!(e.settings().tabstop, 4);
2897 assert!(e.settings().expandtab);
2898 assert_eq!(e.settings().iskeyword, "@,a-z");
2899 assert_eq!(e.settings().wrap, hjkl_buffer::Wrap::Word);
2900 e.handle_key(key(KeyCode::Char('i')));
2902 assert_eq!(e.vim_mode(), VimMode::Insert);
2903 }
2904
2905 #[test]
2906 fn feed_input_char_routes_through_handle_key() {
2907 use crate::{Modifiers, PlannedInput};
2908 let mut e = Editor::new(
2909 hjkl_buffer::Buffer::new(),
2910 crate::types::DefaultHost::new(),
2911 crate::types::Options::default(),
2912 );
2913 e.set_content("abc");
2914 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2916 assert_eq!(e.vim_mode(), VimMode::Insert);
2917 e.feed_input(PlannedInput::Char('X', Modifiers::default()));
2919 assert!(e.content().contains('X'));
2920 }
2921
2922 #[test]
2923 fn feed_input_special_key_routes() {
2924 use crate::{Modifiers, PlannedInput, SpecialKey};
2925 let mut e = Editor::new(
2926 hjkl_buffer::Buffer::new(),
2927 crate::types::DefaultHost::new(),
2928 crate::types::Options::default(),
2929 );
2930 e.set_content("abc");
2931 e.feed_input(PlannedInput::Char('i', Modifiers::default()));
2932 assert_eq!(e.vim_mode(), VimMode::Insert);
2933 e.feed_input(PlannedInput::Key(SpecialKey::Esc, Modifiers::default()));
2934 assert_eq!(e.vim_mode(), VimMode::Normal);
2935 }
2936
2937 #[test]
2938 fn feed_input_mouse_paste_focus_resize_no_op() {
2939 use crate::{MouseEvent, MouseKind, PlannedInput, Pos};
2940 let mut e = Editor::new(
2941 hjkl_buffer::Buffer::new(),
2942 crate::types::DefaultHost::new(),
2943 crate::types::Options::default(),
2944 );
2945 e.set_content("abc");
2946 let mode_before = e.vim_mode();
2947 let consumed = e.feed_input(PlannedInput::Mouse(MouseEvent {
2948 kind: MouseKind::Press,
2949 pos: Pos::new(0, 0),
2950 mods: Default::default(),
2951 }));
2952 assert!(!consumed);
2953 assert_eq!(e.vim_mode(), mode_before);
2954 assert!(!e.feed_input(PlannedInput::Paste("xx".into())));
2955 assert!(!e.feed_input(PlannedInput::FocusGained));
2956 assert!(!e.feed_input(PlannedInput::FocusLost));
2957 assert!(!e.feed_input(PlannedInput::Resize(80, 24)));
2958 }
2959
2960 #[test]
2961 fn intern_style_dedups_engine_native_styles() {
2962 use crate::types::{Attrs, Color, Style};
2963 let mut e = Editor::new(
2964 hjkl_buffer::Buffer::new(),
2965 crate::types::DefaultHost::new(),
2966 crate::types::Options::default(),
2967 );
2968 let s = Style {
2969 fg: Some(Color(255, 0, 0)),
2970 bg: None,
2971 attrs: Attrs::BOLD,
2972 };
2973 let id_a = e.intern_style(s);
2974 let id_b = e.intern_style(s);
2976 assert_eq!(id_a, id_b);
2977 let back = e.engine_style_at(id_a).expect("interned");
2979 assert_eq!(back, s);
2980 }
2981
2982 #[test]
2983 fn engine_style_at_out_of_range_returns_none() {
2984 let e = Editor::new(
2985 hjkl_buffer::Buffer::new(),
2986 crate::types::DefaultHost::new(),
2987 crate::types::Options::default(),
2988 );
2989 assert!(e.engine_style_at(99).is_none());
2990 }
2991
2992 #[test]
2993 fn take_changes_emits_per_row_for_block_insert() {
2994 let mut e = Editor::new(
2999 hjkl_buffer::Buffer::new(),
3000 crate::types::DefaultHost::new(),
3001 crate::types::Options::default(),
3002 );
3003 e.set_content("aaa\nbbb\nccc\nddd");
3004 e.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
3006 e.handle_key(key(KeyCode::Char('j')));
3007 e.handle_key(key(KeyCode::Char('j')));
3008 e.handle_key(shift_key(KeyCode::Char('I')));
3010 e.handle_key(key(KeyCode::Char('X')));
3011 e.handle_key(key(KeyCode::Esc));
3012
3013 let changes = e.take_changes();
3014 assert!(
3018 changes.len() >= 3,
3019 "expected >=3 EditOps for 3-row block insert, got {}: {changes:?}",
3020 changes.len()
3021 );
3022 }
3023
3024 #[test]
3025 fn take_changes_drains_after_insert() {
3026 let mut e = Editor::new(
3027 hjkl_buffer::Buffer::new(),
3028 crate::types::DefaultHost::new(),
3029 crate::types::Options::default(),
3030 );
3031 e.set_content("abc");
3032 assert!(e.take_changes().is_empty());
3034 e.handle_key(key(KeyCode::Char('i')));
3036 e.handle_key(key(KeyCode::Char('X')));
3037 let changes = e.take_changes();
3038 assert!(
3039 !changes.is_empty(),
3040 "insert mode keystroke should produce a change"
3041 );
3042 assert!(e.take_changes().is_empty());
3044 }
3045
3046 #[test]
3047 fn options_bridge_roundtrip() {
3048 let mut e = Editor::new(
3049 hjkl_buffer::Buffer::new(),
3050 crate::types::DefaultHost::new(),
3051 crate::types::Options::default(),
3052 );
3053 let opts = e.current_options();
3054 assert_eq!(opts.shiftwidth, 4);
3056 assert_eq!(opts.tabstop, 4);
3057
3058 let new_opts = crate::types::Options {
3059 shiftwidth: 4,
3060 tabstop: 2,
3061 ignorecase: true,
3062 ..crate::types::Options::default()
3063 };
3064 e.apply_options(&new_opts);
3065
3066 let after = e.current_options();
3067 assert_eq!(after.shiftwidth, 4);
3068 assert_eq!(after.tabstop, 2);
3069 assert!(after.ignorecase);
3070 }
3071
3072 #[test]
3073 fn selection_highlight_none_in_normal() {
3074 let mut e = Editor::new(
3075 hjkl_buffer::Buffer::new(),
3076 crate::types::DefaultHost::new(),
3077 crate::types::Options::default(),
3078 );
3079 e.set_content("hello");
3080 assert!(e.selection_highlight().is_none());
3081 }
3082
3083 #[test]
3084 fn selection_highlight_some_in_visual() {
3085 use crate::types::HighlightKind;
3086 let mut e = Editor::new(
3087 hjkl_buffer::Buffer::new(),
3088 crate::types::DefaultHost::new(),
3089 crate::types::Options::default(),
3090 );
3091 e.set_content("hello world");
3092 e.handle_key(key(KeyCode::Char('v')));
3093 e.handle_key(key(KeyCode::Char('l')));
3094 e.handle_key(key(KeyCode::Char('l')));
3095 let h = e
3096 .selection_highlight()
3097 .expect("visual mode should produce a highlight");
3098 assert_eq!(h.kind, HighlightKind::Selection);
3099 assert_eq!(h.range.start.line, 0);
3100 assert_eq!(h.range.end.line, 0);
3101 }
3102
3103 #[test]
3104 fn highlights_emit_incsearch_during_active_prompt() {
3105 use crate::types::HighlightKind;
3106 let mut e = Editor::new(
3107 hjkl_buffer::Buffer::new(),
3108 crate::types::DefaultHost::new(),
3109 crate::types::Options::default(),
3110 );
3111 e.set_content("foo bar foo\nbaz\n");
3112 e.handle_key(key(KeyCode::Char('/')));
3114 e.handle_key(key(KeyCode::Char('f')));
3115 e.handle_key(key(KeyCode::Char('o')));
3116 e.handle_key(key(KeyCode::Char('o')));
3117 assert!(e.search_prompt().is_some());
3119 let hs = e.highlights_for_line(0);
3120 assert_eq!(hs.len(), 2);
3121 for h in &hs {
3122 assert_eq!(h.kind, HighlightKind::IncSearch);
3123 }
3124 }
3125
3126 #[test]
3127 fn highlights_empty_for_blank_prompt() {
3128 let mut e = Editor::new(
3129 hjkl_buffer::Buffer::new(),
3130 crate::types::DefaultHost::new(),
3131 crate::types::Options::default(),
3132 );
3133 e.set_content("foo");
3134 e.handle_key(key(KeyCode::Char('/')));
3135 assert!(e.search_prompt().is_some());
3137 assert!(e.highlights_for_line(0).is_empty());
3138 }
3139
3140 #[test]
3141 fn highlights_emit_search_matches() {
3142 use crate::types::HighlightKind;
3143 let mut e = Editor::new(
3144 hjkl_buffer::Buffer::new(),
3145 crate::types::DefaultHost::new(),
3146 crate::types::Options::default(),
3147 );
3148 e.set_content("foo bar foo\nbaz qux\n");
3149 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3153 let hs = e.highlights_for_line(0);
3154 assert_eq!(hs.len(), 2);
3155 for h in &hs {
3156 assert_eq!(h.kind, HighlightKind::SearchMatch);
3157 assert_eq!(h.range.start.line, 0);
3158 assert_eq!(h.range.end.line, 0);
3159 }
3160 }
3161
3162 #[test]
3163 fn highlights_empty_without_pattern() {
3164 let mut e = Editor::new(
3165 hjkl_buffer::Buffer::new(),
3166 crate::types::DefaultHost::new(),
3167 crate::types::Options::default(),
3168 );
3169 e.set_content("foo bar");
3170 assert!(e.highlights_for_line(0).is_empty());
3171 }
3172
3173 #[test]
3174 fn highlights_empty_for_out_of_range_line() {
3175 let mut e = Editor::new(
3176 hjkl_buffer::Buffer::new(),
3177 crate::types::DefaultHost::new(),
3178 crate::types::Options::default(),
3179 );
3180 e.set_content("foo");
3181 e.set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
3182 assert!(e.highlights_for_line(99).is_empty());
3183 }
3184
3185 #[test]
3186 fn render_frame_reflects_mode_and_cursor() {
3187 use crate::types::{CursorShape, SnapshotMode};
3188 let mut e = Editor::new(
3189 hjkl_buffer::Buffer::new(),
3190 crate::types::DefaultHost::new(),
3191 crate::types::Options::default(),
3192 );
3193 e.set_content("alpha\nbeta");
3194 let f = e.render_frame();
3195 assert_eq!(f.mode, SnapshotMode::Normal);
3196 assert_eq!(f.cursor_shape, CursorShape::Block);
3197 assert_eq!(f.line_count, 2);
3198
3199 e.handle_key(key(KeyCode::Char('i')));
3200 let f = e.render_frame();
3201 assert_eq!(f.mode, SnapshotMode::Insert);
3202 assert_eq!(f.cursor_shape, CursorShape::Bar);
3203 }
3204
3205 #[test]
3206 fn snapshot_roundtrips_through_restore() {
3207 use crate::types::SnapshotMode;
3208 let mut e = Editor::new(
3209 hjkl_buffer::Buffer::new(),
3210 crate::types::DefaultHost::new(),
3211 crate::types::Options::default(),
3212 );
3213 e.set_content("alpha\nbeta\ngamma");
3214 e.jump_cursor(2, 3);
3215 let snap = e.take_snapshot();
3216 assert_eq!(snap.mode, SnapshotMode::Normal);
3217 assert_eq!(snap.cursor, (2, 3));
3218 assert_eq!(snap.lines.len(), 3);
3219
3220 let mut other = Editor::new(
3221 hjkl_buffer::Buffer::new(),
3222 crate::types::DefaultHost::new(),
3223 crate::types::Options::default(),
3224 );
3225 other.restore_snapshot(snap).expect("restore");
3226 assert_eq!(other.cursor(), (2, 3));
3227 assert_eq!(other.buffer().lines().len(), 3);
3228 }
3229
3230 #[test]
3231 fn restore_snapshot_rejects_version_mismatch() {
3232 let mut e = Editor::new(
3233 hjkl_buffer::Buffer::new(),
3234 crate::types::DefaultHost::new(),
3235 crate::types::Options::default(),
3236 );
3237 let mut snap = e.take_snapshot();
3238 snap.version = 9999;
3239 match e.restore_snapshot(snap) {
3240 Err(crate::EngineError::SnapshotVersion(got, want)) => {
3241 assert_eq!(got, 9999);
3242 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
3243 }
3244 other => panic!("expected SnapshotVersion err, got {other:?}"),
3245 }
3246 }
3247
3248 #[test]
3249 fn take_content_change_returns_some_on_first_dirty() {
3250 let mut e = Editor::new(
3251 hjkl_buffer::Buffer::new(),
3252 crate::types::DefaultHost::new(),
3253 crate::types::Options::default(),
3254 );
3255 e.set_content("hello");
3256 let first = e.take_content_change();
3257 assert!(first.is_some());
3258 let second = e.take_content_change();
3259 assert!(second.is_none());
3260 }
3261
3262 #[test]
3263 fn take_content_change_none_until_mutation() {
3264 let mut e = Editor::new(
3265 hjkl_buffer::Buffer::new(),
3266 crate::types::DefaultHost::new(),
3267 crate::types::Options::default(),
3268 );
3269 e.set_content("hello");
3270 e.take_content_change();
3272 assert!(e.take_content_change().is_none());
3273 e.handle_key(key(KeyCode::Char('i')));
3275 e.handle_key(key(KeyCode::Char('x')));
3276 let after = e.take_content_change();
3277 assert!(after.is_some());
3278 assert!(after.unwrap().contains('x'));
3279 }
3280
3281 #[test]
3282 fn vim_insert_to_normal() {
3283 let mut e = Editor::new(
3284 hjkl_buffer::Buffer::new(),
3285 crate::types::DefaultHost::new(),
3286 crate::types::Options::default(),
3287 );
3288 e.handle_key(key(KeyCode::Char('i')));
3289 e.handle_key(key(KeyCode::Esc));
3290 assert_eq!(e.vim_mode(), VimMode::Normal);
3291 }
3292
3293 #[test]
3294 fn vim_normal_to_visual() {
3295 let mut e = Editor::new(
3296 hjkl_buffer::Buffer::new(),
3297 crate::types::DefaultHost::new(),
3298 crate::types::Options::default(),
3299 );
3300 e.handle_key(key(KeyCode::Char('v')));
3301 assert_eq!(e.vim_mode(), VimMode::Visual);
3302 }
3303
3304 #[test]
3305 fn vim_visual_to_normal() {
3306 let mut e = Editor::new(
3307 hjkl_buffer::Buffer::new(),
3308 crate::types::DefaultHost::new(),
3309 crate::types::Options::default(),
3310 );
3311 e.handle_key(key(KeyCode::Char('v')));
3312 e.handle_key(key(KeyCode::Esc));
3313 assert_eq!(e.vim_mode(), VimMode::Normal);
3314 }
3315
3316 #[test]
3317 fn vim_shift_i_moves_to_first_non_whitespace() {
3318 let mut e = Editor::new(
3319 hjkl_buffer::Buffer::new(),
3320 crate::types::DefaultHost::new(),
3321 crate::types::Options::default(),
3322 );
3323 e.set_content(" hello");
3324 e.jump_cursor(0, 8);
3325 e.handle_key(shift_key(KeyCode::Char('I')));
3326 assert_eq!(e.vim_mode(), VimMode::Insert);
3327 assert_eq!(e.cursor(), (0, 3));
3328 }
3329
3330 #[test]
3331 fn vim_shift_a_moves_to_end_and_insert() {
3332 let mut e = Editor::new(
3333 hjkl_buffer::Buffer::new(),
3334 crate::types::DefaultHost::new(),
3335 crate::types::Options::default(),
3336 );
3337 e.set_content("hello");
3338 e.handle_key(shift_key(KeyCode::Char('A')));
3339 assert_eq!(e.vim_mode(), VimMode::Insert);
3340 assert_eq!(e.cursor().1, 5);
3341 }
3342
3343 #[test]
3344 fn count_10j_moves_down_10() {
3345 let mut e = Editor::new(
3346 hjkl_buffer::Buffer::new(),
3347 crate::types::DefaultHost::new(),
3348 crate::types::Options::default(),
3349 );
3350 e.set_content(
3351 (0..20)
3352 .map(|i| format!("line{i}"))
3353 .collect::<Vec<_>>()
3354 .join("\n")
3355 .as_str(),
3356 );
3357 for d in "10".chars() {
3358 e.handle_key(key(KeyCode::Char(d)));
3359 }
3360 e.handle_key(key(KeyCode::Char('j')));
3361 assert_eq!(e.cursor().0, 10);
3362 }
3363
3364 #[test]
3365 fn count_o_repeats_insert_on_esc() {
3366 let mut e = Editor::new(
3367 hjkl_buffer::Buffer::new(),
3368 crate::types::DefaultHost::new(),
3369 crate::types::Options::default(),
3370 );
3371 e.set_content("hello");
3372 for d in "3".chars() {
3373 e.handle_key(key(KeyCode::Char(d)));
3374 }
3375 e.handle_key(key(KeyCode::Char('o')));
3376 assert_eq!(e.vim_mode(), VimMode::Insert);
3377 for c in "world".chars() {
3378 e.handle_key(key(KeyCode::Char(c)));
3379 }
3380 e.handle_key(key(KeyCode::Esc));
3381 assert_eq!(e.vim_mode(), VimMode::Normal);
3382 assert_eq!(e.buffer().lines().len(), 4);
3383 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
3384 }
3385
3386 #[test]
3387 fn count_i_repeats_text_on_esc() {
3388 let mut e = Editor::new(
3389 hjkl_buffer::Buffer::new(),
3390 crate::types::DefaultHost::new(),
3391 crate::types::Options::default(),
3392 );
3393 e.set_content("");
3394 for d in "3".chars() {
3395 e.handle_key(key(KeyCode::Char(d)));
3396 }
3397 e.handle_key(key(KeyCode::Char('i')));
3398 for c in "ab".chars() {
3399 e.handle_key(key(KeyCode::Char(c)));
3400 }
3401 e.handle_key(key(KeyCode::Esc));
3402 assert_eq!(e.vim_mode(), VimMode::Normal);
3403 assert_eq!(e.buffer().lines()[0], "ababab");
3404 }
3405
3406 #[test]
3407 fn vim_shift_o_opens_line_above() {
3408 let mut e = Editor::new(
3409 hjkl_buffer::Buffer::new(),
3410 crate::types::DefaultHost::new(),
3411 crate::types::Options::default(),
3412 );
3413 e.set_content("hello");
3414 e.handle_key(shift_key(KeyCode::Char('O')));
3415 assert_eq!(e.vim_mode(), VimMode::Insert);
3416 assert_eq!(e.cursor(), (0, 0));
3417 assert_eq!(e.buffer().lines().len(), 2);
3418 }
3419
3420 #[test]
3421 fn vim_gg_goes_to_top() {
3422 let mut e = Editor::new(
3423 hjkl_buffer::Buffer::new(),
3424 crate::types::DefaultHost::new(),
3425 crate::types::Options::default(),
3426 );
3427 e.set_content("a\nb\nc");
3428 e.jump_cursor(2, 0);
3429 e.handle_key(key(KeyCode::Char('g')));
3430 e.handle_key(key(KeyCode::Char('g')));
3431 assert_eq!(e.cursor().0, 0);
3432 }
3433
3434 #[test]
3435 fn vim_shift_g_goes_to_bottom() {
3436 let mut e = Editor::new(
3437 hjkl_buffer::Buffer::new(),
3438 crate::types::DefaultHost::new(),
3439 crate::types::Options::default(),
3440 );
3441 e.set_content("a\nb\nc");
3442 e.handle_key(shift_key(KeyCode::Char('G')));
3443 assert_eq!(e.cursor().0, 2);
3444 }
3445
3446 #[test]
3447 fn vim_dd_deletes_line() {
3448 let mut e = Editor::new(
3449 hjkl_buffer::Buffer::new(),
3450 crate::types::DefaultHost::new(),
3451 crate::types::Options::default(),
3452 );
3453 e.set_content("first\nsecond");
3454 e.handle_key(key(KeyCode::Char('d')));
3455 e.handle_key(key(KeyCode::Char('d')));
3456 assert_eq!(e.buffer().lines().len(), 1);
3457 assert_eq!(e.buffer().lines()[0], "second");
3458 }
3459
3460 #[test]
3461 fn vim_dw_deletes_word() {
3462 let mut e = Editor::new(
3463 hjkl_buffer::Buffer::new(),
3464 crate::types::DefaultHost::new(),
3465 crate::types::Options::default(),
3466 );
3467 e.set_content("hello world");
3468 e.handle_key(key(KeyCode::Char('d')));
3469 e.handle_key(key(KeyCode::Char('w')));
3470 assert_eq!(e.vim_mode(), VimMode::Normal);
3471 assert!(!e.buffer().lines()[0].starts_with("hello"));
3472 }
3473
3474 #[test]
3475 fn vim_yy_yanks_line() {
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\nworld");
3482 e.handle_key(key(KeyCode::Char('y')));
3483 e.handle_key(key(KeyCode::Char('y')));
3484 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
3485 }
3486
3487 #[test]
3488 fn vim_yy_does_not_move_cursor() {
3489 let mut e = Editor::new(
3490 hjkl_buffer::Buffer::new(),
3491 crate::types::DefaultHost::new(),
3492 crate::types::Options::default(),
3493 );
3494 e.set_content("first\nsecond\nthird");
3495 e.jump_cursor(1, 0);
3496 let before = e.cursor();
3497 e.handle_key(key(KeyCode::Char('y')));
3498 e.handle_key(key(KeyCode::Char('y')));
3499 assert_eq!(e.cursor(), before);
3500 assert_eq!(e.vim_mode(), VimMode::Normal);
3501 }
3502
3503 #[test]
3504 fn vim_yw_yanks_word() {
3505 let mut e = Editor::new(
3506 hjkl_buffer::Buffer::new(),
3507 crate::types::DefaultHost::new(),
3508 crate::types::Options::default(),
3509 );
3510 e.set_content("hello world");
3511 e.handle_key(key(KeyCode::Char('y')));
3512 e.handle_key(key(KeyCode::Char('w')));
3513 assert_eq!(e.vim_mode(), VimMode::Normal);
3514 assert!(e.last_yank.is_some());
3515 }
3516
3517 #[test]
3518 fn vim_cc_changes_line() {
3519 let mut e = Editor::new(
3520 hjkl_buffer::Buffer::new(),
3521 crate::types::DefaultHost::new(),
3522 crate::types::Options::default(),
3523 );
3524 e.set_content("hello\nworld");
3525 e.handle_key(key(KeyCode::Char('c')));
3526 e.handle_key(key(KeyCode::Char('c')));
3527 assert_eq!(e.vim_mode(), VimMode::Insert);
3528 }
3529
3530 #[test]
3531 fn vim_u_undoes_insert_session_as_chunk() {
3532 let mut e = Editor::new(
3533 hjkl_buffer::Buffer::new(),
3534 crate::types::DefaultHost::new(),
3535 crate::types::Options::default(),
3536 );
3537 e.set_content("hello");
3538 e.handle_key(key(KeyCode::Char('i')));
3539 e.handle_key(key(KeyCode::Enter));
3540 e.handle_key(key(KeyCode::Enter));
3541 e.handle_key(key(KeyCode::Esc));
3542 assert_eq!(e.buffer().lines().len(), 3);
3543 e.handle_key(key(KeyCode::Char('u')));
3544 assert_eq!(e.buffer().lines().len(), 1);
3545 assert_eq!(e.buffer().lines()[0], "hello");
3546 }
3547
3548 #[test]
3549 fn vim_undo_redo_roundtrip() {
3550 let mut e = Editor::new(
3551 hjkl_buffer::Buffer::new(),
3552 crate::types::DefaultHost::new(),
3553 crate::types::Options::default(),
3554 );
3555 e.set_content("hello");
3556 e.handle_key(key(KeyCode::Char('i')));
3557 for c in "world".chars() {
3558 e.handle_key(key(KeyCode::Char(c)));
3559 }
3560 e.handle_key(key(KeyCode::Esc));
3561 let after = e.buffer().lines()[0].clone();
3562 e.handle_key(key(KeyCode::Char('u')));
3563 assert_eq!(e.buffer().lines()[0], "hello");
3564 e.handle_key(ctrl_key(KeyCode::Char('r')));
3565 assert_eq!(e.buffer().lines()[0], after);
3566 }
3567
3568 #[test]
3569 fn vim_u_undoes_dd() {
3570 let mut e = Editor::new(
3571 hjkl_buffer::Buffer::new(),
3572 crate::types::DefaultHost::new(),
3573 crate::types::Options::default(),
3574 );
3575 e.set_content("first\nsecond");
3576 e.handle_key(key(KeyCode::Char('d')));
3577 e.handle_key(key(KeyCode::Char('d')));
3578 assert_eq!(e.buffer().lines().len(), 1);
3579 e.handle_key(key(KeyCode::Char('u')));
3580 assert_eq!(e.buffer().lines().len(), 2);
3581 assert_eq!(e.buffer().lines()[0], "first");
3582 }
3583
3584 #[test]
3585 fn vim_ctrl_r_redoes() {
3586 let mut e = Editor::new(
3587 hjkl_buffer::Buffer::new(),
3588 crate::types::DefaultHost::new(),
3589 crate::types::Options::default(),
3590 );
3591 e.set_content("hello");
3592 e.handle_key(ctrl_key(KeyCode::Char('r')));
3593 }
3594
3595 #[test]
3596 fn vim_r_replaces_char() {
3597 let mut e = Editor::new(
3598 hjkl_buffer::Buffer::new(),
3599 crate::types::DefaultHost::new(),
3600 crate::types::Options::default(),
3601 );
3602 e.set_content("hello");
3603 e.handle_key(key(KeyCode::Char('r')));
3604 e.handle_key(key(KeyCode::Char('x')));
3605 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
3606 }
3607
3608 #[test]
3609 fn vim_tilde_toggles_case() {
3610 let mut e = Editor::new(
3611 hjkl_buffer::Buffer::new(),
3612 crate::types::DefaultHost::new(),
3613 crate::types::Options::default(),
3614 );
3615 e.set_content("hello");
3616 e.handle_key(key(KeyCode::Char('~')));
3617 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
3618 }
3619
3620 #[test]
3621 fn vim_visual_d_cuts() {
3622 let mut e = Editor::new(
3623 hjkl_buffer::Buffer::new(),
3624 crate::types::DefaultHost::new(),
3625 crate::types::Options::default(),
3626 );
3627 e.set_content("hello");
3628 e.handle_key(key(KeyCode::Char('v')));
3629 e.handle_key(key(KeyCode::Char('l')));
3630 e.handle_key(key(KeyCode::Char('l')));
3631 e.handle_key(key(KeyCode::Char('d')));
3632 assert_eq!(e.vim_mode(), VimMode::Normal);
3633 assert!(e.last_yank.is_some());
3634 }
3635
3636 #[test]
3637 fn vim_visual_c_enters_insert() {
3638 let mut e = Editor::new(
3639 hjkl_buffer::Buffer::new(),
3640 crate::types::DefaultHost::new(),
3641 crate::types::Options::default(),
3642 );
3643 e.set_content("hello");
3644 e.handle_key(key(KeyCode::Char('v')));
3645 e.handle_key(key(KeyCode::Char('l')));
3646 e.handle_key(key(KeyCode::Char('c')));
3647 assert_eq!(e.vim_mode(), VimMode::Insert);
3648 }
3649
3650 #[test]
3651 fn vim_normal_unknown_key_consumed() {
3652 let mut e = Editor::new(
3653 hjkl_buffer::Buffer::new(),
3654 crate::types::DefaultHost::new(),
3655 crate::types::Options::default(),
3656 );
3657 let consumed = e.handle_key(key(KeyCode::Char('z')));
3659 assert!(consumed);
3660 }
3661
3662 #[test]
3663 fn force_normal_clears_operator() {
3664 let mut e = Editor::new(
3665 hjkl_buffer::Buffer::new(),
3666 crate::types::DefaultHost::new(),
3667 crate::types::Options::default(),
3668 );
3669 e.handle_key(key(KeyCode::Char('d')));
3670 e.force_normal();
3671 assert_eq!(e.vim_mode(), VimMode::Normal);
3672 }
3673
3674 fn many_lines(n: usize) -> String {
3675 (0..n)
3676 .map(|i| format!("line{i}"))
3677 .collect::<Vec<_>>()
3678 .join("\n")
3679 }
3680
3681 fn prime_viewport<H: Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, height: u16) {
3682 e.set_viewport_height(height);
3683 }
3684
3685 #[test]
3686 fn zz_centers_cursor_in_viewport() {
3687 let mut e = Editor::new(
3688 hjkl_buffer::Buffer::new(),
3689 crate::types::DefaultHost::new(),
3690 crate::types::Options::default(),
3691 );
3692 e.set_content(&many_lines(100));
3693 prime_viewport(&mut e, 20);
3694 e.jump_cursor(50, 0);
3695 e.handle_key(key(KeyCode::Char('z')));
3696 e.handle_key(key(KeyCode::Char('z')));
3697 assert_eq!(e.host().viewport().top_row, 40);
3698 assert_eq!(e.cursor().0, 50);
3699 }
3700
3701 #[test]
3702 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
3703 let mut e = Editor::new(
3704 hjkl_buffer::Buffer::new(),
3705 crate::types::DefaultHost::new(),
3706 crate::types::Options::default(),
3707 );
3708 e.set_content(&many_lines(100));
3709 prime_viewport(&mut e, 20);
3710 e.jump_cursor(50, 0);
3711 e.handle_key(key(KeyCode::Char('z')));
3712 e.handle_key(key(KeyCode::Char('t')));
3713 assert_eq!(e.host().viewport().top_row, 45);
3716 assert_eq!(e.cursor().0, 50);
3717 }
3718
3719 #[test]
3720 fn ctrl_a_increments_number_at_cursor() {
3721 let mut e = Editor::new(
3722 hjkl_buffer::Buffer::new(),
3723 crate::types::DefaultHost::new(),
3724 crate::types::Options::default(),
3725 );
3726 e.set_content("x = 41");
3727 e.handle_key(ctrl_key(KeyCode::Char('a')));
3728 assert_eq!(e.buffer().lines()[0], "x = 42");
3729 assert_eq!(e.cursor(), (0, 5));
3730 }
3731
3732 #[test]
3733 fn ctrl_a_finds_number_to_right_of_cursor() {
3734 let mut e = Editor::new(
3735 hjkl_buffer::Buffer::new(),
3736 crate::types::DefaultHost::new(),
3737 crate::types::Options::default(),
3738 );
3739 e.set_content("foo 99 bar");
3740 e.handle_key(ctrl_key(KeyCode::Char('a')));
3741 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
3742 assert_eq!(e.cursor(), (0, 6));
3743 }
3744
3745 #[test]
3746 fn ctrl_a_with_count_adds_count() {
3747 let mut e = Editor::new(
3748 hjkl_buffer::Buffer::new(),
3749 crate::types::DefaultHost::new(),
3750 crate::types::Options::default(),
3751 );
3752 e.set_content("x = 10");
3753 for d in "5".chars() {
3754 e.handle_key(key(KeyCode::Char(d)));
3755 }
3756 e.handle_key(ctrl_key(KeyCode::Char('a')));
3757 assert_eq!(e.buffer().lines()[0], "x = 15");
3758 }
3759
3760 #[test]
3761 fn ctrl_x_decrements_number() {
3762 let mut e = Editor::new(
3763 hjkl_buffer::Buffer::new(),
3764 crate::types::DefaultHost::new(),
3765 crate::types::Options::default(),
3766 );
3767 e.set_content("n=5");
3768 e.handle_key(ctrl_key(KeyCode::Char('x')));
3769 assert_eq!(e.buffer().lines()[0], "n=4");
3770 }
3771
3772 #[test]
3773 fn ctrl_x_crosses_zero_into_negative() {
3774 let mut e = Editor::new(
3775 hjkl_buffer::Buffer::new(),
3776 crate::types::DefaultHost::new(),
3777 crate::types::Options::default(),
3778 );
3779 e.set_content("v=0");
3780 e.handle_key(ctrl_key(KeyCode::Char('x')));
3781 assert_eq!(e.buffer().lines()[0], "v=-1");
3782 }
3783
3784 #[test]
3785 fn ctrl_a_on_negative_number_increments_toward_zero() {
3786 let mut e = Editor::new(
3787 hjkl_buffer::Buffer::new(),
3788 crate::types::DefaultHost::new(),
3789 crate::types::Options::default(),
3790 );
3791 e.set_content("a = -5");
3792 e.handle_key(ctrl_key(KeyCode::Char('a')));
3793 assert_eq!(e.buffer().lines()[0], "a = -4");
3794 }
3795
3796 #[test]
3797 fn ctrl_a_noop_when_no_digit_on_line() {
3798 let mut e = Editor::new(
3799 hjkl_buffer::Buffer::new(),
3800 crate::types::DefaultHost::new(),
3801 crate::types::Options::default(),
3802 );
3803 e.set_content("no digits here");
3804 e.handle_key(ctrl_key(KeyCode::Char('a')));
3805 assert_eq!(e.buffer().lines()[0], "no digits here");
3806 }
3807
3808 #[test]
3809 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
3810 let mut e = Editor::new(
3811 hjkl_buffer::Buffer::new(),
3812 crate::types::DefaultHost::new(),
3813 crate::types::Options::default(),
3814 );
3815 e.set_content(&many_lines(100));
3816 prime_viewport(&mut e, 20);
3817 e.jump_cursor(50, 0);
3818 e.handle_key(key(KeyCode::Char('z')));
3819 e.handle_key(key(KeyCode::Char('b')));
3820 assert_eq!(e.host().viewport().top_row, 36);
3824 assert_eq!(e.cursor().0, 50);
3825 }
3826
3827 #[test]
3834 fn set_content_dirties_then_take_dirty_clears() {
3835 let mut e = Editor::new(
3836 hjkl_buffer::Buffer::new(),
3837 crate::types::DefaultHost::new(),
3838 crate::types::Options::default(),
3839 );
3840 e.set_content("hello");
3841 assert!(
3842 e.take_dirty(),
3843 "set_content should leave content_dirty=true"
3844 );
3845 assert!(!e.take_dirty(), "take_dirty should clear the flag");
3846 }
3847
3848 #[test]
3849 fn content_arc_returns_same_arc_until_mutation() {
3850 let mut e = Editor::new(
3851 hjkl_buffer::Buffer::new(),
3852 crate::types::DefaultHost::new(),
3853 crate::types::Options::default(),
3854 );
3855 e.set_content("hello");
3856 let a = e.content_arc();
3857 let b = e.content_arc();
3858 assert!(
3859 std::sync::Arc::ptr_eq(&a, &b),
3860 "repeated content_arc() should hit the cache"
3861 );
3862
3863 e.handle_key(key(KeyCode::Char('i')));
3865 e.handle_key(key(KeyCode::Char('!')));
3866 let c = e.content_arc();
3867 assert!(
3868 !std::sync::Arc::ptr_eq(&a, &c),
3869 "mutation should invalidate content_arc() cache"
3870 );
3871 assert!(c.contains('!'));
3872 }
3873
3874 #[test]
3875 fn content_arc_cache_invalidated_by_set_content() {
3876 let mut e = Editor::new(
3877 hjkl_buffer::Buffer::new(),
3878 crate::types::DefaultHost::new(),
3879 crate::types::Options::default(),
3880 );
3881 e.set_content("one");
3882 let a = e.content_arc();
3883 e.set_content("two");
3884 let b = e.content_arc();
3885 assert!(!std::sync::Arc::ptr_eq(&a, &b));
3886 assert!(b.starts_with("two"));
3887 }
3888
3889 #[test]
3895 fn mouse_click_past_eol_lands_on_last_char() {
3896 let mut e = Editor::new(
3897 hjkl_buffer::Buffer::new(),
3898 crate::types::DefaultHost::new(),
3899 crate::types::Options::default(),
3900 );
3901 e.set_content("hello");
3902 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3906 e.mouse_click_in_rect(area, 78, 1);
3907 assert_eq!(e.cursor(), (0, 4));
3908 }
3909
3910 #[test]
3911 fn mouse_click_past_eol_handles_multibyte_line() {
3912 let mut e = Editor::new(
3913 hjkl_buffer::Buffer::new(),
3914 crate::types::DefaultHost::new(),
3915 crate::types::Options::default(),
3916 );
3917 e.set_content("héllo");
3920 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3921 e.mouse_click_in_rect(area, 78, 1);
3922 assert_eq!(e.cursor(), (0, 4));
3923 }
3924
3925 #[test]
3926 fn mouse_click_inside_line_lands_on_clicked_char() {
3927 let mut e = Editor::new(
3928 hjkl_buffer::Buffer::new(),
3929 crate::types::DefaultHost::new(),
3930 crate::types::Options::default(),
3931 );
3932 e.set_content("hello world");
3933 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3937 e.mouse_click_in_rect(area, 5, 1);
3938 assert_eq!(e.cursor(), (0, 0));
3939 e.mouse_click_in_rect(area, 7, 1);
3940 assert_eq!(e.cursor(), (0, 2));
3941 }
3942
3943 #[test]
3948 fn mouse_click_breaks_insert_undo_group_when_undobreak_on() {
3949 let mut e = Editor::new(
3950 hjkl_buffer::Buffer::new(),
3951 crate::types::DefaultHost::new(),
3952 crate::types::Options::default(),
3953 );
3954 e.set_content("hello world");
3955 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3956 assert!(e.settings().undo_break_on_motion);
3958 e.handle_key(key(KeyCode::Char('i')));
3960 e.handle_key(key(KeyCode::Char('A')));
3961 e.handle_key(key(KeyCode::Char('A')));
3962 e.handle_key(key(KeyCode::Char('A')));
3963 e.mouse_click_in_rect(area, 10, 1);
3965 e.handle_key(key(KeyCode::Char('B')));
3967 e.handle_key(key(KeyCode::Char('B')));
3968 e.handle_key(key(KeyCode::Char('B')));
3969 e.handle_key(key(KeyCode::Esc));
3971 e.handle_key(key(KeyCode::Char('u')));
3972 let line = e.buffer().line(0).unwrap_or("").to_string();
3973 assert!(
3974 line.contains("AAA"),
3975 "AAA must survive undo (separate group): {line:?}"
3976 );
3977 assert!(
3978 !line.contains("BBB"),
3979 "BBB must be undone (post-click group): {line:?}"
3980 );
3981 }
3982
3983 #[test]
3987 fn mouse_click_keeps_one_undo_group_when_undobreak_off() {
3988 let mut e = Editor::new(
3989 hjkl_buffer::Buffer::new(),
3990 crate::types::DefaultHost::new(),
3991 crate::types::Options::default(),
3992 );
3993 e.set_content("hello world");
3994 e.settings_mut().undo_break_on_motion = false;
3995 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
3996 e.handle_key(key(KeyCode::Char('i')));
3997 e.handle_key(key(KeyCode::Char('A')));
3998 e.handle_key(key(KeyCode::Char('A')));
3999 e.mouse_click_in_rect(area, 10, 1);
4000 e.handle_key(key(KeyCode::Char('B')));
4001 e.handle_key(key(KeyCode::Char('B')));
4002 e.handle_key(key(KeyCode::Esc));
4003 e.handle_key(key(KeyCode::Char('u')));
4004 let line = e.buffer().line(0).unwrap_or("").to_string();
4005 assert!(
4006 !line.contains("AA") && !line.contains("BB"),
4007 "with undobreak off, single `u` must reverse whole insert: {line:?}"
4008 );
4009 assert_eq!(line, "hello world");
4010 }
4011
4012 #[test]
4015 fn host_clipboard_round_trip_via_default_host() {
4016 let mut e = Editor::new(
4019 hjkl_buffer::Buffer::new(),
4020 crate::types::DefaultHost::new(),
4021 crate::types::Options::default(),
4022 );
4023 e.host_mut().write_clipboard("payload".to_string());
4024 assert_eq!(e.host_mut().read_clipboard().as_deref(), Some("payload"));
4025 }
4026
4027 #[test]
4028 fn host_records_clipboard_on_yank() {
4029 let mut e = Editor::new(
4033 hjkl_buffer::Buffer::new(),
4034 crate::types::DefaultHost::new(),
4035 crate::types::Options::default(),
4036 );
4037 e.set_content("hello\n");
4038 e.handle_key(key(KeyCode::Char('y')));
4039 e.handle_key(key(KeyCode::Char('y')));
4040 let clip = e.host_mut().read_clipboard();
4042 assert!(
4043 clip.as_deref().unwrap_or("").starts_with("hello"),
4044 "host clipboard should carry the yank: {clip:?}"
4045 );
4046 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
4048 }
4049
4050 #[test]
4051 fn host_cursor_shape_via_shared_recorder() {
4052 let shapes_ptr: &'static std::sync::Mutex<Vec<crate::types::CursorShape>> =
4056 Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
4057 struct LeakHost {
4058 shapes: &'static std::sync::Mutex<Vec<crate::types::CursorShape>>,
4059 viewport: crate::types::Viewport,
4060 }
4061 impl crate::types::Host for LeakHost {
4062 type Intent = ();
4063 fn write_clipboard(&mut self, _: String) {}
4064 fn read_clipboard(&mut self) -> Option<String> {
4065 None
4066 }
4067 fn now(&self) -> core::time::Duration {
4068 core::time::Duration::ZERO
4069 }
4070 fn prompt_search(&mut self) -> Option<String> {
4071 None
4072 }
4073 fn emit_cursor_shape(&mut self, s: crate::types::CursorShape) {
4074 self.shapes.lock().unwrap().push(s);
4075 }
4076 fn viewport(&self) -> &crate::types::Viewport {
4077 &self.viewport
4078 }
4079 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4080 &mut self.viewport
4081 }
4082 fn emit_intent(&mut self, _: Self::Intent) {}
4083 }
4084 let mut e = Editor::new(
4085 hjkl_buffer::Buffer::new(),
4086 LeakHost {
4087 shapes: shapes_ptr,
4088 viewport: crate::types::Viewport::default(),
4089 },
4090 crate::types::Options::default(),
4091 );
4092 e.set_content("abc");
4093 e.handle_key(key(KeyCode::Char('i')));
4095 e.handle_key(key(KeyCode::Esc));
4097 let shapes = shapes_ptr.lock().unwrap().clone();
4098 assert_eq!(
4099 shapes,
4100 vec![
4101 crate::types::CursorShape::Bar,
4102 crate::types::CursorShape::Block,
4103 ],
4104 "host should observe Insert(Bar) → Normal(Block) transitions"
4105 );
4106 }
4107
4108 #[test]
4109 fn host_now_drives_chord_timeout_deterministically() {
4110 let now_ptr: &'static std::sync::Mutex<core::time::Duration> =
4115 Box::leak(Box::new(std::sync::Mutex::new(core::time::Duration::ZERO)));
4116 struct ClockHost {
4117 now: &'static std::sync::Mutex<core::time::Duration>,
4118 viewport: crate::types::Viewport,
4119 }
4120 impl crate::types::Host for ClockHost {
4121 type Intent = ();
4122 fn write_clipboard(&mut self, _: String) {}
4123 fn read_clipboard(&mut self) -> Option<String> {
4124 None
4125 }
4126 fn now(&self) -> core::time::Duration {
4127 *self.now.lock().unwrap()
4128 }
4129 fn prompt_search(&mut self) -> Option<String> {
4130 None
4131 }
4132 fn emit_cursor_shape(&mut self, _: crate::types::CursorShape) {}
4133 fn viewport(&self) -> &crate::types::Viewport {
4134 &self.viewport
4135 }
4136 fn viewport_mut(&mut self) -> &mut crate::types::Viewport {
4137 &mut self.viewport
4138 }
4139 fn emit_intent(&mut self, _: Self::Intent) {}
4140 }
4141 let mut e = Editor::new(
4142 hjkl_buffer::Buffer::new(),
4143 ClockHost {
4144 now: now_ptr,
4145 viewport: crate::types::Viewport::default(),
4146 },
4147 crate::types::Options::default(),
4148 );
4149 e.set_content("a\nb\nc\n");
4150 e.jump_cursor(2, 0);
4151 e.handle_key(key(KeyCode::Char('g')));
4153 *now_ptr.lock().unwrap() = core::time::Duration::from_secs(60);
4155 e.handle_key(key(KeyCode::Char('g')));
4158 assert_eq!(
4159 e.cursor().0,
4160 2,
4161 "Host::now() must drive `:set timeoutlen` deterministically"
4162 );
4163 }
4164
4165 fn fresh_editor(initial: &str) -> Editor {
4168 let buffer = hjkl_buffer::Buffer::from_str(initial);
4169 Editor::new(
4170 buffer,
4171 crate::types::DefaultHost::new(),
4172 crate::types::Options::default(),
4173 )
4174 }
4175
4176 #[test]
4177 fn content_edit_insert_char_at_origin() {
4178 let mut e = fresh_editor("");
4179 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4180 at: hjkl_buffer::Position::new(0, 0),
4181 ch: 'a',
4182 });
4183 let edits = e.take_content_edits();
4184 assert_eq!(edits.len(), 1);
4185 let ce = &edits[0];
4186 assert_eq!(ce.start_byte, 0);
4187 assert_eq!(ce.old_end_byte, 0);
4188 assert_eq!(ce.new_end_byte, 1);
4189 assert_eq!(ce.start_position, (0, 0));
4190 assert_eq!(ce.old_end_position, (0, 0));
4191 assert_eq!(ce.new_end_position, (0, 1));
4192 }
4193
4194 #[test]
4195 fn content_edit_insert_str_multiline() {
4196 let mut e = fresh_editor("x\ny");
4198 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertStr {
4199 at: hjkl_buffer::Position::new(0, 1),
4200 text: "ab\ncd".into(),
4201 });
4202 let edits = e.take_content_edits();
4203 assert_eq!(edits.len(), 1);
4204 let ce = &edits[0];
4205 assert_eq!(ce.start_byte, 1);
4206 assert_eq!(ce.old_end_byte, 1);
4207 assert_eq!(ce.new_end_byte, 1 + 5);
4208 assert_eq!(ce.start_position, (0, 1));
4209 assert_eq!(ce.new_end_position, (1, 2));
4211 }
4212
4213 #[test]
4214 fn content_edit_delete_range_charwise() {
4215 let mut e = fresh_editor("abcdef");
4217 let _ = e.mutate_edit(hjkl_buffer::Edit::DeleteRange {
4218 start: hjkl_buffer::Position::new(0, 1),
4219 end: hjkl_buffer::Position::new(0, 4),
4220 kind: hjkl_buffer::MotionKind::Char,
4221 });
4222 let edits = e.take_content_edits();
4223 assert_eq!(edits.len(), 1);
4224 let ce = &edits[0];
4225 assert_eq!(ce.start_byte, 1);
4226 assert_eq!(ce.old_end_byte, 4);
4227 assert_eq!(ce.new_end_byte, 1);
4228 assert!(ce.old_end_byte > ce.new_end_byte);
4229 }
4230
4231 #[test]
4232 fn content_edit_set_content_resets() {
4233 let mut e = fresh_editor("foo");
4234 let _ = e.mutate_edit(hjkl_buffer::Edit::InsertChar {
4235 at: hjkl_buffer::Position::new(0, 0),
4236 ch: 'X',
4237 });
4238 e.set_content("brand new");
4241 assert!(e.take_content_reset());
4242 assert!(!e.take_content_reset());
4244 assert!(e.take_content_edits().is_empty());
4246 }
4247
4248 #[test]
4249 fn content_edit_multiple_replaces_in_order() {
4250 let mut e = fresh_editor("xax xbx xcx");
4255 let _ = e.take_content_edits();
4256 let _ = e.take_content_reset();
4257 let positions = [(0usize, 0usize), (0, 4), (0, 8)];
4261 for (row, col) in positions {
4262 let _ = e.mutate_edit(hjkl_buffer::Edit::Replace {
4263 start: hjkl_buffer::Position::new(row, col),
4264 end: hjkl_buffer::Position::new(row, col + 1),
4265 with: "yy".into(),
4266 });
4267 }
4268 let edits = e.take_content_edits();
4269 assert_eq!(edits.len(), 3);
4270 for ce in &edits {
4271 assert!(ce.start_byte <= ce.old_end_byte);
4272 assert!(ce.start_byte <= ce.new_end_byte);
4273 }
4274 for w in edits.windows(2) {
4276 assert!(w[0].start_byte <= w[1].start_byte);
4277 }
4278 }
4279}