1use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct CopyModeCursor {
12 pub x: u16,
14 pub y: i32,
16}
17
18impl CopyModeCursor {
19 pub fn new(x: u16, y: i32) -> Self {
21 Self { x, y }
22 }
23
24 pub fn origin() -> Self {
26 Self { x: 0, y: 0 }
27 }
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Selection {
33 pub anchor: CopyModeCursor,
35 pub active: CopyModeCursor,
37}
38
39impl Selection {
40 pub fn new(anchor: CopyModeCursor, active: CopyModeCursor) -> Self {
42 Self { anchor, active }
43 }
44
45 pub fn at_point(cursor: CopyModeCursor) -> Self {
47 Self {
48 anchor: cursor,
49 active: cursor,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
56pub enum SelectionMode {
57 #[default]
59 None,
60 Cell,
62 Line,
64 Block,
66 Word,
68}
69
70#[derive(Clone, Debug, Default, Serialize, Deserialize)]
72pub struct CopyModeState {
73 pub active: bool,
75 pub cursor: CopyModeCursor,
77 pub selection: Option<Selection>,
79 pub selection_mode: SelectionMode,
81 pub viewport_offset: i32,
83}
84
85impl CopyModeState {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn activate(&mut self, cursor: CopyModeCursor) {
93 self.active = true;
94 self.cursor = cursor;
95 self.selection = None;
96 self.selection_mode = SelectionMode::None;
97 }
98
99 pub fn deactivate(&mut self) {
101 self.active = false;
102 self.selection = None;
103 self.selection_mode = SelectionMode::None;
104 }
105
106 pub fn start_selection(&mut self, mode: SelectionMode) {
108 self.selection_mode = mode;
109 self.selection = Some(Selection::at_point(self.cursor));
110 }
111
112 pub fn clear_selection(&mut self) {
114 self.selection = None;
115 self.selection_mode = SelectionMode::None;
116 }
117
118 pub fn update_selection(&mut self) {
120 if let Some(ref mut selection) = self.selection {
121 selection.active = self.cursor;
122 }
123 }
124
125 pub fn toggle_selection(&mut self, mode: SelectionMode) {
127 if self.selection_mode == mode {
128 self.clear_selection();
129 } else {
130 self.start_selection(mode);
131 }
132 }
133
134 pub fn swap_selection_ends(&mut self) {
136 if let Some(ref mut selection) = self.selection {
137 std::mem::swap(&mut selection.anchor, &mut selection.active);
138 self.cursor = selection.active;
139 }
140 }
141
142 pub fn move_left(&mut self) {
146 if self.cursor.x > 0 {
147 self.cursor.x -= 1;
148 }
149 }
150
151 pub fn move_right(&mut self, max_cols: u16) {
153 if self.cursor.x < max_cols.saturating_sub(1) {
154 self.cursor.x += 1;
155 }
156 }
157
158 pub fn move_up(&mut self, min_y: i32) {
160 if self.cursor.y > min_y {
161 self.cursor.y -= 1;
162 }
163 }
164
165 pub fn move_down(&mut self, max_y: i32) {
167 if self.cursor.y < max_y {
168 self.cursor.y += 1;
169 }
170 }
171
172 pub fn move_to_line_start(&mut self) {
174 self.cursor.x = 0;
175 }
176
177 pub fn move_to_line_end(&mut self, line_length: u16) {
179 self.cursor.x = line_length.saturating_sub(1);
180 }
181
182 pub fn move_to_top(&mut self, min_y: i32) {
184 self.cursor.y = min_y;
185 }
186
187 pub fn move_to_bottom(&mut self, max_y: i32) {
189 self.cursor.y = max_y;
190 }
191
192 pub fn toggle_cell_selection(&mut self) {
196 self.toggle_selection(SelectionMode::Cell);
197 }
198
199 pub fn toggle_line_selection(&mut self) {
201 self.toggle_selection(SelectionMode::Line);
202 }
203
204 pub fn toggle_block_selection(&mut self) {
206 self.toggle_selection(SelectionMode::Block);
207 }
208
209 pub fn toggle_word_selection(&mut self) {
211 self.toggle_selection(SelectionMode::Word);
212 }
213
214 pub fn get_selection_text<F>(&self, get_line: F) -> Option<String>
219 where
220 F: Fn(i32) -> Option<String>,
221 {
222 let selection = self.selection.as_ref()?;
223 let (start, end) = normalize_selection(selection);
224
225 let mut result = String::new();
226
227 match self.selection_mode {
228 SelectionMode::None => return None,
229 SelectionMode::Cell => {
230 if start.y == end.y {
232 if let Some(line) = get_line(start.y) {
234 let start_x = start.x as usize;
235 let end_x = (end.x as usize + 1).min(line.len());
236 if start_x < line.len() {
237 result.push_str(&line[start_x..end_x]);
238 }
239 }
240 } else {
241 for y in start.y..=end.y {
243 if let Some(line) = get_line(y) {
244 if y == start.y {
245 let start_x = start.x as usize;
247 if start_x < line.len() {
248 result.push_str(&line[start_x..]);
249 }
250 } else if y == end.y {
251 let end_x = (end.x as usize + 1).min(line.len());
253 result.push_str(&line[..end_x]);
254 } else {
255 result.push_str(&line);
257 }
258 if y < end.y {
260 result.push('\n');
261 }
262 }
263 }
264 }
265 }
266 SelectionMode::Line => {
267 for y in start.y..=end.y {
269 if let Some(line) = get_line(y) {
270 result.push_str(&line);
271 if y < end.y {
272 result.push('\n');
273 }
274 }
275 }
276 }
277 SelectionMode::Block => {
278 let min_x = start.x.min(end.x);
280 let max_x = start.x.max(end.x);
281
282 for y in start.y..=end.y {
283 if let Some(line) = get_line(y) {
284 let start_x = min_x as usize;
285 let end_x = (max_x as usize + 1).min(line.len());
286 if start_x < line.len() {
287 result.push_str(&line[start_x..end_x]);
288 }
289 if y < end.y {
290 result.push('\n');
291 }
292 }
293 }
294 }
295 SelectionMode::Word => {
296 if start.y == end.y {
298 if let Some(line) = get_line(start.y) {
299 let start_x = start.x as usize;
300 let end_x = (end.x as usize + 1).min(line.len());
301 if start_x < line.len() {
302 result.push_str(&line[start_x..end_x]);
303 }
304 }
305 }
306 }
307 }
308
309 if result.is_empty() {
310 None
311 } else {
312 Some(result)
313 }
314 }
315}
316
317#[derive(Clone, Debug, Default, Serialize, Deserialize)]
319pub struct SearchState {
320 pub active: bool,
322 pub query: String,
324 pub direction: SearchDirection,
326 pub matches: Vec<SearchMatch>,
328 pub current_match: Option<usize>,
330}
331
332impl SearchState {
333 pub fn new() -> Self {
335 Self::default()
336 }
337
338 pub fn start_search(&mut self, direction: SearchDirection) {
340 self.active = true;
341 self.direction = direction;
342 self.query.clear();
343 self.matches.clear();
344 self.current_match = None;
345 }
346
347 pub fn update_query(&mut self, query: String, matches: Vec<SearchMatch>) {
349 self.query = query;
350 let is_empty = matches.is_empty();
351 self.matches = matches;
352 self.current_match = if is_empty { None } else { Some(0) };
353 }
354
355 pub fn next_match(&mut self) {
357 if let Some(current) = self.current_match {
358 if !self.matches.is_empty() {
359 self.current_match = Some((current + 1) % self.matches.len());
360 }
361 }
362 }
363
364 pub fn prev_match(&mut self) {
366 if let Some(current) = self.current_match {
367 if !self.matches.is_empty() {
368 self.current_match = Some(if current == 0 {
369 self.matches.len() - 1
370 } else {
371 current - 1
372 });
373 }
374 }
375 }
376
377 pub fn current(&self) -> Option<&SearchMatch> {
379 self.current_match.and_then(|idx| self.matches.get(idx))
380 }
381
382 pub fn deactivate(&mut self) {
384 self.active = false;
385 self.query.clear();
386 self.matches.clear();
387 self.current_match = None;
388 }
389}
390
391#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
393pub enum SearchDirection {
394 #[default]
396 Forward,
397 Backward,
399}
400
401#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
403pub struct SearchMatch {
404 pub start: CopyModeCursor,
406 pub end: CopyModeCursor,
408}
409
410impl SearchMatch {
411 pub fn new(start: CopyModeCursor, end: CopyModeCursor) -> Self {
413 Self { start, end }
414 }
415}
416
417pub fn find_matches<F>(query: &str, get_line: F, min_y: i32, max_y: i32) -> Vec<SearchMatch>
431where
432 F: Fn(i32) -> Option<String>,
433{
434 let mut matches = Vec::new();
435
436 if query.is_empty() {
437 return matches;
438 }
439
440 let query_lower = query.to_lowercase();
441
442 for y in min_y..=max_y {
444 if let Some(line) = get_line(y) {
445 let line_lower = line.to_lowercase();
446
447 let mut start_pos = 0;
449 while let Some(match_pos) = line_lower[start_pos..].find(&query_lower) {
450 let absolute_pos = start_pos + match_pos;
451 let start = CopyModeCursor::new(absolute_pos as u16, y);
452 let end = CopyModeCursor::new((absolute_pos + query.len() - 1) as u16, y);
453 matches.push(SearchMatch::new(start, end));
454
455 start_pos = absolute_pos + 1;
457 }
458 }
459 }
460
461 matches
462}
463
464pub fn normalize_selection(selection: &Selection) -> (CopyModeCursor, CopyModeCursor) {
468 let a = selection.anchor;
469 let b = selection.active;
470
471 if a.y < b.y || (a.y == b.y && a.x <= b.x) {
472 (a, b)
473 } else {
474 (b, a)
475 }
476}
477
478pub fn get_selection_bounds(selection: &Selection) -> (u16, i32, u16, i32) {
482 let (start, end) = normalize_selection(selection);
483
484 let min_x = start.x.min(end.x);
486 let max_x = start.x.max(end.x);
487 let min_y = start.y.min(end.y);
488 let max_y = start.y.max(end.y);
489
490 (min_x, min_y, max_x, max_y)
491}
492
493pub fn find_word_bounds(x: u16, line: &str) -> (u16, u16) {
505 let x_pos = x as usize;
506
507 if line.is_empty() || x_pos >= line.len() {
508 return (x, x);
509 }
510
511 let chars: Vec<char> = line.chars().collect();
512
513 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
515
516 if x_pos >= chars.len() {
517 return (x, x);
518 }
519
520 let current_char = chars[x_pos];
521
522 if !is_word_char(current_char) {
524 return (x, x);
525 }
526
527 let mut start = x_pos;
529 while start > 0 && is_word_char(chars[start - 1]) {
530 start -= 1;
531 }
532
533 let mut end = x_pos;
535 while end < chars.len() - 1 && is_word_char(chars[end + 1]) {
536 end += 1;
537 }
538
539 (start as u16, end as u16)
540}
541
542use crate::status_bar::{Color, RenderItem};
544
545pub fn copy_mode_indicator(state: &CopyModeState, search_active: bool) -> Vec<RenderItem> {
567 if !state.active {
568 return vec![];
569 }
570
571 let mode_text = if search_active {
572 "SEARCH"
573 } else {
574 match state.selection {
575 None => "COPY",
576 Some(_) => match state.selection_mode {
577 SelectionMode::None => "COPY",
578 SelectionMode::Cell => "VISUAL",
579 SelectionMode::Line => "V-LINE",
580 SelectionMode::Block => "V-BLOCK",
581 SelectionMode::Word => "V-WORD",
582 },
583 }
584 };
585
586 vec![
587 RenderItem::Background(Color::Rgb(255, 158, 100)), RenderItem::Foreground(Color::Rgb(26, 27, 38)), RenderItem::Bold,
590 RenderItem::Text(format!(" {} ", mode_text)),
591 RenderItem::ResetAttributes,
592 ]
593}
594
595pub fn copy_mode_position_indicator(state: &CopyModeState) -> Vec<RenderItem> {
617 if !state.active {
618 return vec![];
619 }
620
621 vec![RenderItem::Text(format!(
622 " L{},C{} ",
623 state.cursor.y + 1,
624 state.cursor.x + 1
625 ))]
626}
627
628pub fn search_match_indicator(search: &SearchState) -> Vec<RenderItem> {
654 if !search.active || search.matches.is_empty() {
655 return vec![];
656 }
657
658 let current = search.current_match.map(|i| i + 1).unwrap_or(0);
659 vec![RenderItem::Text(format!(
660 " {}/{} ",
661 current,
662 search.matches.len()
663 ))]
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn test_cursor_creation() {
672 let cursor = CopyModeCursor::new(10, 5);
673 assert_eq!(cursor.x, 10);
674 assert_eq!(cursor.y, 5);
675 }
676
677 #[test]
678 fn test_cursor_origin() {
679 let cursor = CopyModeCursor::origin();
680 assert_eq!(cursor.x, 0);
681 assert_eq!(cursor.y, 0);
682 }
683
684 #[test]
685 fn test_cursor_negative_y() {
686 let cursor = CopyModeCursor::new(5, -100);
688 assert_eq!(cursor.y, -100);
689 }
690
691 #[test]
692 fn test_selection_normalization() {
693 let sel = Selection {
695 anchor: CopyModeCursor::new(0, 0),
696 active: CopyModeCursor::new(10, 5),
697 };
698 let (start, end) = normalize_selection(&sel);
699 assert_eq!(start.x, 0);
700 assert_eq!(start.y, 0);
701 assert_eq!(end.x, 10);
702 assert_eq!(end.y, 5);
703
704 let sel = Selection {
706 anchor: CopyModeCursor::new(10, 5),
707 active: CopyModeCursor::new(5, 3),
708 };
709 let (start, end) = normalize_selection(&sel);
710 assert_eq!(start.x, 5);
711 assert_eq!(start.y, 3);
712 assert_eq!(end.x, 10);
713 assert_eq!(end.y, 5);
714
715 let sel = Selection {
717 anchor: CopyModeCursor::new(10, 5),
718 active: CopyModeCursor::new(3, 5),
719 };
720 let (start, end) = normalize_selection(&sel);
721 assert_eq!(start.x, 3);
722 assert_eq!(start.y, 5);
723 assert_eq!(end.x, 10);
724 assert_eq!(end.y, 5);
725 }
726
727 #[test]
728 fn test_selection_bounds() {
729 let sel = Selection {
730 anchor: CopyModeCursor::new(5, 3),
731 active: CopyModeCursor::new(10, 7),
732 };
733 let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
734 assert_eq!(min_x, 5);
735 assert_eq!(min_y, 3);
736 assert_eq!(max_x, 10);
737 assert_eq!(max_y, 7);
738 }
739
740 #[test]
741 fn test_selection_bounds_reversed() {
742 let sel = Selection {
743 anchor: CopyModeCursor::new(10, 7),
744 active: CopyModeCursor::new(5, 3),
745 };
746 let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
747 assert_eq!(min_x, 5);
748 assert_eq!(min_y, 3);
749 assert_eq!(max_x, 10);
750 assert_eq!(max_y, 7);
751 }
752
753 #[test]
754 fn test_selection_bounds_single_line() {
755 let sel = Selection {
756 anchor: CopyModeCursor::new(3, 5),
757 active: CopyModeCursor::new(10, 5),
758 };
759 let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
760 assert_eq!(min_x, 3);
761 assert_eq!(min_y, 5);
762 assert_eq!(max_x, 10);
763 assert_eq!(max_y, 5);
764 }
765
766 #[test]
767 fn test_copy_mode_state_activation() {
768 let mut state = CopyModeState::new();
769 assert!(!state.active);
770
771 state.activate(CopyModeCursor::new(5, 10));
772 assert!(state.active);
773 assert_eq!(state.cursor.x, 5);
774 assert_eq!(state.cursor.y, 10);
775
776 state.deactivate();
777 assert!(!state.active);
778 }
779
780 #[test]
781 fn test_copy_mode_selection_toggle() {
782 let mut state = CopyModeState::new();
783 state.cursor = CopyModeCursor::new(5, 5);
784
785 state.toggle_selection(SelectionMode::Cell);
787 assert_eq!(state.selection_mode, SelectionMode::Cell);
788 assert!(state.selection.is_some());
789
790 state.toggle_selection(SelectionMode::Cell);
792 assert_eq!(state.selection_mode, SelectionMode::None);
793 assert!(state.selection.is_none());
794 }
795
796 #[test]
797 fn test_copy_mode_swap_selection_ends() {
798 let mut state = CopyModeState::new();
799 state.cursor = CopyModeCursor::new(5, 5);
800 state.start_selection(SelectionMode::Cell);
801
802 state.cursor = CopyModeCursor::new(10, 10);
804 state.update_selection();
805
806 let selection = state.selection.as_ref().unwrap();
807 assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
808 assert_eq!(selection.active, CopyModeCursor::new(10, 10));
809
810 state.swap_selection_ends();
812 let selection = state.selection.as_ref().unwrap();
813 assert_eq!(selection.anchor, CopyModeCursor::new(10, 10));
814 assert_eq!(selection.active, CopyModeCursor::new(5, 5));
815 assert_eq!(state.cursor, CopyModeCursor::new(5, 5));
816 }
817
818 #[test]
819 fn test_search_state_navigation() {
820 let mut search = SearchState::new();
821 let matches = vec![
822 SearchMatch::new(CopyModeCursor::new(0, 0), CopyModeCursor::new(5, 0)),
823 SearchMatch::new(CopyModeCursor::new(10, 0), CopyModeCursor::new(15, 0)),
824 SearchMatch::new(CopyModeCursor::new(0, 1), CopyModeCursor::new(5, 1)),
825 ];
826
827 search.update_query("test".to_string(), matches);
828 assert_eq!(search.current_match, Some(0));
829
830 search.next_match();
832 assert_eq!(search.current_match, Some(1));
833
834 search.next_match();
835 assert_eq!(search.current_match, Some(2));
836
837 search.next_match();
839 assert_eq!(search.current_match, Some(0));
840
841 search.prev_match();
843 assert_eq!(search.current_match, Some(2));
844 }
845
846 #[test]
847 fn test_search_state_no_matches() {
848 let mut search = SearchState::new();
849 search.update_query("notfound".to_string(), vec![]);
850 assert_eq!(search.current_match, None);
851
852 search.next_match();
854 assert_eq!(search.current_match, None);
855
856 search.prev_match();
857 assert_eq!(search.current_match, None);
858 }
859
860 #[test]
861 fn test_find_matches() {
862 let get_line = |y: i32| match y {
863 0 => Some("Hello world, hello Rust!".to_string()),
864 1 => Some("Another HELLO here".to_string()),
865 2 => Some("No match on this line".to_string()),
866 _ => None,
867 };
868
869 let matches = find_matches("hello", get_line, 0, 2);
870
871 assert_eq!(matches.len(), 3);
873
874 assert_eq!(matches[0].start, CopyModeCursor::new(0, 0));
876 assert_eq!(matches[0].end, CopyModeCursor::new(4, 0));
877
878 assert_eq!(matches[1].start, CopyModeCursor::new(13, 0));
880 assert_eq!(matches[1].end, CopyModeCursor::new(17, 0));
881
882 assert_eq!(matches[2].start, CopyModeCursor::new(8, 1));
884 assert_eq!(matches[2].end, CopyModeCursor::new(12, 1));
885 }
886
887 #[test]
888 fn test_find_matches_empty_query() {
889 let get_line = |_y: i32| Some("Some text".to_string());
890 let matches = find_matches("", get_line, 0, 5);
891 assert_eq!(matches.len(), 0);
892 }
893
894 #[test]
895 fn test_find_matches_no_matches() {
896 let get_line = |_y: i32| Some("No matches here".to_string());
897 let matches = find_matches("xyz", get_line, 0, 5);
898 assert_eq!(matches.len(), 0);
899 }
900
901 #[test]
902 fn test_selection_mode_default() {
903 let mode = SelectionMode::default();
904 assert_eq!(mode, SelectionMode::None);
905 }
906
907 #[test]
908 fn test_search_direction_default() {
909 let direction = SearchDirection::default();
910 assert_eq!(direction, SearchDirection::Forward);
911 }
912
913 #[test]
914 fn test_move_left() {
915 let mut state = CopyModeState::new();
916 state.cursor = CopyModeCursor::new(5, 0);
917
918 state.move_left();
919 assert_eq!(state.cursor.x, 4);
920
921 state.move_left();
922 state.move_left();
923 state.move_left();
924 state.move_left();
925 assert_eq!(state.cursor.x, 0);
926
927 state.move_left();
929 assert_eq!(state.cursor.x, 0);
930 }
931
932 #[test]
933 fn test_move_right() {
934 let mut state = CopyModeState::new();
935 state.cursor = CopyModeCursor::new(5, 0);
936
937 state.move_right(80);
938 assert_eq!(state.cursor.x, 6);
939
940 state.cursor.x = 79;
942 state.move_right(80);
943 assert_eq!(state.cursor.x, 79);
944 }
945
946 #[test]
947 fn test_move_up() {
948 let mut state = CopyModeState::new();
949 state.cursor = CopyModeCursor::new(5, 10);
950
951 state.move_up(0);
952 assert_eq!(state.cursor.y, 9);
953
954 for _ in 0..10 {
956 state.move_up(0);
957 }
958 assert_eq!(state.cursor.y, 0);
959
960 state.move_up(0);
962 assert_eq!(state.cursor.y, 0);
963 }
964
965 #[test]
966 fn test_move_down() {
967 let mut state = CopyModeState::new();
968 state.cursor = CopyModeCursor::new(5, 0);
969
970 state.move_down(24);
971 assert_eq!(state.cursor.y, 1);
972
973 state.cursor.y = 23;
975 state.move_down(24);
976 assert_eq!(state.cursor.y, 24);
977
978 state.move_down(24);
980 assert_eq!(state.cursor.y, 24);
981 }
982
983 #[test]
984 fn test_move_up_with_scrollback() {
985 let mut state = CopyModeState::new();
986 state.cursor = CopyModeCursor::new(5, 0);
987
988 state.move_up(-100);
990 assert_eq!(state.cursor.y, -1);
991
992 state.cursor.y = -50;
993 state.move_up(-100);
994 assert_eq!(state.cursor.y, -51);
995
996 state.cursor.y = -100;
998 state.move_up(-100);
999 assert_eq!(state.cursor.y, -100);
1000 }
1001
1002 #[test]
1003 fn test_move_to_line_start() {
1004 let mut state = CopyModeState::new();
1005 state.cursor = CopyModeCursor::new(42, 5);
1006
1007 state.move_to_line_start();
1008 assert_eq!(state.cursor.x, 0);
1009 assert_eq!(state.cursor.y, 5); }
1011
1012 #[test]
1013 fn test_move_to_line_end() {
1014 let mut state = CopyModeState::new();
1015 state.cursor = CopyModeCursor::new(5, 3);
1016
1017 state.move_to_line_end(80);
1018 assert_eq!(state.cursor.x, 79);
1019 assert_eq!(state.cursor.y, 3); }
1021
1022 #[test]
1023 fn test_move_to_top() {
1024 let mut state = CopyModeState::new();
1025 state.cursor = CopyModeCursor::new(5, 10);
1026
1027 state.move_to_top(-100);
1028 assert_eq!(state.cursor.x, 5); assert_eq!(state.cursor.y, -100);
1030 }
1031
1032 #[test]
1033 fn test_move_to_bottom() {
1034 let mut state = CopyModeState::new();
1035 state.cursor = CopyModeCursor::new(5, -50);
1036
1037 state.move_to_bottom(24);
1038 assert_eq!(state.cursor.x, 5); assert_eq!(state.cursor.y, 24);
1040 }
1041
1042 #[test]
1043 fn test_toggle_cell_selection() {
1044 let mut state = CopyModeState::new();
1045 state.cursor = CopyModeCursor::new(5, 5);
1046
1047 state.toggle_cell_selection();
1048 assert_eq!(state.selection_mode, SelectionMode::Cell);
1049 assert!(state.selection.is_some());
1050
1051 state.toggle_cell_selection();
1052 assert_eq!(state.selection_mode, SelectionMode::None);
1053 assert!(state.selection.is_none());
1054 }
1055
1056 #[test]
1057 fn test_toggle_line_selection() {
1058 let mut state = CopyModeState::new();
1059 state.cursor = CopyModeCursor::new(5, 5);
1060
1061 state.toggle_line_selection();
1062 assert_eq!(state.selection_mode, SelectionMode::Line);
1063 assert!(state.selection.is_some());
1064
1065 state.toggle_line_selection();
1066 assert_eq!(state.selection_mode, SelectionMode::None);
1067 assert!(state.selection.is_none());
1068 }
1069
1070 #[test]
1071 fn test_toggle_block_selection() {
1072 let mut state = CopyModeState::new();
1073 state.cursor = CopyModeCursor::new(5, 5);
1074
1075 state.toggle_block_selection();
1076 assert_eq!(state.selection_mode, SelectionMode::Block);
1077 assert!(state.selection.is_some());
1078
1079 state.toggle_block_selection();
1080 assert_eq!(state.selection_mode, SelectionMode::None);
1081 assert!(state.selection.is_none());
1082 }
1083
1084 #[test]
1085 fn test_toggle_word_selection() {
1086 let mut state = CopyModeState::new();
1087 state.cursor = CopyModeCursor::new(5, 5);
1088
1089 state.toggle_word_selection();
1090 assert_eq!(state.selection_mode, SelectionMode::Word);
1091 assert!(state.selection.is_some());
1092
1093 state.toggle_word_selection();
1094 assert_eq!(state.selection_mode, SelectionMode::None);
1095 assert!(state.selection.is_none());
1096 }
1097
1098 #[test]
1099 fn test_get_selection_text_cell_single_line() {
1100 let mut state = CopyModeState::new();
1101 state.cursor = CopyModeCursor::new(5, 0);
1102 state.start_selection(SelectionMode::Cell);
1103 state.cursor = CopyModeCursor::new(9, 0);
1104 state.update_selection();
1105
1106 let get_line = |y: i32| {
1107 if y == 0 {
1108 Some("Hello, World!".to_string())
1109 } else {
1110 None
1111 }
1112 };
1113
1114 let text = state.get_selection_text(get_line);
1115 assert_eq!(text, Some(", Wor".to_string()));
1116 }
1117
1118 #[test]
1119 fn test_get_selection_text_cell_multi_line() {
1120 let mut state = CopyModeState::new();
1121 state.cursor = CopyModeCursor::new(5, 0);
1122 state.start_selection(SelectionMode::Cell);
1123 state.cursor = CopyModeCursor::new(5, 2);
1124 state.update_selection();
1125
1126 let get_line = |y: i32| match y {
1127 0 => Some("Line 0".to_string()),
1128 1 => Some("Line 1".to_string()),
1129 2 => Some("Line 2".to_string()),
1130 _ => None,
1131 };
1132
1133 let text = state.get_selection_text(get_line);
1134 assert_eq!(text, Some("0\nLine 1\nLine 2".to_string()));
1135 }
1136
1137 #[test]
1138 fn test_get_selection_text_line_mode() {
1139 let mut state = CopyModeState::new();
1140 state.cursor = CopyModeCursor::new(5, 0);
1141 state.start_selection(SelectionMode::Line);
1142 state.cursor = CopyModeCursor::new(10, 2);
1143 state.update_selection();
1144
1145 let get_line = |y: i32| match y {
1146 0 => Some("First line".to_string()),
1147 1 => Some("Second line".to_string()),
1148 2 => Some("Third line".to_string()),
1149 _ => None,
1150 };
1151
1152 let text = state.get_selection_text(get_line);
1153 assert_eq!(
1154 text,
1155 Some("First line\nSecond line\nThird line".to_string())
1156 );
1157 }
1158
1159 #[test]
1160 fn test_get_selection_text_block_mode() {
1161 let mut state = CopyModeState::new();
1162 state.cursor = CopyModeCursor::new(2, 0);
1163 state.start_selection(SelectionMode::Block);
1164 state.cursor = CopyModeCursor::new(5, 2);
1165 state.update_selection();
1166
1167 let get_line = |y: i32| match y {
1168 0 => Some("ABCDEFGH".to_string()),
1169 1 => Some("12345678".to_string()),
1170 2 => Some("abcdefgh".to_string()),
1171 _ => None,
1172 };
1173
1174 let text = state.get_selection_text(get_line);
1175 assert_eq!(text, Some("CDEF\n3456\ncdef".to_string()));
1176 }
1177
1178 #[test]
1179 fn test_get_selection_text_no_selection() {
1180 let state = CopyModeState::new();
1181
1182 let get_line = |_y: i32| Some("Line".to_string());
1183
1184 let text = state.get_selection_text(get_line);
1185 assert_eq!(text, None);
1186 }
1187
1188 #[test]
1189 fn test_movement_with_selection_update() {
1190 let mut state = CopyModeState::new();
1191 state.cursor = CopyModeCursor::new(5, 5);
1192 state.start_selection(SelectionMode::Cell);
1193
1194 state.move_right(80);
1196 state.update_selection();
1197
1198 let selection = state.selection.as_ref().unwrap();
1199 assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
1200 assert_eq!(selection.active, CopyModeCursor::new(6, 5));
1201
1202 state.move_down(24);
1204 state.update_selection();
1205
1206 let selection = state.selection.as_ref().unwrap();
1207 assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
1208 assert_eq!(selection.active, CopyModeCursor::new(6, 6));
1209 }
1210
1211 #[test]
1212 fn test_find_word_bounds() {
1213 let line = "Hello world, this is a test";
1214
1215 let (start, end) = find_word_bounds(2, line);
1217 assert_eq!(start, 0);
1218 assert_eq!(end, 4);
1219
1220 let (start, end) = find_word_bounds(6, line);
1222 assert_eq!(start, 6);
1223 assert_eq!(end, 10);
1224
1225 let (start, end) = find_word_bounds(24, line);
1227 assert_eq!(start, 23);
1228 assert_eq!(end, 26);
1229
1230 let (start, end) = find_word_bounds(5, line);
1232 assert_eq!(start, 5);
1233 assert_eq!(end, 5);
1234 }
1235
1236 #[test]
1237 fn test_find_word_bounds_empty_line() {
1238 let line = "";
1239 let (start, end) = find_word_bounds(0, line);
1240 assert_eq!(start, 0);
1241 assert_eq!(end, 0);
1242 }
1243
1244 #[test]
1245 fn test_find_word_bounds_with_underscores() {
1246 let line = "hello_world test_case";
1247
1248 let (start, end) = find_word_bounds(6, line);
1250 assert_eq!(start, 0);
1251 assert_eq!(end, 10); }
1253
1254 #[test]
1255 fn test_copy_mode_indicator() {
1256 let mut state = CopyModeState::new();
1257
1258 let items = copy_mode_indicator(&state, false);
1260 assert_eq!(items.len(), 0);
1261
1262 state.active = true;
1264 let items = copy_mode_indicator(&state, false);
1265 assert!(items.len() > 0);
1266 match &items[3] {
1267 RenderItem::Text(s) => assert_eq!(s, " COPY "),
1268 _ => panic!("Expected text item"),
1269 }
1270
1271 state.selection = Some(Selection::at_point(CopyModeCursor::new(0, 0)));
1273 state.selection_mode = SelectionMode::Cell;
1274 let items = copy_mode_indicator(&state, false);
1275 match &items[3] {
1276 RenderItem::Text(s) => assert_eq!(s, " VISUAL "),
1277 _ => panic!("Expected text item"),
1278 }
1279
1280 state.selection_mode = SelectionMode::Line;
1282 let items = copy_mode_indicator(&state, false);
1283 match &items[3] {
1284 RenderItem::Text(s) => assert_eq!(s, " V-LINE "),
1285 _ => panic!("Expected text item"),
1286 }
1287
1288 let items = copy_mode_indicator(&state, true);
1290 match &items[3] {
1291 RenderItem::Text(s) => assert_eq!(s, " SEARCH "),
1292 _ => panic!("Expected text item"),
1293 }
1294 }
1295
1296 #[test]
1297 fn test_copy_mode_position_indicator() {
1298 let mut state = CopyModeState::new();
1299
1300 let items = copy_mode_position_indicator(&state);
1302 assert_eq!(items.len(), 0);
1303
1304 state.active = true;
1306 state.cursor = CopyModeCursor::new(5, 10);
1307 let items = copy_mode_position_indicator(&state);
1308 assert_eq!(items.len(), 1);
1309 match &items[0] {
1310 RenderItem::Text(s) => assert_eq!(s, " L11,C6 "),
1311 _ => panic!("Expected text item"),
1312 }
1313 }
1314
1315 #[test]
1316 fn test_search_match_indicator() {
1317 let mut search = SearchState::new();
1318
1319 let items = search_match_indicator(&search);
1321 assert_eq!(items.len(), 0);
1322
1323 search.active = true;
1325 let items = search_match_indicator(&search);
1326 assert_eq!(items.len(), 0);
1327
1328 search.matches = vec![
1330 SearchMatch::new(CopyModeCursor::new(0, 0), CopyModeCursor::new(5, 0)),
1331 SearchMatch::new(CopyModeCursor::new(0, 1), CopyModeCursor::new(5, 1)),
1332 SearchMatch::new(CopyModeCursor::new(0, 2), CopyModeCursor::new(5, 2)),
1333 ];
1334 search.current_match = Some(1);
1335
1336 let items = search_match_indicator(&search);
1337 assert_eq!(items.len(), 1);
1338 match &items[0] {
1339 RenderItem::Text(s) => assert_eq!(s, " 2/3 "),
1340 _ => panic!("Expected text item"),
1341 }
1342 }
1343}