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 pub(super) vim: VimState,
36 pub(super) undo_stack: Vec<(Vec<String>, (usize, usize))>,
38 pub(super) redo_stack: Vec<(Vec<String>, (usize, usize))>,
40 pub(super) content_dirty: bool,
42 pub(super) cached_content: Option<std::sync::Arc<String>>,
47 pub(super) viewport_height: AtomicU16,
52 pub(super) pending_lsp: Option<LspIntent>,
56 pub(super) buffer: hjkl_buffer::Buffer,
61 pub(super) style_table: Vec<ratatui::style::Style>,
68 pub(super) registers: crate::registers::Registers,
71 pub styled_spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
77 pub(super) settings: Settings,
81 pub(super) file_marks: std::collections::HashMap<char, (usize, usize)>,
87 pub(super) syntax_fold_ranges: Vec<(usize, usize)>,
92}
93
94#[derive(Debug, Clone)]
97pub struct Settings {
98 pub shiftwidth: usize,
100 pub tabstop: usize,
103 pub ignore_case: bool,
106 pub textwidth: usize,
108 pub wrap: hjkl_buffer::Wrap,
114}
115
116impl Default for Settings {
117 fn default() -> Self {
118 Self {
119 shiftwidth: 2,
120 tabstop: 8,
121 ignore_case: false,
122 textwidth: 79,
123 wrap: hjkl_buffer::Wrap::None,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum LspIntent {
133 GotoDefinition,
135}
136
137impl<'a> Editor<'a> {
138 pub fn new(keybinding_mode: KeybindingMode) -> Self {
139 Self {
140 _marker: std::marker::PhantomData,
141 keybinding_mode,
142 last_yank: None,
143 vim: VimState::default(),
144 undo_stack: Vec::new(),
145 redo_stack: Vec::new(),
146 content_dirty: false,
147 cached_content: None,
148 viewport_height: AtomicU16::new(0),
149 pending_lsp: None,
150 buffer: hjkl_buffer::Buffer::new(),
151 style_table: Vec::new(),
152 registers: crate::registers::Registers::default(),
153 styled_spans: Vec::new(),
154 settings: Settings::default(),
155 file_marks: std::collections::HashMap::new(),
156 syntax_fold_ranges: Vec::new(),
157 }
158 }
159
160 pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
164 self.syntax_fold_ranges = ranges;
165 }
166
167 pub fn settings(&self) -> &Settings {
170 &self.settings
171 }
172
173 pub(super) fn settings_mut(&mut self) -> &mut Settings {
174 &mut self.settings
175 }
176
177 pub fn install_syntax_spans(&mut self, spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>) {
184 let line_byte_lens: Vec<usize> = self.buffer.lines().iter().map(|l| l.len()).collect();
185 let mut by_row: Vec<Vec<hjkl_buffer::Span>> = Vec::with_capacity(spans.len());
186 for (row, row_spans) in spans.iter().enumerate() {
187 let line_len = line_byte_lens.get(row).copied().unwrap_or(0);
188 let mut translated = Vec::with_capacity(row_spans.len());
189 for (start, end, style) in row_spans {
190 let end_clamped = (*end).min(line_len);
191 if end_clamped <= *start {
192 continue;
193 }
194 let id = self.intern_style(*style);
195 translated.push(hjkl_buffer::Span::new(*start, end_clamped, id));
196 }
197 by_row.push(translated);
198 }
199 self.buffer.set_spans(by_row);
200 self.styled_spans = spans;
201 }
202
203 pub fn yank(&self) -> &str {
205 &self.registers.unnamed.text
206 }
207
208 pub fn registers(&self) -> &crate::registers::Registers {
210 &self.registers
211 }
212
213 pub fn sync_clipboard_register(&mut self, text: String, linewise: bool) {
218 self.registers.set_clipboard(text, linewise);
219 }
220
221 pub fn pending_register_is_clipboard(&self) -> bool {
225 matches!(self.vim.pending_register, Some('+') | Some('*'))
226 }
227
228 pub fn set_yank(&mut self, text: impl Into<String>) {
232 let text = text.into();
233 let linewise = self.vim.yank_linewise;
234 self.registers.unnamed = crate::registers::Slot { text, linewise };
235 }
236
237 pub(crate) fn record_yank(&mut self, text: String, linewise: bool) {
241 self.vim.yank_linewise = linewise;
242 let target = self.vim.pending_register.take();
243 self.registers.record_yank(text, linewise, target);
244 }
245
246 pub(crate) fn set_named_register_text(&mut self, reg: char, text: String) {
251 if let Some(slot) = match reg {
252 'a'..='z' => Some(&mut self.registers.named[(reg as u8 - b'a') as usize]),
253 'A'..='Z' => {
254 Some(&mut self.registers.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize])
255 }
256 _ => None,
257 } {
258 slot.text = text;
259 slot.linewise = false;
260 }
261 }
262
263 pub(crate) fn record_delete(&mut self, text: String, linewise: bool) {
266 self.vim.yank_linewise = linewise;
267 let target = self.vim.pending_register.take();
268 self.registers.record_delete(text, linewise, target);
269 }
270
271 pub fn intern_style(&mut self, style: ratatui::style::Style) -> u32 {
277 if let Some(idx) = self.style_table.iter().position(|s| *s == style) {
278 return idx as u32;
279 }
280 self.style_table.push(style);
281 (self.style_table.len() - 1) as u32
282 }
283
284 pub fn style_table(&self) -> &[ratatui::style::Style] {
288 &self.style_table
289 }
290
291 pub fn buffer(&self) -> &hjkl_buffer::Buffer {
294 &self.buffer
295 }
296
297 pub fn buffer_mut(&mut self) -> &mut hjkl_buffer::Buffer {
298 &mut self.buffer
299 }
300
301 pub(crate) fn push_buffer_cursor_to_textarea(&mut self) {}
305
306 pub fn set_viewport_top(&mut self, row: usize) {
314 let last = self.buffer.row_count().saturating_sub(1);
315 let target = row.min(last);
316 self.buffer.viewport_mut().top_row = target;
317 }
318
319 pub(crate) fn jump_cursor(&mut self, row: usize, col: usize) {
324 self.buffer.set_cursor(hjkl_buffer::Position::new(row, col));
325 }
326
327 pub fn cursor(&self) -> (usize, usize) {
335 let pos = self.buffer.cursor();
336 (pos.row, pos.col)
337 }
338
339 pub fn take_lsp_intent(&mut self) -> Option<LspIntent> {
342 self.pending_lsp.take()
343 }
344
345 pub(crate) fn sync_buffer_from_textarea(&mut self) {
349 self.buffer.set_sticky_col(self.vim.sticky_col);
350 let height = self.viewport_height_value();
351 self.buffer.viewport_mut().height = height;
352 }
353
354 pub(crate) fn sync_buffer_content_from_textarea(&mut self) {
358 self.sync_buffer_from_textarea();
359 }
360
361 pub fn record_jump(&mut self, pos: (usize, usize)) {
366 const JUMPLIST_MAX: usize = 100;
367 self.vim.jump_back.push(pos);
368 if self.vim.jump_back.len() > JUMPLIST_MAX {
369 self.vim.jump_back.remove(0);
370 }
371 self.vim.jump_fwd.clear();
372 }
373
374 pub fn set_viewport_height(&self, height: u16) {
377 self.viewport_height.store(height, Ordering::Relaxed);
378 }
379
380 pub fn viewport_height_value(&self) -> u16 {
382 self.viewport_height.load(Ordering::Relaxed)
383 }
384
385 pub(super) fn mutate_edit(&mut self, edit: hjkl_buffer::Edit) -> hjkl_buffer::Edit {
391 let pre_row = self.buffer.cursor().row;
392 let pre_rows = self.buffer.row_count();
393 let inverse = self.buffer.apply_edit(edit);
394 let pos = self.buffer.cursor();
395 let lo = pre_row.min(pos.row);
401 let hi = pre_row.max(pos.row);
402 self.buffer.invalidate_folds_in_range(lo, hi);
403 self.vim.last_edit_pos = Some((pos.row, pos.col));
404 let entry = (pos.row, pos.col);
409 if self.vim.change_list.last() != Some(&entry) {
410 if let Some(idx) = self.vim.change_list_cursor.take() {
411 self.vim.change_list.truncate(idx + 1);
412 }
413 self.vim.change_list.push(entry);
414 let len = self.vim.change_list.len();
415 if len > crate::vim::CHANGE_LIST_MAX {
416 self.vim
417 .change_list
418 .drain(0..len - crate::vim::CHANGE_LIST_MAX);
419 }
420 }
421 self.vim.change_list_cursor = None;
422 let post_rows = self.buffer.row_count();
426 let delta = post_rows as isize - pre_rows as isize;
427 if delta != 0 {
428 self.shift_marks_after_edit(pre_row, delta);
429 }
430 self.push_buffer_content_to_textarea();
431 self.mark_content_dirty();
432 inverse
433 }
434
435 fn shift_marks_after_edit(&mut self, edit_start: usize, delta: isize) {
440 if delta == 0 {
441 return;
442 }
443 let drop_end = if delta < 0 {
446 edit_start.saturating_add((-delta) as usize)
447 } else {
448 edit_start
449 };
450 let shift_threshold = drop_end.max(edit_start.saturating_add(1));
451
452 let mut to_drop: Vec<char> = Vec::new();
453 for (c, (row, _col)) in self.vim.marks.iter_mut() {
454 if (edit_start..drop_end).contains(row) {
455 to_drop.push(*c);
456 } else if *row >= shift_threshold {
457 *row = ((*row as isize) + delta).max(0) as usize;
458 }
459 }
460 for c in to_drop {
461 self.vim.marks.remove(&c);
462 }
463
464 let mut to_drop: Vec<char> = Vec::new();
466 for (c, (row, _col)) in self.file_marks.iter_mut() {
467 if (edit_start..drop_end).contains(row) {
468 to_drop.push(*c);
469 } else if *row >= shift_threshold {
470 *row = ((*row as isize) + delta).max(0) as usize;
471 }
472 }
473 for c in to_drop {
474 self.file_marks.remove(&c);
475 }
476
477 let shift_jumps = |entries: &mut Vec<(usize, usize)>| {
478 entries.retain(|(row, _)| !(edit_start..drop_end).contains(row));
479 for (row, _) in entries.iter_mut() {
480 if *row >= shift_threshold {
481 *row = ((*row as isize) + delta).max(0) as usize;
482 }
483 }
484 };
485 shift_jumps(&mut self.vim.jump_back);
486 shift_jumps(&mut self.vim.jump_fwd);
487 }
488
489 pub(crate) fn push_buffer_content_to_textarea(&mut self) {}
497
498 pub fn mark_content_dirty(&mut self) {
504 self.content_dirty = true;
505 self.cached_content = None;
506 }
507
508 pub fn take_dirty(&mut self) -> bool {
510 let dirty = self.content_dirty;
511 self.content_dirty = false;
512 dirty
513 }
514
515 pub fn take_content_change(&mut self) -> Option<std::sync::Arc<String>> {
525 if !self.content_dirty {
526 return None;
527 }
528 let arc = self.content_arc();
529 self.content_dirty = false;
530 Some(arc)
531 }
532
533 pub fn cursor_screen_row(&mut self, height: u16) -> u16 {
536 let cursor = self.buffer.cursor().row;
537 let top = self.buffer.viewport().top_row;
538 cursor.saturating_sub(top).min(height as usize - 1) as u16
539 }
540
541 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
545 let pos = self.buffer.cursor();
546 let v = self.buffer.viewport();
547 if pos.row < v.top_row || pos.col < v.top_col {
548 return None;
549 }
550 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
551 let dy = (pos.row - v.top_row) as u16;
552 let dx = (pos.col - v.top_col) as u16;
553 if dy >= area.height || dx + lnum_width >= area.width {
554 return None;
555 }
556 Some((area.x + lnum_width + dx, area.y + dy))
557 }
558
559 pub fn vim_mode(&self) -> VimMode {
560 self.vim.public_mode()
561 }
562
563 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
569 self.vim.search_prompt.as_ref()
570 }
571
572 pub fn last_search(&self) -> Option<&str> {
575 self.vim.last_search.as_deref()
576 }
577
578 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
582 if self.vim_mode() != VimMode::Visual {
583 return None;
584 }
585 let anchor = self.vim.visual_anchor;
586 let cursor = self.cursor();
587 let (start, end) = if anchor <= cursor {
588 (anchor, cursor)
589 } else {
590 (cursor, anchor)
591 };
592 Some((start, end))
593 }
594
595 pub fn line_highlight(&self) -> Option<(usize, usize)> {
598 if self.vim_mode() != VimMode::VisualLine {
599 return None;
600 }
601 let anchor = self.vim.visual_line_anchor;
602 let cursor = self.buffer.cursor().row;
603 Some((anchor.min(cursor), anchor.max(cursor)))
604 }
605
606 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
607 if self.vim_mode() != VimMode::VisualBlock {
608 return None;
609 }
610 let (ar, ac) = self.vim.block_anchor;
611 let cr = self.buffer.cursor().row;
612 let cc = self.vim.block_vcol;
613 let top = ar.min(cr);
614 let bot = ar.max(cr);
615 let left = ac.min(cc);
616 let right = ac.max(cc);
617 Some((top, bot, left, right))
618 }
619
620 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
626 use hjkl_buffer::{Position, Selection};
627 match self.vim_mode() {
628 VimMode::Visual => {
629 let (ar, ac) = self.vim.visual_anchor;
630 let head = self.buffer.cursor();
631 Some(Selection::Char {
632 anchor: Position::new(ar, ac),
633 head,
634 })
635 }
636 VimMode::VisualLine => {
637 let anchor_row = self.vim.visual_line_anchor;
638 let head_row = self.buffer.cursor().row;
639 Some(Selection::Line {
640 anchor_row,
641 head_row,
642 })
643 }
644 VimMode::VisualBlock => {
645 let (ar, ac) = self.vim.block_anchor;
646 let cr = self.buffer.cursor().row;
647 let cc = self.vim.block_vcol;
648 Some(Selection::Block {
649 anchor: Position::new(ar, ac),
650 head: Position::new(cr, cc),
651 })
652 }
653 _ => None,
654 }
655 }
656
657 pub fn force_normal(&mut self) {
659 self.vim.force_normal();
660 }
661
662 pub fn content(&self) -> String {
663 let mut s = self.buffer.lines().join("\n");
664 s.push('\n');
665 s
666 }
667
668 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
673 if let Some(arc) = &self.cached_content {
674 return std::sync::Arc::clone(arc);
675 }
676 let arc = std::sync::Arc::new(self.content());
677 self.cached_content = Some(std::sync::Arc::clone(&arc));
678 arc
679 }
680
681 pub fn set_content(&mut self, text: &str) {
682 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
683 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
684 lines.pop();
685 }
686 if lines.is_empty() {
687 lines.push(String::new());
688 }
689 let _ = lines;
690 self.buffer = hjkl_buffer::Buffer::from_str(text);
691 self.undo_stack.clear();
692 self.redo_stack.clear();
693 self.mark_content_dirty();
694 }
695
696 pub fn seed_yank(&mut self, text: String) {
700 let linewise = text.ends_with('\n');
701 self.vim.yank_linewise = linewise;
702 self.registers.unnamed = crate::registers::Slot { text, linewise };
703 }
704
705 pub fn scroll_down(&mut self, rows: i16) {
710 self.scroll_viewport(rows);
711 }
712
713 pub fn scroll_up(&mut self, rows: i16) {
717 self.scroll_viewport(-rows);
718 }
719
720 const SCROLLOFF: usize = 5;
724
725 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
730 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
731 if height == 0 {
732 self.buffer.ensure_cursor_visible();
733 return;
734 }
735 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
739 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
742 self.ensure_scrolloff_wrap(height, margin);
743 return;
744 }
745 let cursor_row = self.buffer.cursor().row;
746 let last_row = self.buffer.row_count().saturating_sub(1);
747 let v = self.buffer.viewport_mut();
748 if cursor_row < v.top_row + margin {
750 v.top_row = cursor_row.saturating_sub(margin);
751 }
752 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
754 if cursor_row > v.top_row + max_bottom {
755 v.top_row = cursor_row.saturating_sub(max_bottom);
756 }
757 let max_top = last_row.saturating_sub(height.saturating_sub(1));
759 if v.top_row > max_top {
760 v.top_row = max_top;
761 }
762 let cursor = self.buffer.cursor();
765 self.buffer.viewport_mut().ensure_visible(cursor);
766 }
767
768 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
773 let cursor_row = self.buffer.cursor().row;
774 if cursor_row < self.buffer.viewport().top_row {
777 self.buffer.viewport_mut().top_row = cursor_row;
778 self.buffer.viewport_mut().top_col = 0;
779 }
780 let max_csr = height.saturating_sub(1).saturating_sub(margin);
783 loop {
784 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
785 if csr <= max_csr {
786 break;
787 }
788 let top = self.buffer.viewport().top_row;
789 let Some(next) = self.buffer.next_visible_row(top) else {
790 break;
791 };
792 if next > cursor_row {
794 self.buffer.viewport_mut().top_row = cursor_row;
795 break;
796 }
797 self.buffer.viewport_mut().top_row = next;
798 }
799 loop {
802 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
803 if csr >= margin {
804 break;
805 }
806 let top = self.buffer.viewport().top_row;
807 let Some(prev) = self.buffer.prev_visible_row(top) else {
808 break;
809 };
810 self.buffer.viewport_mut().top_row = prev;
811 }
812 let max_top = self.buffer.max_top_for_height(height);
817 if self.buffer.viewport().top_row > max_top {
818 self.buffer.viewport_mut().top_row = max_top;
819 }
820 self.buffer.viewport_mut().top_col = 0;
821 }
822
823 fn scroll_viewport(&mut self, delta: i16) {
824 if delta == 0 {
825 return;
826 }
827 let total_rows = self.buffer.row_count() as isize;
829 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
830 let cur_top = self.buffer.viewport().top_row as isize;
831 let new_top = (cur_top + delta as isize)
832 .max(0)
833 .min((total_rows - 1).max(0)) as usize;
834 self.buffer.viewport_mut().top_row = new_top;
835 let _ = cur_top;
838 if height == 0 {
839 return;
840 }
841 let cursor = self.buffer.cursor();
844 let margin = Self::SCROLLOFF.min(height / 2);
845 let min_row = new_top + margin;
846 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
847 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
848 if target_row != cursor.row {
849 let line_len = self
850 .buffer
851 .line(target_row)
852 .map(|l| l.chars().count())
853 .unwrap_or(0);
854 let target_col = cursor.col.min(line_len.saturating_sub(1));
855 self.buffer
856 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
857 }
858 }
859
860 pub fn goto_line(&mut self, line: usize) {
861 let row = line.saturating_sub(1);
862 let max = self.buffer.row_count().saturating_sub(1);
863 let target = row.min(max);
864 self.buffer
865 .set_cursor(hjkl_buffer::Position::new(target, 0));
866 }
867
868 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
872 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
873 if height == 0 {
874 return;
875 }
876 let cur_row = self.buffer.cursor().row;
877 let cur_top = self.buffer.viewport().top_row;
878 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
884 let new_top = match pos {
885 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
886 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
887 CursorScrollTarget::Bottom => {
888 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
889 }
890 };
891 if new_top == cur_top {
892 return;
893 }
894 self.buffer.viewport_mut().top_row = new_top;
895 }
896
897 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
904 let lines = self.buffer.lines();
905 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
907 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
908 let rel_row = row.saturating_sub(inner_top) as usize;
909 let top = self.buffer.viewport().top_row;
910 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
911 let rel_col = col.saturating_sub(content_x) as usize;
912 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
913 let last_col = line_chars.saturating_sub(1);
914 (doc_row, rel_col.min(last_col))
915 }
916
917 pub fn jump_to(&mut self, line: usize, col: usize) {
919 let r = line.saturating_sub(1);
920 let max_row = self.buffer.row_count().saturating_sub(1);
921 let r = r.min(max_row);
922 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
923 let c = col.saturating_sub(1).min(line_len);
924 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
925 }
926
927 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
929 if self.vim.is_visual() {
930 self.vim.force_normal();
931 }
932 let (r, c) = self.mouse_to_doc_pos(area, col, row);
933 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
934 }
935
936 pub fn mouse_begin_drag(&mut self) {
938 if !self.vim.is_visual_char() {
939 let cursor = self.cursor();
940 self.vim.enter_visual(cursor);
941 }
942 }
943
944 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
946 let (r, c) = self.mouse_to_doc_pos(area, col, row);
947 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
948 }
949
950 pub fn insert_str(&mut self, text: &str) {
951 let pos = self.buffer.cursor();
952 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
953 at: pos,
954 text: text.to_string(),
955 });
956 self.push_buffer_content_to_textarea();
957 self.mark_content_dirty();
958 }
959
960 pub fn accept_completion(&mut self, completion: &str) {
961 use hjkl_buffer::{Edit, MotionKind, Position};
962 let cursor = self.buffer.cursor();
963 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
964 let chars: Vec<char> = line.chars().collect();
965 let prefix_len = chars[..cursor.col.min(chars.len())]
966 .iter()
967 .rev()
968 .take_while(|c| c.is_alphanumeric() || **c == '_')
969 .count();
970 if prefix_len > 0 {
971 let start = Position::new(cursor.row, cursor.col - prefix_len);
972 self.buffer.apply_edit(Edit::DeleteRange {
973 start,
974 end: cursor,
975 kind: MotionKind::Char,
976 });
977 }
978 let cursor = self.buffer.cursor();
979 self.buffer.apply_edit(Edit::InsertStr {
980 at: cursor,
981 text: completion.to_string(),
982 });
983 self.push_buffer_content_to_textarea();
984 self.mark_content_dirty();
985 }
986
987 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
988 let pos = self.buffer.cursor();
989 (self.buffer.lines().to_vec(), (pos.row, pos.col))
990 }
991
992 pub(super) fn push_undo(&mut self) {
993 let snap = self.snapshot();
994 if self.undo_stack.len() >= 200 {
995 self.undo_stack.remove(0);
996 }
997 self.undo_stack.push(snap);
998 self.redo_stack.clear();
999 }
1000
1001 pub(super) fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
1002 let text = lines.join("\n");
1003 self.buffer.replace_all(&text);
1004 self.buffer
1005 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
1006 self.mark_content_dirty();
1007 }
1008
1009 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
1011 let input = crossterm_to_input(key);
1012 if input.key == Key::Null {
1013 return false;
1014 }
1015 vim::step(self, input)
1016 }
1017}
1018
1019pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1020 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1021 let alt = key.modifiers.contains(KeyModifiers::ALT);
1022 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1023 let k = match key.code {
1024 KeyCode::Char(c) => Key::Char(c),
1025 KeyCode::Backspace => Key::Backspace,
1026 KeyCode::Delete => Key::Delete,
1027 KeyCode::Enter => Key::Enter,
1028 KeyCode::Left => Key::Left,
1029 KeyCode::Right => Key::Right,
1030 KeyCode::Up => Key::Up,
1031 KeyCode::Down => Key::Down,
1032 KeyCode::Home => Key::Home,
1033 KeyCode::End => Key::End,
1034 KeyCode::Tab => Key::Tab,
1035 KeyCode::Esc => Key::Esc,
1036 _ => Key::Null,
1037 };
1038 Input {
1039 key: k,
1040 ctrl,
1041 alt,
1042 shift,
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049 use crossterm::event::KeyEvent;
1050
1051 fn key(code: KeyCode) -> KeyEvent {
1052 KeyEvent::new(code, KeyModifiers::NONE)
1053 }
1054 fn shift_key(code: KeyCode) -> KeyEvent {
1055 KeyEvent::new(code, KeyModifiers::SHIFT)
1056 }
1057 fn ctrl_key(code: KeyCode) -> KeyEvent {
1058 KeyEvent::new(code, KeyModifiers::CONTROL)
1059 }
1060
1061 #[test]
1062 fn vim_normal_to_insert() {
1063 let mut e = Editor::new(KeybindingMode::Vim);
1064 e.handle_key(key(KeyCode::Char('i')));
1065 assert_eq!(e.vim_mode(), VimMode::Insert);
1066 }
1067
1068 #[test]
1069 fn take_content_change_returns_some_on_first_dirty() {
1070 let mut e = Editor::new(KeybindingMode::Vim);
1071 e.set_content("hello");
1072 let first = e.take_content_change();
1073 assert!(first.is_some());
1074 let second = e.take_content_change();
1075 assert!(second.is_none());
1076 }
1077
1078 #[test]
1079 fn take_content_change_none_until_mutation() {
1080 let mut e = Editor::new(KeybindingMode::Vim);
1081 e.set_content("hello");
1082 e.take_content_change();
1084 assert!(e.take_content_change().is_none());
1085 e.handle_key(key(KeyCode::Char('i')));
1087 e.handle_key(key(KeyCode::Char('x')));
1088 let after = e.take_content_change();
1089 assert!(after.is_some());
1090 assert!(after.unwrap().contains('x'));
1091 }
1092
1093 #[test]
1094 fn vim_insert_to_normal() {
1095 let mut e = Editor::new(KeybindingMode::Vim);
1096 e.handle_key(key(KeyCode::Char('i')));
1097 e.handle_key(key(KeyCode::Esc));
1098 assert_eq!(e.vim_mode(), VimMode::Normal);
1099 }
1100
1101 #[test]
1102 fn vim_normal_to_visual() {
1103 let mut e = Editor::new(KeybindingMode::Vim);
1104 e.handle_key(key(KeyCode::Char('v')));
1105 assert_eq!(e.vim_mode(), VimMode::Visual);
1106 }
1107
1108 #[test]
1109 fn vim_visual_to_normal() {
1110 let mut e = Editor::new(KeybindingMode::Vim);
1111 e.handle_key(key(KeyCode::Char('v')));
1112 e.handle_key(key(KeyCode::Esc));
1113 assert_eq!(e.vim_mode(), VimMode::Normal);
1114 }
1115
1116 #[test]
1117 fn vim_shift_i_moves_to_first_non_whitespace() {
1118 let mut e = Editor::new(KeybindingMode::Vim);
1119 e.set_content(" hello");
1120 e.jump_cursor(0, 8);
1121 e.handle_key(shift_key(KeyCode::Char('I')));
1122 assert_eq!(e.vim_mode(), VimMode::Insert);
1123 assert_eq!(e.cursor(), (0, 3));
1124 }
1125
1126 #[test]
1127 fn vim_shift_a_moves_to_end_and_insert() {
1128 let mut e = Editor::new(KeybindingMode::Vim);
1129 e.set_content("hello");
1130 e.handle_key(shift_key(KeyCode::Char('A')));
1131 assert_eq!(e.vim_mode(), VimMode::Insert);
1132 assert_eq!(e.cursor().1, 5);
1133 }
1134
1135 #[test]
1136 fn count_10j_moves_down_10() {
1137 let mut e = Editor::new(KeybindingMode::Vim);
1138 e.set_content(
1139 (0..20)
1140 .map(|i| format!("line{i}"))
1141 .collect::<Vec<_>>()
1142 .join("\n")
1143 .as_str(),
1144 );
1145 for d in "10".chars() {
1146 e.handle_key(key(KeyCode::Char(d)));
1147 }
1148 e.handle_key(key(KeyCode::Char('j')));
1149 assert_eq!(e.cursor().0, 10);
1150 }
1151
1152 #[test]
1153 fn count_o_repeats_insert_on_esc() {
1154 let mut e = Editor::new(KeybindingMode::Vim);
1155 e.set_content("hello");
1156 for d in "3".chars() {
1157 e.handle_key(key(KeyCode::Char(d)));
1158 }
1159 e.handle_key(key(KeyCode::Char('o')));
1160 assert_eq!(e.vim_mode(), VimMode::Insert);
1161 for c in "world".chars() {
1162 e.handle_key(key(KeyCode::Char(c)));
1163 }
1164 e.handle_key(key(KeyCode::Esc));
1165 assert_eq!(e.vim_mode(), VimMode::Normal);
1166 assert_eq!(e.buffer().lines().len(), 4);
1167 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1168 }
1169
1170 #[test]
1171 fn count_i_repeats_text_on_esc() {
1172 let mut e = Editor::new(KeybindingMode::Vim);
1173 e.set_content("");
1174 for d in "3".chars() {
1175 e.handle_key(key(KeyCode::Char(d)));
1176 }
1177 e.handle_key(key(KeyCode::Char('i')));
1178 for c in "ab".chars() {
1179 e.handle_key(key(KeyCode::Char(c)));
1180 }
1181 e.handle_key(key(KeyCode::Esc));
1182 assert_eq!(e.vim_mode(), VimMode::Normal);
1183 assert_eq!(e.buffer().lines()[0], "ababab");
1184 }
1185
1186 #[test]
1187 fn vim_shift_o_opens_line_above() {
1188 let mut e = Editor::new(KeybindingMode::Vim);
1189 e.set_content("hello");
1190 e.handle_key(shift_key(KeyCode::Char('O')));
1191 assert_eq!(e.vim_mode(), VimMode::Insert);
1192 assert_eq!(e.cursor(), (0, 0));
1193 assert_eq!(e.buffer().lines().len(), 2);
1194 }
1195
1196 #[test]
1197 fn vim_gg_goes_to_top() {
1198 let mut e = Editor::new(KeybindingMode::Vim);
1199 e.set_content("a\nb\nc");
1200 e.jump_cursor(2, 0);
1201 e.handle_key(key(KeyCode::Char('g')));
1202 e.handle_key(key(KeyCode::Char('g')));
1203 assert_eq!(e.cursor().0, 0);
1204 }
1205
1206 #[test]
1207 fn vim_shift_g_goes_to_bottom() {
1208 let mut e = Editor::new(KeybindingMode::Vim);
1209 e.set_content("a\nb\nc");
1210 e.handle_key(shift_key(KeyCode::Char('G')));
1211 assert_eq!(e.cursor().0, 2);
1212 }
1213
1214 #[test]
1215 fn vim_dd_deletes_line() {
1216 let mut e = Editor::new(KeybindingMode::Vim);
1217 e.set_content("first\nsecond");
1218 e.handle_key(key(KeyCode::Char('d')));
1219 e.handle_key(key(KeyCode::Char('d')));
1220 assert_eq!(e.buffer().lines().len(), 1);
1221 assert_eq!(e.buffer().lines()[0], "second");
1222 }
1223
1224 #[test]
1225 fn vim_dw_deletes_word() {
1226 let mut e = Editor::new(KeybindingMode::Vim);
1227 e.set_content("hello world");
1228 e.handle_key(key(KeyCode::Char('d')));
1229 e.handle_key(key(KeyCode::Char('w')));
1230 assert_eq!(e.vim_mode(), VimMode::Normal);
1231 assert!(!e.buffer().lines()[0].starts_with("hello"));
1232 }
1233
1234 #[test]
1235 fn vim_yy_yanks_line() {
1236 let mut e = Editor::new(KeybindingMode::Vim);
1237 e.set_content("hello\nworld");
1238 e.handle_key(key(KeyCode::Char('y')));
1239 e.handle_key(key(KeyCode::Char('y')));
1240 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1241 }
1242
1243 #[test]
1244 fn vim_yy_does_not_move_cursor() {
1245 let mut e = Editor::new(KeybindingMode::Vim);
1246 e.set_content("first\nsecond\nthird");
1247 e.jump_cursor(1, 0);
1248 let before = e.cursor();
1249 e.handle_key(key(KeyCode::Char('y')));
1250 e.handle_key(key(KeyCode::Char('y')));
1251 assert_eq!(e.cursor(), before);
1252 assert_eq!(e.vim_mode(), VimMode::Normal);
1253 }
1254
1255 #[test]
1256 fn vim_yw_yanks_word() {
1257 let mut e = Editor::new(KeybindingMode::Vim);
1258 e.set_content("hello world");
1259 e.handle_key(key(KeyCode::Char('y')));
1260 e.handle_key(key(KeyCode::Char('w')));
1261 assert_eq!(e.vim_mode(), VimMode::Normal);
1262 assert!(e.last_yank.is_some());
1263 }
1264
1265 #[test]
1266 fn vim_cc_changes_line() {
1267 let mut e = Editor::new(KeybindingMode::Vim);
1268 e.set_content("hello\nworld");
1269 e.handle_key(key(KeyCode::Char('c')));
1270 e.handle_key(key(KeyCode::Char('c')));
1271 assert_eq!(e.vim_mode(), VimMode::Insert);
1272 }
1273
1274 #[test]
1275 fn vim_u_undoes_insert_session_as_chunk() {
1276 let mut e = Editor::new(KeybindingMode::Vim);
1277 e.set_content("hello");
1278 e.handle_key(key(KeyCode::Char('i')));
1279 e.handle_key(key(KeyCode::Enter));
1280 e.handle_key(key(KeyCode::Enter));
1281 e.handle_key(key(KeyCode::Esc));
1282 assert_eq!(e.buffer().lines().len(), 3);
1283 e.handle_key(key(KeyCode::Char('u')));
1284 assert_eq!(e.buffer().lines().len(), 1);
1285 assert_eq!(e.buffer().lines()[0], "hello");
1286 }
1287
1288 #[test]
1289 fn vim_undo_redo_roundtrip() {
1290 let mut e = Editor::new(KeybindingMode::Vim);
1291 e.set_content("hello");
1292 e.handle_key(key(KeyCode::Char('i')));
1293 for c in "world".chars() {
1294 e.handle_key(key(KeyCode::Char(c)));
1295 }
1296 e.handle_key(key(KeyCode::Esc));
1297 let after = e.buffer().lines()[0].clone();
1298 e.handle_key(key(KeyCode::Char('u')));
1299 assert_eq!(e.buffer().lines()[0], "hello");
1300 e.handle_key(ctrl_key(KeyCode::Char('r')));
1301 assert_eq!(e.buffer().lines()[0], after);
1302 }
1303
1304 #[test]
1305 fn vim_u_undoes_dd() {
1306 let mut e = Editor::new(KeybindingMode::Vim);
1307 e.set_content("first\nsecond");
1308 e.handle_key(key(KeyCode::Char('d')));
1309 e.handle_key(key(KeyCode::Char('d')));
1310 assert_eq!(e.buffer().lines().len(), 1);
1311 e.handle_key(key(KeyCode::Char('u')));
1312 assert_eq!(e.buffer().lines().len(), 2);
1313 assert_eq!(e.buffer().lines()[0], "first");
1314 }
1315
1316 #[test]
1317 fn vim_ctrl_r_redoes() {
1318 let mut e = Editor::new(KeybindingMode::Vim);
1319 e.set_content("hello");
1320 e.handle_key(ctrl_key(KeyCode::Char('r')));
1321 }
1322
1323 #[test]
1324 fn vim_r_replaces_char() {
1325 let mut e = Editor::new(KeybindingMode::Vim);
1326 e.set_content("hello");
1327 e.handle_key(key(KeyCode::Char('r')));
1328 e.handle_key(key(KeyCode::Char('x')));
1329 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1330 }
1331
1332 #[test]
1333 fn vim_tilde_toggles_case() {
1334 let mut e = Editor::new(KeybindingMode::Vim);
1335 e.set_content("hello");
1336 e.handle_key(key(KeyCode::Char('~')));
1337 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1338 }
1339
1340 #[test]
1341 fn vim_visual_d_cuts() {
1342 let mut e = Editor::new(KeybindingMode::Vim);
1343 e.set_content("hello");
1344 e.handle_key(key(KeyCode::Char('v')));
1345 e.handle_key(key(KeyCode::Char('l')));
1346 e.handle_key(key(KeyCode::Char('l')));
1347 e.handle_key(key(KeyCode::Char('d')));
1348 assert_eq!(e.vim_mode(), VimMode::Normal);
1349 assert!(e.last_yank.is_some());
1350 }
1351
1352 #[test]
1353 fn vim_visual_c_enters_insert() {
1354 let mut e = Editor::new(KeybindingMode::Vim);
1355 e.set_content("hello");
1356 e.handle_key(key(KeyCode::Char('v')));
1357 e.handle_key(key(KeyCode::Char('l')));
1358 e.handle_key(key(KeyCode::Char('c')));
1359 assert_eq!(e.vim_mode(), VimMode::Insert);
1360 }
1361
1362 #[test]
1363 fn vim_normal_unknown_key_consumed() {
1364 let mut e = Editor::new(KeybindingMode::Vim);
1365 let consumed = e.handle_key(key(KeyCode::Char('z')));
1367 assert!(consumed);
1368 }
1369
1370 #[test]
1371 fn force_normal_clears_operator() {
1372 let mut e = Editor::new(KeybindingMode::Vim);
1373 e.handle_key(key(KeyCode::Char('d')));
1374 e.force_normal();
1375 assert_eq!(e.vim_mode(), VimMode::Normal);
1376 }
1377
1378 fn many_lines(n: usize) -> String {
1379 (0..n)
1380 .map(|i| format!("line{i}"))
1381 .collect::<Vec<_>>()
1382 .join("\n")
1383 }
1384
1385 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1386 e.set_viewport_height(height);
1387 }
1388
1389 #[test]
1390 fn zz_centers_cursor_in_viewport() {
1391 let mut e = Editor::new(KeybindingMode::Vim);
1392 e.set_content(&many_lines(100));
1393 prime_viewport(&mut e, 20);
1394 e.jump_cursor(50, 0);
1395 e.handle_key(key(KeyCode::Char('z')));
1396 e.handle_key(key(KeyCode::Char('z')));
1397 assert_eq!(e.buffer().viewport().top_row, 40);
1398 assert_eq!(e.cursor().0, 50);
1399 }
1400
1401 #[test]
1402 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1403 let mut e = Editor::new(KeybindingMode::Vim);
1404 e.set_content(&many_lines(100));
1405 prime_viewport(&mut e, 20);
1406 e.jump_cursor(50, 0);
1407 e.handle_key(key(KeyCode::Char('z')));
1408 e.handle_key(key(KeyCode::Char('t')));
1409 assert_eq!(e.buffer().viewport().top_row, 45);
1412 assert_eq!(e.cursor().0, 50);
1413 }
1414
1415 #[test]
1416 fn ctrl_a_increments_number_at_cursor() {
1417 let mut e = Editor::new(KeybindingMode::Vim);
1418 e.set_content("x = 41");
1419 e.handle_key(ctrl_key(KeyCode::Char('a')));
1420 assert_eq!(e.buffer().lines()[0], "x = 42");
1421 assert_eq!(e.cursor(), (0, 5));
1422 }
1423
1424 #[test]
1425 fn ctrl_a_finds_number_to_right_of_cursor() {
1426 let mut e = Editor::new(KeybindingMode::Vim);
1427 e.set_content("foo 99 bar");
1428 e.handle_key(ctrl_key(KeyCode::Char('a')));
1429 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1430 assert_eq!(e.cursor(), (0, 6));
1431 }
1432
1433 #[test]
1434 fn ctrl_a_with_count_adds_count() {
1435 let mut e = Editor::new(KeybindingMode::Vim);
1436 e.set_content("x = 10");
1437 for d in "5".chars() {
1438 e.handle_key(key(KeyCode::Char(d)));
1439 }
1440 e.handle_key(ctrl_key(KeyCode::Char('a')));
1441 assert_eq!(e.buffer().lines()[0], "x = 15");
1442 }
1443
1444 #[test]
1445 fn ctrl_x_decrements_number() {
1446 let mut e = Editor::new(KeybindingMode::Vim);
1447 e.set_content("n=5");
1448 e.handle_key(ctrl_key(KeyCode::Char('x')));
1449 assert_eq!(e.buffer().lines()[0], "n=4");
1450 }
1451
1452 #[test]
1453 fn ctrl_x_crosses_zero_into_negative() {
1454 let mut e = Editor::new(KeybindingMode::Vim);
1455 e.set_content("v=0");
1456 e.handle_key(ctrl_key(KeyCode::Char('x')));
1457 assert_eq!(e.buffer().lines()[0], "v=-1");
1458 }
1459
1460 #[test]
1461 fn ctrl_a_on_negative_number_increments_toward_zero() {
1462 let mut e = Editor::new(KeybindingMode::Vim);
1463 e.set_content("a = -5");
1464 e.handle_key(ctrl_key(KeyCode::Char('a')));
1465 assert_eq!(e.buffer().lines()[0], "a = -4");
1466 }
1467
1468 #[test]
1469 fn ctrl_a_noop_when_no_digit_on_line() {
1470 let mut e = Editor::new(KeybindingMode::Vim);
1471 e.set_content("no digits here");
1472 e.handle_key(ctrl_key(KeyCode::Char('a')));
1473 assert_eq!(e.buffer().lines()[0], "no digits here");
1474 }
1475
1476 #[test]
1477 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1478 let mut e = Editor::new(KeybindingMode::Vim);
1479 e.set_content(&many_lines(100));
1480 prime_viewport(&mut e, 20);
1481 e.jump_cursor(50, 0);
1482 e.handle_key(key(KeyCode::Char('z')));
1483 e.handle_key(key(KeyCode::Char('b')));
1484 assert_eq!(e.buffer().viewport().top_row, 36);
1488 assert_eq!(e.cursor().0, 50);
1489 }
1490
1491 #[test]
1498 fn set_content_dirties_then_take_dirty_clears() {
1499 let mut e = Editor::new(KeybindingMode::Vim);
1500 e.set_content("hello");
1501 assert!(
1502 e.take_dirty(),
1503 "set_content should leave content_dirty=true"
1504 );
1505 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1506 }
1507
1508 #[test]
1509 fn content_arc_returns_same_arc_until_mutation() {
1510 let mut e = Editor::new(KeybindingMode::Vim);
1511 e.set_content("hello");
1512 let a = e.content_arc();
1513 let b = e.content_arc();
1514 assert!(
1515 std::sync::Arc::ptr_eq(&a, &b),
1516 "repeated content_arc() should hit the cache"
1517 );
1518
1519 e.handle_key(key(KeyCode::Char('i')));
1521 e.handle_key(key(KeyCode::Char('!')));
1522 let c = e.content_arc();
1523 assert!(
1524 !std::sync::Arc::ptr_eq(&a, &c),
1525 "mutation should invalidate content_arc() cache"
1526 );
1527 assert!(c.contains('!'));
1528 }
1529
1530 #[test]
1531 fn content_arc_cache_invalidated_by_set_content() {
1532 let mut e = Editor::new(KeybindingMode::Vim);
1533 e.set_content("one");
1534 let a = e.content_arc();
1535 e.set_content("two");
1536 let b = e.content_arc();
1537 assert!(!std::sync::Arc::ptr_eq(&a, &b));
1538 assert!(b.starts_with("two"));
1539 }
1540
1541 #[test]
1547 fn mouse_click_past_eol_lands_on_last_char() {
1548 let mut e = Editor::new(KeybindingMode::Vim);
1549 e.set_content("hello");
1550 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1554 e.mouse_click(area, 78, 1);
1555 assert_eq!(e.cursor(), (0, 4));
1556 }
1557
1558 #[test]
1559 fn mouse_click_past_eol_handles_multibyte_line() {
1560 let mut e = Editor::new(KeybindingMode::Vim);
1561 e.set_content("héllo");
1564 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1565 e.mouse_click(area, 78, 1);
1566 assert_eq!(e.cursor(), (0, 4));
1567 }
1568
1569 #[test]
1570 fn mouse_click_inside_line_lands_on_clicked_char() {
1571 let mut e = Editor::new(KeybindingMode::Vim);
1572 e.set_content("hello world");
1573 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1576 e.mouse_click(area, 4, 1);
1577 assert_eq!(e.cursor(), (0, 0));
1578 e.mouse_click(area, 6, 1);
1579 assert_eq!(e.cursor(), (0, 2));
1580 }
1581}