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 take_snapshot(&self) -> crate::types::EditorSnapshot {
718 use crate::types::{EditorSnapshot, SnapshotMode};
719 let mode = match self.vim_mode() {
720 crate::VimMode::Normal => SnapshotMode::Normal,
721 crate::VimMode::Insert => SnapshotMode::Insert,
722 crate::VimMode::Visual => SnapshotMode::Visual,
723 crate::VimMode::VisualLine => SnapshotMode::VisualLine,
724 crate::VimMode::VisualBlock => SnapshotMode::VisualBlock,
725 };
726 let cursor = self.cursor();
727 let cursor = (cursor.0 as u32, cursor.1 as u32);
728 let lines: Vec<String> = self.buffer.lines().to_vec();
729 let viewport_top = self.buffer.viewport().top_row as u32;
730 EditorSnapshot {
731 version: EditorSnapshot::VERSION,
732 mode,
733 cursor,
734 lines,
735 viewport_top,
736 }
737 }
738
739 pub fn restore_snapshot(
747 &mut self,
748 snap: crate::types::EditorSnapshot,
749 ) -> Result<(), crate::EngineError> {
750 use crate::types::EditorSnapshot;
751 if snap.version != EditorSnapshot::VERSION {
752 return Err(crate::EngineError::SnapshotVersion(
753 snap.version,
754 EditorSnapshot::VERSION,
755 ));
756 }
757 let text = snap.lines.join("\n");
758 self.set_content(&text);
759 self.jump_cursor(snap.cursor.0 as usize, snap.cursor.1 as usize);
760 let mut vp = self.buffer.viewport();
761 vp.top_row = snap.viewport_top as usize;
762 *self.buffer.viewport_mut() = vp;
763 Ok(())
764 }
765
766 pub fn seed_yank(&mut self, text: String) {
770 let linewise = text.ends_with('\n');
771 self.vim.yank_linewise = linewise;
772 self.registers.unnamed = crate::registers::Slot { text, linewise };
773 }
774
775 pub fn scroll_down(&mut self, rows: i16) {
780 self.scroll_viewport(rows);
781 }
782
783 pub fn scroll_up(&mut self, rows: i16) {
787 self.scroll_viewport(-rows);
788 }
789
790 const SCROLLOFF: usize = 5;
794
795 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
800 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
801 if height == 0 {
802 self.buffer.ensure_cursor_visible();
803 return;
804 }
805 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
809 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
812 self.ensure_scrolloff_wrap(height, margin);
813 return;
814 }
815 let cursor_row = self.buffer.cursor().row;
816 let last_row = self.buffer.row_count().saturating_sub(1);
817 let v = self.buffer.viewport_mut();
818 if cursor_row < v.top_row + margin {
820 v.top_row = cursor_row.saturating_sub(margin);
821 }
822 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
824 if cursor_row > v.top_row + max_bottom {
825 v.top_row = cursor_row.saturating_sub(max_bottom);
826 }
827 let max_top = last_row.saturating_sub(height.saturating_sub(1));
829 if v.top_row > max_top {
830 v.top_row = max_top;
831 }
832 let cursor = self.buffer.cursor();
835 self.buffer.viewport_mut().ensure_visible(cursor);
836 }
837
838 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
843 let cursor_row = self.buffer.cursor().row;
844 if cursor_row < self.buffer.viewport().top_row {
847 self.buffer.viewport_mut().top_row = cursor_row;
848 self.buffer.viewport_mut().top_col = 0;
849 }
850 let max_csr = height.saturating_sub(1).saturating_sub(margin);
853 loop {
854 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
855 if csr <= max_csr {
856 break;
857 }
858 let top = self.buffer.viewport().top_row;
859 let Some(next) = self.buffer.next_visible_row(top) else {
860 break;
861 };
862 if next > cursor_row {
864 self.buffer.viewport_mut().top_row = cursor_row;
865 break;
866 }
867 self.buffer.viewport_mut().top_row = next;
868 }
869 loop {
872 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
873 if csr >= margin {
874 break;
875 }
876 let top = self.buffer.viewport().top_row;
877 let Some(prev) = self.buffer.prev_visible_row(top) else {
878 break;
879 };
880 self.buffer.viewport_mut().top_row = prev;
881 }
882 let max_top = self.buffer.max_top_for_height(height);
887 if self.buffer.viewport().top_row > max_top {
888 self.buffer.viewport_mut().top_row = max_top;
889 }
890 self.buffer.viewport_mut().top_col = 0;
891 }
892
893 fn scroll_viewport(&mut self, delta: i16) {
894 if delta == 0 {
895 return;
896 }
897 let total_rows = self.buffer.row_count() as isize;
899 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
900 let cur_top = self.buffer.viewport().top_row as isize;
901 let new_top = (cur_top + delta as isize)
902 .max(0)
903 .min((total_rows - 1).max(0)) as usize;
904 self.buffer.viewport_mut().top_row = new_top;
905 let _ = cur_top;
908 if height == 0 {
909 return;
910 }
911 let cursor = self.buffer.cursor();
914 let margin = Self::SCROLLOFF.min(height / 2);
915 let min_row = new_top + margin;
916 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
917 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
918 if target_row != cursor.row {
919 let line_len = self
920 .buffer
921 .line(target_row)
922 .map(|l| l.chars().count())
923 .unwrap_or(0);
924 let target_col = cursor.col.min(line_len.saturating_sub(1));
925 self.buffer
926 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
927 }
928 }
929
930 pub fn goto_line(&mut self, line: usize) {
931 let row = line.saturating_sub(1);
932 let max = self.buffer.row_count().saturating_sub(1);
933 let target = row.min(max);
934 self.buffer
935 .set_cursor(hjkl_buffer::Position::new(target, 0));
936 }
937
938 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
942 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
943 if height == 0 {
944 return;
945 }
946 let cur_row = self.buffer.cursor().row;
947 let cur_top = self.buffer.viewport().top_row;
948 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
954 let new_top = match pos {
955 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
956 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
957 CursorScrollTarget::Bottom => {
958 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
959 }
960 };
961 if new_top == cur_top {
962 return;
963 }
964 self.buffer.viewport_mut().top_row = new_top;
965 }
966
967 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
974 let lines = self.buffer.lines();
975 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
977 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
978 let rel_row = row.saturating_sub(inner_top) as usize;
979 let top = self.buffer.viewport().top_row;
980 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
981 let rel_col = col.saturating_sub(content_x) as usize;
982 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
983 let last_col = line_chars.saturating_sub(1);
984 (doc_row, rel_col.min(last_col))
985 }
986
987 pub fn jump_to(&mut self, line: usize, col: usize) {
989 let r = line.saturating_sub(1);
990 let max_row = self.buffer.row_count().saturating_sub(1);
991 let r = r.min(max_row);
992 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
993 let c = col.saturating_sub(1).min(line_len);
994 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
995 }
996
997 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
999 if self.vim.is_visual() {
1000 self.vim.force_normal();
1001 }
1002 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1003 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1004 }
1005
1006 pub fn mouse_begin_drag(&mut self) {
1008 if !self.vim.is_visual_char() {
1009 let cursor = self.cursor();
1010 self.vim.enter_visual(cursor);
1011 }
1012 }
1013
1014 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
1016 let (r, c) = self.mouse_to_doc_pos(area, col, row);
1017 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
1018 }
1019
1020 pub fn insert_str(&mut self, text: &str) {
1021 let pos = self.buffer.cursor();
1022 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
1023 at: pos,
1024 text: text.to_string(),
1025 });
1026 self.push_buffer_content_to_textarea();
1027 self.mark_content_dirty();
1028 }
1029
1030 pub fn accept_completion(&mut self, completion: &str) {
1031 use hjkl_buffer::{Edit, MotionKind, Position};
1032 let cursor = self.buffer.cursor();
1033 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
1034 let chars: Vec<char> = line.chars().collect();
1035 let prefix_len = chars[..cursor.col.min(chars.len())]
1036 .iter()
1037 .rev()
1038 .take_while(|c| c.is_alphanumeric() || **c == '_')
1039 .count();
1040 if prefix_len > 0 {
1041 let start = Position::new(cursor.row, cursor.col - prefix_len);
1042 self.buffer.apply_edit(Edit::DeleteRange {
1043 start,
1044 end: cursor,
1045 kind: MotionKind::Char,
1046 });
1047 }
1048 let cursor = self.buffer.cursor();
1049 self.buffer.apply_edit(Edit::InsertStr {
1050 at: cursor,
1051 text: completion.to_string(),
1052 });
1053 self.push_buffer_content_to_textarea();
1054 self.mark_content_dirty();
1055 }
1056
1057 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
1058 let pos = self.buffer.cursor();
1059 (self.buffer.lines().to_vec(), (pos.row, pos.col))
1060 }
1061
1062 #[doc(hidden)]
1063 pub fn push_undo(&mut self) {
1064 let snap = self.snapshot();
1065 if self.undo_stack.len() >= 200 {
1066 self.undo_stack.remove(0);
1067 }
1068 self.undo_stack.push(snap);
1069 self.redo_stack.clear();
1070 }
1071
1072 #[doc(hidden)]
1073 pub fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1074 let text = lines.join("\n");
1075 self.buffer.replace_all(&text);
1076 self.buffer
1077 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1078 self.mark_content_dirty();
1079 }
1080
1081 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1083 let input = crossterm_to_input(key);
1084 if input.key == Key::Null {
1085 return false;
1086 }
1087 vim::step(self, input)
1088 }
1089}
1090
1091pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1092 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1093 let alt = key.modifiers.contains(KeyModifiers::ALT);
1094 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1095 let k = match key.code {
1096 KeyCode::Char(c) => Key::Char(c),
1097 KeyCode::Backspace => Key::Backspace,
1098 KeyCode::Delete => Key::Delete,
1099 KeyCode::Enter => Key::Enter,
1100 KeyCode::Left => Key::Left,
1101 KeyCode::Right => Key::Right,
1102 KeyCode::Up => Key::Up,
1103 KeyCode::Down => Key::Down,
1104 KeyCode::Home => Key::Home,
1105 KeyCode::End => Key::End,
1106 KeyCode::Tab => Key::Tab,
1107 KeyCode::Esc => Key::Esc,
1108 _ => Key::Null,
1109 };
1110 Input {
1111 key: k,
1112 ctrl,
1113 alt,
1114 shift,
1115 }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use crossterm::event::KeyEvent;
1122
1123 fn key(code: KeyCode) -> KeyEvent {
1124 KeyEvent::new(code, KeyModifiers::NONE)
1125 }
1126 fn shift_key(code: KeyCode) -> KeyEvent {
1127 KeyEvent::new(code, KeyModifiers::SHIFT)
1128 }
1129 fn ctrl_key(code: KeyCode) -> KeyEvent {
1130 KeyEvent::new(code, KeyModifiers::CONTROL)
1131 }
1132
1133 #[test]
1134 fn vim_normal_to_insert() {
1135 let mut e = Editor::new(KeybindingMode::Vim);
1136 e.handle_key(key(KeyCode::Char('i')));
1137 assert_eq!(e.vim_mode(), VimMode::Insert);
1138 }
1139
1140 #[test]
1141 fn snapshot_roundtrips_through_restore() {
1142 use crate::types::SnapshotMode;
1143 let mut e = Editor::new(KeybindingMode::Vim);
1144 e.set_content("alpha\nbeta\ngamma");
1145 e.jump_cursor(2, 3);
1146 let snap = e.take_snapshot();
1147 assert_eq!(snap.mode, SnapshotMode::Normal);
1148 assert_eq!(snap.cursor, (2, 3));
1149 assert_eq!(snap.lines.len(), 3);
1150
1151 let mut other = Editor::new(KeybindingMode::Vim);
1152 other.restore_snapshot(snap).expect("restore");
1153 assert_eq!(other.cursor(), (2, 3));
1154 assert_eq!(other.buffer().lines().len(), 3);
1155 }
1156
1157 #[test]
1158 fn restore_snapshot_rejects_version_mismatch() {
1159 let mut e = Editor::new(KeybindingMode::Vim);
1160 let mut snap = e.take_snapshot();
1161 snap.version = 9999;
1162 match e.restore_snapshot(snap) {
1163 Err(crate::EngineError::SnapshotVersion(got, want)) => {
1164 assert_eq!(got, 9999);
1165 assert_eq!(want, crate::types::EditorSnapshot::VERSION);
1166 }
1167 other => panic!("expected SnapshotVersion err, got {other:?}"),
1168 }
1169 }
1170
1171 #[test]
1172 fn take_content_change_returns_some_on_first_dirty() {
1173 let mut e = Editor::new(KeybindingMode::Vim);
1174 e.set_content("hello");
1175 let first = e.take_content_change();
1176 assert!(first.is_some());
1177 let second = e.take_content_change();
1178 assert!(second.is_none());
1179 }
1180
1181 #[test]
1182 fn take_content_change_none_until_mutation() {
1183 let mut e = Editor::new(KeybindingMode::Vim);
1184 e.set_content("hello");
1185 e.take_content_change();
1187 assert!(e.take_content_change().is_none());
1188 e.handle_key(key(KeyCode::Char('i')));
1190 e.handle_key(key(KeyCode::Char('x')));
1191 let after = e.take_content_change();
1192 assert!(after.is_some());
1193 assert!(after.unwrap().contains('x'));
1194 }
1195
1196 #[test]
1197 fn vim_insert_to_normal() {
1198 let mut e = Editor::new(KeybindingMode::Vim);
1199 e.handle_key(key(KeyCode::Char('i')));
1200 e.handle_key(key(KeyCode::Esc));
1201 assert_eq!(e.vim_mode(), VimMode::Normal);
1202 }
1203
1204 #[test]
1205 fn vim_normal_to_visual() {
1206 let mut e = Editor::new(KeybindingMode::Vim);
1207 e.handle_key(key(KeyCode::Char('v')));
1208 assert_eq!(e.vim_mode(), VimMode::Visual);
1209 }
1210
1211 #[test]
1212 fn vim_visual_to_normal() {
1213 let mut e = Editor::new(KeybindingMode::Vim);
1214 e.handle_key(key(KeyCode::Char('v')));
1215 e.handle_key(key(KeyCode::Esc));
1216 assert_eq!(e.vim_mode(), VimMode::Normal);
1217 }
1218
1219 #[test]
1220 fn vim_shift_i_moves_to_first_non_whitespace() {
1221 let mut e = Editor::new(KeybindingMode::Vim);
1222 e.set_content(" hello");
1223 e.jump_cursor(0, 8);
1224 e.handle_key(shift_key(KeyCode::Char('I')));
1225 assert_eq!(e.vim_mode(), VimMode::Insert);
1226 assert_eq!(e.cursor(), (0, 3));
1227 }
1228
1229 #[test]
1230 fn vim_shift_a_moves_to_end_and_insert() {
1231 let mut e = Editor::new(KeybindingMode::Vim);
1232 e.set_content("hello");
1233 e.handle_key(shift_key(KeyCode::Char('A')));
1234 assert_eq!(e.vim_mode(), VimMode::Insert);
1235 assert_eq!(e.cursor().1, 5);
1236 }
1237
1238 #[test]
1239 fn count_10j_moves_down_10() {
1240 let mut e = Editor::new(KeybindingMode::Vim);
1241 e.set_content(
1242 (0..20)
1243 .map(|i| format!("line{i}"))
1244 .collect::<Vec<_>>()
1245 .join("\n")
1246 .as_str(),
1247 );
1248 for d in "10".chars() {
1249 e.handle_key(key(KeyCode::Char(d)));
1250 }
1251 e.handle_key(key(KeyCode::Char('j')));
1252 assert_eq!(e.cursor().0, 10);
1253 }
1254
1255 #[test]
1256 fn count_o_repeats_insert_on_esc() {
1257 let mut e = Editor::new(KeybindingMode::Vim);
1258 e.set_content("hello");
1259 for d in "3".chars() {
1260 e.handle_key(key(KeyCode::Char(d)));
1261 }
1262 e.handle_key(key(KeyCode::Char('o')));
1263 assert_eq!(e.vim_mode(), VimMode::Insert);
1264 for c in "world".chars() {
1265 e.handle_key(key(KeyCode::Char(c)));
1266 }
1267 e.handle_key(key(KeyCode::Esc));
1268 assert_eq!(e.vim_mode(), VimMode::Normal);
1269 assert_eq!(e.buffer().lines().len(), 4);
1270 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1271 }
1272
1273 #[test]
1274 fn count_i_repeats_text_on_esc() {
1275 let mut e = Editor::new(KeybindingMode::Vim);
1276 e.set_content("");
1277 for d in "3".chars() {
1278 e.handle_key(key(KeyCode::Char(d)));
1279 }
1280 e.handle_key(key(KeyCode::Char('i')));
1281 for c in "ab".chars() {
1282 e.handle_key(key(KeyCode::Char(c)));
1283 }
1284 e.handle_key(key(KeyCode::Esc));
1285 assert_eq!(e.vim_mode(), VimMode::Normal);
1286 assert_eq!(e.buffer().lines()[0], "ababab");
1287 }
1288
1289 #[test]
1290 fn vim_shift_o_opens_line_above() {
1291 let mut e = Editor::new(KeybindingMode::Vim);
1292 e.set_content("hello");
1293 e.handle_key(shift_key(KeyCode::Char('O')));
1294 assert_eq!(e.vim_mode(), VimMode::Insert);
1295 assert_eq!(e.cursor(), (0, 0));
1296 assert_eq!(e.buffer().lines().len(), 2);
1297 }
1298
1299 #[test]
1300 fn vim_gg_goes_to_top() {
1301 let mut e = Editor::new(KeybindingMode::Vim);
1302 e.set_content("a\nb\nc");
1303 e.jump_cursor(2, 0);
1304 e.handle_key(key(KeyCode::Char('g')));
1305 e.handle_key(key(KeyCode::Char('g')));
1306 assert_eq!(e.cursor().0, 0);
1307 }
1308
1309 #[test]
1310 fn vim_shift_g_goes_to_bottom() {
1311 let mut e = Editor::new(KeybindingMode::Vim);
1312 e.set_content("a\nb\nc");
1313 e.handle_key(shift_key(KeyCode::Char('G')));
1314 assert_eq!(e.cursor().0, 2);
1315 }
1316
1317 #[test]
1318 fn vim_dd_deletes_line() {
1319 let mut e = Editor::new(KeybindingMode::Vim);
1320 e.set_content("first\nsecond");
1321 e.handle_key(key(KeyCode::Char('d')));
1322 e.handle_key(key(KeyCode::Char('d')));
1323 assert_eq!(e.buffer().lines().len(), 1);
1324 assert_eq!(e.buffer().lines()[0], "second");
1325 }
1326
1327 #[test]
1328 fn vim_dw_deletes_word() {
1329 let mut e = Editor::new(KeybindingMode::Vim);
1330 e.set_content("hello world");
1331 e.handle_key(key(KeyCode::Char('d')));
1332 e.handle_key(key(KeyCode::Char('w')));
1333 assert_eq!(e.vim_mode(), VimMode::Normal);
1334 assert!(!e.buffer().lines()[0].starts_with("hello"));
1335 }
1336
1337 #[test]
1338 fn vim_yy_yanks_line() {
1339 let mut e = Editor::new(KeybindingMode::Vim);
1340 e.set_content("hello\nworld");
1341 e.handle_key(key(KeyCode::Char('y')));
1342 e.handle_key(key(KeyCode::Char('y')));
1343 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1344 }
1345
1346 #[test]
1347 fn vim_yy_does_not_move_cursor() {
1348 let mut e = Editor::new(KeybindingMode::Vim);
1349 e.set_content("first\nsecond\nthird");
1350 e.jump_cursor(1, 0);
1351 let before = e.cursor();
1352 e.handle_key(key(KeyCode::Char('y')));
1353 e.handle_key(key(KeyCode::Char('y')));
1354 assert_eq!(e.cursor(), before);
1355 assert_eq!(e.vim_mode(), VimMode::Normal);
1356 }
1357
1358 #[test]
1359 fn vim_yw_yanks_word() {
1360 let mut e = Editor::new(KeybindingMode::Vim);
1361 e.set_content("hello world");
1362 e.handle_key(key(KeyCode::Char('y')));
1363 e.handle_key(key(KeyCode::Char('w')));
1364 assert_eq!(e.vim_mode(), VimMode::Normal);
1365 assert!(e.last_yank.is_some());
1366 }
1367
1368 #[test]
1369 fn vim_cc_changes_line() {
1370 let mut e = Editor::new(KeybindingMode::Vim);
1371 e.set_content("hello\nworld");
1372 e.handle_key(key(KeyCode::Char('c')));
1373 e.handle_key(key(KeyCode::Char('c')));
1374 assert_eq!(e.vim_mode(), VimMode::Insert);
1375 }
1376
1377 #[test]
1378 fn vim_u_undoes_insert_session_as_chunk() {
1379 let mut e = Editor::new(KeybindingMode::Vim);
1380 e.set_content("hello");
1381 e.handle_key(key(KeyCode::Char('i')));
1382 e.handle_key(key(KeyCode::Enter));
1383 e.handle_key(key(KeyCode::Enter));
1384 e.handle_key(key(KeyCode::Esc));
1385 assert_eq!(e.buffer().lines().len(), 3);
1386 e.handle_key(key(KeyCode::Char('u')));
1387 assert_eq!(e.buffer().lines().len(), 1);
1388 assert_eq!(e.buffer().lines()[0], "hello");
1389 }
1390
1391 #[test]
1392 fn vim_undo_redo_roundtrip() {
1393 let mut e = Editor::new(KeybindingMode::Vim);
1394 e.set_content("hello");
1395 e.handle_key(key(KeyCode::Char('i')));
1396 for c in "world".chars() {
1397 e.handle_key(key(KeyCode::Char(c)));
1398 }
1399 e.handle_key(key(KeyCode::Esc));
1400 let after = e.buffer().lines()[0].clone();
1401 e.handle_key(key(KeyCode::Char('u')));
1402 assert_eq!(e.buffer().lines()[0], "hello");
1403 e.handle_key(ctrl_key(KeyCode::Char('r')));
1404 assert_eq!(e.buffer().lines()[0], after);
1405 }
1406
1407 #[test]
1408 fn vim_u_undoes_dd() {
1409 let mut e = Editor::new(KeybindingMode::Vim);
1410 e.set_content("first\nsecond");
1411 e.handle_key(key(KeyCode::Char('d')));
1412 e.handle_key(key(KeyCode::Char('d')));
1413 assert_eq!(e.buffer().lines().len(), 1);
1414 e.handle_key(key(KeyCode::Char('u')));
1415 assert_eq!(e.buffer().lines().len(), 2);
1416 assert_eq!(e.buffer().lines()[0], "first");
1417 }
1418
1419 #[test]
1420 fn vim_ctrl_r_redoes() {
1421 let mut e = Editor::new(KeybindingMode::Vim);
1422 e.set_content("hello");
1423 e.handle_key(ctrl_key(KeyCode::Char('r')));
1424 }
1425
1426 #[test]
1427 fn vim_r_replaces_char() {
1428 let mut e = Editor::new(KeybindingMode::Vim);
1429 e.set_content("hello");
1430 e.handle_key(key(KeyCode::Char('r')));
1431 e.handle_key(key(KeyCode::Char('x')));
1432 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1433 }
1434
1435 #[test]
1436 fn vim_tilde_toggles_case() {
1437 let mut e = Editor::new(KeybindingMode::Vim);
1438 e.set_content("hello");
1439 e.handle_key(key(KeyCode::Char('~')));
1440 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1441 }
1442
1443 #[test]
1444 fn vim_visual_d_cuts() {
1445 let mut e = Editor::new(KeybindingMode::Vim);
1446 e.set_content("hello");
1447 e.handle_key(key(KeyCode::Char('v')));
1448 e.handle_key(key(KeyCode::Char('l')));
1449 e.handle_key(key(KeyCode::Char('l')));
1450 e.handle_key(key(KeyCode::Char('d')));
1451 assert_eq!(e.vim_mode(), VimMode::Normal);
1452 assert!(e.last_yank.is_some());
1453 }
1454
1455 #[test]
1456 fn vim_visual_c_enters_insert() {
1457 let mut e = Editor::new(KeybindingMode::Vim);
1458 e.set_content("hello");
1459 e.handle_key(key(KeyCode::Char('v')));
1460 e.handle_key(key(KeyCode::Char('l')));
1461 e.handle_key(key(KeyCode::Char('c')));
1462 assert_eq!(e.vim_mode(), VimMode::Insert);
1463 }
1464
1465 #[test]
1466 fn vim_normal_unknown_key_consumed() {
1467 let mut e = Editor::new(KeybindingMode::Vim);
1468 let consumed = e.handle_key(key(KeyCode::Char('z')));
1470 assert!(consumed);
1471 }
1472
1473 #[test]
1474 fn force_normal_clears_operator() {
1475 let mut e = Editor::new(KeybindingMode::Vim);
1476 e.handle_key(key(KeyCode::Char('d')));
1477 e.force_normal();
1478 assert_eq!(e.vim_mode(), VimMode::Normal);
1479 }
1480
1481 fn many_lines(n: usize) -> String {
1482 (0..n)
1483 .map(|i| format!("line{i}"))
1484 .collect::<Vec<_>>()
1485 .join("\n")
1486 }
1487
1488 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1489 e.set_viewport_height(height);
1490 }
1491
1492 #[test]
1493 fn zz_centers_cursor_in_viewport() {
1494 let mut e = Editor::new(KeybindingMode::Vim);
1495 e.set_content(&many_lines(100));
1496 prime_viewport(&mut e, 20);
1497 e.jump_cursor(50, 0);
1498 e.handle_key(key(KeyCode::Char('z')));
1499 e.handle_key(key(KeyCode::Char('z')));
1500 assert_eq!(e.buffer().viewport().top_row, 40);
1501 assert_eq!(e.cursor().0, 50);
1502 }
1503
1504 #[test]
1505 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1506 let mut e = Editor::new(KeybindingMode::Vim);
1507 e.set_content(&many_lines(100));
1508 prime_viewport(&mut e, 20);
1509 e.jump_cursor(50, 0);
1510 e.handle_key(key(KeyCode::Char('z')));
1511 e.handle_key(key(KeyCode::Char('t')));
1512 assert_eq!(e.buffer().viewport().top_row, 45);
1515 assert_eq!(e.cursor().0, 50);
1516 }
1517
1518 #[test]
1519 fn ctrl_a_increments_number_at_cursor() {
1520 let mut e = Editor::new(KeybindingMode::Vim);
1521 e.set_content("x = 41");
1522 e.handle_key(ctrl_key(KeyCode::Char('a')));
1523 assert_eq!(e.buffer().lines()[0], "x = 42");
1524 assert_eq!(e.cursor(), (0, 5));
1525 }
1526
1527 #[test]
1528 fn ctrl_a_finds_number_to_right_of_cursor() {
1529 let mut e = Editor::new(KeybindingMode::Vim);
1530 e.set_content("foo 99 bar");
1531 e.handle_key(ctrl_key(KeyCode::Char('a')));
1532 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1533 assert_eq!(e.cursor(), (0, 6));
1534 }
1535
1536 #[test]
1537 fn ctrl_a_with_count_adds_count() {
1538 let mut e = Editor::new(KeybindingMode::Vim);
1539 e.set_content("x = 10");
1540 for d in "5".chars() {
1541 e.handle_key(key(KeyCode::Char(d)));
1542 }
1543 e.handle_key(ctrl_key(KeyCode::Char('a')));
1544 assert_eq!(e.buffer().lines()[0], "x = 15");
1545 }
1546
1547 #[test]
1548 fn ctrl_x_decrements_number() {
1549 let mut e = Editor::new(KeybindingMode::Vim);
1550 e.set_content("n=5");
1551 e.handle_key(ctrl_key(KeyCode::Char('x')));
1552 assert_eq!(e.buffer().lines()[0], "n=4");
1553 }
1554
1555 #[test]
1556 fn ctrl_x_crosses_zero_into_negative() {
1557 let mut e = Editor::new(KeybindingMode::Vim);
1558 e.set_content("v=0");
1559 e.handle_key(ctrl_key(KeyCode::Char('x')));
1560 assert_eq!(e.buffer().lines()[0], "v=-1");
1561 }
1562
1563 #[test]
1564 fn ctrl_a_on_negative_number_increments_toward_zero() {
1565 let mut e = Editor::new(KeybindingMode::Vim);
1566 e.set_content("a = -5");
1567 e.handle_key(ctrl_key(KeyCode::Char('a')));
1568 assert_eq!(e.buffer().lines()[0], "a = -4");
1569 }
1570
1571 #[test]
1572 fn ctrl_a_noop_when_no_digit_on_line() {
1573 let mut e = Editor::new(KeybindingMode::Vim);
1574 e.set_content("no digits here");
1575 e.handle_key(ctrl_key(KeyCode::Char('a')));
1576 assert_eq!(e.buffer().lines()[0], "no digits here");
1577 }
1578
1579 #[test]
1580 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1581 let mut e = Editor::new(KeybindingMode::Vim);
1582 e.set_content(&many_lines(100));
1583 prime_viewport(&mut e, 20);
1584 e.jump_cursor(50, 0);
1585 e.handle_key(key(KeyCode::Char('z')));
1586 e.handle_key(key(KeyCode::Char('b')));
1587 assert_eq!(e.buffer().viewport().top_row, 36);
1591 assert_eq!(e.cursor().0, 50);
1592 }
1593
1594 #[test]
1601 fn set_content_dirties_then_take_dirty_clears() {
1602 let mut e = Editor::new(KeybindingMode::Vim);
1603 e.set_content("hello");
1604 assert!(
1605 e.take_dirty(),
1606 "set_content should leave content_dirty=true"
1607 );
1608 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1609 }
1610
1611 #[test]
1612 fn content_arc_returns_same_arc_until_mutation() {
1613 let mut e = Editor::new(KeybindingMode::Vim);
1614 e.set_content("hello");
1615 let a = e.content_arc();
1616 let b = e.content_arc();
1617 assert!(
1618 std::sync::Arc::ptr_eq(&a, &b),
1619 "repeated content_arc() should hit the cache"
1620 );
1621
1622 e.handle_key(key(KeyCode::Char('i')));
1624 e.handle_key(key(KeyCode::Char('!')));
1625 let c = e.content_arc();
1626 assert!(
1627 !std::sync::Arc::ptr_eq(&a, &c),
1628 "mutation should invalidate content_arc() cache"
1629 );
1630 assert!(c.contains('!'));
1631 }
1632
1633 #[test]
1634 fn content_arc_cache_invalidated_by_set_content() {
1635 let mut e = Editor::new(KeybindingMode::Vim);
1636 e.set_content("one");
1637 let a = e.content_arc();
1638 e.set_content("two");
1639 let b = e.content_arc();
1640 assert!(!std::sync::Arc::ptr_eq(&a, &b));
1641 assert!(b.starts_with("two"));
1642 }
1643
1644 #[test]
1650 fn mouse_click_past_eol_lands_on_last_char() {
1651 let mut e = Editor::new(KeybindingMode::Vim);
1652 e.set_content("hello");
1653 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1657 e.mouse_click(area, 78, 1);
1658 assert_eq!(e.cursor(), (0, 4));
1659 }
1660
1661 #[test]
1662 fn mouse_click_past_eol_handles_multibyte_line() {
1663 let mut e = Editor::new(KeybindingMode::Vim);
1664 e.set_content("héllo");
1667 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1668 e.mouse_click(area, 78, 1);
1669 assert_eq!(e.cursor(), (0, 4));
1670 }
1671
1672 #[test]
1673 fn mouse_click_inside_line_lands_on_clicked_char() {
1674 let mut e = Editor::new(KeybindingMode::Vim);
1675 e.set_content("hello world");
1676 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1679 e.mouse_click(area, 4, 1);
1680 assert_eq!(e.cursor(), (0, 0));
1681 e.mouse_click(area, 6, 1);
1682 assert_eq!(e.cursor(), (0, 2));
1683 }
1684}