1use crate::input::{Input, Key};
10use crate::vim::{self, VimState};
11use crate::{KeybindingMode, VimMode};
12use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
13use ratatui::layout::Rect;
14use std::sync::atomic::{AtomicU16, Ordering};
15
16fn edit_to_editop(edit: &hjkl_buffer::Edit) -> Option<crate::types::Edit> {
22 use crate::types::{Edit as Op, Pos};
23 use hjkl_buffer::Edit as B;
24 let to_pos = |p: hjkl_buffer::Position| Pos {
25 line: p.row as u32,
26 col: p.col as u32,
27 };
28 Some(match edit {
29 B::InsertChar { at, ch } => Op {
30 range: to_pos(*at)..to_pos(*at),
31 replacement: ch.to_string(),
32 },
33 B::InsertStr { at, text } => Op {
34 range: to_pos(*at)..to_pos(*at),
35 replacement: text.clone(),
36 },
37 B::DeleteRange { start, end, .. } => Op {
38 range: to_pos(*start)..to_pos(*end),
39 replacement: String::new(),
40 },
41 B::Replace { start, end, with } => Op {
42 range: to_pos(*start)..to_pos(*end),
43 replacement: with.clone(),
44 },
45 B::JoinLines { row, count, .. } => {
46 let start = Pos {
47 line: *row as u32,
48 col: 0,
49 };
50 let end = Pos {
51 line: (*row + *count) as u32,
52 col: 0,
53 };
54 Op {
55 range: start..end,
56 replacement: String::new(),
57 }
58 }
59 B::SplitLines { row, .. } => {
60 let p = Pos {
61 line: *row as u32,
62 col: 0,
63 };
64 Op {
65 range: p..p,
66 replacement: String::new(),
67 }
68 }
69 B::InsertBlock { at, .. } => {
70 let p = to_pos(*at);
71 Op {
72 range: p..p,
73 replacement: String::new(),
74 }
75 }
76 B::DeleteBlockChunks { at, .. } => {
77 let p = to_pos(*at);
78 Op {
79 range: p..p,
80 replacement: String::new(),
81 }
82 }
83 })
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub(super) enum CursorScrollTarget {
90 Center,
91 Top,
92 Bottom,
93}
94
95pub struct Editor<'a> {
96 pub keybinding_mode: KeybindingMode,
97 _marker: std::marker::PhantomData<&'a ()>,
102 pub last_yank: Option<String>,
104 #[doc(hidden)]
106 pub vim: VimState,
107 #[doc(hidden)]
109 pub undo_stack: Vec<(Vec<String>, (usize, usize))>,
110 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
112 pub(super) content_dirty: bool,
114 pub(super) cached_content: Option<std::sync::Arc<String>>,
119 pub(super) viewport_height: AtomicU16,
124 pub(super) pending_lsp: Option<LspIntent>,
128 pub(super) buffer: hjkl_buffer::Buffer,
133 pub(super) style_table: Vec<ratatui::style::Style>,
140 #[doc(hidden)]
143 pub registers: crate::registers::Registers,
144 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
150 #[doc(hidden)]
154 pub settings: Settings,
155 #[doc(hidden)]
161 pub file_marks: std::collections::HashMap<char, (usize, usize)>,
162 #[doc(hidden)]
167 pub syntax_fold_ranges: Vec<(usize, usize)>,
168 #[doc(hidden)]
176 pub change_log: Vec<crate::types::Edit>,
177}
178
179#[derive(Debug, Clone)]
182pub struct Settings {
183 pub shiftwidth: usize,
185 pub tabstop: usize,
188 pub ignore_case: bool,
191 pub textwidth: usize,
193 pub wrap: hjkl_buffer::Wrap,
199}
200
201impl Default for Settings {
202 fn default() -> Self {
203 Self {
204 shiftwidth: 2,
205 tabstop: 8,
206 ignore_case: false,
207 textwidth: 79,
208 wrap: hjkl_buffer::Wrap::None,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum LspIntent {
218 GotoDefinition,
220}
221
222impl<'a> Editor<'a> {
223 pub fn new(keybinding_mode: KeybindingMode) -> Self {
224 Self {
225 _marker: std::marker::PhantomData,
226 keybinding_mode,
227 last_yank: None,
228 vim: VimState::default(),
229 undo_stack: Vec::new(),
230 redo_stack: Vec::new(),
231 content_dirty: false,
232 cached_content: None,
233 viewport_height: AtomicU16::new(0),
234 pending_lsp: None,
235 buffer: hjkl_buffer::Buffer::new(),
236 style_table: Vec::new(),
237 registers: crate::registers::Registers::default(),
238 styled_spans: Vec::new(),
239 settings: Settings::default(),
240 file_marks: std::collections::HashMap::new(),
241 syntax_fold_ranges: Vec::new(),
242 change_log: Vec::new(),
243 }
244 }
245
246 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
250 self.syntax_fold_ranges = ranges;
251 }
252
253 pub fn settings(&self) -> &Settings {
256 &self.settings
257 }
258
259 #[doc(hidden)]
260 pub fn settings_mut(&mut self) -> &mut Settings {
261 &mut self.settings
262 }
263
264 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>) {
271 let line_byte_lens: Vec<usize> = self.buffer.lines().iter().map(|l| l.len()).collect();
272 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
273 for (row, row_spans) in spans.iter().enumerate() {
274 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
275 let mut translated = Vec::with_capacity(row_spans.len());
276 for (start, end, style) in row_spans {
277 let end_clamped = (*end).min(line_len);
278 if end_clamped <= *start {
279 continue;
280 }
281 let id = self.intern_style(*style);
282 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
283 }
284 by_row.push(translated);
285 }
286 self.buffer.set_spans(by_row);
287 self.styled_spans = spans;
288 }
289
290 pub fn yank(&self) -> &str {
292 &self.registers.unnamed.text
293 }
294
295 pub fn registers(&self) -> &crate::registers::Registers {
297 &self.registers
298 }
299
300 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
305 self.registers.set_clipboard(text, linewise);
306 }
307
308 pub fn pending_register_is_clipboard(&self) -> bool {
312 matches!(self.vim.pending_register, Some('+') | Some('*'))
313 }
314
315 pub fn set_yank(&mut self, text: impl Into<String>) {
319 let text = text.into();
320 let linewise = self.vim.yank_linewise;
321 self.registers.unnamed = crate::registers::Slot { text, linewise };
322 }
323
324 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
328 self.vim.yank_linewise = linewise;
329 let target = self.vim.pending_register.take();
330 self.registers.record_yank(text, linewise, target);
331 }
332
333 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
338 if let Some(slot) = match reg {
339 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
340 'A'..='Z' => {
341 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
342 }
343 _ => None,
344 } {
345 slot.text = text;
346 slot.linewise = false;
347 }
348 }
349
350 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
353 self.vim.yank_linewise = linewise;
354 let target = self.vim.pending_register.take();
355 self.registers.record_delete(text, linewise, target);
356 }
357
358 pub fn intern_style(&mut self, style: ratatui::style::Style) -> u32 {
364 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
365 return idx as u32;
366 }
367 self.style_table.push(style);
368 (self.style_table.len() - 1) as u32
369 }
370
371 pub fn style_table(&self) -> &[ratatui::style::Style] {
375 &self.style_table
376 }
377
378 pub fn buffer(&self) -> &hjkl_buffer::Buffer {
381 &self.buffer
382 }
383
384 pub fn buffer_mut(&mut self) -> &mut hjkl_buffer::Buffer {
385 &mut self.buffer
386 }
387
388 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
392
393 pub fn set_viewport_top(&mut self, row: usize) {
401 let last = self.buffer.row_count().saturating_sub(1);
402 let target = row.min(last);
403 self.buffer.viewport_mut().top_row = target;
404 }
405
406 #[doc(hidden)]
411 pub fn jump_cursor(&mut self, row: usize, col: usize) {
412 self.buffer.set_cursor(hjkl_buffer::Position::new(row, col));
413 }
414
415 pub fn cursor(&self) -> (usize, usize) {
423 let pos = self.buffer.cursor();
424 (pos.row, pos.col)
425 }
426
427 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
430 self.pending_lsp.take()
431 }
432
433 pub(crate) fn sync_buffer_from_textarea(&mut self) {
437 self.buffer.set_sticky_col(self.vim.sticky_col);
438 let height = self.viewport_height_value();
439 self.buffer.viewport_mut().height = height;
440 }
441
442 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
446 self.sync_buffer_from_textarea();
447 }
448
449 pub fn record_jump(&mut self, pos: (usize, usize)) {
454 const JUMPLIST_MAX: usize = 100;
455 self.vim.jump_back.push(pos);
456 if self.vim.jump_back.len() > JUMPLIST_MAX {
457 self.vim.jump_back.remove(0);
458 }
459 self.vim.jump_fwd.clear();
460 }
461
462 pub fn set_viewport_height(&self, height: u16) {
465 self.viewport_height.store(height, Ordering::Relaxed);
466 }
467
468 pub fn viewport_height_value(&self) -> u16 {
470 self.viewport_height.load(Ordering::Relaxed)
471 }
472
473 #[doc(hidden)]
479 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
480 let pre_row = self.buffer.cursor().row;
481 let pre_rows = self.buffer.row_count();
482 if let Some(op) = edit_to_editop(&edit) {
486 self.change_log.push(op);
487 }
488 let inverse = self.buffer.apply_edit(edit);
489 let pos = self.buffer.cursor();
490 let lo = pre_row.min(pos.row);
496 let hi = pre_row.max(pos.row);
497 self.buffer.invalidate_folds_in_range(lo, hi);
498 self.vim.last_edit_pos = Some((pos.row, pos.col));
499 let entry = (pos.row, pos.col);
504 if self.vim.change_list.last() != Some(&entry) {
505 if let Some(idx) = self.vim.change_list_cursor.take() {
506 self.vim.change_list.truncate(idx + 1);
507 }
508 self.vim.change_list.push(entry);
509 let len = self.vim.change_list.len();
510 if len > crate::vim::CHANGE_LIST_MAX {
511 self.vim
512 .change_list
513 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
514 }
515 }
516 self.vim.change_list_cursor = None;
517 let post_rows = self.buffer.row_count();
521 let delta = post_rows as isize - pre_rows as isize;
522 if delta != 0 {
523 self.shift_marks_after_edit(pre_row, delta);
524 }
525 self.push_buffer_content_to_textarea();
526 self.mark_content_dirty();
527 inverse
528 }
529
530 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
535 if delta == 0 {
536 return;
537 }
538 let drop_end = if delta < 0 {
541 edit_start.saturating_add((-delta) as usize)
542 } else {
543 edit_start
544 };
545 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
546
547 let mut to_drop: Vec<char> = Vec::new();
548 for (c, (row, _col)) in self.vim.marks.iter_mut() {
549 if (edit_start..drop_end).contains(row) {
550 to_drop.push(*c);
551 } else if *row >= shift_threshold {
552 *row = ((*row as isize) + delta).max(0) as usize;
553 }
554 }
555 for c in to_drop {
556 self.vim.marks.remove(&c);
557 }
558
559 let mut to_drop: Vec<char> = Vec::new();
561 for (c, (row, _col)) in self.file_marks.iter_mut() {
562 if (edit_start..drop_end).contains(row) {
563 to_drop.push(*c);
564 } else if *row >= shift_threshold {
565 *row = ((*row as isize) + delta).max(0) as usize;
566 }
567 }
568 for c in to_drop {
569 self.file_marks.remove(&c);
570 }
571
572 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
573 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
574 for (row, _) in entries.iter_mut() {
575 if *row >= shift_threshold {
576 *row = ((*row as isize) + delta).max(0) as usize;
577 }
578 }
579 };
580 shift_jumps(&mut self.vim.jump_back);
581 shift_jumps(&mut self.vim.jump_fwd);
582 }
583
584 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
592
593 pub fn mark_content_dirty(&mut self) {
599 self.content_dirty = true;
600 self.cached_content = None;
601 }
602
603 pub fn take_dirty(&mut self) -> bool {
605 let dirty = self.content_dirty;
606 self.content_dirty = false;
607 dirty
608 }
609
610 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
620 if !self.content_dirty {
621 return None;
622 }
623 let arc = self.content_arc();
624 self.content_dirty = false;
625 Some(arc)
626 }
627
628 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
631 let cursor = self.buffer.cursor().row;
632 let top = self.buffer.viewport().top_row;
633 cursor.saturating_sub(top).min(height as usize - 1) as u16
634 }
635
636 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
640 let pos = self.buffer.cursor();
641 let v = self.buffer.viewport();
642 if pos.row < v.top_row || pos.col < v.top_col {
643 return None;
644 }
645 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
646 let dy = (pos.row - v.top_row) as u16;
647 let dx = (pos.col - v.top_col) as u16;
648 if dy >= area.height || dx + lnum_width >= area.width {
649 return None;
650 }
651 Some((area.x + lnum_width + dx, area.y + dy))
652 }
653
654 pub fn vim_mode(&self) -> VimMode {
655 self.vim.public_mode()
656 }
657
658 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
664 self.vim.search_prompt.as_ref()
665 }
666
667 pub fn last_search(&self) -> Option<&str> {
670 self.vim.last_search.as_deref()
671 }
672
673 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
677 if self.vim_mode() != VimMode::Visual {
678 return None;
679 }
680 let anchor = self.vim.visual_anchor;
681 let cursor = self.cursor();
682 let (start, end) = if anchor <= cursor {
683 (anchor, cursor)
684 } else {
685 (cursor, anchor)
686 };
687 Some((start, end))
688 }
689
690 pub fn line_highlight(&self) -> Option<(usize, usize)> {
693 if self.vim_mode() != VimMode::VisualLine {
694 return None;
695 }
696 let anchor = self.vim.visual_line_anchor;
697 let cursor = self.buffer.cursor().row;
698 Some((anchor.min(cursor), anchor.max(cursor)))
699 }
700
701 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
702 if self.vim_mode() != VimMode::VisualBlock {
703 return None;
704 }
705 let (ar, ac) = self.vim.block_anchor;
706 let cr = self.buffer.cursor().row;
707 let cc = self.vim.block_vcol;
708 let top = ar.min(cr);
709 let bot = ar.max(cr);
710 let left = ac.min(cc);
711 let right = ac.max(cc);
712 Some((top, bot, left, right))
713 }
714
715 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
721 use hjkl_buffer::{Position, Selection};
722 match self.vim_mode() {
723 VimMode::Visual => {
724 let (ar, ac) = self.vim.visual_anchor;
725 let head = self.buffer.cursor();
726 Some(Selection::Char {
727 anchor: Position::new(ar, ac),
728 head,
729 })
730 }
731 VimMode::VisualLine => {
732 let anchor_row = self.vim.visual_line_anchor;
733 let head_row = self.buffer.cursor().row;
734 Some(Selection::Line {
735 anchor_row,
736 head_row,
737 })
738 }
739 VimMode::VisualBlock => {
740 let (ar, ac) = self.vim.block_anchor;
741 let cr = self.buffer.cursor().row;
742 let cc = self.vim.block_vcol;
743 Some(Selection::Block {
744 anchor: Position::new(ar, ac),
745 head: Position::new(cr, cc),
746 })
747 }
748 _ => None,
749 }
750 }
751
752 pub fn force_normal(&mut self) {
754 self.vim.force_normal();
755 }
756
757 pub fn content(&self) -> String {
758 let mut s = self.buffer.lines().join("\n");
759 s.push('\n');
760 s
761 }
762
763 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
768 if let Some(arc) = &self.cached_content {
769 return std::sync::Arc::clone(arc);
770 }
771 let arc = std::sync::Arc::new(self.content());
772 self.cached_content = Some(std::sync::Arc::clone(&arc));
773 arc
774 }
775
776 pub fn set_content(&mut self, text: &str) {
777 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
778 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
779 lines.pop();
780 }
781 if lines.is_empty() {
782 lines.push(String::new());
783 }
784 let _ = lines;
785 self.buffer = hjkl_buffer::Buffer::from_str(text);
786 self.undo_stack.clear();
787 self.redo_stack.clear();
788 self.mark_content_dirty();
789 }
790
791 pub fn take_changes(&mut self) -> Vec<crate::types::Edit> {
808 std::mem::take(&mut self.change_log)
809 }
810
811 pub fn current_options(&self) -> crate::types::Options {
821 let mut o = crate::types::Options::default();
822 o.shiftwidth = self.settings.shiftwidth as u32;
823 o.tabstop = self.settings.tabstop as u32;
824 o.ignorecase = self.settings.ignore_case;
825 o
826 }
827
828 pub fn apply_options(&mut self, opts: &crate::types::Options) {
833 self.settings.shiftwidth = opts.shiftwidth as usize;
834 self.settings.tabstop = opts.tabstop as usize;
835 self.settings.ignore_case = opts.ignorecase;
836 }
837
838 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
848 use crate::types::{Highlight, HighlightKind, Pos};
849 let sel = self.buffer_selection()?;
850 let (start, end) = match sel {
851 hjkl_buffer::Selection::Char { anchor, head } => {
852 let a = (anchor.row, anchor.col);
853 let h = (head.row, head.col);
854 if a <= h { (a, h) } else { (h, a) }
855 }
856 hjkl_buffer::Selection::Line {
857 anchor_row,
858 head_row,
859 } => {
860 let (top, bot) = if anchor_row <= head_row {
861 (anchor_row, head_row)
862 } else {
863 (head_row, anchor_row)
864 };
865 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
866 ((top, 0), (bot, last_col))
867 }
868 hjkl_buffer::Selection::Block { anchor, head } => {
869 let (top, bot) = if anchor.row <= head.row {
870 (anchor.row, head.row)
871 } else {
872 (head.row, anchor.row)
873 };
874 let (left, right) = if anchor.col <= head.col {
875 (anchor.col, head.col)
876 } else {
877 (head.col, anchor.col)
878 };
879 ((top, left), (bot, right))
880 }
881 };
882 Some(Highlight {
883 range: Pos {
884 line: start.0 as u32,
885 col: start.1 as u32,
886 }..Pos {
887 line: end.0 as u32,
888 col: end.1 as u32,
889 },
890 kind: HighlightKind::Selection,
891 })
892 }
893
894 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
907 use crate::types::{Highlight, HighlightKind, Pos};
908 let row = line as usize;
909 if row >= self.buffer.lines().len() {
910 return Vec::new();
911 }
912 if self.buffer.search_pattern().is_none() {
913 return Vec::new();
914 }
915 self.buffer
916 .search_matches(row)
917 .into_iter()
918 .map(|(start, end)| Highlight {
919 range: Pos {
920 line,
921 col: start as u32,
922 }..Pos {
923 line,
924 col: end as u32,
925 },
926 kind: HighlightKind::SearchMatch,
927 })
928 .collect()
929 }
930
931 pub fn render_frame(&self) -> crate::types::RenderFrame {
941 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
942 let (cursor_row, cursor_col) = self.cursor();
943 let (mode, shape) = match self.vim_mode() {
944 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
945 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
946 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
947 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
948 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
949 };
950 RenderFrame {
951 mode,
952 cursor_row: cursor_row as u32,
953 cursor_col: cursor_col as u32,
954 cursor_shape: shape,
955 viewport_top: self.buffer.viewport().top_row as u32,
956 line_count: self.buffer.lines().len() as u32,
957 }
958 }
959
960 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
973 use crate::types::{EditorSnapshot, SnapshotMode};
974 let mode = match self.vim_mode() {
975 crate::VimMode::Normal => SnapshotMode::Normal,
976 crate::VimMode::Insert => SnapshotMode::Insert,
977 crate::VimMode::Visual => SnapshotMode::Visual,
978 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
979 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
980 };
981 let cursor = self.cursor();
982 let cursor = (cursor.0 as u32, cursor.1 as u32);
983 let lines: Vec<String> = self.buffer.lines().to_vec();
984 let viewport_top = self.buffer.viewport().top_row as u32;
985 let file_marks = self
986 .file_marks
987 .iter()
988 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
989 .collect();
990 EditorSnapshot {
991 version: EditorSnapshot::VERSION,
992 mode,
993 cursor,
994 lines,
995 viewport_top,
996 registers: self.registers.clone(),
997 file_marks,
998 }
999 }
1000
1001 pub fn restore_snapshot(
1009 &mut self,
1010 snap: crate::types::EditorSnapshot,
1011 ) -> Result<(), crate::EngineError> {
1012 use crate::types::EditorSnapshot;
1013 if snap.version != EditorSnapshot::VERSION {
1014 return Err(crate::EngineError::SnapshotVersion(
1015 snap.version,
1016 EditorSnapshot::VERSION,
1017 ));
1018 }
1019 let text = snap.lines.join("\n");
1020 self.set_content(&text);
1021 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
1022 let mut vp = self.buffer.viewport();
1023 vp.top_row = snap.viewport_top as usize;
1024 *self.buffer.viewport_mut() = vp;
1025 self.registers = snap.registers;
1026 self.file_marks = snap
1027 .file_marks
1028 .into_iter()
1029 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
1030 .collect();
1031 Ok(())
1032 }
1033
1034 pub fn seed_yank(&mut self, text: String) {
1038 let linewise = text.ends_with('\n');
1039 self.vim.yank_linewise = linewise;
1040 self.registers.unnamed = crate::registers::Slot { text, linewise };
1041 }
1042
1043 pub fn scroll_down(&mut self, rows: i16) {
1048 self.scroll_viewport(rows);
1049 }
1050
1051 pub fn scroll_up(&mut self, rows: i16) {
1055 self.scroll_viewport(-rows);
1056 }
1057
1058 const SCROLLOFF: usize = 5;
1062
1063 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
1068 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1069 if height == 0 {
1070 self.buffer.ensure_cursor_visible();
1071 return;
1072 }
1073 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1077 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
1080 self.ensure_scrolloff_wrap(height, margin);
1081 return;
1082 }
1083 let cursor_row = self.buffer.cursor().row;
1084 let last_row = self.buffer.row_count().saturating_sub(1);
1085 let v = self.buffer.viewport_mut();
1086 if cursor_row < v.top_row + margin {
1088 v.top_row = cursor_row.saturating_sub(margin);
1089 }
1090 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
1092 if cursor_row > v.top_row + max_bottom {
1093 v.top_row = cursor_row.saturating_sub(max_bottom);
1094 }
1095 let max_top = last_row.saturating_sub(height.saturating_sub(1));
1097 if v.top_row > max_top {
1098 v.top_row = max_top;
1099 }
1100 let cursor = self.buffer.cursor();
1103 self.buffer.viewport_mut().ensure_visible(cursor);
1104 }
1105
1106 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
1111 let cursor_row = self.buffer.cursor().row;
1112 if cursor_row < self.buffer.viewport().top_row {
1115 self.buffer.viewport_mut().top_row = cursor_row;
1116 self.buffer.viewport_mut().top_col = 0;
1117 }
1118 let max_csr = height.saturating_sub(1).saturating_sub(margin);
1121 loop {
1122 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1123 if csr <= max_csr {
1124 break;
1125 }
1126 let top = self.buffer.viewport().top_row;
1127 let Some(next) = self.buffer.next_visible_row(top) else {
1128 break;
1129 };
1130 if next > cursor_row {
1132 self.buffer.viewport_mut().top_row = cursor_row;
1133 break;
1134 }
1135 self.buffer.viewport_mut().top_row = next;
1136 }
1137 loop {
1140 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1141 if csr >= margin {
1142 break;
1143 }
1144 let top = self.buffer.viewport().top_row;
1145 let Some(prev) = self.buffer.prev_visible_row(top) else {
1146 break;
1147 };
1148 self.buffer.viewport_mut().top_row = prev;
1149 }
1150 let max_top = self.buffer.max_top_for_height(height);
1155 if self.buffer.viewport().top_row > max_top {
1156 self.buffer.viewport_mut().top_row = max_top;
1157 }
1158 self.buffer.viewport_mut().top_col = 0;
1159 }
1160
1161 fn scroll_viewport(&mut self, delta: i16) {
1162 if delta == 0 {
1163 return;
1164 }
1165 let total_rows = self.buffer.row_count() as isize;
1167 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1168 let cur_top = self.buffer.viewport().top_row as isize;
1169 let new_top = (cur_top + delta as isize)
1170 .max(0)
1171 .min((total_rows - 1).max(0)) as usize;
1172 self.buffer.viewport_mut().top_row = new_top;
1173 let _ = cur_top;
1176 if height == 0 {
1177 return;
1178 }
1179 let cursor = self.buffer.cursor();
1182 let margin = Self::SCROLLOFF.min(height / 2);
1183 let min_row = new_top + margin;
1184 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1185 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1186 if target_row != cursor.row {
1187 let line_len = self
1188 .buffer
1189 .line(target_row)
1190 .map(|l| l.chars().count())
1191 .unwrap_or(0);
1192 let target_col = cursor.col.min(line_len.saturating_sub(1));
1193 self.buffer
1194 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1195 }
1196 }
1197
1198 pub fn goto_line(&mut self, line: usize) {
1199 let row = line.saturating_sub(1);
1200 let max = self.buffer.row_count().saturating_sub(1);
1201 let target = row.min(max);
1202 self.buffer
1203 .set_cursor(hjkl_buffer::Position::new(target, 0));
1204 }
1205
1206 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1210 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1211 if height == 0 {
1212 return;
1213 }
1214 let cur_row = self.buffer.cursor().row;
1215 let cur_top = self.buffer.viewport().top_row;
1216 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1222 let new_top = match pos {
1223 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1224 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1225 CursorScrollTarget::Bottom => {
1226 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1227 }
1228 };
1229 if new_top == cur_top {
1230 return;
1231 }
1232 self.buffer.viewport_mut().top_row = new_top;
1233 }
1234
1235 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1242 let lines = self.buffer.lines();
1243 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1245 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1246 let rel_row = row.saturating_sub(inner_top) as usize;
1247 let top = self.buffer.viewport().top_row;
1248 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1249 let rel_col = col.saturating_sub(content_x) as usize;
1250 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1251 let last_col = line_chars.saturating_sub(1);
1252 (doc_row, rel_col.min(last_col))
1253 }
1254
1255 pub fn jump_to(&mut self, line: usize, col: usize) {
1257 let r = line.saturating_sub(1);
1258 let max_row = self.buffer.row_count().saturating_sub(1);
1259 let r = r.min(max_row);
1260 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1261 let c = col.saturating_sub(1).min(line_len);
1262 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1263 }
1264
1265 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1267 if self.vim.is_visual() {
1268 self.vim.force_normal();
1269 }
1270 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1271 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1272 }
1273
1274 pub fn mouse_begin_drag(&mut self) {
1276 if !self.vim.is_visual_char() {
1277 let cursor = self.cursor();
1278 self.vim.enter_visual(cursor);
1279 }
1280 }
1281
1282 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1284 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1285 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1286 }
1287
1288 pub fn insert_str(&mut self, text: &str) {
1289 let pos = self.buffer.cursor();
1290 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1291 at: pos,
1292 text: text.to_string(),
1293 });
1294 self.push_buffer_content_to_textarea();
1295 self.mark_content_dirty();
1296 }
1297
1298 pub fn accept_completion(&mut self, completion: &str) {
1299 use hjkl_buffer::{Edit, MotionKind, Position};
1300 let cursor = self.buffer.cursor();
1301 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1302 let chars: Vec<char> = line.chars().collect();
1303 let prefix_len = chars[..cursor.col.min(chars.len())]
1304 .iter()
1305 .rev()
1306 .take_while(|c| c.is_alphanumeric() || **c == '_')
1307 .count();
1308 if prefix_len > 0 {
1309 let start = Position::new(cursor.row, cursor.col - prefix_len);
1310 self.buffer.apply_edit(Edit::DeleteRange {
1311 start,
1312 end: cursor,
1313 kind: MotionKind::Char,
1314 });
1315 }
1316 let cursor = self.buffer.cursor();
1317 self.buffer.apply_edit(Edit::InsertStr {
1318 at: cursor,
1319 text: completion.to_string(),
1320 });
1321 self.push_buffer_content_to_textarea();
1322 self.mark_content_dirty();
1323 }
1324
1325 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1326 let pos = self.buffer.cursor();
1327 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1328 }
1329
1330 #[doc(hidden)]
1331 pub fn push_undo(&mut self) {
1332 let snap = self.snapshot();
1333 if self.undo_stack.len() >= 200 {
1334 self.undo_stack.remove(0);
1335 }
1336 self.undo_stack.push(snap);
1337 self.redo_stack.clear();
1338 }
1339
1340 #[doc(hidden)]
1341 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1342 let text = lines.join("\n");
1343 self.buffer.replace_all(&text);
1344 self.buffer
1345 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1346 self.mark_content_dirty();
1347 }
1348
1349 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1351 let input = crossterm_to_input(key);
1352 if input.key == Key::Null {
1353 return false;
1354 }
1355 vim::step(self, input)
1356 }
1357}
1358
1359pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1360 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1361 let alt = key.modifiers.contains(KeyModifiers::ALT);
1362 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1363 let k = match key.code {
1364 KeyCode::Char(c) => Key::Char(c),
1365 KeyCode::Backspace => Key::Backspace,
1366 KeyCode::Delete => Key::Delete,
1367 KeyCode::Enter => Key::Enter,
1368 KeyCode::Left => Key::Left,
1369 KeyCode::Right => Key::Right,
1370 KeyCode::Up => Key::Up,
1371 KeyCode::Down => Key::Down,
1372 KeyCode::Home => Key::Home,
1373 KeyCode::End => Key::End,
1374 KeyCode::Tab => Key::Tab,
1375 KeyCode::Esc => Key::Esc,
1376 _ => Key::Null,
1377 };
1378 Input {
1379 key: k,
1380 ctrl,
1381 alt,
1382 shift,
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 use super::*;
1389 use crossterm::event::KeyEvent;
1390
1391 fn key(code: KeyCode) -> KeyEvent {
1392 KeyEvent::new(code, KeyModifiers::NONE)
1393 }
1394 fn shift_key(code: KeyCode) -> KeyEvent {
1395 KeyEvent::new(code, KeyModifiers::SHIFT)
1396 }
1397 fn ctrl_key(code: KeyCode) -> KeyEvent {
1398 KeyEvent::new(code, KeyModifiers::CONTROL)
1399 }
1400
1401 #[test]
1402 fn vim_normal_to_insert() {
1403 let mut e = Editor::new(KeybindingMode::Vim);
1404 e.handle_key(key(KeyCode::Char('i')));
1405 assert_eq!(e.vim_mode(), VimMode::Insert);
1406 }
1407
1408 #[test]
1409 fn take_changes_drains_after_insert() {
1410 let mut e = Editor::new(KeybindingMode::Vim);
1411 e.set_content("abc");
1412 assert!(e.take_changes().is_empty());
1414 e.handle_key(key(KeyCode::Char('i')));
1416 e.handle_key(key(KeyCode::Char('X')));
1417 let changes = e.take_changes();
1418 assert!(
1419 !changes.is_empty(),
1420 "insert mode keystroke should produce a change"
1421 );
1422 assert!(e.take_changes().is_empty());
1424 }
1425
1426 #[test]
1427 fn options_bridge_roundtrip() {
1428 let mut e = Editor::new(KeybindingMode::Vim);
1429 let opts = e.current_options();
1430 assert_eq!(opts.shiftwidth, 2); assert_eq!(opts.tabstop, 8);
1432
1433 let mut new_opts = crate::types::Options::default();
1434 new_opts.shiftwidth = 4;
1435 new_opts.tabstop = 2;
1436 new_opts.ignorecase = true;
1437 e.apply_options(&new_opts);
1438
1439 let after = e.current_options();
1440 assert_eq!(after.shiftwidth, 4);
1441 assert_eq!(after.tabstop, 2);
1442 assert!(after.ignorecase);
1443 }
1444
1445 #[test]
1446 fn selection_highlight_none_in_normal() {
1447 let mut e = Editor::new(KeybindingMode::Vim);
1448 e.set_content("hello");
1449 assert!(e.selection_highlight().is_none());
1450 }
1451
1452 #[test]
1453 fn selection_highlight_some_in_visual() {
1454 use crate::types::HighlightKind;
1455 let mut e = Editor::new(KeybindingMode::Vim);
1456 e.set_content("hello world");
1457 e.handle_key(key(KeyCode::Char('v')));
1458 e.handle_key(key(KeyCode::Char('l')));
1459 e.handle_key(key(KeyCode::Char('l')));
1460 let h = e
1461 .selection_highlight()
1462 .expect("visual mode should produce a highlight");
1463 assert_eq!(h.kind, HighlightKind::Selection);
1464 assert_eq!(h.range.start.line, 0);
1465 assert_eq!(h.range.end.line, 0);
1466 }
1467
1468 #[test]
1469 fn highlights_emit_search_matches() {
1470 use crate::types::HighlightKind;
1471 let mut e = Editor::new(KeybindingMode::Vim);
1472 e.set_content("foo bar foo\nbaz qux\n");
1473 e.buffer_mut()
1475 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1476 let hs = e.highlights_for_line(0);
1477 assert_eq!(hs.len(), 2);
1478 for h in &hs {
1479 assert_eq!(h.kind, HighlightKind::SearchMatch);
1480 assert_eq!(h.range.start.line, 0);
1481 assert_eq!(h.range.end.line, 0);
1482 }
1483 }
1484
1485 #[test]
1486 fn highlights_empty_without_pattern() {
1487 let mut e = Editor::new(KeybindingMode::Vim);
1488 e.set_content("foo bar");
1489 assert!(e.highlights_for_line(0).is_empty());
1490 }
1491
1492 #[test]
1493 fn highlights_empty_for_out_of_range_line() {
1494 let mut e = Editor::new(KeybindingMode::Vim);
1495 e.set_content("foo");
1496 e.buffer_mut()
1497 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1498 assert!(e.highlights_for_line(99).is_empty());
1499 }
1500
1501 #[test]
1502 fn render_frame_reflects_mode_and_cursor() {
1503 use crate::types::{CursorShape, SnapshotMode};
1504 let mut e = Editor::new(KeybindingMode::Vim);
1505 e.set_content("alpha\nbeta");
1506 let f = e.render_frame();
1507 assert_eq!(f.mode, SnapshotMode::Normal);
1508 assert_eq!(f.cursor_shape, CursorShape::Block);
1509 assert_eq!(f.line_count, 2);
1510
1511 e.handle_key(key(KeyCode::Char('i')));
1512 let f = e.render_frame();
1513 assert_eq!(f.mode, SnapshotMode::Insert);
1514 assert_eq!(f.cursor_shape, CursorShape::Bar);
1515 }
1516
1517 #[test]
1518 fn snapshot_roundtrips_through_restore() {
1519 use crate::types::SnapshotMode;
1520 let mut e = Editor::new(KeybindingMode::Vim);
1521 e.set_content("alpha\nbeta\ngamma");
1522 e.jump_cursor(2, 3);
1523 let snap = e.take_snapshot();
1524 assert_eq!(snap.mode, SnapshotMode::Normal);
1525 assert_eq!(snap.cursor, (2, 3));
1526 assert_eq!(snap.lines.len(), 3);
1527
1528 let mut other = Editor::new(KeybindingMode::Vim);
1529 other.restore_snapshot(snap).expect("restore");
1530 assert_eq!(other.cursor(), (2, 3));
1531 assert_eq!(other.buffer().lines().len(), 3);
1532 }
1533
1534 #[test]
1535 fn restore_snapshot_rejects_version_mismatch() {
1536 let mut e = Editor::new(KeybindingMode::Vim);
1537 let mut snap = e.take_snapshot();
1538 snap.version = 9999;
1539 match e.restore_snapshot(snap) {
1540 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1541 assert_eq!(got, 9999);
1542 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1543 }
1544 other => panic!("expected SnapshotVersion err, got {other:?}"),
1545 }
1546 }
1547
1548 #[test]
1549 fn take_content_change_returns_some_on_first_dirty() {
1550 let mut e = Editor::new(KeybindingMode::Vim);
1551 e.set_content("hello");
1552 let first = e.take_content_change();
1553 assert!(first.is_some());
1554 let second = e.take_content_change();
1555 assert!(second.is_none());
1556 }
1557
1558 #[test]
1559 fn take_content_change_none_until_mutation() {
1560 let mut e = Editor::new(KeybindingMode::Vim);
1561 e.set_content("hello");
1562 e.take_content_change();
1564 assert!(e.take_content_change().is_none());
1565 e.handle_key(key(KeyCode::Char('i')));
1567 e.handle_key(key(KeyCode::Char('x')));
1568 let after = e.take_content_change();
1569 assert!(after.is_some());
1570 assert!(after.unwrap().contains('x'));
1571 }
1572
1573 #[test]
1574 fn vim_insert_to_normal() {
1575 let mut e = Editor::new(KeybindingMode::Vim);
1576 e.handle_key(key(KeyCode::Char('i')));
1577 e.handle_key(key(KeyCode::Esc));
1578 assert_eq!(e.vim_mode(), VimMode::Normal);
1579 }
1580
1581 #[test]
1582 fn vim_normal_to_visual() {
1583 let mut e = Editor::new(KeybindingMode::Vim);
1584 e.handle_key(key(KeyCode::Char('v')));
1585 assert_eq!(e.vim_mode(), VimMode::Visual);
1586 }
1587
1588 #[test]
1589 fn vim_visual_to_normal() {
1590 let mut e = Editor::new(KeybindingMode::Vim);
1591 e.handle_key(key(KeyCode::Char('v')));
1592 e.handle_key(key(KeyCode::Esc));
1593 assert_eq!(e.vim_mode(), VimMode::Normal);
1594 }
1595
1596 #[test]
1597 fn vim_shift_i_moves_to_first_non_whitespace() {
1598 let mut e = Editor::new(KeybindingMode::Vim);
1599 e.set_content(" hello");
1600 e.jump_cursor(0, 8);
1601 e.handle_key(shift_key(KeyCode::Char('I')));
1602 assert_eq!(e.vim_mode(), VimMode::Insert);
1603 assert_eq!(e.cursor(), (0, 3));
1604 }
1605
1606 #[test]
1607 fn vim_shift_a_moves_to_end_and_insert() {
1608 let mut e = Editor::new(KeybindingMode::Vim);
1609 e.set_content("hello");
1610 e.handle_key(shift_key(KeyCode::Char('A')));
1611 assert_eq!(e.vim_mode(), VimMode::Insert);
1612 assert_eq!(e.cursor().1, 5);
1613 }
1614
1615 #[test]
1616 fn count_10j_moves_down_10() {
1617 let mut e = Editor::new(KeybindingMode::Vim);
1618 e.set_content(
1619 (0..20)
1620 .map(|i| format!("line{i}"))
1621 .collect::<Vec<_>>()
1622 .join("\n")
1623 .as_str(),
1624 );
1625 for d in "10".chars() {
1626 e.handle_key(key(KeyCode::Char(d)));
1627 }
1628 e.handle_key(key(KeyCode::Char('j')));
1629 assert_eq!(e.cursor().0, 10);
1630 }
1631
1632 #[test]
1633 fn count_o_repeats_insert_on_esc() {
1634 let mut e = Editor::new(KeybindingMode::Vim);
1635 e.set_content("hello");
1636 for d in "3".chars() {
1637 e.handle_key(key(KeyCode::Char(d)));
1638 }
1639 e.handle_key(key(KeyCode::Char('o')));
1640 assert_eq!(e.vim_mode(), VimMode::Insert);
1641 for c in "world".chars() {
1642 e.handle_key(key(KeyCode::Char(c)));
1643 }
1644 e.handle_key(key(KeyCode::Esc));
1645 assert_eq!(e.vim_mode(), VimMode::Normal);
1646 assert_eq!(e.buffer().lines().len(), 4);
1647 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1648 }
1649
1650 #[test]
1651 fn count_i_repeats_text_on_esc() {
1652 let mut e = Editor::new(KeybindingMode::Vim);
1653 e.set_content("");
1654 for d in "3".chars() {
1655 e.handle_key(key(KeyCode::Char(d)));
1656 }
1657 e.handle_key(key(KeyCode::Char('i')));
1658 for c in "ab".chars() {
1659 e.handle_key(key(KeyCode::Char(c)));
1660 }
1661 e.handle_key(key(KeyCode::Esc));
1662 assert_eq!(e.vim_mode(), VimMode::Normal);
1663 assert_eq!(e.buffer().lines()[0], "ababab");
1664 }
1665
1666 #[test]
1667 fn vim_shift_o_opens_line_above() {
1668 let mut e = Editor::new(KeybindingMode::Vim);
1669 e.set_content("hello");
1670 e.handle_key(shift_key(KeyCode::Char('O')));
1671 assert_eq!(e.vim_mode(), VimMode::Insert);
1672 assert_eq!(e.cursor(), (0, 0));
1673 assert_eq!(e.buffer().lines().len(), 2);
1674 }
1675
1676 #[test]
1677 fn vim_gg_goes_to_top() {
1678 let mut e = Editor::new(KeybindingMode::Vim);
1679 e.set_content("a\nb\nc");
1680 e.jump_cursor(2, 0);
1681 e.handle_key(key(KeyCode::Char('g')));
1682 e.handle_key(key(KeyCode::Char('g')));
1683 assert_eq!(e.cursor().0, 0);
1684 }
1685
1686 #[test]
1687 fn vim_shift_g_goes_to_bottom() {
1688 let mut e = Editor::new(KeybindingMode::Vim);
1689 e.set_content("a\nb\nc");
1690 e.handle_key(shift_key(KeyCode::Char('G')));
1691 assert_eq!(e.cursor().0, 2);
1692 }
1693
1694 #[test]
1695 fn vim_dd_deletes_line() {
1696 let mut e = Editor::new(KeybindingMode::Vim);
1697 e.set_content("first\nsecond");
1698 e.handle_key(key(KeyCode::Char('d')));
1699 e.handle_key(key(KeyCode::Char('d')));
1700 assert_eq!(e.buffer().lines().len(), 1);
1701 assert_eq!(e.buffer().lines()[0], "second");
1702 }
1703
1704 #[test]
1705 fn vim_dw_deletes_word() {
1706 let mut e = Editor::new(KeybindingMode::Vim);
1707 e.set_content("hello world");
1708 e.handle_key(key(KeyCode::Char('d')));
1709 e.handle_key(key(KeyCode::Char('w')));
1710 assert_eq!(e.vim_mode(), VimMode::Normal);
1711 assert!(!e.buffer().lines()[0].starts_with("hello"));
1712 }
1713
1714 #[test]
1715 fn vim_yy_yanks_line() {
1716 let mut e = Editor::new(KeybindingMode::Vim);
1717 e.set_content("hello\nworld");
1718 e.handle_key(key(KeyCode::Char('y')));
1719 e.handle_key(key(KeyCode::Char('y')));
1720 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1721 }
1722
1723 #[test]
1724 fn vim_yy_does_not_move_cursor() {
1725 let mut e = Editor::new(KeybindingMode::Vim);
1726 e.set_content("first\nsecond\nthird");
1727 e.jump_cursor(1, 0);
1728 let before = e.cursor();
1729 e.handle_key(key(KeyCode::Char('y')));
1730 e.handle_key(key(KeyCode::Char('y')));
1731 assert_eq!(e.cursor(), before);
1732 assert_eq!(e.vim_mode(), VimMode::Normal);
1733 }
1734
1735 #[test]
1736 fn vim_yw_yanks_word() {
1737 let mut e = Editor::new(KeybindingMode::Vim);
1738 e.set_content("hello world");
1739 e.handle_key(key(KeyCode::Char('y')));
1740 e.handle_key(key(KeyCode::Char('w')));
1741 assert_eq!(e.vim_mode(), VimMode::Normal);
1742 assert!(e.last_yank.is_some());
1743 }
1744
1745 #[test]
1746 fn vim_cc_changes_line() {
1747 let mut e = Editor::new(KeybindingMode::Vim);
1748 e.set_content("hello\nworld");
1749 e.handle_key(key(KeyCode::Char('c')));
1750 e.handle_key(key(KeyCode::Char('c')));
1751 assert_eq!(e.vim_mode(), VimMode::Insert);
1752 }
1753
1754 #[test]
1755 fn vim_u_undoes_insert_session_as_chunk() {
1756 let mut e = Editor::new(KeybindingMode::Vim);
1757 e.set_content("hello");
1758 e.handle_key(key(KeyCode::Char('i')));
1759 e.handle_key(key(KeyCode::Enter));
1760 e.handle_key(key(KeyCode::Enter));
1761 e.handle_key(key(KeyCode::Esc));
1762 assert_eq!(e.buffer().lines().len(), 3);
1763 e.handle_key(key(KeyCode::Char('u')));
1764 assert_eq!(e.buffer().lines().len(), 1);
1765 assert_eq!(e.buffer().lines()[0], "hello");
1766 }
1767
1768 #[test]
1769 fn vim_undo_redo_roundtrip() {
1770 let mut e = Editor::new(KeybindingMode::Vim);
1771 e.set_content("hello");
1772 e.handle_key(key(KeyCode::Char('i')));
1773 for c in "world".chars() {
1774 e.handle_key(key(KeyCode::Char(c)));
1775 }
1776 e.handle_key(key(KeyCode::Esc));
1777 let after = e.buffer().lines()[0].clone();
1778 e.handle_key(key(KeyCode::Char('u')));
1779 assert_eq!(e.buffer().lines()[0], "hello");
1780 e.handle_key(ctrl_key(KeyCode::Char('r')));
1781 assert_eq!(e.buffer().lines()[0], after);
1782 }
1783
1784 #[test]
1785 fn vim_u_undoes_dd() {
1786 let mut e = Editor::new(KeybindingMode::Vim);
1787 e.set_content("first\nsecond");
1788 e.handle_key(key(KeyCode::Char('d')));
1789 e.handle_key(key(KeyCode::Char('d')));
1790 assert_eq!(e.buffer().lines().len(), 1);
1791 e.handle_key(key(KeyCode::Char('u')));
1792 assert_eq!(e.buffer().lines().len(), 2);
1793 assert_eq!(e.buffer().lines()[0], "first");
1794 }
1795
1796 #[test]
1797 fn vim_ctrl_r_redoes() {
1798 let mut e = Editor::new(KeybindingMode::Vim);
1799 e.set_content("hello");
1800 e.handle_key(ctrl_key(KeyCode::Char('r')));
1801 }
1802
1803 #[test]
1804 fn vim_r_replaces_char() {
1805 let mut e = Editor::new(KeybindingMode::Vim);
1806 e.set_content("hello");
1807 e.handle_key(key(KeyCode::Char('r')));
1808 e.handle_key(key(KeyCode::Char('x')));
1809 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1810 }
1811
1812 #[test]
1813 fn vim_tilde_toggles_case() {
1814 let mut e = Editor::new(KeybindingMode::Vim);
1815 e.set_content("hello");
1816 e.handle_key(key(KeyCode::Char('~')));
1817 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1818 }
1819
1820 #[test]
1821 fn vim_visual_d_cuts() {
1822 let mut e = Editor::new(KeybindingMode::Vim);
1823 e.set_content("hello");
1824 e.handle_key(key(KeyCode::Char('v')));
1825 e.handle_key(key(KeyCode::Char('l')));
1826 e.handle_key(key(KeyCode::Char('l')));
1827 e.handle_key(key(KeyCode::Char('d')));
1828 assert_eq!(e.vim_mode(), VimMode::Normal);
1829 assert!(e.last_yank.is_some());
1830 }
1831
1832 #[test]
1833 fn vim_visual_c_enters_insert() {
1834 let mut e = Editor::new(KeybindingMode::Vim);
1835 e.set_content("hello");
1836 e.handle_key(key(KeyCode::Char('v')));
1837 e.handle_key(key(KeyCode::Char('l')));
1838 e.handle_key(key(KeyCode::Char('c')));
1839 assert_eq!(e.vim_mode(), VimMode::Insert);
1840 }
1841
1842 #[test]
1843 fn vim_normal_unknown_key_consumed() {
1844 let mut e = Editor::new(KeybindingMode::Vim);
1845 let consumed = e.handle_key(key(KeyCode::Char('z')));
1847 assert!(consumed);
1848 }
1849
1850 #[test]
1851 fn force_normal_clears_operator() {
1852 let mut e = Editor::new(KeybindingMode::Vim);
1853 e.handle_key(key(KeyCode::Char('d')));
1854 e.force_normal();
1855 assert_eq!(e.vim_mode(), VimMode::Normal);
1856 }
1857
1858 fn many_lines(n: usize) -> String {
1859 (0..n)
1860 .map(|i| format!("line{i}"))
1861 .collect::<Vec<_>>()
1862 .join("\n")
1863 }
1864
1865 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1866 e.set_viewport_height(height);
1867 }
1868
1869 #[test]
1870 fn zz_centers_cursor_in_viewport() {
1871 let mut e = Editor::new(KeybindingMode::Vim);
1872 e.set_content(&many_lines(100));
1873 prime_viewport(&mut e, 20);
1874 e.jump_cursor(50, 0);
1875 e.handle_key(key(KeyCode::Char('z')));
1876 e.handle_key(key(KeyCode::Char('z')));
1877 assert_eq!(e.buffer().viewport().top_row, 40);
1878 assert_eq!(e.cursor().0, 50);
1879 }
1880
1881 #[test]
1882 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1883 let mut e = Editor::new(KeybindingMode::Vim);
1884 e.set_content(&many_lines(100));
1885 prime_viewport(&mut e, 20);
1886 e.jump_cursor(50, 0);
1887 e.handle_key(key(KeyCode::Char('z')));
1888 e.handle_key(key(KeyCode::Char('t')));
1889 assert_eq!(e.buffer().viewport().top_row, 45);
1892 assert_eq!(e.cursor().0, 50);
1893 }
1894
1895 #[test]
1896 fn ctrl_a_increments_number_at_cursor() {
1897 let mut e = Editor::new(KeybindingMode::Vim);
1898 e.set_content("x = 41");
1899 e.handle_key(ctrl_key(KeyCode::Char('a')));
1900 assert_eq!(e.buffer().lines()[0], "x = 42");
1901 assert_eq!(e.cursor(), (0, 5));
1902 }
1903
1904 #[test]
1905 fn ctrl_a_finds_number_to_right_of_cursor() {
1906 let mut e = Editor::new(KeybindingMode::Vim);
1907 e.set_content("foo 99 bar");
1908 e.handle_key(ctrl_key(KeyCode::Char('a')));
1909 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1910 assert_eq!(e.cursor(), (0, 6));
1911 }
1912
1913 #[test]
1914 fn ctrl_a_with_count_adds_count() {
1915 let mut e = Editor::new(KeybindingMode::Vim);
1916 e.set_content("x = 10");
1917 for d in "5".chars() {
1918 e.handle_key(key(KeyCode::Char(d)));
1919 }
1920 e.handle_key(ctrl_key(KeyCode::Char('a')));
1921 assert_eq!(e.buffer().lines()[0], "x = 15");
1922 }
1923
1924 #[test]
1925 fn ctrl_x_decrements_number() {
1926 let mut e = Editor::new(KeybindingMode::Vim);
1927 e.set_content("n=5");
1928 e.handle_key(ctrl_key(KeyCode::Char('x')));
1929 assert_eq!(e.buffer().lines()[0], "n=4");
1930 }
1931
1932 #[test]
1933 fn ctrl_x_crosses_zero_into_negative() {
1934 let mut e = Editor::new(KeybindingMode::Vim);
1935 e.set_content("v=0");
1936 e.handle_key(ctrl_key(KeyCode::Char('x')));
1937 assert_eq!(e.buffer().lines()[0], "v=-1");
1938 }
1939
1940 #[test]
1941 fn ctrl_a_on_negative_number_increments_toward_zero() {
1942 let mut e = Editor::new(KeybindingMode::Vim);
1943 e.set_content("a = -5");
1944 e.handle_key(ctrl_key(KeyCode::Char('a')));
1945 assert_eq!(e.buffer().lines()[0], "a = -4");
1946 }
1947
1948 #[test]
1949 fn ctrl_a_noop_when_no_digit_on_line() {
1950 let mut e = Editor::new(KeybindingMode::Vim);
1951 e.set_content("no digits here");
1952 e.handle_key(ctrl_key(KeyCode::Char('a')));
1953 assert_eq!(e.buffer().lines()[0], "no digits here");
1954 }
1955
1956 #[test]
1957 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1958 let mut e = Editor::new(KeybindingMode::Vim);
1959 e.set_content(&many_lines(100));
1960 prime_viewport(&mut e, 20);
1961 e.jump_cursor(50, 0);
1962 e.handle_key(key(KeyCode::Char('z')));
1963 e.handle_key(key(KeyCode::Char('b')));
1964 assert_eq!(e.buffer().viewport().top_row, 36);
1968 assert_eq!(e.cursor().0, 50);
1969 }
1970
1971 #[test]
1978 fn set_content_dirties_then_take_dirty_clears() {
1979 let mut e = Editor::new(KeybindingMode::Vim);
1980 e.set_content("hello");
1981 assert!(
1982 e.take_dirty(),
1983 "set_content should leave content_dirty=true"
1984 );
1985 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1986 }
1987
1988 #[test]
1989 fn content_arc_returns_same_arc_until_mutation() {
1990 let mut e = Editor::new(KeybindingMode::Vim);
1991 e.set_content("hello");
1992 let a = e.content_arc();
1993 let b = e.content_arc();
1994 assert!(
1995 std::sync::Arc::ptr_eq(&a, &b),
1996 "repeated content_arc() should hit the cache"
1997 );
1998
1999 e.handle_key(key(KeyCode::Char('i')));
2001 e.handle_key(key(KeyCode::Char('!')));
2002 let c = e.content_arc();
2003 assert!(
2004 !std::sync::Arc::ptr_eq(&a, &c),
2005 "mutation should invalidate content_arc() cache"
2006 );
2007 assert!(c.contains('!'));
2008 }
2009
2010 #[test]
2011 fn content_arc_cache_invalidated_by_set_content() {
2012 let mut e = Editor::new(KeybindingMode::Vim);
2013 e.set_content("one");
2014 let a = e.content_arc();
2015 e.set_content("two");
2016 let b = e.content_arc();
2017 assert!(!std::sync::Arc::ptr_eq(&a, &b));
2018 assert!(b.starts_with("two"));
2019 }
2020
2021 #[test]
2027 fn mouse_click_past_eol_lands_on_last_char() {
2028 let mut e = Editor::new(KeybindingMode::Vim);
2029 e.set_content("hello");
2030 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2034 e.mouse_click(area, 78, 1);
2035 assert_eq!(e.cursor(), (0, 4));
2036 }
2037
2038 #[test]
2039 fn mouse_click_past_eol_handles_multibyte_line() {
2040 let mut e = Editor::new(KeybindingMode::Vim);
2041 e.set_content("héllo");
2044 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2045 e.mouse_click(area, 78, 1);
2046 assert_eq!(e.cursor(), (0, 4));
2047 }
2048
2049 #[test]
2050 fn mouse_click_inside_line_lands_on_clicked_char() {
2051 let mut e = Editor::new(KeybindingMode::Vim);
2052 e.set_content("hello world");
2053 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
2056 e.mouse_click(area, 4, 1);
2057 assert_eq!(e.cursor(), (0, 0));
2058 e.mouse_click(area, 6, 1);
2059 assert_eq!(e.cursor(), (0, 2));
2060 }
2061}