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 EditorSnapshot {
853 version: EditorSnapshot::VERSION,
854 mode,
855 cursor,
856 lines,
857 viewport_top,
858 registers: self.registers.clone(),
859 }
860 }
861
862 pub fn restore_snapshot(
870 &mut self,
871 snap: crate::types::EditorSnapshot,
872 ) -> Result<(), crate::EngineError> {
873 use crate::types::EditorSnapshot;
874 if snap.version != EditorSnapshot::VERSION {
875 return Err(crate::EngineError::SnapshotVersion(
876 snap.version,
877 EditorSnapshot::VERSION,
878 ));
879 }
880 let text = snap.lines.join("\n");
881 self.set_content(&text);
882 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
883 let mut vp = self.buffer.viewport();
884 vp.top_row = snap.viewport_top as usize;
885 *self.buffer.viewport_mut() = vp;
886 self.registers = snap.registers;
887 Ok(())
888 }
889
890 pub fn seed_yank(&mut self, text: String) {
894 let linewise = text.ends_with('\n');
895 self.vim.yank_linewise = linewise;
896 self.registers.unnamed = crate::registers::Slot { text, linewise };
897 }
898
899 pub fn scroll_down(&mut self, rows: i16) {
904 self.scroll_viewport(rows);
905 }
906
907 pub fn scroll_up(&mut self, rows: i16) {
911 self.scroll_viewport(-rows);
912 }
913
914 const SCROLLOFF: usize = 5;
918
919 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
924 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
925 if height == 0 {
926 self.buffer.ensure_cursor_visible();
927 return;
928 }
929 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
933 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
936 self.ensure_scrolloff_wrap(height, margin);
937 return;
938 }
939 let cursor_row = self.buffer.cursor().row;
940 let last_row = self.buffer.row_count().saturating_sub(1);
941 let v = self.buffer.viewport_mut();
942 if cursor_row < v.top_row + margin {
944 v.top_row = cursor_row.saturating_sub(margin);
945 }
946 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
948 if cursor_row > v.top_row + max_bottom {
949 v.top_row = cursor_row.saturating_sub(max_bottom);
950 }
951 let max_top = last_row.saturating_sub(height.saturating_sub(1));
953 if v.top_row > max_top {
954 v.top_row = max_top;
955 }
956 let cursor = self.buffer.cursor();
959 self.buffer.viewport_mut().ensure_visible(cursor);
960 }
961
962 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
967 let cursor_row = self.buffer.cursor().row;
968 if cursor_row < self.buffer.viewport().top_row {
971 self.buffer.viewport_mut().top_row = cursor_row;
972 self.buffer.viewport_mut().top_col = 0;
973 }
974 let max_csr = height.saturating_sub(1).saturating_sub(margin);
977 loop {
978 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
979 if csr <= max_csr {
980 break;
981 }
982 let top = self.buffer.viewport().top_row;
983 let Some(next) = self.buffer.next_visible_row(top) else {
984 break;
985 };
986 if next > cursor_row {
988 self.buffer.viewport_mut().top_row = cursor_row;
989 break;
990 }
991 self.buffer.viewport_mut().top_row = next;
992 }
993 loop {
996 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
997 if csr >= margin {
998 break;
999 }
1000 let top = self.buffer.viewport().top_row;
1001 let Some(prev) = self.buffer.prev_visible_row(top) else {
1002 break;
1003 };
1004 self.buffer.viewport_mut().top_row = prev;
1005 }
1006 let max_top = self.buffer.max_top_for_height(height);
1011 if self.buffer.viewport().top_row > max_top {
1012 self.buffer.viewport_mut().top_row = max_top;
1013 }
1014 self.buffer.viewport_mut().top_col = 0;
1015 }
1016
1017 fn scroll_viewport(&mut self, delta: i16) {
1018 if delta == 0 {
1019 return;
1020 }
1021 let total_rows = self.buffer.row_count() as isize;
1023 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1024 let cur_top = self.buffer.viewport().top_row as isize;
1025 let new_top = (cur_top + delta as isize)
1026 .max(0)
1027 .min((total_rows - 1).max(0)) as usize;
1028 self.buffer.viewport_mut().top_row = new_top;
1029 let _ = cur_top;
1032 if height == 0 {
1033 return;
1034 }
1035 let cursor = self.buffer.cursor();
1038 let margin = Self::SCROLLOFF.min(height / 2);
1039 let min_row = new_top + margin;
1040 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
1041 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
1042 if target_row != cursor.row {
1043 let line_len = self
1044 .buffer
1045 .line(target_row)
1046 .map(|l| l.chars().count())
1047 .unwrap_or(0);
1048 let target_col = cursor.col.min(line_len.saturating_sub(1));
1049 self.buffer
1050 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
1051 }
1052 }
1053
1054 pub fn goto_line(&mut self, line: usize) {
1055 let row = line.saturating_sub(1);
1056 let max = self.buffer.row_count().saturating_sub(1);
1057 let target = row.min(max);
1058 self.buffer
1059 .set_cursor(hjkl_buffer::Position::new(target, 0));
1060 }
1061
1062 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
1066 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
1067 if height == 0 {
1068 return;
1069 }
1070 let cur_row = self.buffer.cursor().row;
1071 let cur_top = self.buffer.viewport().top_row;
1072 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
1078 let new_top = match pos {
1079 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
1080 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
1081 CursorScrollTarget::Bottom => {
1082 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
1083 }
1084 };
1085 if new_top == cur_top {
1086 return;
1087 }
1088 self.buffer.viewport_mut().top_row = new_top;
1089 }
1090
1091 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
1098 let lines = self.buffer.lines();
1099 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
1101 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
1102 let rel_row = row.saturating_sub(inner_top) as usize;
1103 let top = self.buffer.viewport().top_row;
1104 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
1105 let rel_col = col.saturating_sub(content_x) as usize;
1106 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
1107 let last_col = line_chars.saturating_sub(1);
1108 (doc_row, rel_col.min(last_col))
1109 }
1110
1111 pub fn jump_to(&mut self, line: usize, col: usize) {
1113 let r = line.saturating_sub(1);
1114 let max_row = self.buffer.row_count().saturating_sub(1);
1115 let r = r.min(max_row);
1116 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
1117 let c = col.saturating_sub(1).min(line_len);
1118 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1119 }
1120
1121 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
1123 if self.vim.is_visual() {
1124 self.vim.force_normal();
1125 }
1126 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1127 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1128 }
1129
1130 pub fn mouse_begin_drag(&mut self) {
1132 if !self.vim.is_visual_char() {
1133 let cursor = self.cursor();
1134 self.vim.enter_visual(cursor);
1135 }
1136 }
1137
1138 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1140 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1141 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1142 }
1143
1144 pub fn insert_str(&mut self, text: &str) {
1145 let pos = self.buffer.cursor();
1146 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1147 at: pos,
1148 text: text.to_string(),
1149 });
1150 self.push_buffer_content_to_textarea();
1151 self.mark_content_dirty();
1152 }
1153
1154 pub fn accept_completion(&mut self, completion: &str) {
1155 use hjkl_buffer::{Edit, MotionKind, Position};
1156 let cursor = self.buffer.cursor();
1157 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1158 let chars: Vec<char> = line.chars().collect();
1159 let prefix_len = chars[..cursor.col.min(chars.len())]
1160 .iter()
1161 .rev()
1162 .take_while(|c| c.is_alphanumeric() || **c == '_')
1163 .count();
1164 if prefix_len > 0 {
1165 let start = Position::new(cursor.row, cursor.col - prefix_len);
1166 self.buffer.apply_edit(Edit::DeleteRange {
1167 start,
1168 end: cursor,
1169 kind: MotionKind::Char,
1170 });
1171 }
1172 let cursor = self.buffer.cursor();
1173 self.buffer.apply_edit(Edit::InsertStr {
1174 at: cursor,
1175 text: completion.to_string(),
1176 });
1177 self.push_buffer_content_to_textarea();
1178 self.mark_content_dirty();
1179 }
1180
1181 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1182 let pos = self.buffer.cursor();
1183 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1184 }
1185
1186 #[doc(hidden)]
1187 pub fn push_undo(&mut self) {
1188 let snap = self.snapshot();
1189 if self.undo_stack.len() >= 200 {
1190 self.undo_stack.remove(0);
1191 }
1192 self.undo_stack.push(snap);
1193 self.redo_stack.clear();
1194 }
1195
1196 #[doc(hidden)]
1197 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1198 let text = lines.join("\n");
1199 self.buffer.replace_all(&text);
1200 self.buffer
1201 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1202 self.mark_content_dirty();
1203 }
1204
1205 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1207 let input = crossterm_to_input(key);
1208 if input.key == Key::Null {
1209 return false;
1210 }
1211 vim::step(self, input)
1212 }
1213}
1214
1215pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1216 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1217 let alt = key.modifiers.contains(KeyModifiers::ALT);
1218 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1219 let k = match key.code {
1220 KeyCode::Char(c) => Key::Char(c),
1221 KeyCode::Backspace => Key::Backspace,
1222 KeyCode::Delete => Key::Delete,
1223 KeyCode::Enter => Key::Enter,
1224 KeyCode::Left => Key::Left,
1225 KeyCode::Right => Key::Right,
1226 KeyCode::Up => Key::Up,
1227 KeyCode::Down => Key::Down,
1228 KeyCode::Home => Key::Home,
1229 KeyCode::End => Key::End,
1230 KeyCode::Tab => Key::Tab,
1231 KeyCode::Esc => Key::Esc,
1232 _ => Key::Null,
1233 };
1234 Input {
1235 key: k,
1236 ctrl,
1237 alt,
1238 shift,
1239 }
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244 use super::*;
1245 use crossterm::event::KeyEvent;
1246
1247 fn key(code: KeyCode) -> KeyEvent {
1248 KeyEvent::new(code, KeyModifiers::NONE)
1249 }
1250 fn shift_key(code: KeyCode) -> KeyEvent {
1251 KeyEvent::new(code, KeyModifiers::SHIFT)
1252 }
1253 fn ctrl_key(code: KeyCode) -> KeyEvent {
1254 KeyEvent::new(code, KeyModifiers::CONTROL)
1255 }
1256
1257 #[test]
1258 fn vim_normal_to_insert() {
1259 let mut e = Editor::new(KeybindingMode::Vim);
1260 e.handle_key(key(KeyCode::Char('i')));
1261 assert_eq!(e.vim_mode(), VimMode::Insert);
1262 }
1263
1264 #[test]
1265 fn selection_highlight_none_in_normal() {
1266 let mut e = Editor::new(KeybindingMode::Vim);
1267 e.set_content("hello");
1268 assert!(e.selection_highlight().is_none());
1269 }
1270
1271 #[test]
1272 fn selection_highlight_some_in_visual() {
1273 use crate::types::HighlightKind;
1274 let mut e = Editor::new(KeybindingMode::Vim);
1275 e.set_content("hello world");
1276 e.handle_key(key(KeyCode::Char('v')));
1277 e.handle_key(key(KeyCode::Char('l')));
1278 e.handle_key(key(KeyCode::Char('l')));
1279 let h = e
1280 .selection_highlight()
1281 .expect("visual mode should produce a highlight");
1282 assert_eq!(h.kind, HighlightKind::Selection);
1283 assert_eq!(h.range.start.line, 0);
1284 assert_eq!(h.range.end.line, 0);
1285 }
1286
1287 #[test]
1288 fn highlights_emit_search_matches() {
1289 use crate::types::HighlightKind;
1290 let mut e = Editor::new(KeybindingMode::Vim);
1291 e.set_content("foo bar foo\nbaz qux\n");
1292 e.buffer_mut()
1294 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1295 let hs = e.highlights_for_line(0);
1296 assert_eq!(hs.len(), 2);
1297 for h in &hs {
1298 assert_eq!(h.kind, HighlightKind::SearchMatch);
1299 assert_eq!(h.range.start.line, 0);
1300 assert_eq!(h.range.end.line, 0);
1301 }
1302 }
1303
1304 #[test]
1305 fn highlights_empty_without_pattern() {
1306 let mut e = Editor::new(KeybindingMode::Vim);
1307 e.set_content("foo bar");
1308 assert!(e.highlights_for_line(0).is_empty());
1309 }
1310
1311 #[test]
1312 fn highlights_empty_for_out_of_range_line() {
1313 let mut e = Editor::new(KeybindingMode::Vim);
1314 e.set_content("foo");
1315 e.buffer_mut()
1316 .set_search_pattern(Some(regex::Regex::new("foo").unwrap()));
1317 assert!(e.highlights_for_line(99).is_empty());
1318 }
1319
1320 #[test]
1321 fn render_frame_reflects_mode_and_cursor() {
1322 use crate::types::{CursorShape, SnapshotMode};
1323 let mut e = Editor::new(KeybindingMode::Vim);
1324 e.set_content("alpha\nbeta");
1325 let f = e.render_frame();
1326 assert_eq!(f.mode, SnapshotMode::Normal);
1327 assert_eq!(f.cursor_shape, CursorShape::Block);
1328 assert_eq!(f.line_count, 2);
1329
1330 e.handle_key(key(KeyCode::Char('i')));
1331 let f = e.render_frame();
1332 assert_eq!(f.mode, SnapshotMode::Insert);
1333 assert_eq!(f.cursor_shape, CursorShape::Bar);
1334 }
1335
1336 #[test]
1337 fn snapshot_roundtrips_through_restore() {
1338 use crate::types::SnapshotMode;
1339 let mut e = Editor::new(KeybindingMode::Vim);
1340 e.set_content("alpha\nbeta\ngamma");
1341 e.jump_cursor(2, 3);
1342 let snap = e.take_snapshot();
1343 assert_eq!(snap.mode, SnapshotMode::Normal);
1344 assert_eq!(snap.cursor, (2, 3));
1345 assert_eq!(snap.lines.len(), 3);
1346
1347 let mut other = Editor::new(KeybindingMode::Vim);
1348 other.restore_snapshot(snap).expect("restore");
1349 assert_eq!(other.cursor(), (2, 3));
1350 assert_eq!(other.buffer().lines().len(), 3);
1351 }
1352
1353 #[test]
1354 fn restore_snapshot_rejects_version_mismatch() {
1355 let mut e = Editor::new(KeybindingMode::Vim);
1356 let mut snap = e.take_snapshot();
1357 snap.version = 9999;
1358 match e.restore_snapshot(snap) {
1359 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1360 assert_eq!(got, 9999);
1361 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1362 }
1363 other => panic!("expected SnapshotVersion err, got {other:?}"),
1364 }
1365 }
1366
1367 #[test]
1368 fn take_content_change_returns_some_on_first_dirty() {
1369 let mut e = Editor::new(KeybindingMode::Vim);
1370 e.set_content("hello");
1371 let first = e.take_content_change();
1372 assert!(first.is_some());
1373 let second = e.take_content_change();
1374 assert!(second.is_none());
1375 }
1376
1377 #[test]
1378 fn take_content_change_none_until_mutation() {
1379 let mut e = Editor::new(KeybindingMode::Vim);
1380 e.set_content("hello");
1381 e.take_content_change();
1383 assert!(e.take_content_change().is_none());
1384 e.handle_key(key(KeyCode::Char('i')));
1386 e.handle_key(key(KeyCode::Char('x')));
1387 let after = e.take_content_change();
1388 assert!(after.is_some());
1389 assert!(after.unwrap().contains('x'));
1390 }
1391
1392 #[test]
1393 fn vim_insert_to_normal() {
1394 let mut e = Editor::new(KeybindingMode::Vim);
1395 e.handle_key(key(KeyCode::Char('i')));
1396 e.handle_key(key(KeyCode::Esc));
1397 assert_eq!(e.vim_mode(), VimMode::Normal);
1398 }
1399
1400 #[test]
1401 fn vim_normal_to_visual() {
1402 let mut e = Editor::new(KeybindingMode::Vim);
1403 e.handle_key(key(KeyCode::Char('v')));
1404 assert_eq!(e.vim_mode(), VimMode::Visual);
1405 }
1406
1407 #[test]
1408 fn vim_visual_to_normal() {
1409 let mut e = Editor::new(KeybindingMode::Vim);
1410 e.handle_key(key(KeyCode::Char('v')));
1411 e.handle_key(key(KeyCode::Esc));
1412 assert_eq!(e.vim_mode(), VimMode::Normal);
1413 }
1414
1415 #[test]
1416 fn vim_shift_i_moves_to_first_non_whitespace() {
1417 let mut e = Editor::new(KeybindingMode::Vim);
1418 e.set_content(" hello");
1419 e.jump_cursor(0, 8);
1420 e.handle_key(shift_key(KeyCode::Char('I')));
1421 assert_eq!(e.vim_mode(), VimMode::Insert);
1422 assert_eq!(e.cursor(), (0, 3));
1423 }
1424
1425 #[test]
1426 fn vim_shift_a_moves_to_end_and_insert() {
1427 let mut e = Editor::new(KeybindingMode::Vim);
1428 e.set_content("hello");
1429 e.handle_key(shift_key(KeyCode::Char('A')));
1430 assert_eq!(e.vim_mode(), VimMode::Insert);
1431 assert_eq!(e.cursor().1, 5);
1432 }
1433
1434 #[test]
1435 fn count_10j_moves_down_10() {
1436 let mut e = Editor::new(KeybindingMode::Vim);
1437 e.set_content(
1438 (0..20)
1439 .map(|i| format!("line{i}"))
1440 .collect::<Vec<_>>()
1441 .join("\n")
1442 .as_str(),
1443 );
1444 for d in "10".chars() {
1445 e.handle_key(key(KeyCode::Char(d)));
1446 }
1447 e.handle_key(key(KeyCode::Char('j')));
1448 assert_eq!(e.cursor().0, 10);
1449 }
1450
1451 #[test]
1452 fn count_o_repeats_insert_on_esc() {
1453 let mut e = Editor::new(KeybindingMode::Vim);
1454 e.set_content("hello");
1455 for d in "3".chars() {
1456 e.handle_key(key(KeyCode::Char(d)));
1457 }
1458 e.handle_key(key(KeyCode::Char('o')));
1459 assert_eq!(e.vim_mode(), VimMode::Insert);
1460 for c in "world".chars() {
1461 e.handle_key(key(KeyCode::Char(c)));
1462 }
1463 e.handle_key(key(KeyCode::Esc));
1464 assert_eq!(e.vim_mode(), VimMode::Normal);
1465 assert_eq!(e.buffer().lines().len(), 4);
1466 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1467 }
1468
1469 #[test]
1470 fn count_i_repeats_text_on_esc() {
1471 let mut e = Editor::new(KeybindingMode::Vim);
1472 e.set_content("");
1473 for d in "3".chars() {
1474 e.handle_key(key(KeyCode::Char(d)));
1475 }
1476 e.handle_key(key(KeyCode::Char('i')));
1477 for c in "ab".chars() {
1478 e.handle_key(key(KeyCode::Char(c)));
1479 }
1480 e.handle_key(key(KeyCode::Esc));
1481 assert_eq!(e.vim_mode(), VimMode::Normal);
1482 assert_eq!(e.buffer().lines()[0], "ababab");
1483 }
1484
1485 #[test]
1486 fn vim_shift_o_opens_line_above() {
1487 let mut e = Editor::new(KeybindingMode::Vim);
1488 e.set_content("hello");
1489 e.handle_key(shift_key(KeyCode::Char('O')));
1490 assert_eq!(e.vim_mode(), VimMode::Insert);
1491 assert_eq!(e.cursor(), (0, 0));
1492 assert_eq!(e.buffer().lines().len(), 2);
1493 }
1494
1495 #[test]
1496 fn vim_gg_goes_to_top() {
1497 let mut e = Editor::new(KeybindingMode::Vim);
1498 e.set_content("a\nb\nc");
1499 e.jump_cursor(2, 0);
1500 e.handle_key(key(KeyCode::Char('g')));
1501 e.handle_key(key(KeyCode::Char('g')));
1502 assert_eq!(e.cursor().0, 0);
1503 }
1504
1505 #[test]
1506 fn vim_shift_g_goes_to_bottom() {
1507 let mut e = Editor::new(KeybindingMode::Vim);
1508 e.set_content("a\nb\nc");
1509 e.handle_key(shift_key(KeyCode::Char('G')));
1510 assert_eq!(e.cursor().0, 2);
1511 }
1512
1513 #[test]
1514 fn vim_dd_deletes_line() {
1515 let mut e = Editor::new(KeybindingMode::Vim);
1516 e.set_content("first\nsecond");
1517 e.handle_key(key(KeyCode::Char('d')));
1518 e.handle_key(key(KeyCode::Char('d')));
1519 assert_eq!(e.buffer().lines().len(), 1);
1520 assert_eq!(e.buffer().lines()[0], "second");
1521 }
1522
1523 #[test]
1524 fn vim_dw_deletes_word() {
1525 let mut e = Editor::new(KeybindingMode::Vim);
1526 e.set_content("hello world");
1527 e.handle_key(key(KeyCode::Char('d')));
1528 e.handle_key(key(KeyCode::Char('w')));
1529 assert_eq!(e.vim_mode(), VimMode::Normal);
1530 assert!(!e.buffer().lines()[0].starts_with("hello"));
1531 }
1532
1533 #[test]
1534 fn vim_yy_yanks_line() {
1535 let mut e = Editor::new(KeybindingMode::Vim);
1536 e.set_content("hello\nworld");
1537 e.handle_key(key(KeyCode::Char('y')));
1538 e.handle_key(key(KeyCode::Char('y')));
1539 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1540 }
1541
1542 #[test]
1543 fn vim_yy_does_not_move_cursor() {
1544 let mut e = Editor::new(KeybindingMode::Vim);
1545 e.set_content("first\nsecond\nthird");
1546 e.jump_cursor(1, 0);
1547 let before = e.cursor();
1548 e.handle_key(key(KeyCode::Char('y')));
1549 e.handle_key(key(KeyCode::Char('y')));
1550 assert_eq!(e.cursor(), before);
1551 assert_eq!(e.vim_mode(), VimMode::Normal);
1552 }
1553
1554 #[test]
1555 fn vim_yw_yanks_word() {
1556 let mut e = Editor::new(KeybindingMode::Vim);
1557 e.set_content("hello world");
1558 e.handle_key(key(KeyCode::Char('y')));
1559 e.handle_key(key(KeyCode::Char('w')));
1560 assert_eq!(e.vim_mode(), VimMode::Normal);
1561 assert!(e.last_yank.is_some());
1562 }
1563
1564 #[test]
1565 fn vim_cc_changes_line() {
1566 let mut e = Editor::new(KeybindingMode::Vim);
1567 e.set_content("hello\nworld");
1568 e.handle_key(key(KeyCode::Char('c')));
1569 e.handle_key(key(KeyCode::Char('c')));
1570 assert_eq!(e.vim_mode(), VimMode::Insert);
1571 }
1572
1573 #[test]
1574 fn vim_u_undoes_insert_session_as_chunk() {
1575 let mut e = Editor::new(KeybindingMode::Vim);
1576 e.set_content("hello");
1577 e.handle_key(key(KeyCode::Char('i')));
1578 e.handle_key(key(KeyCode::Enter));
1579 e.handle_key(key(KeyCode::Enter));
1580 e.handle_key(key(KeyCode::Esc));
1581 assert_eq!(e.buffer().lines().len(), 3);
1582 e.handle_key(key(KeyCode::Char('u')));
1583 assert_eq!(e.buffer().lines().len(), 1);
1584 assert_eq!(e.buffer().lines()[0], "hello");
1585 }
1586
1587 #[test]
1588 fn vim_undo_redo_roundtrip() {
1589 let mut e = Editor::new(KeybindingMode::Vim);
1590 e.set_content("hello");
1591 e.handle_key(key(KeyCode::Char('i')));
1592 for c in "world".chars() {
1593 e.handle_key(key(KeyCode::Char(c)));
1594 }
1595 e.handle_key(key(KeyCode::Esc));
1596 let after = e.buffer().lines()[0].clone();
1597 e.handle_key(key(KeyCode::Char('u')));
1598 assert_eq!(e.buffer().lines()[0], "hello");
1599 e.handle_key(ctrl_key(KeyCode::Char('r')));
1600 assert_eq!(e.buffer().lines()[0], after);
1601 }
1602
1603 #[test]
1604 fn vim_u_undoes_dd() {
1605 let mut e = Editor::new(KeybindingMode::Vim);
1606 e.set_content("first\nsecond");
1607 e.handle_key(key(KeyCode::Char('d')));
1608 e.handle_key(key(KeyCode::Char('d')));
1609 assert_eq!(e.buffer().lines().len(), 1);
1610 e.handle_key(key(KeyCode::Char('u')));
1611 assert_eq!(e.buffer().lines().len(), 2);
1612 assert_eq!(e.buffer().lines()[0], "first");
1613 }
1614
1615 #[test]
1616 fn vim_ctrl_r_redoes() {
1617 let mut e = Editor::new(KeybindingMode::Vim);
1618 e.set_content("hello");
1619 e.handle_key(ctrl_key(KeyCode::Char('r')));
1620 }
1621
1622 #[test]
1623 fn vim_r_replaces_char() {
1624 let mut e = Editor::new(KeybindingMode::Vim);
1625 e.set_content("hello");
1626 e.handle_key(key(KeyCode::Char('r')));
1627 e.handle_key(key(KeyCode::Char('x')));
1628 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1629 }
1630
1631 #[test]
1632 fn vim_tilde_toggles_case() {
1633 let mut e = Editor::new(KeybindingMode::Vim);
1634 e.set_content("hello");
1635 e.handle_key(key(KeyCode::Char('~')));
1636 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1637 }
1638
1639 #[test]
1640 fn vim_visual_d_cuts() {
1641 let mut e = Editor::new(KeybindingMode::Vim);
1642 e.set_content("hello");
1643 e.handle_key(key(KeyCode::Char('v')));
1644 e.handle_key(key(KeyCode::Char('l')));
1645 e.handle_key(key(KeyCode::Char('l')));
1646 e.handle_key(key(KeyCode::Char('d')));
1647 assert_eq!(e.vim_mode(), VimMode::Normal);
1648 assert!(e.last_yank.is_some());
1649 }
1650
1651 #[test]
1652 fn vim_visual_c_enters_insert() {
1653 let mut e = Editor::new(KeybindingMode::Vim);
1654 e.set_content("hello");
1655 e.handle_key(key(KeyCode::Char('v')));
1656 e.handle_key(key(KeyCode::Char('l')));
1657 e.handle_key(key(KeyCode::Char('c')));
1658 assert_eq!(e.vim_mode(), VimMode::Insert);
1659 }
1660
1661 #[test]
1662 fn vim_normal_unknown_key_consumed() {
1663 let mut e = Editor::new(KeybindingMode::Vim);
1664 let consumed = e.handle_key(key(KeyCode::Char('z')));
1666 assert!(consumed);
1667 }
1668
1669 #[test]
1670 fn force_normal_clears_operator() {
1671 let mut e = Editor::new(KeybindingMode::Vim);
1672 e.handle_key(key(KeyCode::Char('d')));
1673 e.force_normal();
1674 assert_eq!(e.vim_mode(), VimMode::Normal);
1675 }
1676
1677 fn many_lines(n: usize) -> String {
1678 (0..n)
1679 .map(|i| format!("line{i}"))
1680 .collect::<Vec<_>>()
1681 .join("\n")
1682 }
1683
1684 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1685 e.set_viewport_height(height);
1686 }
1687
1688 #[test]
1689 fn zz_centers_cursor_in_viewport() {
1690 let mut e = Editor::new(KeybindingMode::Vim);
1691 e.set_content(&many_lines(100));
1692 prime_viewport(&mut e, 20);
1693 e.jump_cursor(50, 0);
1694 e.handle_key(key(KeyCode::Char('z')));
1695 e.handle_key(key(KeyCode::Char('z')));
1696 assert_eq!(e.buffer().viewport().top_row, 40);
1697 assert_eq!(e.cursor().0, 50);
1698 }
1699
1700 #[test]
1701 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1702 let mut e = Editor::new(KeybindingMode::Vim);
1703 e.set_content(&many_lines(100));
1704 prime_viewport(&mut e, 20);
1705 e.jump_cursor(50, 0);
1706 e.handle_key(key(KeyCode::Char('z')));
1707 e.handle_key(key(KeyCode::Char('t')));
1708 assert_eq!(e.buffer().viewport().top_row, 45);
1711 assert_eq!(e.cursor().0, 50);
1712 }
1713
1714 #[test]
1715 fn ctrl_a_increments_number_at_cursor() {
1716 let mut e = Editor::new(KeybindingMode::Vim);
1717 e.set_content("x = 41");
1718 e.handle_key(ctrl_key(KeyCode::Char('a')));
1719 assert_eq!(e.buffer().lines()[0], "x = 42");
1720 assert_eq!(e.cursor(), (0, 5));
1721 }
1722
1723 #[test]
1724 fn ctrl_a_finds_number_to_right_of_cursor() {
1725 let mut e = Editor::new(KeybindingMode::Vim);
1726 e.set_content("foo 99 bar");
1727 e.handle_key(ctrl_key(KeyCode::Char('a')));
1728 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1729 assert_eq!(e.cursor(), (0, 6));
1730 }
1731
1732 #[test]
1733 fn ctrl_a_with_count_adds_count() {
1734 let mut e = Editor::new(KeybindingMode::Vim);
1735 e.set_content("x = 10");
1736 for d in "5".chars() {
1737 e.handle_key(key(KeyCode::Char(d)));
1738 }
1739 e.handle_key(ctrl_key(KeyCode::Char('a')));
1740 assert_eq!(e.buffer().lines()[0], "x = 15");
1741 }
1742
1743 #[test]
1744 fn ctrl_x_decrements_number() {
1745 let mut e = Editor::new(KeybindingMode::Vim);
1746 e.set_content("n=5");
1747 e.handle_key(ctrl_key(KeyCode::Char('x')));
1748 assert_eq!(e.buffer().lines()[0], "n=4");
1749 }
1750
1751 #[test]
1752 fn ctrl_x_crosses_zero_into_negative() {
1753 let mut e = Editor::new(KeybindingMode::Vim);
1754 e.set_content("v=0");
1755 e.handle_key(ctrl_key(KeyCode::Char('x')));
1756 assert_eq!(e.buffer().lines()[0], "v=-1");
1757 }
1758
1759 #[test]
1760 fn ctrl_a_on_negative_number_increments_toward_zero() {
1761 let mut e = Editor::new(KeybindingMode::Vim);
1762 e.set_content("a = -5");
1763 e.handle_key(ctrl_key(KeyCode::Char('a')));
1764 assert_eq!(e.buffer().lines()[0], "a = -4");
1765 }
1766
1767 #[test]
1768 fn ctrl_a_noop_when_no_digit_on_line() {
1769 let mut e = Editor::new(KeybindingMode::Vim);
1770 e.set_content("no digits here");
1771 e.handle_key(ctrl_key(KeyCode::Char('a')));
1772 assert_eq!(e.buffer().lines()[0], "no digits here");
1773 }
1774
1775 #[test]
1776 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1777 let mut e = Editor::new(KeybindingMode::Vim);
1778 e.set_content(&many_lines(100));
1779 prime_viewport(&mut e, 20);
1780 e.jump_cursor(50, 0);
1781 e.handle_key(key(KeyCode::Char('z')));
1782 e.handle_key(key(KeyCode::Char('b')));
1783 assert_eq!(e.buffer().viewport().top_row, 36);
1787 assert_eq!(e.cursor().0, 50);
1788 }
1789
1790 #[test]
1797 fn set_content_dirties_then_take_dirty_clears() {
1798 let mut e = Editor::new(KeybindingMode::Vim);
1799 e.set_content("hello");
1800 assert!(
1801 e.take_dirty(),
1802 "set_content should leave content_dirty=true"
1803 );
1804 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1805 }
1806
1807 #[test]
1808 fn content_arc_returns_same_arc_until_mutation() {
1809 let mut e = Editor::new(KeybindingMode::Vim);
1810 e.set_content("hello");
1811 let a = e.content_arc();
1812 let b = e.content_arc();
1813 assert!(
1814 std::sync::Arc::ptr_eq(&a, &b),
1815 "repeated content_arc() should hit the cache"
1816 );
1817
1818 e.handle_key(key(KeyCode::Char('i')));
1820 e.handle_key(key(KeyCode::Char('!')));
1821 let c = e.content_arc();
1822 assert!(
1823 !std::sync::Arc::ptr_eq(&a, &c),
1824 "mutation should invalidate content_arc() cache"
1825 );
1826 assert!(c.contains('!'));
1827 }
1828
1829 #[test]
1830 fn content_arc_cache_invalidated_by_set_content() {
1831 let mut e = Editor::new(KeybindingMode::Vim);
1832 e.set_content("one");
1833 let a = e.content_arc();
1834 e.set_content("two");
1835 let b = e.content_arc();
1836 assert!(!std::sync::Arc::ptr_eq(&a, &b));
1837 assert!(b.starts_with("two"));
1838 }
1839
1840 #[test]
1846 fn mouse_click_past_eol_lands_on_last_char() {
1847 let mut e = Editor::new(KeybindingMode::Vim);
1848 e.set_content("hello");
1849 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1853 e.mouse_click(area, 78, 1);
1854 assert_eq!(e.cursor(), (0, 4));
1855 }
1856
1857 #[test]
1858 fn mouse_click_past_eol_handles_multibyte_line() {
1859 let mut e = Editor::new(KeybindingMode::Vim);
1860 e.set_content("héllo");
1863 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_inside_line_lands_on_clicked_char() {
1870 let mut e = Editor::new(KeybindingMode::Vim);
1871 e.set_content("hello world");
1872 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1875 e.mouse_click(area, 4, 1);
1876 assert_eq!(e.cursor(), (0, 0));
1877 e.mouse_click(area, 6, 1);
1878 assert_eq!(e.cursor(), (0, 2));
1879 }
1880}