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