1use crate::selection::{Selection, SelectionMode};
8use crate::smart_selection::is_word_char;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum VisualMode {
14 None,
16 Char,
18 Line,
20 Block,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PendingOperator {
27 Yank,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum SearchDirection {
34 Forward,
35 Backward,
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct Mark {
41 pub col: usize,
42 pub absolute_line: usize,
43}
44
45pub struct CopyModeState {
53 pub active: bool,
55 pub cursor_col: usize,
57 pub cursor_absolute_line: usize,
59 pub visual_mode: VisualMode,
61 pub selection_anchor: Option<(usize, usize)>,
63 pub count: Option<usize>,
65 pub pending_operator: Option<PendingOperator>,
67 pub marks: HashMap<char, Mark>,
69 pub cols: usize,
71 pub rows: usize,
73 pub scrollback_len: usize,
75 pub search_query: String,
77 pub search_direction: SearchDirection,
79 pub is_searching: bool,
81 pub(crate) pending_g: bool,
83 pub(crate) pending_mark_set: bool,
85 pub(crate) pending_mark_goto: bool,
87}
88
89impl Default for CopyModeState {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl CopyModeState {
96 pub fn new() -> Self {
98 Self {
99 active: false,
100 cursor_col: 0,
101 cursor_absolute_line: 0,
102 visual_mode: VisualMode::None,
103 selection_anchor: None,
104 count: None,
105 pending_operator: None,
106 marks: HashMap::new(),
107 cols: 80,
108 rows: 24,
109 scrollback_len: 0,
110 search_query: String::new(),
111 search_direction: SearchDirection::Forward,
112 is_searching: false,
113 pending_g: false,
114 pending_mark_set: false,
115 pending_mark_goto: false,
116 }
117 }
118
119 pub fn enter(
121 &mut self,
122 cursor_col: usize,
123 cursor_row: usize,
124 cols: usize,
125 rows: usize,
126 scrollback_len: usize,
127 ) {
128 self.active = true;
129 self.cols = cols;
130 self.rows = rows;
131 self.scrollback_len = scrollback_len;
132 self.cursor_absolute_line = scrollback_len + cursor_row;
134 self.cursor_col = cursor_col.min(cols.saturating_sub(1));
135 self.visual_mode = VisualMode::None;
136 self.selection_anchor = None;
137 self.count = None;
138 self.pending_operator = None;
139 self.search_query.clear();
140 self.is_searching = false;
141 self.pending_g = false;
142 self.pending_mark_set = false;
143 self.pending_mark_goto = false;
144 }
145
146 pub fn exit(&mut self) {
148 self.active = false;
149 self.visual_mode = VisualMode::None;
150 self.selection_anchor = None;
151 self.count = None;
152 self.pending_operator = None;
153 self.is_searching = false;
154 self.pending_g = false;
155 self.pending_mark_set = false;
156 self.pending_mark_goto = false;
157 }
158
159 pub fn push_count_digit(&mut self, digit: u8) {
165 let current = self.count.unwrap_or(0);
166 self.count = Some(current * 10 + digit as usize);
167 }
168
169 pub fn effective_count(&mut self) -> usize {
171 let c = self.count.unwrap_or(1);
172 self.count = None;
173 c
174 }
175
176 fn total_lines(&self) -> usize {
178 self.scrollback_len + self.rows
179 }
180
181 fn max_line(&self) -> usize {
183 self.total_lines().saturating_sub(1)
184 }
185
186 pub fn move_left(&mut self) {
192 let count = self.effective_count();
193 self.cursor_col = self.cursor_col.saturating_sub(count);
194 }
195
196 pub fn move_right(&mut self) {
198 let count = self.effective_count();
199 self.cursor_col = (self.cursor_col + count).min(self.cols.saturating_sub(1));
200 }
201
202 pub fn move_up(&mut self) {
204 let count = self.effective_count();
205 self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(count);
206 }
207
208 pub fn move_down(&mut self) {
210 let count = self.effective_count();
211 self.cursor_absolute_line = (self.cursor_absolute_line + count).min(self.max_line());
212 }
213
214 pub fn move_to_line_start(&mut self) {
216 self.cursor_col = 0;
217 }
218
219 pub fn move_to_line_end(&mut self) {
221 self.cursor_col = self.cols.saturating_sub(1);
222 }
223
224 pub fn move_to_first_non_blank(&mut self, line_text: &str) {
226 let first_non_blank = line_text
227 .chars()
228 .position(|c| !c.is_whitespace())
229 .unwrap_or(0);
230 self.cursor_col = first_non_blank.min(self.cols.saturating_sub(1));
231 }
232
233 pub fn move_word_forward(&mut self, line_text: &str, word_chars: &str) {
239 let count = self.effective_count();
240 let chars: Vec<char> = line_text.chars().collect();
241 let mut col = self.cursor_col;
242
243 for _ in 0..count {
244 if col >= chars.len() {
245 break;
246 }
247 while col < chars.len() && is_word_char(chars[col], word_chars) {
249 col += 1;
250 }
251 while col < chars.len() && !is_word_char(chars[col], word_chars) {
253 col += 1;
254 }
255 }
256
257 self.cursor_col = col.min(self.cols.saturating_sub(1));
258 }
259
260 pub fn move_word_backward(&mut self, line_text: &str, word_chars: &str) {
262 let count = self.effective_count();
263 let chars: Vec<char> = line_text.chars().collect();
264 let mut col = self.cursor_col;
265
266 for _ in 0..count {
267 if col == 0 {
268 break;
269 }
270 col = col.saturating_sub(1);
271 while col > 0 && !is_word_char(chars[col], word_chars) {
273 col -= 1;
274 }
275 while col > 0 && is_word_char(chars[col - 1], word_chars) {
277 col -= 1;
278 }
279 }
280
281 self.cursor_col = col;
282 }
283
284 pub fn move_word_end(&mut self, line_text: &str, word_chars: &str) {
286 let count = self.effective_count();
287 let chars: Vec<char> = line_text.chars().collect();
288 let mut col = self.cursor_col;
289
290 for _ in 0..count {
291 if col >= chars.len().saturating_sub(1) {
292 break;
293 }
294 col += 1;
295 while col < chars.len() && !is_word_char(chars[col], word_chars) {
297 col += 1;
298 }
299 while col < chars.len().saturating_sub(1) && is_word_char(chars[col + 1], word_chars) {
301 col += 1;
302 }
303 }
304
305 self.cursor_col = col.min(self.cols.saturating_sub(1));
306 }
307
308 pub fn move_big_word_forward(&mut self, line_text: &str) {
310 let count = self.effective_count();
311 let chars: Vec<char> = line_text.chars().collect();
312 let mut col = self.cursor_col;
313
314 for _ in 0..count {
315 while col < chars.len() && !chars[col].is_whitespace() {
317 col += 1;
318 }
319 while col < chars.len() && chars[col].is_whitespace() {
321 col += 1;
322 }
323 }
324
325 self.cursor_col = col.min(self.cols.saturating_sub(1));
326 }
327
328 pub fn move_big_word_backward(&mut self, line_text: &str) {
330 let count = self.effective_count();
331 let chars: Vec<char> = line_text.chars().collect();
332 let mut col = self.cursor_col;
333
334 for _ in 0..count {
335 if col == 0 {
336 break;
337 }
338 col = col.saturating_sub(1);
339 while col > 0 && chars[col].is_whitespace() {
341 col -= 1;
342 }
343 while col > 0 && !chars[col - 1].is_whitespace() {
345 col -= 1;
346 }
347 }
348
349 self.cursor_col = col;
350 }
351
352 pub fn move_big_word_end(&mut self, line_text: &str) {
354 let count = self.effective_count();
355 let chars: Vec<char> = line_text.chars().collect();
356 let mut col = self.cursor_col;
357
358 for _ in 0..count {
359 if col >= chars.len().saturating_sub(1) {
360 break;
361 }
362 col += 1;
363 while col < chars.len() && chars[col].is_whitespace() {
365 col += 1;
366 }
367 while col < chars.len().saturating_sub(1) && !chars[col + 1].is_whitespace() {
369 col += 1;
370 }
371 }
372
373 self.cursor_col = col.min(self.cols.saturating_sub(1));
374 }
375
376 pub fn half_page_up(&mut self) {
382 let half = self.rows / 2;
383 let count = self.effective_count();
384 self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(half * count);
385 }
386
387 pub fn half_page_down(&mut self) {
389 let half = self.rows / 2;
390 let count = self.effective_count();
391 self.cursor_absolute_line = (self.cursor_absolute_line + half * count).min(self.max_line());
392 }
393
394 pub fn page_up(&mut self) {
396 let count = self.effective_count();
397 self.cursor_absolute_line = self.cursor_absolute_line.saturating_sub(self.rows * count);
398 }
399
400 pub fn page_down(&mut self) {
402 let count = self.effective_count();
403 self.cursor_absolute_line =
404 (self.cursor_absolute_line + self.rows * count).min(self.max_line());
405 }
406
407 pub fn goto_top(&mut self) {
409 self.cursor_absolute_line = 0;
410 }
411
412 pub fn goto_bottom(&mut self) {
414 self.cursor_absolute_line = self.max_line();
415 }
416
417 pub fn goto_line(&mut self, line: usize) {
419 self.cursor_absolute_line = line.min(self.max_line());
420 }
421
422 pub fn toggle_visual_char(&mut self) {
428 if self.visual_mode == VisualMode::Char {
429 self.visual_mode = VisualMode::None;
430 self.selection_anchor = None;
431 } else {
432 self.visual_mode = VisualMode::Char;
433 self.selection_anchor = Some((self.cursor_absolute_line, self.cursor_col));
434 }
435 }
436
437 pub fn toggle_visual_line(&mut self) {
439 if self.visual_mode == VisualMode::Line {
440 self.visual_mode = VisualMode::None;
441 self.selection_anchor = None;
442 } else {
443 self.visual_mode = VisualMode::Line;
444 self.selection_anchor = Some((self.cursor_absolute_line, self.cursor_col));
445 }
446 }
447
448 pub fn toggle_visual_block(&mut self) {
450 if self.visual_mode == VisualMode::Block {
451 self.visual_mode = VisualMode::None;
452 self.selection_anchor = None;
453 } else {
454 self.visual_mode = VisualMode::Block;
455 self.selection_anchor = Some((self.cursor_absolute_line, self.cursor_col));
456 }
457 }
458
459 pub fn compute_selection(&self, scroll_offset: usize) -> Option<Selection> {
465 if self.visual_mode == VisualMode::None {
466 return None;
467 }
468
469 let (anchor_line, anchor_col) = self.selection_anchor?;
470
471 let viewport_top = self.scrollback_len.saturating_sub(scroll_offset);
474
475 let anchor_row = anchor_line.saturating_sub(viewport_top);
479 let cursor_row = self.cursor_absolute_line.saturating_sub(viewport_top);
480
481 let mode = match self.visual_mode {
482 VisualMode::None => return None,
483 VisualMode::Char => SelectionMode::Normal,
484 VisualMode::Line => SelectionMode::Line,
485 VisualMode::Block => SelectionMode::Rectangular,
486 };
487
488 let start = (anchor_col, anchor_row);
489 let end = (self.cursor_col, cursor_row);
490
491 Some(Selection::new(start, end, mode))
492 }
493
494 pub fn set_mark(&mut self, name: char) {
500 self.marks.insert(
501 name,
502 Mark {
503 col: self.cursor_col,
504 absolute_line: self.cursor_absolute_line,
505 },
506 );
507 }
508
509 pub fn goto_mark(&mut self, name: char) -> bool {
511 if let Some(mark) = self.marks.get(&name) {
512 self.cursor_col = mark.col;
513 self.cursor_absolute_line = mark.absolute_line;
514 true
515 } else {
516 false
517 }
518 }
519
520 pub fn start_search(&mut self, direction: SearchDirection) {
526 self.is_searching = true;
527 self.search_direction = direction;
528 self.search_query.clear();
529 }
530
531 pub fn search_input(&mut self, ch: char) {
533 self.search_query.push(ch);
534 }
535
536 pub fn search_backspace(&mut self) {
538 self.search_query.pop();
539 }
540
541 pub fn cancel_search(&mut self) {
543 self.is_searching = false;
544 self.search_query.clear();
545 }
546
547 pub fn screen_cursor_pos(&self, scroll_offset: usize) -> Option<(usize, usize)> {
556 let viewport_top = self.scrollback_len.saturating_sub(scroll_offset);
557 let viewport_bottom = viewport_top + self.rows;
558
559 if self.cursor_absolute_line >= viewport_top && self.cursor_absolute_line < viewport_bottom
560 {
561 let screen_row = self.cursor_absolute_line - viewport_top;
562 Some((self.cursor_col, screen_row))
563 } else {
564 None
565 }
566 }
567
568 pub fn required_scroll_offset(&self, current_offset: usize) -> Option<usize> {
572 let viewport_top = self.scrollback_len.saturating_sub(current_offset);
573 let viewport_bottom = viewport_top + self.rows;
574
575 if self.cursor_absolute_line < viewport_top {
576 let new_offset = self
578 .scrollback_len
579 .saturating_sub(self.cursor_absolute_line);
580 Some(new_offset)
581 } else if self.cursor_absolute_line >= viewport_bottom {
582 let lines_below = self.cursor_absolute_line - viewport_top;
584 let needed_offset = current_offset
585 .saturating_sub(lines_below.saturating_sub(self.rows.saturating_sub(1)));
586 let target_viewport_top = self
588 .cursor_absolute_line
589 .saturating_sub(self.rows.saturating_sub(1));
590 let new_offset = self.scrollback_len.saturating_sub(target_viewport_top);
591 let _ = needed_offset; Some(new_offset.min(self.scrollback_len))
594 } else {
595 None
596 }
597 }
598
599 pub fn update_dimensions(&mut self, cols: usize, rows: usize, scrollback_len: usize) {
601 self.cols = cols;
602 self.rows = rows;
603 self.scrollback_len = scrollback_len;
604 self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
606 self.cursor_absolute_line = self.cursor_absolute_line.min(self.max_line());
607 }
608
609 pub fn status_text(&self) -> String {
611 if self.is_searching {
612 let dir = match self.search_direction {
613 SearchDirection::Forward => '/',
614 SearchDirection::Backward => '?',
615 };
616 format!("{}{}", dir, self.search_query)
617 } else {
618 let mode = match self.visual_mode {
619 VisualMode::None => "COPY",
620 VisualMode::Char => "VISUAL",
621 VisualMode::Line => "VISUAL LINE",
622 VisualMode::Block => "VISUAL BLOCK",
623 };
624 let pos = format!(
625 "{}:{} (abs {})",
626 self.cursor_absolute_line
627 .saturating_sub(self.scrollback_len),
628 self.cursor_col,
629 self.cursor_absolute_line,
630 );
631 format!("-- {} -- {}", mode, pos)
632 }
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
641 fn test_enter_exit() {
642 let mut cm = CopyModeState::new();
643 assert!(!cm.active);
644
645 cm.enter(5, 10, 80, 24, 100);
646 assert!(cm.active);
647 assert_eq!(cm.cursor_col, 5);
648 assert_eq!(cm.cursor_absolute_line, 110); assert_eq!(cm.cols, 80);
650 assert_eq!(cm.rows, 24);
651
652 cm.exit();
653 assert!(!cm.active);
654 }
655
656 #[test]
657 fn test_basic_motions() {
658 let mut cm = CopyModeState::new();
659 cm.enter(10, 5, 80, 24, 100);
660
661 cm.move_left();
662 assert_eq!(cm.cursor_col, 9);
663
664 cm.move_right();
665 assert_eq!(cm.cursor_col, 10);
666
667 cm.move_up();
668 assert_eq!(cm.cursor_absolute_line, 104);
669
670 cm.move_down();
671 assert_eq!(cm.cursor_absolute_line, 105);
672
673 cm.move_to_line_start();
674 assert_eq!(cm.cursor_col, 0);
675
676 cm.move_to_line_end();
677 assert_eq!(cm.cursor_col, 79);
678 }
679
680 #[test]
681 fn test_count_prefix() {
682 let mut cm = CopyModeState::new();
683 cm.enter(10, 12, 80, 24, 100);
684
685 cm.push_count_digit(5);
686 cm.move_down();
687 assert_eq!(cm.cursor_absolute_line, 117);
688 }
689
690 #[test]
691 fn test_boundary_clamping() {
692 let mut cm = CopyModeState::new();
693 cm.enter(0, 0, 80, 24, 0);
694
695 cm.move_up();
697 assert_eq!(cm.cursor_absolute_line, 0);
698
699 cm.move_left();
701 assert_eq!(cm.cursor_col, 0);
702
703 cm.goto_bottom();
705 assert_eq!(cm.cursor_absolute_line, 23);
706 cm.move_down();
707 assert_eq!(cm.cursor_absolute_line, 23);
708 }
709
710 #[test]
711 fn test_visual_modes() {
712 let mut cm = CopyModeState::new();
713 cm.enter(5, 5, 80, 24, 100);
714
715 cm.toggle_visual_char();
717 assert_eq!(cm.visual_mode, VisualMode::Char);
718 assert!(cm.selection_anchor.is_some());
719
720 cm.toggle_visual_char();
722 assert_eq!(cm.visual_mode, VisualMode::None);
723 assert!(cm.selection_anchor.is_none());
724
725 cm.toggle_visual_line();
727 assert_eq!(cm.visual_mode, VisualMode::Line);
728
729 cm.toggle_visual_block();
731 assert_eq!(cm.visual_mode, VisualMode::Block);
732 }
733
734 #[test]
735 fn test_screen_cursor_pos() {
736 let mut cm = CopyModeState::new();
737 cm.enter(5, 10, 80, 24, 100);
738 assert_eq!(cm.screen_cursor_pos(0), Some((5, 10)));
742
743 cm.cursor_absolute_line = 50;
745 assert_eq!(cm.screen_cursor_pos(0), None);
746
747 assert_eq!(cm.screen_cursor_pos(50), Some((5, 0)));
749 }
750
751 #[test]
752 fn test_compute_selection() {
753 let mut cm = CopyModeState::new();
754 cm.enter(5, 5, 80, 24, 100);
755
756 assert!(cm.compute_selection(0).is_none());
758
759 cm.toggle_visual_char();
761 cm.move_right();
762 cm.move_right();
763 cm.move_down();
764
765 let sel = cm.compute_selection(0).unwrap();
766 assert_eq!(sel.mode, SelectionMode::Normal);
767 assert_eq!(sel.start, (5, 5));
769 assert_eq!(sel.end, (7, 6));
770 }
771
772 #[test]
773 fn test_marks() {
774 let mut cm = CopyModeState::new();
775 cm.enter(10, 5, 80, 24, 100);
776
777 cm.set_mark('a');
778 cm.move_down();
779 cm.move_right();
780
781 assert!(cm.goto_mark('a'));
782 assert_eq!(cm.cursor_col, 10);
783 assert_eq!(cm.cursor_absolute_line, 105);
784
785 assert!(!cm.goto_mark('b')); }
787
788 #[test]
789 fn test_word_motions() {
790 let mut cm = CopyModeState::new();
791 cm.enter(0, 0, 80, 24, 0);
792
793 let line = "hello world foo";
794 cm.move_word_forward(line, "");
795 assert_eq!(cm.cursor_col, 6); cm.move_word_end(line, "");
798 assert_eq!(cm.cursor_col, 10); cm.move_word_backward(line, "");
801 assert_eq!(cm.cursor_col, 6); }
803
804 #[test]
805 fn test_page_motions() {
806 let mut cm = CopyModeState::new();
807 cm.enter(0, 12, 80, 24, 200);
808 cm.half_page_up();
811 assert_eq!(cm.cursor_absolute_line, 200); cm.page_down();
814 assert_eq!(cm.cursor_absolute_line, 223); cm.goto_top();
817 assert_eq!(cm.cursor_absolute_line, 0);
818
819 cm.goto_bottom();
820 assert_eq!(cm.cursor_absolute_line, 223);
821 }
822
823 #[test]
824 fn test_search_state() {
825 let mut cm = CopyModeState::new();
826 cm.enter(0, 0, 80, 24, 0);
827
828 cm.start_search(SearchDirection::Forward);
829 assert!(cm.is_searching);
830
831 cm.search_input('h');
832 cm.search_input('e');
833 assert_eq!(cm.search_query, "he");
834
835 cm.search_backspace();
836 assert_eq!(cm.search_query, "h");
837
838 cm.cancel_search();
839 assert!(!cm.is_searching);
840 assert!(cm.search_query.is_empty());
841 }
842
843 #[test]
844 fn test_required_scroll_offset() {
845 let mut cm = CopyModeState::new();
846 cm.enter(0, 12, 80, 24, 100);
847 assert_eq!(cm.required_scroll_offset(0), None);
851
852 cm.cursor_absolute_line = 50;
854 let offset = cm.required_scroll_offset(0).unwrap();
855 assert_eq!(offset, 50); }
857}