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 cursor_screen_row(&mut self, height: u16) -> u16 {
518 let cursor = self.buffer.cursor().row;
519 let top = self.buffer.viewport().top_row;
520 cursor.saturating_sub(top).min(height as usize - 1) as u16
521 }
522
523 pub fn cursor_screen_pos(&self, area: Rect) -> Option<(u16, u16)> {
527 let pos = self.buffer.cursor();
528 let v = self.buffer.viewport();
529 if pos.row < v.top_row || pos.col < v.top_col {
530 return None;
531 }
532 let lnum_width = self.buffer.row_count().to_string().len() as u16 + 2;
533 let dy = (pos.row - v.top_row) as u16;
534 let dx = (pos.col - v.top_col) as u16;
535 if dy >= area.height || dx + lnum_width >= area.width {
536 return None;
537 }
538 Some((area.x + lnum_width + dx, area.y + dy))
539 }
540
541 pub fn vim_mode(&self) -> VimMode {
542 self.vim.public_mode()
543 }
544
545 pub fn search_prompt(&self) -> Option<&crate::vim::SearchPrompt> {
551 self.vim.search_prompt.as_ref()
552 }
553
554 pub fn last_search(&self) -> Option<&str> {
557 self.vim.last_search.as_deref()
558 }
559
560 pub fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
564 if self.vim_mode() != VimMode::Visual {
565 return None;
566 }
567 let anchor = self.vim.visual_anchor;
568 let cursor = self.cursor();
569 let (start, end) = if anchor <= cursor {
570 (anchor, cursor)
571 } else {
572 (cursor, anchor)
573 };
574 Some((start, end))
575 }
576
577 pub fn line_highlight(&self) -> Option<(usize, usize)> {
580 if self.vim_mode() != VimMode::VisualLine {
581 return None;
582 }
583 let anchor = self.vim.visual_line_anchor;
584 let cursor = self.buffer.cursor().row;
585 Some((anchor.min(cursor), anchor.max(cursor)))
586 }
587
588 pub fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
589 if self.vim_mode() != VimMode::VisualBlock {
590 return None;
591 }
592 let (ar, ac) = self.vim.block_anchor;
593 let cr = self.buffer.cursor().row;
594 let cc = self.vim.block_vcol;
595 let top = ar.min(cr);
596 let bot = ar.max(cr);
597 let left = ac.min(cc);
598 let right = ac.max(cc);
599 Some((top, bot, left, right))
600 }
601
602 pub fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
608 use hjkl_buffer::{Position, Selection};
609 match self.vim_mode() {
610 VimMode::Visual => {
611 let (ar, ac) = self.vim.visual_anchor;
612 let head = self.buffer.cursor();
613 Some(Selection::Char {
614 anchor: Position::new(ar, ac),
615 head,
616 })
617 }
618 VimMode::VisualLine => {
619 let anchor_row = self.vim.visual_line_anchor;
620 let head_row = self.buffer.cursor().row;
621 Some(Selection::Line {
622 anchor_row,
623 head_row,
624 })
625 }
626 VimMode::VisualBlock => {
627 let (ar, ac) = self.vim.block_anchor;
628 let cr = self.buffer.cursor().row;
629 let cc = self.vim.block_vcol;
630 Some(Selection::Block {
631 anchor: Position::new(ar, ac),
632 head: Position::new(cr, cc),
633 })
634 }
635 _ => None,
636 }
637 }
638
639 pub fn force_normal(&mut self) {
641 self.vim.force_normal();
642 }
643
644 pub fn content(&self) -> String {
645 let mut s = self.buffer.lines().join("\n");
646 s.push('\n');
647 s
648 }
649
650 pub fn content_arc(&mut self) -> std::sync::Arc<String> {
655 if let Some(arc) = &self.cached_content {
656 return std::sync::Arc::clone(arc);
657 }
658 let arc = std::sync::Arc::new(self.content());
659 self.cached_content = Some(std::sync::Arc::clone(&arc));
660 arc
661 }
662
663 pub fn set_content(&mut self, text: &str) {
664 let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
665 while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
666 lines.pop();
667 }
668 if lines.is_empty() {
669 lines.push(String::new());
670 }
671 let _ = lines;
672 self.buffer = hjkl_buffer::Buffer::from_str(text);
673 self.undo_stack.clear();
674 self.redo_stack.clear();
675 self.mark_content_dirty();
676 }
677
678 pub fn seed_yank(&mut self, text: String) {
682 let linewise = text.ends_with('\n');
683 self.vim.yank_linewise = linewise;
684 self.registers.unnamed = crate::registers::Slot { text, linewise };
685 }
686
687 pub fn scroll_down(&mut self, rows: i16) {
692 self.scroll_viewport(rows);
693 }
694
695 pub fn scroll_up(&mut self, rows: i16) {
699 self.scroll_viewport(-rows);
700 }
701
702 const SCROLLOFF: usize = 5;
706
707 pub(crate) fn ensure_cursor_in_scrolloff(&mut self) {
712 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
713 if height == 0 {
714 self.buffer.ensure_cursor_visible();
715 return;
716 }
717 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
721 if !matches!(self.buffer.viewport().wrap, hjkl_buffer::Wrap::None) {
724 self.ensure_scrolloff_wrap(height, margin);
725 return;
726 }
727 let cursor_row = self.buffer.cursor().row;
728 let last_row = self.buffer.row_count().saturating_sub(1);
729 let v = self.buffer.viewport_mut();
730 if cursor_row < v.top_row + margin {
732 v.top_row = cursor_row.saturating_sub(margin);
733 }
734 let max_bottom = height.saturating_sub(1).saturating_sub(margin);
736 if cursor_row > v.top_row + max_bottom {
737 v.top_row = cursor_row.saturating_sub(max_bottom);
738 }
739 let max_top = last_row.saturating_sub(height.saturating_sub(1));
741 if v.top_row > max_top {
742 v.top_row = max_top;
743 }
744 let cursor = self.buffer.cursor();
747 self.buffer.viewport_mut().ensure_visible(cursor);
748 }
749
750 fn ensure_scrolloff_wrap(&mut self, height: usize, margin: usize) {
755 let cursor_row = self.buffer.cursor().row;
756 if cursor_row < self.buffer.viewport().top_row {
759 self.buffer.viewport_mut().top_row = cursor_row;
760 self.buffer.viewport_mut().top_col = 0;
761 }
762 let max_csr = height.saturating_sub(1).saturating_sub(margin);
765 loop {
766 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
767 if csr <= max_csr {
768 break;
769 }
770 let top = self.buffer.viewport().top_row;
771 let Some(next) = self.buffer.next_visible_row(top) else {
772 break;
773 };
774 if next > cursor_row {
776 self.buffer.viewport_mut().top_row = cursor_row;
777 break;
778 }
779 self.buffer.viewport_mut().top_row = next;
780 }
781 loop {
784 let csr = self.buffer.cursor_screen_row().unwrap_or(0);
785 if csr >= margin {
786 break;
787 }
788 let top = self.buffer.viewport().top_row;
789 let Some(prev) = self.buffer.prev_visible_row(top) else {
790 break;
791 };
792 self.buffer.viewport_mut().top_row = prev;
793 }
794 let max_top = self.buffer.max_top_for_height(height);
799 if self.buffer.viewport().top_row > max_top {
800 self.buffer.viewport_mut().top_row = max_top;
801 }
802 self.buffer.viewport_mut().top_col = 0;
803 }
804
805 fn scroll_viewport(&mut self, delta: i16) {
806 if delta == 0 {
807 return;
808 }
809 let total_rows = self.buffer.row_count() as isize;
811 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
812 let cur_top = self.buffer.viewport().top_row as isize;
813 let new_top = (cur_top + delta as isize)
814 .max(0)
815 .min((total_rows - 1).max(0)) as usize;
816 self.buffer.viewport_mut().top_row = new_top;
817 let _ = cur_top;
820 if height == 0 {
821 return;
822 }
823 let cursor = self.buffer.cursor();
826 let margin = Self::SCROLLOFF.min(height / 2);
827 let min_row = new_top + margin;
828 let max_row = new_top + height.saturating_sub(1).saturating_sub(margin);
829 let target_row = cursor.row.clamp(min_row, max_row.max(min_row));
830 if target_row != cursor.row {
831 let line_len = self
832 .buffer
833 .line(target_row)
834 .map(|l| l.chars().count())
835 .unwrap_or(0);
836 let target_col = cursor.col.min(line_len.saturating_sub(1));
837 self.buffer
838 .set_cursor(hjkl_buffer::Position::new(target_row, target_col));
839 }
840 }
841
842 pub fn goto_line(&mut self, line: usize) {
843 let row = line.saturating_sub(1);
844 let max = self.buffer.row_count().saturating_sub(1);
845 let target = row.min(max);
846 self.buffer
847 .set_cursor(hjkl_buffer::Position::new(target, 0));
848 }
849
850 pub(super) fn scroll_cursor_to(&mut self, pos: CursorScrollTarget) {
854 let height = self.viewport_height.load(Ordering::Relaxed) as usize;
855 if height == 0 {
856 return;
857 }
858 let cur_row = self.buffer.cursor().row;
859 let cur_top = self.buffer.viewport().top_row;
860 let margin = Self::SCROLLOFF.min(height.saturating_sub(1) / 2);
866 let new_top = match pos {
867 CursorScrollTarget::Center => cur_row.saturating_sub(height / 2),
868 CursorScrollTarget::Top => cur_row.saturating_sub(margin),
869 CursorScrollTarget::Bottom => {
870 cur_row.saturating_sub(height.saturating_sub(1).saturating_sub(margin))
871 }
872 };
873 if new_top == cur_top {
874 return;
875 }
876 self.buffer.viewport_mut().top_row = new_top;
877 }
878
879 fn mouse_to_doc_pos(&self, area: Rect, col: u16, row: u16) -> (usize, usize) {
886 let lines = self.buffer.lines();
887 let inner_top = area.y.saturating_add(1); let lnum_width = lines.len().to_string().len() as u16 + 2;
889 let content_x = area.x.saturating_add(1).saturating_add(lnum_width);
890 let rel_row = row.saturating_sub(inner_top) as usize;
891 let top = self.buffer.viewport().top_row;
892 let doc_row = (top + rel_row).min(lines.len().saturating_sub(1));
893 let rel_col = col.saturating_sub(content_x) as usize;
894 let line_chars = lines.get(doc_row).map(|l| l.chars().count()).unwrap_or(0);
895 let last_col = line_chars.saturating_sub(1);
896 (doc_row, rel_col.min(last_col))
897 }
898
899 pub fn jump_to(&mut self, line: usize, col: usize) {
901 let r = line.saturating_sub(1);
902 let max_row = self.buffer.row_count().saturating_sub(1);
903 let r = r.min(max_row);
904 let line_len = self.buffer.line(r).map(|l| l.chars().count()).unwrap_or(0);
905 let c = col.saturating_sub(1).min(line_len);
906 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
907 }
908
909 pub fn mouse_click(&mut self, area: Rect, col: u16, row: u16) {
911 if self.vim.is_visual() {
912 self.vim.force_normal();
913 }
914 let (r, c) = self.mouse_to_doc_pos(area, col, row);
915 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
916 }
917
918 pub fn mouse_begin_drag(&mut self) {
920 if !self.vim.is_visual_char() {
921 let cursor = self.cursor();
922 self.vim.enter_visual(cursor);
923 }
924 }
925
926 pub fn mouse_extend_drag(&mut self, area: Rect, col: u16, row: u16) {
928 let (r, c) = self.mouse_to_doc_pos(area, col, row);
929 self.buffer.set_cursor(hjkl_buffer::Position::new(r, c));
930 }
931
932 pub fn insert_str(&mut self, text: &str) {
933 let pos = self.buffer.cursor();
934 self.buffer.apply_edit(hjkl_buffer::Edit::InsertStr {
935 at: pos,
936 text: text.to_string(),
937 });
938 self.push_buffer_content_to_textarea();
939 self.mark_content_dirty();
940 }
941
942 pub fn accept_completion(&mut self, completion: &str) {
943 use hjkl_buffer::{Edit, MotionKind, Position};
944 let cursor = self.buffer.cursor();
945 let line = self.buffer.line(cursor.row).unwrap_or("").to_string();
946 let chars: Vec<char> = line.chars().collect();
947 let prefix_len = chars[..cursor.col.min(chars.len())]
948 .iter()
949 .rev()
950 .take_while(|c| c.is_alphanumeric() || **c == '_')
951 .count();
952 if prefix_len > 0 {
953 let start = Position::new(cursor.row, cursor.col - prefix_len);
954 self.buffer.apply_edit(Edit::DeleteRange {
955 start,
956 end: cursor,
957 kind: MotionKind::Char,
958 });
959 }
960 let cursor = self.buffer.cursor();
961 self.buffer.apply_edit(Edit::InsertStr {
962 at: cursor,
963 text: completion.to_string(),
964 });
965 self.push_buffer_content_to_textarea();
966 self.mark_content_dirty();
967 }
968
969 pub(super) fn snapshot(&self) -> (Vec<String>, (usize, usize)) {
970 let pos = self.buffer.cursor();
971 (self.buffer.lines().to_vec(), (pos.row, pos.col))
972 }
973
974 pub(super) fn push_undo(&mut self) {
975 let snap = self.snapshot();
976 if self.undo_stack.len() >= 200 {
977 self.undo_stack.remove(0);
978 }
979 self.undo_stack.push(snap);
980 self.redo_stack.clear();
981 }
982
983 pub(super) fn restore(&mut self, lines: Vec<String>, cursor: (usize, usize)) {
984 let text = lines.join("\n");
985 self.buffer.replace_all(&text);
986 self.buffer
987 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
988 self.mark_content_dirty();
989 }
990
991 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
993 let input = crossterm_to_input(key);
994 if input.key == Key::Null {
995 return false;
996 }
997 vim::step(self, input)
998 }
999}
1000
1001pub(super) fn crossterm_to_input(key: KeyEvent) -> Input {
1002 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
1003 let alt = key.modifiers.contains(KeyModifiers::ALT);
1004 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
1005 let k = match key.code {
1006 KeyCode::Char(c) => Key::Char(c),
1007 KeyCode::Backspace => Key::Backspace,
1008 KeyCode::Delete => Key::Delete,
1009 KeyCode::Enter => Key::Enter,
1010 KeyCode::Left => Key::Left,
1011 KeyCode::Right => Key::Right,
1012 KeyCode::Up => Key::Up,
1013 KeyCode::Down => Key::Down,
1014 KeyCode::Home => Key::Home,
1015 KeyCode::End => Key::End,
1016 KeyCode::Tab => Key::Tab,
1017 KeyCode::Esc => Key::Esc,
1018 _ => Key::Null,
1019 };
1020 Input {
1021 key: k,
1022 ctrl,
1023 alt,
1024 shift,
1025 }
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031 use crossterm::event::KeyEvent;
1032
1033 fn key(code: KeyCode) -> KeyEvent {
1034 KeyEvent::new(code, KeyModifiers::NONE)
1035 }
1036 fn shift_key(code: KeyCode) -> KeyEvent {
1037 KeyEvent::new(code, KeyModifiers::SHIFT)
1038 }
1039 fn ctrl_key(code: KeyCode) -> KeyEvent {
1040 KeyEvent::new(code, KeyModifiers::CONTROL)
1041 }
1042
1043 #[test]
1044 fn vim_normal_to_insert() {
1045 let mut e = Editor::new(KeybindingMode::Vim);
1046 e.handle_key(key(KeyCode::Char('i')));
1047 assert_eq!(e.vim_mode(), VimMode::Insert);
1048 }
1049
1050 #[test]
1051 fn vim_insert_to_normal() {
1052 let mut e = Editor::new(KeybindingMode::Vim);
1053 e.handle_key(key(KeyCode::Char('i')));
1054 e.handle_key(key(KeyCode::Esc));
1055 assert_eq!(e.vim_mode(), VimMode::Normal);
1056 }
1057
1058 #[test]
1059 fn vim_normal_to_visual() {
1060 let mut e = Editor::new(KeybindingMode::Vim);
1061 e.handle_key(key(KeyCode::Char('v')));
1062 assert_eq!(e.vim_mode(), VimMode::Visual);
1063 }
1064
1065 #[test]
1066 fn vim_visual_to_normal() {
1067 let mut e = Editor::new(KeybindingMode::Vim);
1068 e.handle_key(key(KeyCode::Char('v')));
1069 e.handle_key(key(KeyCode::Esc));
1070 assert_eq!(e.vim_mode(), VimMode::Normal);
1071 }
1072
1073 #[test]
1074 fn vim_shift_i_moves_to_first_non_whitespace() {
1075 let mut e = Editor::new(KeybindingMode::Vim);
1076 e.set_content(" hello");
1077 e.jump_cursor(0, 8);
1078 e.handle_key(shift_key(KeyCode::Char('I')));
1079 assert_eq!(e.vim_mode(), VimMode::Insert);
1080 assert_eq!(e.cursor(), (0, 3));
1081 }
1082
1083 #[test]
1084 fn vim_shift_a_moves_to_end_and_insert() {
1085 let mut e = Editor::new(KeybindingMode::Vim);
1086 e.set_content("hello");
1087 e.handle_key(shift_key(KeyCode::Char('A')));
1088 assert_eq!(e.vim_mode(), VimMode::Insert);
1089 assert_eq!(e.cursor().1, 5);
1090 }
1091
1092 #[test]
1093 fn count_10j_moves_down_10() {
1094 let mut e = Editor::new(KeybindingMode::Vim);
1095 e.set_content(
1096 (0..20)
1097 .map(|i| format!("line{i}"))
1098 .collect::<Vec<_>>()
1099 .join("\n")
1100 .as_str(),
1101 );
1102 for d in "10".chars() {
1103 e.handle_key(key(KeyCode::Char(d)));
1104 }
1105 e.handle_key(key(KeyCode::Char('j')));
1106 assert_eq!(e.cursor().0, 10);
1107 }
1108
1109 #[test]
1110 fn count_o_repeats_insert_on_esc() {
1111 let mut e = Editor::new(KeybindingMode::Vim);
1112 e.set_content("hello");
1113 for d in "3".chars() {
1114 e.handle_key(key(KeyCode::Char(d)));
1115 }
1116 e.handle_key(key(KeyCode::Char('o')));
1117 assert_eq!(e.vim_mode(), VimMode::Insert);
1118 for c in "world".chars() {
1119 e.handle_key(key(KeyCode::Char(c)));
1120 }
1121 e.handle_key(key(KeyCode::Esc));
1122 assert_eq!(e.vim_mode(), VimMode::Normal);
1123 assert_eq!(e.buffer().lines().len(), 4);
1124 assert!(e.buffer().lines().iter().skip(1).all(|l| l == "world"));
1125 }
1126
1127 #[test]
1128 fn count_i_repeats_text_on_esc() {
1129 let mut e = Editor::new(KeybindingMode::Vim);
1130 e.set_content("");
1131 for d in "3".chars() {
1132 e.handle_key(key(KeyCode::Char(d)));
1133 }
1134 e.handle_key(key(KeyCode::Char('i')));
1135 for c in "ab".chars() {
1136 e.handle_key(key(KeyCode::Char(c)));
1137 }
1138 e.handle_key(key(KeyCode::Esc));
1139 assert_eq!(e.vim_mode(), VimMode::Normal);
1140 assert_eq!(e.buffer().lines()[0], "ababab");
1141 }
1142
1143 #[test]
1144 fn vim_shift_o_opens_line_above() {
1145 let mut e = Editor::new(KeybindingMode::Vim);
1146 e.set_content("hello");
1147 e.handle_key(shift_key(KeyCode::Char('O')));
1148 assert_eq!(e.vim_mode(), VimMode::Insert);
1149 assert_eq!(e.cursor(), (0, 0));
1150 assert_eq!(e.buffer().lines().len(), 2);
1151 }
1152
1153 #[test]
1154 fn vim_gg_goes_to_top() {
1155 let mut e = Editor::new(KeybindingMode::Vim);
1156 e.set_content("a\nb\nc");
1157 e.jump_cursor(2, 0);
1158 e.handle_key(key(KeyCode::Char('g')));
1159 e.handle_key(key(KeyCode::Char('g')));
1160 assert_eq!(e.cursor().0, 0);
1161 }
1162
1163 #[test]
1164 fn vim_shift_g_goes_to_bottom() {
1165 let mut e = Editor::new(KeybindingMode::Vim);
1166 e.set_content("a\nb\nc");
1167 e.handle_key(shift_key(KeyCode::Char('G')));
1168 assert_eq!(e.cursor().0, 2);
1169 }
1170
1171 #[test]
1172 fn vim_dd_deletes_line() {
1173 let mut e = Editor::new(KeybindingMode::Vim);
1174 e.set_content("first\nsecond");
1175 e.handle_key(key(KeyCode::Char('d')));
1176 e.handle_key(key(KeyCode::Char('d')));
1177 assert_eq!(e.buffer().lines().len(), 1);
1178 assert_eq!(e.buffer().lines()[0], "second");
1179 }
1180
1181 #[test]
1182 fn vim_dw_deletes_word() {
1183 let mut e = Editor::new(KeybindingMode::Vim);
1184 e.set_content("hello world");
1185 e.handle_key(key(KeyCode::Char('d')));
1186 e.handle_key(key(KeyCode::Char('w')));
1187 assert_eq!(e.vim_mode(), VimMode::Normal);
1188 assert!(!e.buffer().lines()[0].starts_with("hello"));
1189 }
1190
1191 #[test]
1192 fn vim_yy_yanks_line() {
1193 let mut e = Editor::new(KeybindingMode::Vim);
1194 e.set_content("hello\nworld");
1195 e.handle_key(key(KeyCode::Char('y')));
1196 e.handle_key(key(KeyCode::Char('y')));
1197 assert!(e.last_yank.as_deref().unwrap_or("").starts_with("hello"));
1198 }
1199
1200 #[test]
1201 fn vim_yy_does_not_move_cursor() {
1202 let mut e = Editor::new(KeybindingMode::Vim);
1203 e.set_content("first\nsecond\nthird");
1204 e.jump_cursor(1, 0);
1205 let before = e.cursor();
1206 e.handle_key(key(KeyCode::Char('y')));
1207 e.handle_key(key(KeyCode::Char('y')));
1208 assert_eq!(e.cursor(), before);
1209 assert_eq!(e.vim_mode(), VimMode::Normal);
1210 }
1211
1212 #[test]
1213 fn vim_yw_yanks_word() {
1214 let mut e = Editor::new(KeybindingMode::Vim);
1215 e.set_content("hello world");
1216 e.handle_key(key(KeyCode::Char('y')));
1217 e.handle_key(key(KeyCode::Char('w')));
1218 assert_eq!(e.vim_mode(), VimMode::Normal);
1219 assert!(e.last_yank.is_some());
1220 }
1221
1222 #[test]
1223 fn vim_cc_changes_line() {
1224 let mut e = Editor::new(KeybindingMode::Vim);
1225 e.set_content("hello\nworld");
1226 e.handle_key(key(KeyCode::Char('c')));
1227 e.handle_key(key(KeyCode::Char('c')));
1228 assert_eq!(e.vim_mode(), VimMode::Insert);
1229 }
1230
1231 #[test]
1232 fn vim_u_undoes_insert_session_as_chunk() {
1233 let mut e = Editor::new(KeybindingMode::Vim);
1234 e.set_content("hello");
1235 e.handle_key(key(KeyCode::Char('i')));
1236 e.handle_key(key(KeyCode::Enter));
1237 e.handle_key(key(KeyCode::Enter));
1238 e.handle_key(key(KeyCode::Esc));
1239 assert_eq!(e.buffer().lines().len(), 3);
1240 e.handle_key(key(KeyCode::Char('u')));
1241 assert_eq!(e.buffer().lines().len(), 1);
1242 assert_eq!(e.buffer().lines()[0], "hello");
1243 }
1244
1245 #[test]
1246 fn vim_undo_redo_roundtrip() {
1247 let mut e = Editor::new(KeybindingMode::Vim);
1248 e.set_content("hello");
1249 e.handle_key(key(KeyCode::Char('i')));
1250 for c in "world".chars() {
1251 e.handle_key(key(KeyCode::Char(c)));
1252 }
1253 e.handle_key(key(KeyCode::Esc));
1254 let after = e.buffer().lines()[0].clone();
1255 e.handle_key(key(KeyCode::Char('u')));
1256 assert_eq!(e.buffer().lines()[0], "hello");
1257 e.handle_key(ctrl_key(KeyCode::Char('r')));
1258 assert_eq!(e.buffer().lines()[0], after);
1259 }
1260
1261 #[test]
1262 fn vim_u_undoes_dd() {
1263 let mut e = Editor::new(KeybindingMode::Vim);
1264 e.set_content("first\nsecond");
1265 e.handle_key(key(KeyCode::Char('d')));
1266 e.handle_key(key(KeyCode::Char('d')));
1267 assert_eq!(e.buffer().lines().len(), 1);
1268 e.handle_key(key(KeyCode::Char('u')));
1269 assert_eq!(e.buffer().lines().len(), 2);
1270 assert_eq!(e.buffer().lines()[0], "first");
1271 }
1272
1273 #[test]
1274 fn vim_ctrl_r_redoes() {
1275 let mut e = Editor::new(KeybindingMode::Vim);
1276 e.set_content("hello");
1277 e.handle_key(ctrl_key(KeyCode::Char('r')));
1278 }
1279
1280 #[test]
1281 fn vim_r_replaces_char() {
1282 let mut e = Editor::new(KeybindingMode::Vim);
1283 e.set_content("hello");
1284 e.handle_key(key(KeyCode::Char('r')));
1285 e.handle_key(key(KeyCode::Char('x')));
1286 assert_eq!(e.buffer().lines()[0].chars().next(), Some('x'));
1287 }
1288
1289 #[test]
1290 fn vim_tilde_toggles_case() {
1291 let mut e = Editor::new(KeybindingMode::Vim);
1292 e.set_content("hello");
1293 e.handle_key(key(KeyCode::Char('~')));
1294 assert_eq!(e.buffer().lines()[0].chars().next(), Some('H'));
1295 }
1296
1297 #[test]
1298 fn vim_visual_d_cuts() {
1299 let mut e = Editor::new(KeybindingMode::Vim);
1300 e.set_content("hello");
1301 e.handle_key(key(KeyCode::Char('v')));
1302 e.handle_key(key(KeyCode::Char('l')));
1303 e.handle_key(key(KeyCode::Char('l')));
1304 e.handle_key(key(KeyCode::Char('d')));
1305 assert_eq!(e.vim_mode(), VimMode::Normal);
1306 assert!(e.last_yank.is_some());
1307 }
1308
1309 #[test]
1310 fn vim_visual_c_enters_insert() {
1311 let mut e = Editor::new(KeybindingMode::Vim);
1312 e.set_content("hello");
1313 e.handle_key(key(KeyCode::Char('v')));
1314 e.handle_key(key(KeyCode::Char('l')));
1315 e.handle_key(key(KeyCode::Char('c')));
1316 assert_eq!(e.vim_mode(), VimMode::Insert);
1317 }
1318
1319 #[test]
1320 fn vim_normal_unknown_key_consumed() {
1321 let mut e = Editor::new(KeybindingMode::Vim);
1322 let consumed = e.handle_key(key(KeyCode::Char('z')));
1324 assert!(consumed);
1325 }
1326
1327 #[test]
1328 fn force_normal_clears_operator() {
1329 let mut e = Editor::new(KeybindingMode::Vim);
1330 e.handle_key(key(KeyCode::Char('d')));
1331 e.force_normal();
1332 assert_eq!(e.vim_mode(), VimMode::Normal);
1333 }
1334
1335 fn many_lines(n: usize) -> String {
1336 (0..n)
1337 .map(|i| format!("line{i}"))
1338 .collect::<Vec<_>>()
1339 .join("\n")
1340 }
1341
1342 fn prime_viewport(e: &mut Editor<'_>, height: u16) {
1343 e.set_viewport_height(height);
1344 }
1345
1346 #[test]
1347 fn zz_centers_cursor_in_viewport() {
1348 let mut e = Editor::new(KeybindingMode::Vim);
1349 e.set_content(&many_lines(100));
1350 prime_viewport(&mut e, 20);
1351 e.jump_cursor(50, 0);
1352 e.handle_key(key(KeyCode::Char('z')));
1353 e.handle_key(key(KeyCode::Char('z')));
1354 assert_eq!(e.buffer().viewport().top_row, 40);
1355 assert_eq!(e.cursor().0, 50);
1356 }
1357
1358 #[test]
1359 fn zt_puts_cursor_at_viewport_top_with_scrolloff() {
1360 let mut e = Editor::new(KeybindingMode::Vim);
1361 e.set_content(&many_lines(100));
1362 prime_viewport(&mut e, 20);
1363 e.jump_cursor(50, 0);
1364 e.handle_key(key(KeyCode::Char('z')));
1365 e.handle_key(key(KeyCode::Char('t')));
1366 assert_eq!(e.buffer().viewport().top_row, 45);
1369 assert_eq!(e.cursor().0, 50);
1370 }
1371
1372 #[test]
1373 fn ctrl_a_increments_number_at_cursor() {
1374 let mut e = Editor::new(KeybindingMode::Vim);
1375 e.set_content("x = 41");
1376 e.handle_key(ctrl_key(KeyCode::Char('a')));
1377 assert_eq!(e.buffer().lines()[0], "x = 42");
1378 assert_eq!(e.cursor(), (0, 5));
1379 }
1380
1381 #[test]
1382 fn ctrl_a_finds_number_to_right_of_cursor() {
1383 let mut e = Editor::new(KeybindingMode::Vim);
1384 e.set_content("foo 99 bar");
1385 e.handle_key(ctrl_key(KeyCode::Char('a')));
1386 assert_eq!(e.buffer().lines()[0], "foo 100 bar");
1387 assert_eq!(e.cursor(), (0, 6));
1388 }
1389
1390 #[test]
1391 fn ctrl_a_with_count_adds_count() {
1392 let mut e = Editor::new(KeybindingMode::Vim);
1393 e.set_content("x = 10");
1394 for d in "5".chars() {
1395 e.handle_key(key(KeyCode::Char(d)));
1396 }
1397 e.handle_key(ctrl_key(KeyCode::Char('a')));
1398 assert_eq!(e.buffer().lines()[0], "x = 15");
1399 }
1400
1401 #[test]
1402 fn ctrl_x_decrements_number() {
1403 let mut e = Editor::new(KeybindingMode::Vim);
1404 e.set_content("n=5");
1405 e.handle_key(ctrl_key(KeyCode::Char('x')));
1406 assert_eq!(e.buffer().lines()[0], "n=4");
1407 }
1408
1409 #[test]
1410 fn ctrl_x_crosses_zero_into_negative() {
1411 let mut e = Editor::new(KeybindingMode::Vim);
1412 e.set_content("v=0");
1413 e.handle_key(ctrl_key(KeyCode::Char('x')));
1414 assert_eq!(e.buffer().lines()[0], "v=-1");
1415 }
1416
1417 #[test]
1418 fn ctrl_a_on_negative_number_increments_toward_zero() {
1419 let mut e = Editor::new(KeybindingMode::Vim);
1420 e.set_content("a = -5");
1421 e.handle_key(ctrl_key(KeyCode::Char('a')));
1422 assert_eq!(e.buffer().lines()[0], "a = -4");
1423 }
1424
1425 #[test]
1426 fn ctrl_a_noop_when_no_digit_on_line() {
1427 let mut e = Editor::new(KeybindingMode::Vim);
1428 e.set_content("no digits here");
1429 e.handle_key(ctrl_key(KeyCode::Char('a')));
1430 assert_eq!(e.buffer().lines()[0], "no digits here");
1431 }
1432
1433 #[test]
1434 fn zb_puts_cursor_at_viewport_bottom_with_scrolloff() {
1435 let mut e = Editor::new(KeybindingMode::Vim);
1436 e.set_content(&many_lines(100));
1437 prime_viewport(&mut e, 20);
1438 e.jump_cursor(50, 0);
1439 e.handle_key(key(KeyCode::Char('z')));
1440 e.handle_key(key(KeyCode::Char('b')));
1441 assert_eq!(e.buffer().viewport().top_row, 36);
1445 assert_eq!(e.cursor().0, 50);
1446 }
1447
1448 #[test]
1455 fn set_content_dirties_then_take_dirty_clears() {
1456 let mut e = Editor::new(KeybindingMode::Vim);
1457 e.set_content("hello");
1458 assert!(
1459 e.take_dirty(),
1460 "set_content should leave content_dirty=true"
1461 );
1462 assert!(!e.take_dirty(), "take_dirty should clear the flag");
1463 }
1464
1465 #[test]
1466 fn content_arc_returns_same_arc_until_mutation() {
1467 let mut e = Editor::new(KeybindingMode::Vim);
1468 e.set_content("hello");
1469 let a = e.content_arc();
1470 let b = e.content_arc();
1471 assert!(
1472 std::sync::Arc::ptr_eq(&a, &b),
1473 "repeated content_arc() should hit the cache"
1474 );
1475
1476 e.handle_key(key(KeyCode::Char('i')));
1478 e.handle_key(key(KeyCode::Char('!')));
1479 let c = e.content_arc();
1480 assert!(
1481 !std::sync::Arc::ptr_eq(&a, &c),
1482 "mutation should invalidate content_arc() cache"
1483 );
1484 assert!(c.contains('!'));
1485 }
1486
1487 #[test]
1488 fn content_arc_cache_invalidated_by_set_content() {
1489 let mut e = Editor::new(KeybindingMode::Vim);
1490 e.set_content("one");
1491 let a = e.content_arc();
1492 e.set_content("two");
1493 let b = e.content_arc();
1494 assert!(!std::sync::Arc::ptr_eq(&a, &b));
1495 assert!(b.starts_with("two"));
1496 }
1497
1498 #[test]
1504 fn mouse_click_past_eol_lands_on_last_char() {
1505 let mut e = Editor::new(KeybindingMode::Vim);
1506 e.set_content("hello");
1507 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1511 e.mouse_click(area, 78, 1);
1512 assert_eq!(e.cursor(), (0, 4));
1513 }
1514
1515 #[test]
1516 fn mouse_click_past_eol_handles_multibyte_line() {
1517 let mut e = Editor::new(KeybindingMode::Vim);
1518 e.set_content("héllo");
1521 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1522 e.mouse_click(area, 78, 1);
1523 assert_eq!(e.cursor(), (0, 4));
1524 }
1525
1526 #[test]
1527 fn mouse_click_inside_line_lands_on_clicked_char() {
1528 let mut e = Editor::new(KeybindingMode::Vim);
1529 e.set_content("hello world");
1530 let area = ratatui::layout::Rect::new(0, 0, 80, 10);
1533 e.mouse_click(area, 4, 1);
1534 assert_eq!(e.cursor(), (0, 0));
1535 e.mouse_click(area, 6, 1);
1536 assert_eq!(e.cursor(), (0, 2));
1537 }
1538}