1use crate::input::commands::Suggestion;
4use crate::primitives::grapheme;
5use crate::primitives::word_navigation::{
6 find_word_end_bytes, find_word_start_bytes, is_word_char,
7};
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum PromptType {
12 OpenFile,
14 OpenFileWithEncoding { path: std::path::PathBuf },
17 ReloadWithEncoding,
20 SwitchProject,
22 SaveFileAs,
24 Search,
26 ReplaceSearch,
28 Replace { search: String },
30 QueryReplaceSearch,
32 QueryReplace { search: String },
34 QueryReplaceConfirm,
36 QuickOpen,
39 GotoLine,
41 GotoByteOffset,
43 GotoLineScanConfirm,
45 SetBackgroundFile,
47 SetBackgroundBlend,
49 Plugin { custom_type: String },
52 LspRename {
55 original_text: String,
56 start_pos: usize,
57 end_pos: usize,
58 overlay_handle: crate::view::overlay::OverlayHandle,
59 },
60 RecordMacro,
62 PlayMacro,
64 SetBookmark,
66 JumpToBookmark,
68 SetPageWidth,
70 AddRuler,
72 RemoveRuler,
74 SetTabSize,
76 SetLineEnding,
78 SetEncoding,
80 SetLanguage,
82 StopLspServer,
84 RestartLspServer,
86 SelectTheme { original_theme: String },
89 SelectKeybindingMap,
91 SelectCursorStyle,
93 SelectLocale,
95 CopyWithFormattingTheme,
97 ConfirmRevert,
99 ConfirmSaveConflict,
101 ConfirmSudoSave {
103 info: crate::model::buffer::SudoSaveRequired,
104 },
105 ConfirmOverwriteFile { path: std::path::PathBuf },
107 ConfirmCreateDirectory { path: std::path::PathBuf },
109 ConfirmCloseBuffer {
112 buffer_id: crate::model::event::BufferId,
113 },
114 ConfirmQuitWithModified,
116 FileExplorerRename {
119 original_path: std::path::PathBuf,
120 original_name: String,
121 is_new_file: bool,
124 },
125 ConfirmDeleteFile {
127 path: std::path::PathBuf,
128 is_dir: bool,
129 },
130 ConfirmLargeFileEncoding { path: std::path::PathBuf },
133 SwitchToTab,
135 ShellCommand { replace: bool },
139 AsyncPrompt,
142}
143
144#[derive(Debug, Clone)]
146pub struct Prompt {
147 pub message: String,
149 pub input: String,
151 pub cursor_pos: usize,
153 pub prompt_type: PromptType,
155 pub suggestions: Vec<Suggestion>,
157 pub original_suggestions: Option<Vec<Suggestion>>,
159 pub selected_suggestion: Option<usize>,
161 pub selection_anchor: Option<usize>,
164 pub suggestions_set_for_input: Option<String>,
167 pub sync_input_on_navigate: bool,
170}
171
172impl Prompt {
173 pub fn new(message: String, prompt_type: PromptType) -> Self {
175 Self {
176 message,
177 input: String::new(),
178 cursor_pos: 0,
179 prompt_type,
180 suggestions: Vec::new(),
181 original_suggestions: None,
182 selected_suggestion: None,
183 selection_anchor: None,
184 suggestions_set_for_input: None,
185 sync_input_on_navigate: false,
186 }
187 }
188
189 pub fn with_suggestions(
194 message: String,
195 prompt_type: PromptType,
196 suggestions: Vec<Suggestion>,
197 ) -> Self {
198 let selected_suggestion = if suggestions.is_empty() {
199 None
200 } else {
201 Some(0)
202 };
203 Self {
204 message,
205 input: String::new(),
206 cursor_pos: 0,
207 prompt_type,
208 original_suggestions: Some(suggestions.clone()),
209 suggestions,
210 selected_suggestion,
211 selection_anchor: None,
212 suggestions_set_for_input: None,
213 sync_input_on_navigate: false,
214 }
215 }
216
217 pub fn with_initial_text(
219 message: String,
220 prompt_type: PromptType,
221 initial_text: String,
222 ) -> Self {
223 let cursor_pos = initial_text.len();
224 let selection_anchor = if initial_text.is_empty() {
226 None
227 } else {
228 Some(0)
229 };
230 Self {
231 message,
232 input: initial_text,
233 cursor_pos,
234 prompt_type,
235 suggestions: Vec::new(),
236 original_suggestions: None,
237 selected_suggestion: None,
238 selection_anchor,
239 suggestions_set_for_input: None,
240 sync_input_on_navigate: false,
241 }
242 }
243
244 pub fn cursor_left(&mut self) {
249 if self.cursor_pos > 0 {
250 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
251 }
252 }
253
254 pub fn cursor_right(&mut self) {
259 if self.cursor_pos < self.input.len() {
260 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
261 }
262 }
263
264 pub fn insert_char(&mut self, ch: char) {
266 self.input.insert(self.cursor_pos, ch);
267 self.cursor_pos += ch.len_utf8();
268 }
269
270 pub fn backspace(&mut self) {
276 if self.cursor_pos > 0 {
277 let prev_boundary = self.input[..self.cursor_pos]
280 .char_indices()
281 .next_back()
282 .map(|(i, _)| i)
283 .unwrap_or(0);
284 self.input.drain(prev_boundary..self.cursor_pos);
285 self.cursor_pos = prev_boundary;
286 }
287 }
288
289 pub fn delete(&mut self) {
293 if self.cursor_pos < self.input.len() {
294 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
295 self.input.drain(self.cursor_pos..next_boundary);
296 }
297 }
298
299 pub fn move_to_start(&mut self) {
301 self.cursor_pos = 0;
302 }
303
304 pub fn move_to_end(&mut self) {
306 self.cursor_pos = self.input.len();
307 }
308
309 pub fn set_input(&mut self, text: String) {
326 self.cursor_pos = text.len();
327 self.input = text;
328 self.clear_selection();
329 }
330
331 pub fn select_next_suggestion(&mut self) {
333 if !self.suggestions.is_empty() {
334 self.selected_suggestion = Some(match self.selected_suggestion {
335 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
336 Some(_) => 0, None => 0,
338 });
339 }
340 }
341
342 pub fn select_prev_suggestion(&mut self) {
344 if !self.suggestions.is_empty() {
345 self.selected_suggestion = Some(match self.selected_suggestion {
346 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
348 None => 0,
349 });
350 }
351 }
352
353 pub fn selected_value(&self) -> Option<String> {
355 self.selected_suggestion
356 .and_then(|idx| self.suggestions.get(idx))
357 .map(|s| s.get_value().to_string())
358 }
359
360 pub fn get_final_input(&self) -> String {
362 self.selected_value().unwrap_or_else(|| self.input.clone())
363 }
364
365 pub fn filter_suggestions(&mut self, match_description: bool) {
370 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
371
372 if let Some(ref set_for_input) = self.suggestions_set_for_input {
376 if set_for_input == &self.input {
377 return;
378 }
379 }
380
381 let Some(original) = &self.original_suggestions else {
382 return;
383 };
384
385 let input = &self.input;
386 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
387 .iter()
388 .filter_map(|s| {
389 let text_result = fuzzy_match(input, &s.text);
390 let desc_result = if match_description {
391 s.description
392 .as_ref()
393 .map(|d| fuzzy_match(input, d))
394 .unwrap_or_else(FuzzyMatch::no_match)
395 } else {
396 FuzzyMatch::no_match()
397 };
398 if text_result.matched || desc_result.matched {
399 Some((s.clone(), text_result.score.max(desc_result.score)))
400 } else {
401 None
402 }
403 })
404 .collect();
405
406 filtered.sort_by(|a, b| b.1.cmp(&a.1));
407 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
408 self.selected_suggestion = if self.suggestions.is_empty() {
409 None
410 } else {
411 Some(0)
412 };
413 }
414
415 pub fn delete_word_forward(&mut self) {
445 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
446 if word_end > self.cursor_pos {
447 self.input.drain(self.cursor_pos..word_end);
448 }
450 }
451
452 pub fn delete_word_backward(&mut self) {
468 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
469 if word_start < self.cursor_pos {
470 self.input.drain(word_start..self.cursor_pos);
471 self.cursor_pos = word_start;
472 }
473 }
474
475 pub fn delete_to_end(&mut self) {
490 if self.cursor_pos < self.input.len() {
491 self.input.truncate(self.cursor_pos);
492 }
493 }
494
495 pub fn get_text(&self) -> String {
508 self.input.clone()
509 }
510
511 pub fn clear(&mut self) {
526 self.input.clear();
527 self.cursor_pos = 0;
528 self.selected_suggestion = None;
530 }
531
532 pub fn insert_str(&mut self, text: &str) {
548 if self.has_selection() {
550 self.delete_selection();
551 }
552 self.input.insert_str(self.cursor_pos, text);
553 self.cursor_pos += text.len();
554 }
555
556 pub fn has_selection(&self) -> bool {
562 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
563 }
564
565 pub fn selection_range(&self) -> Option<(usize, usize)> {
567 if let Some(anchor) = self.selection_anchor {
568 if anchor != self.cursor_pos {
569 let start = anchor.min(self.cursor_pos);
570 let end = anchor.max(self.cursor_pos);
571 return Some((start, end));
572 }
573 }
574 None
575 }
576
577 pub fn selected_text(&self) -> Option<String> {
579 self.selection_range()
580 .map(|(start, end)| self.input[start..end].to_string())
581 }
582
583 pub fn delete_selection(&mut self) -> Option<String> {
585 if let Some((start, end)) = self.selection_range() {
586 let deleted = self.input[start..end].to_string();
587 self.input.drain(start..end);
588 self.cursor_pos = start;
589 self.selection_anchor = None;
590 Some(deleted)
591 } else {
592 None
593 }
594 }
595
596 pub fn clear_selection(&mut self) {
598 self.selection_anchor = None;
599 }
600
601 pub fn move_left_selecting(&mut self) {
603 if self.selection_anchor.is_none() {
605 self.selection_anchor = Some(self.cursor_pos);
606 }
607
608 if self.cursor_pos > 0 {
610 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
611 }
612 }
613
614 pub fn move_right_selecting(&mut self) {
616 if self.selection_anchor.is_none() {
618 self.selection_anchor = Some(self.cursor_pos);
619 }
620
621 if self.cursor_pos < self.input.len() {
623 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
624 }
625 }
626
627 pub fn move_home_selecting(&mut self) {
629 if self.selection_anchor.is_none() {
630 self.selection_anchor = Some(self.cursor_pos);
631 }
632 self.cursor_pos = 0;
633 }
634
635 pub fn move_end_selecting(&mut self) {
637 if self.selection_anchor.is_none() {
638 self.selection_anchor = Some(self.cursor_pos);
639 }
640 self.cursor_pos = self.input.len();
641 }
642
643 pub fn move_word_left_selecting(&mut self) {
646 if self.selection_anchor.is_none() {
647 self.selection_anchor = Some(self.cursor_pos);
648 }
649
650 let bytes = self.input.as_bytes();
651 if self.cursor_pos == 0 {
652 return;
653 }
654
655 let mut new_pos = self.cursor_pos.saturating_sub(1);
656
657 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
659 new_pos = new_pos.saturating_sub(1);
660 }
661
662 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
664 new_pos = new_pos.saturating_sub(1);
665 }
666
667 self.cursor_pos = new_pos;
668 }
669
670 pub fn move_word_right_selecting(&mut self) {
673 if self.selection_anchor.is_none() {
674 self.selection_anchor = Some(self.cursor_pos);
675 }
676
677 let bytes = self.input.as_bytes();
679 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
680
681 if new_pos == self.cursor_pos && new_pos < bytes.len() {
683 new_pos = (new_pos + 1).min(bytes.len());
684 new_pos = find_word_end_bytes(bytes, new_pos);
685 }
686
687 self.cursor_pos = new_pos;
688 }
689
690 pub fn move_word_left(&mut self) {
693 self.clear_selection();
694
695 let bytes = self.input.as_bytes();
696 if self.cursor_pos == 0 {
697 return;
698 }
699
700 let mut new_pos = self.cursor_pos.saturating_sub(1);
701
702 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
704 new_pos = new_pos.saturating_sub(1);
705 }
706
707 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
709 new_pos = new_pos.saturating_sub(1);
710 }
711
712 self.cursor_pos = new_pos;
713 }
714
715 pub fn move_word_right(&mut self) {
718 self.clear_selection();
719
720 let bytes = self.input.as_bytes();
721 if self.cursor_pos >= bytes.len() {
722 return;
723 }
724
725 let mut new_pos = self.cursor_pos;
726
727 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
729 new_pos += 1;
730 }
731
732 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
734 new_pos += 1;
735 }
736
737 self.cursor_pos = new_pos;
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn test_delete_word_forward_basic() {
747 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
748 prompt.input = "hello world test".to_string();
749 prompt.cursor_pos = 0;
750
751 prompt.delete_word_forward();
752 assert_eq!(prompt.input, " world test");
753 assert_eq!(prompt.cursor_pos, 0);
754 }
755
756 #[test]
757 fn test_delete_word_forward_middle() {
758 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
759 prompt.input = "hello world test".to_string();
760 prompt.cursor_pos = 3; prompt.delete_word_forward();
763 assert_eq!(prompt.input, "hel world test");
764 assert_eq!(prompt.cursor_pos, 3);
765 }
766
767 #[test]
768 fn test_delete_word_forward_at_space() {
769 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
770 prompt.input = "hello world".to_string();
771 prompt.cursor_pos = 5; prompt.delete_word_forward();
774 assert_eq!(prompt.input, "hello");
775 assert_eq!(prompt.cursor_pos, 5);
776 }
777
778 #[test]
779 fn test_delete_word_backward_basic() {
780 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
781 prompt.input = "hello world test".to_string();
782 prompt.cursor_pos = 5; prompt.delete_word_backward();
785 assert_eq!(prompt.input, " world test");
786 assert_eq!(prompt.cursor_pos, 0);
787 }
788
789 #[test]
790 fn test_delete_word_backward_middle() {
791 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
792 prompt.input = "hello world test".to_string();
793 prompt.cursor_pos = 8; prompt.delete_word_backward();
796 assert_eq!(prompt.input, "hello rld test");
797 assert_eq!(prompt.cursor_pos, 6);
798 }
799
800 #[test]
801 fn test_delete_word_backward_at_end() {
802 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
803 prompt.input = "hello world".to_string();
804 prompt.cursor_pos = 11; prompt.delete_word_backward();
807 assert_eq!(prompt.input, "hello ");
808 assert_eq!(prompt.cursor_pos, 6);
809 }
810
811 #[test]
812 fn test_delete_word_with_special_chars() {
813 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
814 prompt.input = "save-file-as".to_string();
815 prompt.cursor_pos = 12; prompt.delete_word_backward();
819 assert_eq!(prompt.input, "save-file-");
820 assert_eq!(prompt.cursor_pos, 10);
821
822 prompt.delete_word_backward();
824 assert_eq!(prompt.input, "save-");
825 assert_eq!(prompt.cursor_pos, 5);
826 }
827
828 #[test]
829 fn test_get_text() {
830 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
831 prompt.input = "test content".to_string();
832
833 assert_eq!(prompt.get_text(), "test content");
834 }
835
836 #[test]
837 fn test_clear() {
838 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
839 prompt.input = "some text".to_string();
840 prompt.cursor_pos = 5;
841 prompt.selected_suggestion = Some(0);
842
843 prompt.clear();
844
845 assert_eq!(prompt.input, "");
846 assert_eq!(prompt.cursor_pos, 0);
847 assert_eq!(prompt.selected_suggestion, None);
848 }
849
850 #[test]
851 fn test_delete_forward_basic() {
852 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
853 prompt.input = "hello".to_string();
854 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
858
859 assert_eq!(prompt.input, "hllo");
860 assert_eq!(prompt.cursor_pos, 1);
861 }
862
863 #[test]
864 fn test_delete_at_end() {
865 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
866 prompt.input = "hello".to_string();
867 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
871 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
872 }
873
874 assert_eq!(prompt.input, "hello");
875 assert_eq!(prompt.cursor_pos, 5);
876 }
877
878 #[test]
879 fn test_insert_str_at_start() {
880 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
881 prompt.input = "world".to_string();
882 prompt.cursor_pos = 0;
883
884 prompt.insert_str("hello ");
885 assert_eq!(prompt.input, "hello world");
886 assert_eq!(prompt.cursor_pos, 6);
887 }
888
889 #[test]
890 fn test_insert_str_at_middle() {
891 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
892 prompt.input = "helloworld".to_string();
893 prompt.cursor_pos = 5;
894
895 prompt.insert_str(" ");
896 assert_eq!(prompt.input, "hello world");
897 assert_eq!(prompt.cursor_pos, 6);
898 }
899
900 #[test]
901 fn test_insert_str_at_end() {
902 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
903 prompt.input = "hello".to_string();
904 prompt.cursor_pos = 5;
905
906 prompt.insert_str(" world");
907 assert_eq!(prompt.input, "hello world");
908 assert_eq!(prompt.cursor_pos, 11);
909 }
910
911 #[test]
912 fn test_delete_word_forward_empty() {
913 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
914 prompt.input = "".to_string();
915 prompt.cursor_pos = 0;
916
917 prompt.delete_word_forward();
918 assert_eq!(prompt.input, "");
919 assert_eq!(prompt.cursor_pos, 0);
920 }
921
922 #[test]
923 fn test_delete_word_backward_empty() {
924 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
925 prompt.input = "".to_string();
926 prompt.cursor_pos = 0;
927
928 prompt.delete_word_backward();
929 assert_eq!(prompt.input, "");
930 assert_eq!(prompt.cursor_pos, 0);
931 }
932
933 #[test]
934 fn test_delete_word_forward_only_spaces() {
935 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
936 prompt.input = " ".to_string();
937 prompt.cursor_pos = 0;
938
939 prompt.delete_word_forward();
940 assert_eq!(prompt.input, "");
941 assert_eq!(prompt.cursor_pos, 0);
942 }
943
944 #[test]
945 fn test_multiple_word_deletions() {
946 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
947 prompt.input = "one two three four".to_string();
948 prompt.cursor_pos = 18;
949
950 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
952
953 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
955
956 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
958 }
959
960 #[test]
962 fn test_selection_with_shift_arrows() {
963 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
964 prompt.input = "hello world".to_string();
965 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
969 assert_eq!(prompt.selected_text(), None);
970
971 prompt.move_right_selecting();
973 assert!(prompt.has_selection());
974 assert_eq!(prompt.selection_range(), Some((5, 6)));
975 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
976
977 prompt.move_right_selecting();
979 assert_eq!(prompt.selection_range(), Some((5, 7)));
980 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
981
982 prompt.move_left_selecting();
984 assert_eq!(prompt.selection_range(), Some((5, 6)));
985 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
986 }
987
988 #[test]
989 fn test_selection_backward() {
990 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
991 prompt.input = "abcdef".to_string();
992 prompt.cursor_pos = 4; prompt.move_left_selecting();
996 prompt.move_left_selecting();
997 assert!(prompt.has_selection());
998 assert_eq!(prompt.selection_range(), Some((2, 4)));
999 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1000 }
1001
1002 #[test]
1003 fn test_selection_with_home_end() {
1004 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1005 prompt.input = "select this text".to_string();
1006 prompt.cursor_pos = 7; prompt.move_end_selecting();
1010 assert_eq!(prompt.selection_range(), Some((7, 16)));
1011 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1012
1013 prompt.clear_selection();
1015 prompt.move_home_selecting();
1016 assert_eq!(prompt.selection_range(), Some((0, 16)));
1017 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1018 }
1019
1020 #[test]
1021 fn test_word_selection() {
1022 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1023 prompt.input = "one two three".to_string();
1024 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1028 assert_eq!(prompt.selection_range(), Some((4, 7)));
1029 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1030
1031 prompt.move_word_right_selecting();
1033 assert_eq!(prompt.selection_range(), Some((4, 13)));
1034 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1035 }
1036
1037 #[test]
1038 fn test_word_selection_backward() {
1039 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1040 prompt.input = "one two three".to_string();
1041 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1045 assert_eq!(prompt.selection_range(), Some((8, 13)));
1046 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1047
1048 }
1053
1054 #[test]
1055 fn test_delete_selection() {
1056 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1057 prompt.input = "hello world".to_string();
1058 prompt.cursor_pos = 5;
1059
1060 prompt.move_end_selecting();
1062 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1063
1064 let deleted = prompt.delete_selection();
1066 assert_eq!(deleted, Some(" world".to_string()));
1067 assert_eq!(prompt.input, "hello");
1068 assert_eq!(prompt.cursor_pos, 5);
1069 assert!(!prompt.has_selection());
1070 }
1071
1072 #[test]
1073 fn test_insert_deletes_selection() {
1074 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1075 prompt.input = "hello world".to_string();
1076 prompt.cursor_pos = 0;
1077
1078 for _ in 0..5 {
1080 prompt.move_right_selecting();
1081 }
1082 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1083
1084 prompt.insert_str("goodbye");
1086 assert_eq!(prompt.input, "goodbye world");
1087 assert_eq!(prompt.cursor_pos, 7);
1088 assert!(!prompt.has_selection());
1089 }
1090
1091 #[test]
1092 fn test_clear_selection() {
1093 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1094 prompt.input = "test".to_string();
1095 prompt.cursor_pos = 0;
1096
1097 prompt.move_end_selecting();
1099 assert!(prompt.has_selection());
1100
1101 prompt.clear_selection();
1103 assert!(!prompt.has_selection());
1104 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1107
1108 #[test]
1109 fn test_selection_edge_cases() {
1110 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1111 prompt.input = "abc".to_string();
1112 prompt.cursor_pos = 3;
1113
1114 prompt.move_right_selecting();
1116 assert_eq!(prompt.cursor_pos, 3);
1117 assert_eq!(prompt.selection_range(), None);
1119 assert_eq!(prompt.selected_text(), None);
1120
1121 assert_eq!(prompt.delete_selection(), None);
1123 assert_eq!(prompt.input, "abc");
1124 }
1125
1126 #[test]
1127 fn test_selection_with_unicode() {
1128 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1129 prompt.input = "hello 世界 world".to_string();
1130 prompt.cursor_pos = 6; for _ in 0..2 {
1134 prompt.move_right_selecting();
1135 }
1136
1137 let selected = prompt.selected_text().unwrap();
1138 assert_eq!(selected, "世界");
1139
1140 prompt.delete_selection();
1142 assert_eq!(prompt.input, "hello world");
1143 }
1144
1145 #[test]
1149 fn test_word_selection_continues_across_words() {
1150 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1151 prompt.input = "one two three".to_string();
1152 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1156 assert_eq!(prompt.selection_range(), Some((8, 13)));
1157 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1158
1159 prompt.move_word_left_selecting();
1162
1163 assert_eq!(prompt.selection_range(), Some((4, 13)));
1165 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1166 }
1167
1168 #[cfg(test)]
1170 mod property_tests {
1171 use super::*;
1172 use proptest::prelude::*;
1173
1174 proptest! {
1175 #[test]
1177 fn prop_delete_word_backward_shrinks(
1178 input in "[a-zA-Z0-9_ ]{0,50}",
1179 cursor_pos in 0usize..50
1180 ) {
1181 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1182 prompt.input = input.clone();
1183 prompt.cursor_pos = cursor_pos.min(input.len());
1184
1185 let original_len = prompt.input.len();
1186 prompt.delete_word_backward();
1187
1188 prop_assert!(prompt.input.len() <= original_len);
1189 }
1190
1191 #[test]
1193 fn prop_delete_word_forward_shrinks(
1194 input in "[a-zA-Z0-9_ ]{0,50}",
1195 cursor_pos in 0usize..50
1196 ) {
1197 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1198 prompt.input = input.clone();
1199 prompt.cursor_pos = cursor_pos.min(input.len());
1200
1201 let original_len = prompt.input.len();
1202 prompt.delete_word_forward();
1203
1204 prop_assert!(prompt.input.len() <= original_len);
1205 }
1206
1207 #[test]
1209 fn prop_delete_word_backward_cursor_valid(
1210 input in "[a-zA-Z0-9_ ]{0,50}",
1211 cursor_pos in 0usize..50
1212 ) {
1213 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1214 prompt.input = input.clone();
1215 prompt.cursor_pos = cursor_pos.min(input.len());
1216
1217 prompt.delete_word_backward();
1218
1219 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1220 }
1221
1222 #[test]
1224 fn prop_delete_word_forward_cursor_valid(
1225 input in "[a-zA-Z0-9_ ]{0,50}",
1226 cursor_pos in 0usize..50
1227 ) {
1228 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1229 prompt.input = input.clone();
1230 prompt.cursor_pos = cursor_pos.min(input.len());
1231
1232 prompt.delete_word_forward();
1233
1234 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1235 }
1236
1237 #[test]
1239 fn prop_insert_str_length(
1240 input in "[a-zA-Z0-9_ ]{0,30}",
1241 insert in "[a-zA-Z0-9_ ]{0,20}",
1242 cursor_pos in 0usize..30
1243 ) {
1244 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1245 prompt.input = input.clone();
1246 prompt.cursor_pos = cursor_pos.min(input.len());
1247
1248 let original_len = prompt.input.len();
1249 prompt.insert_str(&insert);
1250
1251 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1252 }
1253
1254 #[test]
1256 fn prop_insert_str_cursor(
1257 input in "[a-zA-Z0-9_ ]{0,30}",
1258 insert in "[a-zA-Z0-9_ ]{0,20}",
1259 cursor_pos in 0usize..30
1260 ) {
1261 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1262 prompt.input = input.clone();
1263 let original_pos = cursor_pos.min(input.len());
1264 prompt.cursor_pos = original_pos;
1265
1266 prompt.insert_str(&insert);
1267
1268 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1269 }
1270
1271 #[test]
1273 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1274 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1275 prompt.input = input;
1276 prompt.cursor_pos = prompt.input.len();
1277
1278 prompt.clear();
1279
1280 prop_assert_eq!(prompt.input, "");
1281 prop_assert_eq!(prompt.cursor_pos, 0);
1282 }
1283 }
1284 }
1285}