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
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(super) enum CursorScrollTarget {
20 Center,
21 Top,
22 Bottom,
23}
24
25pub struct Editor<'a> {
26 pub keybinding_mode: KeybindingMode,
27 _marker: std::marker::PhantomData<&'a ()>,
32 pub last_yank: Option<String>,
34 #[doc(hidden)]
36 pub vim: VimState,
37 #[doc(hidden)]
39 pub undo_stack: Vec<(Vec<String>, (usize, usize))>,
40 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
42 pub(super) content_dirty: bool,
44 pub(super) cached_content: Option<std::sync::Arc<String>>,
49 pub(super) viewport_height: AtomicU16,
54 pub(super) pending_lsp: Option<LspIntent>,
58 pub(super) buffer: hjkl_buffer::Buffer,
63 pub(super) style_table: Vec<ratatui::style::Style>,
70 #[doc(hidden)]
73 pub registers: crate::registers::Registers,
74 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
80 #[doc(hidden)]
84 pub settings: Settings,
85 #[doc(hidden)]
91 pub file_marks: std::collections::HashMap<char, (usize, usize)>,
92 #[doc(hidden)]
97 pub syntax_fold_ranges: Vec<(usize, usize)>,
98}
99
100#[derive(Debug, Clone)]
103pub struct Settings {
104 pub shiftwidth: usize,
106 pub tabstop: usize,
109 pub ignore_case: bool,
112 pub textwidth: usize,
114 pub wrap: hjkl_buffer::Wrap,
120}
121
122impl Default for Settings {
123 fn default() -> Self {
124 Self {
125 shiftwidth: 2,
126 tabstop: 8,
127 ignore_case: false,
128 textwidth: 79,
129 wrap: hjkl_buffer::Wrap::None,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum LspIntent {
139 GotoDefinition,
141}
142
143impl<'a> Editor<'a> {
144 pub fn new(keybinding_mode: KeybindingMode) -> Self {
145 Self {
146 _marker: std::marker::PhantomData,
147 keybinding_mode,
148 last_yank: None,
149 vim: VimState::default(),
150 undo_stack: Vec::new(),
151 redo_stack: Vec::new(),
152 content_dirty: false,
153 cached_content: None,
154 viewport_height: AtomicU16::new(0),
155 pending_lsp: None,
156 buffer: hjkl_buffer::Buffer::new(),
157 style_table: Vec::new(),
158 registers: crate::registers::Registers::default(),
159 styled_spans: Vec::new(),
160 settings: Settings::default(),
161 file_marks: std::collections::HashMap::new(),
162 syntax_fold_ranges: Vec::new(),
163 }
164 }
165
166 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
170 self.syntax_fold_ranges = ranges;
171 }
172
173 pub fn settings(&self) -> &Settings {
176 &self.settings
177 }
178
179 #[doc(hidden)]
180 pub fn settings_mut(&mut self) -> &mut Settings {
181 &mut self.settings
182 }
183
184 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>) {
191 let line_byte_lens: Vec<usize> = self.buffer.lines().iter().map(|l| l.len()).collect();
192 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
193 for (row, row_spans) in spans.iter().enumerate() {
194 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
195 let mut translated = Vec::with_capacity(row_spans.len());
196 for (start, end, style) in row_spans {
197 let end_clamped = (*end).min(line_len);
198 if end_clamped <= *start {
199 continue;
200 }
201 let id = self.intern_style(*style);
202 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
203 }
204 by_row.push(translated);
205 }
206 self.buffer.set_spans(by_row);
207 self.styled_spans = spans;
208 }
209
210 pub fn yank(&self) -> &str {
212 &self.registers.unnamed.text
213 }
214
215 pub fn registers(&self) -> &crate::registers::Registers {
217 &self.registers
218 }
219
220 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
225 self.registers.set_clipboard(text, linewise);
226 }
227
228 pub fn pending_register_is_clipboard(&self) -> bool {
232 matches!(self.vim.pending_register, Some('+') | Some('*'))
233 }
234
235 pub fn set_yank(&mut self, text: impl Into<String>) {
239 let text = text.into();
240 let linewise = self.vim.yank_linewise;
241 self.registers.unnamed = crate::registers::Slot { text, linewise };
242 }
243
244 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
248 self.vim.yank_linewise = linewise;
249 let target = self.vim.pending_register.take();
250 self.registers.record_yank(text, linewise, target);
251 }
252
253 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
258 if let Some(slot) = match reg {
259 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
260 'A'..='Z' => {
261 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
262 }
263 _ => None,
264 } {
265 slot.text = text;
266 slot.linewise = false;
267 }
268 }
269
270 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
273 self.vim.yank_linewise = linewise;
274 let target = self.vim.pending_register.take();
275 self.registers.record_delete(text, linewise, target);
276 }
277
278 pub fn intern_style(&mut self, style: ratatui::style::Style) -> u32 {
284 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
285 return idx as u32;
286 }
287 self.style_table.push(style);
288 (self.style_table.len() - 1) as u32
289 }
290
291 pub fn style_table(&self) -> &[ratatui::style::Style] {
295 &self.style_table
296 }
297
298 pub fn buffer(&self) -> &hjkl_buffer::Buffer {
301 &self.buffer
302 }
303
304 pub fn buffer_mut(&mut self) -> &mut hjkl_buffer::Buffer {
305 &mut self.buffer
306 }
307
308 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
312
313 pub fn set_viewport_top(&mut self, row: usize) {
321 let last = self.buffer.row_count().saturating_sub(1);
322 let target = row.min(last);
323 self.buffer.viewport_mut().top_row = target;
324 }
325
326 #[doc(hidden)]
331 pub fn jump_cursor(&mut self, row: usize, col: usize) {
332 self.buffer.set_cursor(hjkl_buffer::Position::new(row, col));
333 }
334
335 pub fn cursor(&self) -> (usize, usize) {
343 let pos = self.buffer.cursor();
344 (pos.row, pos.col)
345 }
346
347 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
350 self.pending_lsp.take()
351 }
352
353 pub(crate) fn sync_buffer_from_textarea(&mut self) {
357 self.buffer.set_sticky_col(self.vim.sticky_col);
358 let height = self.viewport_height_value();
359 self.buffer.viewport_mut().height = height;
360 }
361
362 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
366 self.sync_buffer_from_textarea();
367 }
368
369 pub fn record_jump(&mut self, pos: (usize, usize)) {
374 const JUMPLIST_MAX: usize = 100;
375 self.vim.jump_back.push(pos);
376 if self.vim.jump_back.len() > JUMPLIST_MAX {
377 self.vim.jump_back.remove(0);
378 }
379 self.vim.jump_fwd.clear();
380 }
381
382 pub fn set_viewport_height(&self, height: u16) {
385 self.viewport_height.store(height, Ordering::Relaxed);
386 }
387
388 pub fn viewport_height_value(&self) -> u16 {
390 self.viewport_height.load(Ordering::Relaxed)
391 }
392
393 #[doc(hidden)]
399 pub fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
400 let pre_row = self.buffer.cursor().row;
401 let pre_rows = self.buffer.row_count();
402 let inverse = self.buffer.apply_edit(edit);
403 let pos = self.buffer.cursor();
404 let lo = pre_row.min(pos.row);
410 let hi = pre_row.max(pos.row);
411 self.buffer.invalidate_folds_in_range(lo, hi);
412 self.vim.last_edit_pos = Some((pos.row, pos.col));
413 let entry = (pos.row, pos.col);
418 if self.vim.change_list.last() != Some(&entry) {
419 if let Some(idx) = self.vim.change_list_cursor.take() {
420 self.vim.change_list.truncate(idx + 1);
421 }
422 self.vim.change_list.push(entry);
423 let len = self.vim.change_list.len();
424 if len > crate::vim::CHANGE_LIST_MAX {
425 self.vim
426 .change_list
427 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
428 }
429 }
430 self.vim.change_list_cursor = None;
431 let post_rows = self.buffer.row_count();
435 let delta = post_rows as isize - pre_rows as isize;
436 if delta != 0 {
437 self.shift_marks_after_edit(pre_row, delta);
438 }
439 self.push_buffer_content_to_textarea();
440 self.mark_content_dirty();
441 inverse
442 }
443
444 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
449 if delta == 0 {
450 return;
451 }
452 let drop_end = if delta < 0 {
455 edit_start.saturating_add((-delta) as usize)
456 } else {
457 edit_start
458 };
459 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
460
461 let mut to_drop: Vec<char> = Vec::new();
462 for (c, (row, _col)) in self.vim.marks.iter_mut() {
463 if (edit_start..drop_end).contains(row) {
464 to_drop.push(*c);
465 } else if *row >= shift_threshold {
466 *row = ((*row as isize) + delta).max(0) as usize;
467 }
468 }
469 for c in to_drop {
470 self.vim.marks.remove(&c);
471 }
472
473 let mut to_drop: Vec<char> = Vec::new();
475 for (c, (row, _col)) in self.file_marks.iter_mut() {
476 if (edit_start..drop_end).contains(row) {
477 to_drop.push(*c);
478 } else if *row >= shift_threshold {
479 *row = ((*row as isize) + delta).max(0) as usize;
480 }
481 }
482 for c in to_drop {
483 self.file_marks.remove(&c);
484 }
485
486 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
487 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
488 for (row, _) in entries.iter_mut() {
489 if *row >= shift_threshold {
490 *row = ((*row as isize) + delta).max(0) as usize;
491 }
492 }
493 };
494 shift_jumps(&mut self.vim.jump_back);
495 shift_jumps(&mut self.vim.jump_fwd);
496 }
497
498 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
506
507 pub fn mark_content_dirty(&mut self) {
513 self.content_dirty = true;
514 self.cached_content = None;
515 }
516
517 pub fn take_dirty(&mut self) -> bool {
519 let dirty = self.content_dirty;
520 self.content_dirty = false;
521 dirty
522 }
523
524 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
534 if !self.content_dirty {
535 return None;
536 }
537 let arc = self.content_arc();
538 self.content_dirty = false;
539 Some(arc)
540 }
541
542 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
545 let cursor = self.buffer.cursor().row;
546 let top = self.buffer.viewport().top_row;
547 cursor.saturating_sub(top).min(height as usize - 1) as u16
548 }
549
550 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
554 let pos = self.buffer.cursor();
555 let v = self.buffer.viewport();
556 if pos.row < v.top_row || pos.col < v.top_col {
557 return None;
558 }
559 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
560 let dy = (pos.row - v.top_row) as u16;
561 let dx = (pos.col - v.top_col) as u16;
562 if dy >= area.height || dx + lnum_width >= area.width {
563 return None;
564 }
565 Some((area.x + lnum_width + dx, area.y + dy))
566 }
567
568 pub fn vim_mode(&self) -> VimMode {
569 self.vim.public_mode()
570 }
571
572 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
578 self.vim.search_prompt.as_ref()
579 }
580
581 pub fn last_search(&self) -> Option<&str> {
584 self.vim.last_search.as_deref()
585 }
586
587 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
591 if self.vim_mode() != VimMode::Visual {
592 return None;
593 }
594 let anchor = self.vim.visual_anchor;
595 let cursor = self.cursor();
596 let (start, end) = if anchor <= cursor {
597 (anchor, cursor)
598 } else {
599 (cursor, anchor)
600 };
601 Some((start, end))
602 }
603
604 pub fn line_highlight(&self) -> Option<(usize, usize)> {
607 if self.vim_mode() != VimMode::VisualLine {
608 return None;
609 }
610 let anchor = self.vim.visual_line_anchor;
611 let cursor = self.buffer.cursor().row;
612 Some((anchor.min(cursor), anchor.max(cursor)))
613 }
614
615 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
616 if self.vim_mode() != VimMode::VisualBlock {
617 return None;
618 }
619 let (ar, ac) = self.vim.block_anchor;
620 let cr = self.buffer.cursor().row;
621 let cc = self.vim.block_vcol;
622 let top = ar.min(cr);
623 let bot = ar.max(cr);
624 let left = ac.min(cc);
625 let right = ac.max(cc);
626 Some((top, bot, left, right))
627 }
628
629 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
635 use hjkl_buffer::{Position, Selection};
636 match self.vim_mode() {
637 VimMode::Visual => {
638 let (ar, ac) = self.vim.visual_anchor;
639 let head = self.buffer.cursor();
640 Some(Selection::Char {
641 anchor: Position::new(ar, ac),
642 head,
643 })
644 }
645 VimMode::VisualLine => {
646 let anchor_row = self.vim.visual_line_anchor;
647 let head_row = self.buffer.cursor().row;
648 Some(Selection::Line {
649 anchor_row,
650 head_row,
651 })
652 }
653 VimMode::VisualBlock => {
654 let (ar, ac) = self.vim.block_anchor;
655 let cr = self.buffer.cursor().row;
656 let cc = self.vim.block_vcol;
657 Some(Selection::Block {
658 anchor: Position::new(ar, ac),
659 head: Position::new(cr, cc),
660 })
661 }
662 _ => None,
663 }
664 }
665
666 pub fn force_normal(&mut self) {
668 self.vim.force_normal();
669 }
670
671 pub fn content(&self) -> String {
672 let mut s = self.buffer.lines().join("\n");
673 s.push('\n');
674 s
675 }
676
677 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
682 if let Some(arc) = &self.cached_content {
683 return std::sync::Arc::clone(arc);
684 }
685 let arc = std::sync::Arc::new(self.content());
686 self.cached_content = Some(std::sync::Arc::clone(&arc));
687 arc
688 }
689
690 pub fn set_content(&mut self, text: &str) {
691 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
692 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
693 lines.pop();
694 }
695 if lines.is_empty() {
696 lines.push(String::new());
697 }
698 let _ = lines;
699 self.buffer = hjkl_buffer::Buffer::from_str(text);
700 self.undo_stack.clear();
701 self.redo_stack.clear();
702 self.mark_content_dirty();
703 }
704
705 pub fn selection_highlight(&self) -> Option<crate::types::Highlight> {
715 use crate::types::{Highlight, HighlightKind, Pos};
716 let sel = self.buffer_selection()?;
717 let (start, end) = match sel {
718 hjkl_buffer::Selection::Char { anchor, head } => {
719 let a = (anchor.row, anchor.col);
720 let h = (head.row, head.col);
721 if a <= h { (a, h) } else { (h, a) }
722 }
723 hjkl_buffer::Selection::Line {
724 anchor_row,
725 head_row,
726 } => {
727 let (top, bot) = if anchor_row <= head_row {
728 (anchor_row, head_row)
729 } else {
730 (head_row, anchor_row)
731 };
732 let last_col = self.buffer.line(bot).map(|l| l.len()).unwrap_or(0);
733 ((top, 0), (bot, last_col))
734 }
735 hjkl_buffer::Selection::Block { anchor, head } => {
736 let (top, bot) = if anchor.row <= head.row {
737 (anchor.row, head.row)
738 } else {
739 (head.row, anchor.row)
740 };
741 let (left, right) = if anchor.col <= head.col {
742 (anchor.col, head.col)
743 } else {
744 (head.col, anchor.col)
745 };
746 ((top, left), (bot, right))
747 }
748 };
749 Some(Highlight {
750 range: Pos {
751 line: start.0 as u32,
752 col: start.1 as u32,
753 }..Pos {
754 line: end.0 as u32,
755 col: end.1 as u32,
756 },
757 kind: HighlightKind::Selection,
758 })
759 }
760
761 pub fn highlights_for_line(&mut self, line: u32) -> Vec<crate::types::Highlight> {
774 use crate::types::{Highlight, HighlightKind, Pos};
775 let row = line as usize;
776 if row >= self.buffer.lines().len() {
777 return Vec::new();
778 }
779 if self.buffer.search_pattern().is_none() {
780 return Vec::new();
781 }
782 self.buffer
783 .search_matches(row)
784 .into_iter()
785 .map(|(start, end)| Highlight {
786 range: Pos {
787 line,
788 col: start as u32,
789 }..Pos {
790 line,
791 col: end as u32,
792 },
793 kind: HighlightKind::SearchMatch,
794 })
795 .collect()
796 }
797
798 pub fn render_frame(&self) -> crate::types::RenderFrame {
808 use crate::types::{CursorShape, RenderFrame, SnapshotMode};
809 let (cursor_row, cursor_col) = self.cursor();
810 let (mode, shape) = match self.vim_mode() {
811 crate::VimMode::Normal => (SnapshotMode::Normal, CursorShape::Block),
812 crate::VimMode::Insert => (SnapshotMode::Insert, CursorShape::Bar),
813 crate::VimMode::Visual => (SnapshotMode::Visual, CursorShape::Block),
814 crate::VimMode::VisualLine => (SnapshotMode::VisualLine, CursorShape::Block),
815 crate::VimMode::VisualBlock => (SnapshotMode::VisualBlock, CursorShape::Block),
816 };
817 RenderFrame {
818 mode,
819 cursor_row: cursor_row as u32,
820 cursor_col: cursor_col as u32,
821 cursor_shape: shape,
822 viewport_top: self.buffer.viewport().top_row as u32,
823 line_count: self.buffer.lines().len() as u32,
824 }
825 }
826
827 pub fn take_snapshot(&self) -> crate::types::EditorSnapshot {
840 use crate::types::{EditorSnapshot, SnapshotMode};
841 let mode = match self.vim_mode() {
842 crate::VimMode::Normal => SnapshotMode::Normal,
843 crate::VimMode::Insert => SnapshotMode::Insert,
844 crate::VimMode::Visual => SnapshotMode::Visual,
845 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
846 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
847 };
848 let cursor = self.cursor();
849 let cursor = (cursor.0 as u32, cursor.1 as u32);
850 let lines: Vec<String> = self.buffer.lines().to_vec();
851 let viewport_top = self.buffer.viewport().top_row as u32;
852 let file_marks = self
853 .file_marks
854 .iter()
855 .map(|(c, (r, col))| (*c, (*r as u32, *col as u32)))
856 .collect();
857 EditorSnapshot {
858 version: EditorSnapshot::VERSION,
859 mode,
860 cursor,
861 lines,
862 viewport_top,
863 registers: self.registers.clone(),
864 file_marks,
865 }
866 }
867
868 pub fn restore_snapshot(
876 &mut self,
877 snap: crate::types::EditorSnapshot,
878 ) -> Result<(), crate::EngineError> {
879 use crate::types::EditorSnapshot;
880 if snap.version != EditorSnapshot::VERSION {
881 return Err(crate::EngineError::SnapshotVersion(
882 snap.version,
883 EditorSnapshot::VERSION,
884 ));
885 }
886 let text = snap.lines.join("\n");
887 self.set_content(&text);
888 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
889 let mut vp = self.buffer.viewport();
890 vp.top_row = snap.viewport_top as usize;
891 *self.buffer.viewport_mut() = vp;
892 self.registers = snap.registers;
893 self.file_marks = snap
894 .file_marks
895 .into_iter()
896 .map(|(c, (r, col))| (c, (r as usize, col as usize)))
897 .collect();
898 Ok(())
899 }
900
901 pub fn seed_yank(&mut self, text: String) {
905 let linewise = text.ends_with('\n');
906 self.vim.yank_linewise = linewise;
907 self.registers.unnamed = crate::registers::Slot { text, linewise };
908 }
909
910 pub fn scroll_down(&mut self, rows: i16) {
915 self.scroll_viewport(rows);
916 }
917
918 pub fn scroll_up(&mut self, rows: i16) {
922 self.scroll_viewport(-rows);
923 }
924
925 const SCROLLOFF: usize = 5;
929
930 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
935 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
936 if height == 0 {
937 self.buffer.ensure_cursor_visible();
938 return;
939 }
940 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
944 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
947 self.ensure_scrolloff_wrap(height, margin);
948 return;
949 }
950 let cursor_row = self.buffer.cursor().row;
951 let last_row = self.buffer.row_count().saturating_sub(1);
952 let v = self.buffer.viewport_mut();
953 if cursor_row < v.top_row + margin {
955 v.top_row = cursor_row.saturating_sub(margin);
956 }
957 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
959 if cursor_row > v.top_row + max_bottom {
960 v.top_row = cursor_row.saturating_sub(max_bottom);
961 }
962 let max_top = last_row.saturating_sub(height.saturating_sub(1));
964 if v.top_row > max_top {
965 v.top_row = max_top;
966 }
967 let cursor = self.buffer.cursor();
970 self.buffer.viewport_mut().ensure_visible(cursor);
971 }
972
973 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
978 let cursor_row = self.buffer.cursor().row;
979 if cursor_row < self.buffer.viewport().top_row {
982 self.buffer.viewport_mut().top_row = cursor_row;
983 self.buffer.viewport_mut().top_col = 0;
984 }
985 let max_csr = height.saturating_sub(1).saturating_sub(margin);
988 loop {
989 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
990 if csr <= max_csr {
991 break;
992 }
993 let top = self.buffer.viewport().top_row;
994 let Some(next) = self.buffer.next_visible_row(top) else {
995 break;
996 };
997 if next > cursor_row {
999 self.buffer.viewport_mut().top_row = cursor_row;
1000 break;
1001 }
1002 self.buffer.viewport_mut().top_row = next;
1003 }
1004 loop {
1007 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
1008 if csr >= margin {
1009 break;
1010 }
1011 let top = self.buffer.viewport().top_row;
1012 let Some(prev) = self.buffer.prev_visible_row(top) else {
1013 break;
1014 };
1015 self.buffer.viewport_mut().top_row = prev;
1016 }
1017 let max_top = self.buffer.max_top_for_height(height);
1022 if self.buffer.viewport().top_row > max_top {
1023 self.buffer.viewport_mut().top_row = max_top;
1024 }
1025 self.buffer.viewport_mut().top_col = 0;
1026 }
1027
1028 fn scroll_viewport(&mut self, delta: i16) {
1029 if delta == 0 {
1030 return;
1031 }
1032 let total_rows = self.buffer.row_count() as isize;
1034 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1035 let cur_top = self.buffer.viewport().top_row as isize;
1036 let new_top = (cur_top + delta as isize)
1037 .max(0)
1038 .min((total_rows - 1).max(0)) as usize;
1039 self.buffer.viewport_mut().top_row = new_top;
1040 let _ = cur_top;
1043 if height == 0 {
1044 return;
1045 }
1046 let cursor = self.buffer.cursor();
1049 let margin = Self::SCROLLOFF.min(height / 2);
1050 let min_row = new_top + margin;
1051 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1052 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1053 if target_row != cursor.row {
1054 let line_len = self
1055 .buffer
1056 .line(target_row)
1057 .map(|l| l.chars().count())
1058 .unwrap_or(0);
1059 let target_col = cursor.col.min(line_len.saturating_sub(1));
1060 self.buffer
1061 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1062 }
1063 }
1064
1065 pub fn goto_line(&mut self, line: usize) {
1066 let row = line.saturating_sub(1);
1067 let max = self.buffer.row_count().saturating_sub(1);
1068 let target = row.min(max);
1069 self.buffer
1070 .set_cursor(hjkl_buffer::Position::new(target, 0));
1071 }
1072
1073 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1077 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1078 if height == 0 {
1079 return;
1080 }
1081 let cur_row = self.buffer.cursor().row;
1082 let cur_top = self.buffer.viewport().top_row;
1083 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1089 let new_top = match pos {
1090 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1091 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1092 CursorScrollTarget::Bottom => {
1093 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1094 }
1095 };
1096 if new_top == cur_top {
1097 return;
1098 }
1099 self.buffer.viewport_mut().top_row = new_top;
1100 }
1101
1102 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1109 let lines = self.buffer.lines();
1110 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1112 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1113 let rel_row = row.saturating_sub(inner_top) as usize;
1114 let top = self.buffer.viewport().top_row;
1115 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1116 let rel_col = col.saturating_sub(content_x) as usize;
1117 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1118 let last_col = line_chars.saturating_sub(1);
1119 (doc_row, rel_col.min(last_col))
1120 }
1121
1122 pub fn jump_to(&mut self, line: usize, col: usize) {
1124 let r = line.saturating_sub(1);
1125 let max_row = self.buffer.row_count().saturating_sub(1);
1126 let r = r.min(max_row);
1127 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1128 let c = col.saturating_sub(1).min(line_len);
1129 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1130 }
1131
1132 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1134 if self.vim.is_visual() {
1135 self.vim.force_normal();
1136 }
1137 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1138 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1139 }
1140
1141 pub fn mouse_begin_drag(&mut self) {
1143 if !self.vim.is_visual_char() {
1144 let cursor = self.cursor();
1145 self.vim.enter_visual(cursor);
1146 }
1147 }
1148
1149 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1151 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1152 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1153 }
1154
1155 pub fn insert_str(&mut self, text: &str) {
1156 let pos = self.buffer.cursor();
1157 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1158 at: pos,
1159 text: text.to_string(),
1160 });
1161 self.push_buffer_content_to_textarea();
1162 self.mark_content_dirty();
1163 }
1164
1165 pub fn accept_completion(&mut self, completion: &str) {
1166 use hjkl_buffer::{Edit, MotionKind, Position};
1167 let cursor = self.buffer.cursor();
1168 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1169 let chars: Vec<char> = line.chars().collect();
1170 let prefix_len = chars[..cursor.col.min(chars.len())]
1171 .iter()
1172 .rev()
1173 .take_while(|c| c.is_alphanumeric() || **c == '_')
1174 .count();
1175 if prefix_len > 0 {
1176 let start = Position::new(cursor.row, cursor.col - prefix_len);
1177 self.buffer.apply_edit(Edit::DeleteRange {
1178 start,
1179 end: cursor,
1180 kind: MotionKind::Char,
1181 });
1182 }
1183 let cursor = self.buffer.cursor();
1184 self.buffer.apply_edit(Edit::InsertStr {
1185 at: cursor,
1186 text: completion.to_string(),
1187 });
1188 self.push_buffer_content_to_textarea();
1189 self.mark_content_dirty();
1190 }
1191
1192 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1193 let pos = self.buffer.cursor();
1194 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1195 }
1196
1197 #[doc(hidden)]
1198 pub fn push_undo(&mut self) {
1199 let snap = self.snapshot();
1200 if self.undo_stack.len() >= 200 {
1201 self.undo_stack.remove(0);
1202 }
1203 self.undo_stack.push(snap);
1204 self.redo_stack.clear();
1205 }
1206
1207 #[doc(hidden)]
1208 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1209 let text = lines.join("\n");
1210 self.buffer.replace_all(&text);
1211 self.buffer
1212 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1213 self.mark_content_dirty();
1214 }
1215
1216 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1218 let input = crossterm_to_input(key);
1219 if input.key == Key::Null {
1220 return false;
1221 }
1222 vim::step(self, input)
1223 }
1224}
1225
1226pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1227 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1228 let alt = key.modifiers.contains(KeyModifiers::ALT);
1229 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1230 let k = match key.code {
1231 KeyCode::Char(c) => Key::Char(c),
1232 KeyCode::Backspace => Key::Backspace,
1233 KeyCode::Delete => Key::Delete,
1234 KeyCode::Enter => Key::Enter,
1235 KeyCode::Left => Key::Left,
1236 KeyCode::Right => Key::Right,
1237 KeyCode::Up => Key::Up,
1238 KeyCode::Down => Key::Down,
1239 KeyCode::Home => Key::Home,
1240 KeyCode::End => Key::End,
1241 KeyCode::Tab => Key::Tab,
1242 KeyCode::Esc => Key::Esc,
1243 _ => Key::Null,
1244 };
1245 Input {
1246 key: k,
1247 ctrl,
1248 alt,
1249 shift,
1250 }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255 use super::*;
1256 use crossterm::event::KeyEvent;
1257
1258 fn key(code: KeyCode) -> KeyEvent {
1259 KeyEvent::new(code, KeyModifiers::NONE)
1260 }
1261 fn shift_key(code: KeyCode) -> KeyEvent {
1262 KeyEvent::new(code, KeyModifiers::SHIFT)
1263 }
1264 fn ctrl_key(code: KeyCode) -> KeyEvent {
1265 KeyEvent::new(code, KeyModifiers::CONTROL)
1266 }
1267
1268 #[test]
1269 fn vim_normal_to_insert() {
1270 let mut e = Editor::new(KeybindingMode::Vim);
1271 e.handle_key(key(KeyCode::Char('i')));
1272 assert_eq!(e.vim_mode(), VimMode::Insert);
1273 }
1274
1275 #[test]
1276 fn selection_highlight_none_in_normal() {
1277 let mut e = Editor::new(KeybindingMode::Vim);
1278 e.set_content("hello");
1279 assert!(e.selection_highlight().is_none());
1280 }
1281
1282 #[test]
1283 fn selection_highlight_some_in_visual() {
1284 use crate::types::HighlightKind;
1285 let mut e = Editor::new(KeybindingMode::Vim);
1286 e.set_content("hello world");
1287 e.handle_key(key(KeyCode::Char('v')));
1288 e.handle_key(key(KeyCode::Char('l')));
1289 e.handle_key(key(KeyCode::Char('l')));
1290 let h = e
1291 .selection_highlight()
1292 .expect("visual mode should produce a highlight");
1293 assert_eq!(h.kind, HighlightKind::Selection);
1294 assert_eq!(h.range.start.line, 0);
1295 assert_eq!(h.range.end.line, 0);
1296 }
1297
1298 #[test]
1299 fn highlights_emit_search_matches() {
1300 use crate::types::HighlightKind;
1301 let mut e = Editor::new(KeybindingMode::Vim);
1302 e.set_content("foo bar foo\nbaz qux\n");
1303 e.buffer_mut()
1305 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1306 let hs = e.highlights_for_line(0);
1307 assert_eq!(hs.len(), 2);
1308 for h in &hs {
1309 assert_eq!(h.kind, HighlightKind::SearchMatch);
1310 assert_eq!(h.range.start.line, 0);
1311 assert_eq!(h.range.end.line, 0);
1312 }
1313 }
1314
1315 #[test]
1316 fn highlights_empty_without_pattern() {
1317 let mut e = Editor::new(KeybindingMode::Vim);
1318 e.set_content("foo bar");
1319 assert!(e.highlights_for_line(0).is_empty());
1320 }
1321
1322 #[test]
1323 fn highlights_empty_for_out_of_range_line() {
1324 let mut e = Editor::new(KeybindingMode::Vim);
1325 e.set_content("foo");
1326 e.buffer_mut()
1327 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1328 assert!(e.highlights_for_line(99).is_empty());
1329 }
1330
1331 #[test]
1332 fn render_frame_reflects_mode_and_cursor() {
1333 use crate::types::{CursorShape, SnapshotMode};
1334 let mut e = Editor::new(KeybindingMode::Vim);
1335 e.set_content("alpha\nbeta");
1336 let f = e.render_frame();
1337 assert_eq!(f.mode, SnapshotMode::Normal);
1338 assert_eq!(f.cursor_shape, CursorShape::Block);
1339 assert_eq!(f.line_count, 2);
1340
1341 e.handle_key(key(KeyCode::Char('i')));
1342 let f = e.render_frame();
1343 assert_eq!(f.mode, SnapshotMode::Insert);
1344 assert_eq!(f.cursor_shape, CursorShape::Bar);
1345 }
1346
1347 #[test]
1348 fn snapshot_roundtrips_through_restore() {
1349 use crate::types::SnapshotMode;
1350 let mut e = Editor::new(KeybindingMode::Vim);
1351 e.set_content("alpha\nbeta\ngamma");
1352 e.jump_cursor(2, 3);
1353 let snap = e.take_snapshot();
1354 assert_eq!(snap.mode, SnapshotMode::Normal);
1355 assert_eq!(snap.cursor, (2, 3));
1356 assert_eq!(snap.lines.len(), 3);
1357
1358 let mut other = Editor::new(KeybindingMode::Vim);
1359 other.restore_snapshot(snap).expect("restore");
1360 assert_eq!(other.cursor(), (2, 3));
1361 assert_eq!(other.buffer().lines().len(), 3);
1362 }
1363
1364 #[test]
1365 fn restore_snapshot_rejects_version_mismatch() {
1366 let mut e = Editor::new(KeybindingMode::Vim);
1367 let mut snap = e.take_snapshot();
1368 snap.version = 9999;
1369 match e.restore_snapshot(snap) {
1370 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1371 assert_eq!(got, 9999);
1372 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1373 }
1374 other => panic!("expected SnapshotVersion err, got {other:?}"),
1375 }
1376 }
1377
1378 #[test]
1379 fn take_content_change_returns_some_on_first_dirty() {
1380 let mut e = Editor::new(KeybindingMode::Vim);
1381 e.set_content("hello");
1382 let first = e.take_content_change();
1383 assert!(first.is_some());
1384 let second = e.take_content_change();
1385 assert!(second.is_none());
1386 }
1387
1388 #[test]
1389 fn take_content_change_none_until_mutation() {
1390 let mut e = Editor::new(KeybindingMode::Vim);
1391 e.set_content("hello");
1392 e.take_content_change();
1394 assert!(e.take_content_change().is_none());
1395 e.handle_key(key(KeyCode::Char('i')));
1397 e.handle_key(key(KeyCode::Char('x')));
1398 let after = e.take_content_change();
1399 assert!(after.is_some());
1400 assert!(after.unwrap().contains('x'));
1401 }
1402
1403 #[test]
1404 fn vim_insert_to_normal() {
1405 let mut e = Editor::new(KeybindingMode::Vim);
1406 e.handle_key(key(KeyCode::Char('i')));
1407 e.handle_key(key(KeyCode::Esc));
1408 assert_eq!(e.vim_mode(), VimMode::Normal);
1409 }
1410
1411 #[test]
1412 fn vim_normal_to_visual() {
1413 let mut e = Editor::new(KeybindingMode::Vim);
1414 e.handle_key(key(KeyCode::Char('v')));
1415 assert_eq!(e.vim_mode(), VimMode::Visual);
1416 }
1417
1418 #[test]
1419 fn vim_visual_to_normal() {
1420 let mut e = Editor::new(KeybindingMode::Vim);
1421 e.handle_key(key(KeyCode::Char('v')));
1422 e.handle_key(key(KeyCode::Esc));
1423 assert_eq!(e.vim_mode(), VimMode::Normal);
1424 }
1425
1426 #[test]
1427 fn vim_shift_i_moves_to_first_non_whitespace() {
1428 let mut e = Editor::new(KeybindingMode::Vim);
1429 e.set_content(" hello");
1430 e.jump_cursor(0, 8);
1431 e.handle_key(shift_key(KeyCode::Char('I')));
1432 assert_eq!(e.vim_mode(), VimMode::Insert);
1433 assert_eq!(e.cursor(), (0, 3));
1434 }
1435
1436 #[test]
1437 fn vim_shift_a_moves_to_end_and_insert() {
1438 let mut e = Editor::new(KeybindingMode::Vim);
1439 e.set_content("hello");
1440 e.handle_key(shift_key(KeyCode::Char('A')));
1441 assert_eq!(e.vim_mode(), VimMode::Insert);
1442 assert_eq!(e.cursor().1, 5);
1443 }
1444
1445 #[test]
1446 fn count_10j_moves_down_10() {
1447 let mut e = Editor::new(KeybindingMode::Vim);
1448 e.set_content(
1449 (0..20)
1450 .map(|i| format!("line{i}"))
1451 .collect::<Vec<_>>()
1452 .join("\n")
1453 .as_str(),
1454 );
1455 for d in "10".chars() {
1456 e.handle_key(key(KeyCode::Char(d)));
1457 }
1458 e.handle_key(key(KeyCode::Char('j')));
1459 assert_eq!(e.cursor().0, 10);
1460 }
1461
1462 #[test]
1463 fn count_o_repeats_insert_on_esc() {
1464 let mut e = Editor::new(KeybindingMode::Vim);
1465 e.set_content("hello");
1466 for d in "3".chars() {
1467 e.handle_key(key(KeyCode::Char(d)));
1468 }
1469 e.handle_key(key(KeyCode::Char('o')));
1470 assert_eq!(e.vim_mode(), VimMode::Insert);
1471 for c in "world".chars() {
1472 e.handle_key(key(KeyCode::Char(c)));
1473 }
1474 e.handle_key(key(KeyCode::Esc));
1475 assert_eq!(e.vim_mode(), VimMode::Normal);
1476 assert_eq!(e.buffer().lines().len(), 4);
1477 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1478 }
1479
1480 #[test]
1481 fn count_i_repeats_text_on_esc() {
1482 let mut e = Editor::new(KeybindingMode::Vim);
1483 e.set_content("");
1484 for d in "3".chars() {
1485 e.handle_key(key(KeyCode::Char(d)));
1486 }
1487 e.handle_key(key(KeyCode::Char('i')));
1488 for c in "ab".chars() {
1489 e.handle_key(key(KeyCode::Char(c)));
1490 }
1491 e.handle_key(key(KeyCode::Esc));
1492 assert_eq!(e.vim_mode(), VimMode::Normal);
1493 assert_eq!(e.buffer().lines()[0], "ababab");
1494 }
1495
1496 #[test]
1497 fn vim_shift_o_opens_line_above() {
1498 let mut e = Editor::new(KeybindingMode::Vim);
1499 e.set_content("hello");
1500 e.handle_key(shift_key(KeyCode::Char('O')));
1501 assert_eq!(e.vim_mode(), VimMode::Insert);
1502 assert_eq!(e.cursor(), (0, 0));
1503 assert_eq!(e.buffer().lines().len(), 2);
1504 }
1505
1506 #[test]
1507 fn vim_gg_goes_to_top() {
1508 let mut e = Editor::new(KeybindingMode::Vim);
1509 e.set_content("a\nb\nc");
1510 e.jump_cursor(2, 0);
1511 e.handle_key(key(KeyCode::Char('g')));
1512 e.handle_key(key(KeyCode::Char('g')));
1513 assert_eq!(e.cursor().0, 0);
1514 }
1515
1516 #[test]
1517 fn vim_shift_g_goes_to_bottom() {
1518 let mut e = Editor::new(KeybindingMode::Vim);
1519 e.set_content("a\nb\nc");
1520 e.handle_key(shift_key(KeyCode::Char('G')));
1521 assert_eq!(e.cursor().0, 2);
1522 }
1523
1524 #[test]
1525 fn vim_dd_deletes_line() {
1526 let mut e = Editor::new(KeybindingMode::Vim);
1527 e.set_content("first\nsecond");
1528 e.handle_key(key(KeyCode::Char('d')));
1529 e.handle_key(key(KeyCode::Char('d')));
1530 assert_eq!(e.buffer().lines().len(), 1);
1531 assert_eq!(e.buffer().lines()[0], "second");
1532 }
1533
1534 #[test]
1535 fn vim_dw_deletes_word() {
1536 let mut e = Editor::new(KeybindingMode::Vim);
1537 e.set_content("hello world");
1538 e.handle_key(key(KeyCode::Char('d')));
1539 e.handle_key(key(KeyCode::Char('w')));
1540 assert_eq!(e.vim_mode(), VimMode::Normal);
1541 assert!(!e.buffer().lines()[0].starts_with("hello"));
1542 }
1543
1544 #[test]
1545 fn vim_yy_yanks_line() {
1546 let mut e = Editor::new(KeybindingMode::Vim);
1547 e.set_content("hello\nworld");
1548 e.handle_key(key(KeyCode::Char('y')));
1549 e.handle_key(key(KeyCode::Char('y')));
1550 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1551 }
1552
1553 #[test]
1554 fn vim_yy_does_not_move_cursor() {
1555 let mut e = Editor::new(KeybindingMode::Vim);
1556 e.set_content("first\nsecond\nthird");
1557 e.jump_cursor(1, 0);
1558 let before = e.cursor();
1559 e.handle_key(key(KeyCode::Char('y')));
1560 e.handle_key(key(KeyCode::Char('y')));
1561 assert_eq!(e.cursor(), before);
1562 assert_eq!(e.vim_mode(), VimMode::Normal);
1563 }
1564
1565 #[test]
1566 fn vim_yw_yanks_word() {
1567 let mut e = Editor::new(KeybindingMode::Vim);
1568 e.set_content("hello world");
1569 e.handle_key(key(KeyCode::Char('y')));
1570 e.handle_key(key(KeyCode::Char('w')));
1571 assert_eq!(e.vim_mode(), VimMode::Normal);
1572 assert!(e.last_yank.is_some());
1573 }
1574
1575 #[test]
1576 fn vim_cc_changes_line() {
1577 let mut e = Editor::new(KeybindingMode::Vim);
1578 e.set_content("hello\nworld");
1579 e.handle_key(key(KeyCode::Char('c')));
1580 e.handle_key(key(KeyCode::Char('c')));
1581 assert_eq!(e.vim_mode(), VimMode::Insert);
1582 }
1583
1584 #[test]
1585 fn vim_u_undoes_insert_session_as_chunk() {
1586 let mut e = Editor::new(KeybindingMode::Vim);
1587 e.set_content("hello");
1588 e.handle_key(key(KeyCode::Char('i')));
1589 e.handle_key(key(KeyCode::Enter));
1590 e.handle_key(key(KeyCode::Enter));
1591 e.handle_key(key(KeyCode::Esc));
1592 assert_eq!(e.buffer().lines().len(), 3);
1593 e.handle_key(key(KeyCode::Char('u')));
1594 assert_eq!(e.buffer().lines().len(), 1);
1595 assert_eq!(e.buffer().lines()[0], "hello");
1596 }
1597
1598 #[test]
1599 fn vim_undo_redo_roundtrip() {
1600 let mut e = Editor::new(KeybindingMode::Vim);
1601 e.set_content("hello");
1602 e.handle_key(key(KeyCode::Char('i')));
1603 for c in "world".chars() {
1604 e.handle_key(key(KeyCode::Char(c)));
1605 }
1606 e.handle_key(key(KeyCode::Esc));
1607 let after = e.buffer().lines()[0].clone();
1608 e.handle_key(key(KeyCode::Char('u')));
1609 assert_eq!(e.buffer().lines()[0], "hello");
1610 e.handle_key(ctrl_key(KeyCode::Char('r')));
1611 assert_eq!(e.buffer().lines()[0], after);
1612 }
1613
1614 #[test]
1615 fn vim_u_undoes_dd() {
1616 let mut e = Editor::new(KeybindingMode::Vim);
1617 e.set_content("first\nsecond");
1618 e.handle_key(key(KeyCode::Char('d')));
1619 e.handle_key(key(KeyCode::Char('d')));
1620 assert_eq!(e.buffer().lines().len(), 1);
1621 e.handle_key(key(KeyCode::Char('u')));
1622 assert_eq!(e.buffer().lines().len(), 2);
1623 assert_eq!(e.buffer().lines()[0], "first");
1624 }
1625
1626 #[test]
1627 fn vim_ctrl_r_redoes() {
1628 let mut e = Editor::new(KeybindingMode::Vim);
1629 e.set_content("hello");
1630 e.handle_key(ctrl_key(KeyCode::Char('r')));
1631 }
1632
1633 #[test]
1634 fn vim_r_replaces_char() {
1635 let mut e = Editor::new(KeybindingMode::Vim);
1636 e.set_content("hello");
1637 e.handle_key(key(KeyCode::Char('r')));
1638 e.handle_key(key(KeyCode::Char('x')));
1639 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1640 }
1641
1642 #[test]
1643 fn vim_tilde_toggles_case() {
1644 let mut e = Editor::new(KeybindingMode::Vim);
1645 e.set_content("hello");
1646 e.handle_key(key(KeyCode::Char('~')));
1647 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1648 }
1649
1650 #[test]
1651 fn vim_visual_d_cuts() {
1652 let mut e = Editor::new(KeybindingMode::Vim);
1653 e.set_content("hello");
1654 e.handle_key(key(KeyCode::Char('v')));
1655 e.handle_key(key(KeyCode::Char('l')));
1656 e.handle_key(key(KeyCode::Char('l')));
1657 e.handle_key(key(KeyCode::Char('d')));
1658 assert_eq!(e.vim_mode(), VimMode::Normal);
1659 assert!(e.last_yank.is_some());
1660 }
1661
1662 #[test]
1663 fn vim_visual_c_enters_insert() {
1664 let mut e = Editor::new(KeybindingMode::Vim);
1665 e.set_content("hello");
1666 e.handle_key(key(KeyCode::Char('v')));
1667 e.handle_key(key(KeyCode::Char('l')));
1668 e.handle_key(key(KeyCode::Char('c')));
1669 assert_eq!(e.vim_mode(), VimMode::Insert);
1670 }
1671
1672 #[test]
1673 fn vim_normal_unknown_key_consumed() {
1674 let mut e = Editor::new(KeybindingMode::Vim);
1675 let consumed = e.handle_key(key(KeyCode::Char('z')));
1677 assert!(consumed);
1678 }
1679
1680 #[test]
1681 fn force_normal_clears_operator() {
1682 let mut e = Editor::new(KeybindingMode::Vim);
1683 e.handle_key(key(KeyCode::Char('d')));
1684 e.force_normal();
1685 assert_eq!(e.vim_mode(), VimMode::Normal);
1686 }
1687
1688 fn many_lines(n: usize) -> String {
1689 (0..n)
1690 .map(|i| format!("line{i}"))
1691 .collect::<Vec<_>>()
1692 .join("\n")
1693 }
1694
1695 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1696 e.set_viewport_height(height);
1697 }
1698
1699 #[test]
1700 fn zz_centers_cursor_in_viewport() {
1701 let mut e = Editor::new(KeybindingMode::Vim);
1702 e.set_content(&many_lines(100));
1703 prime_viewport(&mut e, 20);
1704 e.jump_cursor(50, 0);
1705 e.handle_key(key(KeyCode::Char('z')));
1706 e.handle_key(key(KeyCode::Char('z')));
1707 assert_eq!(e.buffer().viewport().top_row, 40);
1708 assert_eq!(e.cursor().0, 50);
1709 }
1710
1711 #[test]
1712 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1713 let mut e = Editor::new(KeybindingMode::Vim);
1714 e.set_content(&many_lines(100));
1715 prime_viewport(&mut e, 20);
1716 e.jump_cursor(50, 0);
1717 e.handle_key(key(KeyCode::Char('z')));
1718 e.handle_key(key(KeyCode::Char('t')));
1719 assert_eq!(e.buffer().viewport().top_row, 45);
1722 assert_eq!(e.cursor().0, 50);
1723 }
1724
1725 #[test]
1726 fn ctrl_a_increments_number_at_cursor() {
1727 let mut e = Editor::new(KeybindingMode::Vim);
1728 e.set_content("x = 41");
1729 e.handle_key(ctrl_key(KeyCode::Char('a')));
1730 assert_eq!(e.buffer().lines()[0], "x = 42");
1731 assert_eq!(e.cursor(), (0, 5));
1732 }
1733
1734 #[test]
1735 fn ctrl_a_finds_number_to_right_of_cursor() {
1736 let mut e = Editor::new(KeybindingMode::Vim);
1737 e.set_content("foo 99 bar");
1738 e.handle_key(ctrl_key(KeyCode::Char('a')));
1739 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1740 assert_eq!(e.cursor(), (0, 6));
1741 }
1742
1743 #[test]
1744 fn ctrl_a_with_count_adds_count() {
1745 let mut e = Editor::new(KeybindingMode::Vim);
1746 e.set_content("x = 10");
1747 for d in "5".chars() {
1748 e.handle_key(key(KeyCode::Char(d)));
1749 }
1750 e.handle_key(ctrl_key(KeyCode::Char('a')));
1751 assert_eq!(e.buffer().lines()[0], "x = 15");
1752 }
1753
1754 #[test]
1755 fn ctrl_x_decrements_number() {
1756 let mut e = Editor::new(KeybindingMode::Vim);
1757 e.set_content("n=5");
1758 e.handle_key(ctrl_key(KeyCode::Char('x')));
1759 assert_eq!(e.buffer().lines()[0], "n=4");
1760 }
1761
1762 #[test]
1763 fn ctrl_x_crosses_zero_into_negative() {
1764 let mut e = Editor::new(KeybindingMode::Vim);
1765 e.set_content("v=0");
1766 e.handle_key(ctrl_key(KeyCode::Char('x')));
1767 assert_eq!(e.buffer().lines()[0], "v=-1");
1768 }
1769
1770 #[test]
1771 fn ctrl_a_on_negative_number_increments_toward_zero() {
1772 let mut e = Editor::new(KeybindingMode::Vim);
1773 e.set_content("a = -5");
1774 e.handle_key(ctrl_key(KeyCode::Char('a')));
1775 assert_eq!(e.buffer().lines()[0], "a = -4");
1776 }
1777
1778 #[test]
1779 fn ctrl_a_noop_when_no_digit_on_line() {
1780 let mut e = Editor::new(KeybindingMode::Vim);
1781 e.set_content("no digits here");
1782 e.handle_key(ctrl_key(KeyCode::Char('a')));
1783 assert_eq!(e.buffer().lines()[0], "no digits here");
1784 }
1785
1786 #[test]
1787 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1788 let mut e = Editor::new(KeybindingMode::Vim);
1789 e.set_content(&many_lines(100));
1790 prime_viewport(&mut e, 20);
1791 e.jump_cursor(50, 0);
1792 e.handle_key(key(KeyCode::Char('z')));
1793 e.handle_key(key(KeyCode::Char('b')));
1794 assert_eq!(e.buffer().viewport().top_row, 36);
1798 assert_eq!(e.cursor().0, 50);
1799 }
1800
1801 #[test]
1808 fn set_content_dirties_then_take_dirty_clears() {
1809 let mut e = Editor::new(KeybindingMode::Vim);
1810 e.set_content("hello");
1811 assert!(
1812 e.take_dirty(),
1813 "set_content should leave content_dirty=true"
1814 );
1815 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1816 }
1817
1818 #[test]
1819 fn content_arc_returns_same_arc_until_mutation() {
1820 let mut e = Editor::new(KeybindingMode::Vim);
1821 e.set_content("hello");
1822 let a = e.content_arc();
1823 let b = e.content_arc();
1824 assert!(
1825 std::sync::Arc::ptr_eq(&a, &b),
1826 "repeated content_arc() should hit the cache"
1827 );
1828
1829 e.handle_key(key(KeyCode::Char('i')));
1831 e.handle_key(key(KeyCode::Char('!')));
1832 let c = e.content_arc();
1833 assert!(
1834 !std::sync::Arc::ptr_eq(&a, &c),
1835 "mutation should invalidate content_arc() cache"
1836 );
1837 assert!(c.contains('!'));
1838 }
1839
1840 #[test]
1841 fn content_arc_cache_invalidated_by_set_content() {
1842 let mut e = Editor::new(KeybindingMode::Vim);
1843 e.set_content("one");
1844 let a = e.content_arc();
1845 e.set_content("two");
1846 let b = e.content_arc();
1847 assert!(!std::sync::Arc::ptr_eq(&a, &b));
1848 assert!(b.starts_with("two"));
1849 }
1850
1851 #[test]
1857 fn mouse_click_past_eol_lands_on_last_char() {
1858 let mut e = Editor::new(KeybindingMode::Vim);
1859 e.set_content("hello");
1860 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1864 e.mouse_click(area, 78, 1);
1865 assert_eq!(e.cursor(), (0, 4));
1866 }
1867
1868 #[test]
1869 fn mouse_click_past_eol_handles_multibyte_line() {
1870 let mut e = Editor::new(KeybindingMode::Vim);
1871 e.set_content("héllo");
1874 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1875 e.mouse_click(area, 78, 1);
1876 assert_eq!(e.cursor(), (0, 4));
1877 }
1878
1879 #[test]
1880 fn mouse_click_inside_line_lands_on_clicked_char() {
1881 let mut e = Editor::new(KeybindingMode::Vim);
1882 e.set_content("hello world");
1883 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1886 e.mouse_click(area, 4, 1);
1887 assert_eq!(e.cursor(), (0, 0));
1888 e.mouse_click(area, 6, 1);
1889 assert_eq!(e.cursor(), (0, 2));
1890 }
1891}