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 ConfirmPasteConflict {
132 src: std::path::PathBuf,
133 dst: std::path::PathBuf,
134 is_cut: bool,
135 },
136 FileExplorerPasteRename {
138 src: std::path::PathBuf,
139 dst_dir: std::path::PathBuf,
140 is_cut: bool,
141 },
142 ConfirmMultiDelete { paths: Vec<std::path::PathBuf> },
144 ConfirmMultiPasteConflict {
148 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
149 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
150 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
151 is_cut: bool,
152 },
153 ConfirmLargeFileEncoding { path: std::path::PathBuf },
156 SwitchToTab,
158 ShellCommand { replace: bool },
162 AsyncPrompt,
165}
166
167impl PromptType {
168 pub fn click_confirms(&self) -> bool {
175 !matches!(self, PromptType::ReloadWithEncoding)
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct Prompt {
182 pub message: String,
184 pub input: String,
186 pub cursor_pos: usize,
188 pub prompt_type: PromptType,
190 pub suggestions: Vec<Suggestion>,
192 pub original_suggestions: Option<Vec<Suggestion>>,
194 pub selected_suggestion: Option<usize>,
196 pub scroll_offset: usize,
201 pub selection_anchor: Option<usize>,
204 pub suggestions_set_for_input: Option<String>,
207 pub sync_input_on_navigate: bool,
210}
211
212pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
216
217impl Prompt {
218 pub fn new(message: String, prompt_type: PromptType) -> Self {
220 Self {
221 message,
222 input: String::new(),
223 cursor_pos: 0,
224 prompt_type,
225 suggestions: Vec::new(),
226 original_suggestions: None,
227 selected_suggestion: None,
228 scroll_offset: 0,
229 selection_anchor: None,
230 suggestions_set_for_input: None,
231 sync_input_on_navigate: false,
232 }
233 }
234
235 pub fn with_suggestions(
240 message: String,
241 prompt_type: PromptType,
242 suggestions: Vec<Suggestion>,
243 ) -> Self {
244 let selected_suggestion = if suggestions.is_empty() {
245 None
246 } else {
247 Some(0)
248 };
249 Self {
250 message,
251 input: String::new(),
252 cursor_pos: 0,
253 prompt_type,
254 original_suggestions: Some(suggestions.clone()),
255 suggestions,
256 selected_suggestion,
257 scroll_offset: 0,
258 selection_anchor: None,
259 suggestions_set_for_input: None,
260 sync_input_on_navigate: false,
261 }
262 }
263
264 pub fn with_initial_text_for_edit(
269 message: String,
270 prompt_type: PromptType,
271 initial_text: String,
272 ) -> Self {
273 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
274 }
275
276 pub fn with_initial_text(
278 message: String,
279 prompt_type: PromptType,
280 initial_text: String,
281 ) -> Self {
282 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
283 }
284
285 fn with_initial_text_inner(
286 message: String,
287 prompt_type: PromptType,
288 initial_text: String,
289 select_all: bool,
290 ) -> Self {
291 let cursor_pos = initial_text.len();
292 let selection_anchor = if select_all && !initial_text.is_empty() {
293 Some(0)
294 } else {
295 None
296 };
297 Self {
298 message,
299 input: initial_text,
300 cursor_pos,
301 prompt_type,
302 suggestions: Vec::new(),
303 original_suggestions: None,
304 selected_suggestion: None,
305 scroll_offset: 0,
306 selection_anchor,
307 suggestions_set_for_input: None,
308 sync_input_on_navigate: false,
309 }
310 }
311
312 pub fn cursor_left(&mut self) {
317 if self.cursor_pos > 0 {
318 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
319 }
320 }
321
322 pub fn cursor_right(&mut self) {
327 if self.cursor_pos < self.input.len() {
328 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
329 }
330 }
331
332 pub fn insert_char(&mut self, ch: char) {
334 self.input.insert(self.cursor_pos, ch);
335 self.cursor_pos += ch.len_utf8();
336 }
337
338 pub fn backspace(&mut self) {
344 if self.cursor_pos > 0 {
345 let prev_boundary = self.input[..self.cursor_pos]
348 .char_indices()
349 .next_back()
350 .map(|(i, _)| i)
351 .unwrap_or(0);
352 self.input.drain(prev_boundary..self.cursor_pos);
353 self.cursor_pos = prev_boundary;
354 }
355 }
356
357 pub fn delete(&mut self) {
361 if self.cursor_pos < self.input.len() {
362 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
363 self.input.drain(self.cursor_pos..next_boundary);
364 }
365 }
366
367 pub fn move_to_start(&mut self) {
369 self.cursor_pos = 0;
370 }
371
372 pub fn move_to_end(&mut self) {
374 self.cursor_pos = self.input.len();
375 }
376
377 pub fn set_input(&mut self, text: String) {
394 self.cursor_pos = text.len();
395 self.input = text;
396 self.clear_selection();
397 }
398
399 pub fn select_next_suggestion(&mut self) {
401 if !self.suggestions.is_empty() {
402 self.selected_suggestion = Some(match self.selected_suggestion {
403 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
404 Some(_) => 0, None => 0,
406 });
407 }
408 }
409
410 pub fn select_prev_suggestion(&mut self) {
412 if !self.suggestions.is_empty() {
413 self.selected_suggestion = Some(match self.selected_suggestion {
414 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
416 None => 0,
417 });
418 }
419 }
420
421 pub fn selected_value(&self) -> Option<String> {
423 self.selected_suggestion
424 .and_then(|idx| self.suggestions.get(idx))
425 .map(|s| s.get_value().to_string())
426 }
427
428 pub fn get_final_input(&self) -> String {
430 self.selected_value().unwrap_or_else(|| self.input.clone())
431 }
432
433 pub fn filter_suggestions(&mut self, match_description: bool) {
438 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
439
440 if let Some(ref set_for_input) = self.suggestions_set_for_input {
444 if set_for_input == &self.input {
445 return;
446 }
447 }
448
449 let Some(original) = &self.original_suggestions else {
450 return;
451 };
452
453 let input = &self.input;
454 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
455 .iter()
456 .filter_map(|s| {
457 let text_result = fuzzy_match(input, &s.text);
458 let desc_result = if match_description {
459 s.description
460 .as_ref()
461 .map(|d| fuzzy_match(input, d))
462 .unwrap_or_else(FuzzyMatch::no_match)
463 } else {
464 FuzzyMatch::no_match()
465 };
466 if text_result.matched || desc_result.matched {
467 Some((s.clone(), text_result.score.max(desc_result.score)))
468 } else {
469 None
470 }
471 })
472 .collect();
473
474 filtered.sort_by(|a, b| b.1.cmp(&a.1));
475 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
476 self.selected_suggestion = if self.suggestions.is_empty() {
477 None
478 } else {
479 Some(0)
480 };
481 self.scroll_offset = 0;
482 }
483
484 pub fn ensure_selected_visible(&mut self) {
490 let total = self.suggestions.len();
491 let visible = total.min(MAX_VISIBLE_SUGGESTIONS);
492 let max_offset = total.saturating_sub(visible);
493 if visible == 0 {
494 self.scroll_offset = 0;
495 return;
496 }
497 if let Some(selected) = self.selected_suggestion {
498 if selected < self.scroll_offset {
499 self.scroll_offset = selected;
500 } else if selected >= self.scroll_offset + visible {
501 self.scroll_offset = selected + 1 - visible;
502 }
503 }
504 if self.scroll_offset > max_offset {
505 self.scroll_offset = max_offset;
506 }
507 }
508
509 pub fn delete_word_forward(&mut self) {
539 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
540 if word_end > self.cursor_pos {
541 self.input.drain(self.cursor_pos..word_end);
542 }
544 }
545
546 pub fn delete_word_backward(&mut self) {
562 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
563 if word_start < self.cursor_pos {
564 self.input.drain(word_start..self.cursor_pos);
565 self.cursor_pos = word_start;
566 }
567 }
568
569 pub fn delete_to_end(&mut self) {
584 if self.cursor_pos < self.input.len() {
585 self.input.truncate(self.cursor_pos);
586 }
587 }
588
589 pub fn get_text(&self) -> String {
602 self.input.clone()
603 }
604
605 pub fn clear(&mut self) {
620 self.input.clear();
621 self.cursor_pos = 0;
622 self.selected_suggestion = None;
624 }
625
626 pub fn insert_str(&mut self, text: &str) {
642 if self.has_selection() {
644 self.delete_selection();
645 }
646 self.input.insert_str(self.cursor_pos, text);
647 self.cursor_pos += text.len();
648 }
649
650 pub fn has_selection(&self) -> bool {
656 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
657 }
658
659 pub fn selection_range(&self) -> Option<(usize, usize)> {
661 if let Some(anchor) = self.selection_anchor {
662 if anchor != self.cursor_pos {
663 let start = anchor.min(self.cursor_pos);
664 let end = anchor.max(self.cursor_pos);
665 return Some((start, end));
666 }
667 }
668 None
669 }
670
671 pub fn selected_text(&self) -> Option<String> {
673 self.selection_range()
674 .map(|(start, end)| self.input[start..end].to_string())
675 }
676
677 pub fn delete_selection(&mut self) -> Option<String> {
679 if let Some((start, end)) = self.selection_range() {
680 let deleted = self.input[start..end].to_string();
681 self.input.drain(start..end);
682 self.cursor_pos = start;
683 self.selection_anchor = None;
684 Some(deleted)
685 } else {
686 None
687 }
688 }
689
690 pub fn clear_selection(&mut self) {
692 self.selection_anchor = None;
693 }
694
695 pub fn move_left_selecting(&mut self) {
697 if self.selection_anchor.is_none() {
699 self.selection_anchor = Some(self.cursor_pos);
700 }
701
702 if self.cursor_pos > 0 {
704 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
705 }
706 }
707
708 pub fn move_right_selecting(&mut self) {
710 if self.selection_anchor.is_none() {
712 self.selection_anchor = Some(self.cursor_pos);
713 }
714
715 if self.cursor_pos < self.input.len() {
717 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
718 }
719 }
720
721 pub fn move_home_selecting(&mut self) {
723 if self.selection_anchor.is_none() {
724 self.selection_anchor = Some(self.cursor_pos);
725 }
726 self.cursor_pos = 0;
727 }
728
729 pub fn move_end_selecting(&mut self) {
731 if self.selection_anchor.is_none() {
732 self.selection_anchor = Some(self.cursor_pos);
733 }
734 self.cursor_pos = self.input.len();
735 }
736
737 pub fn move_word_left_selecting(&mut self) {
740 if self.selection_anchor.is_none() {
741 self.selection_anchor = Some(self.cursor_pos);
742 }
743
744 let bytes = self.input.as_bytes();
745 if self.cursor_pos == 0 {
746 return;
747 }
748
749 let mut new_pos = self.cursor_pos.saturating_sub(1);
750
751 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
753 new_pos = new_pos.saturating_sub(1);
754 }
755
756 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
758 new_pos = new_pos.saturating_sub(1);
759 }
760
761 self.cursor_pos = new_pos;
762 }
763
764 pub fn move_word_right_selecting(&mut self) {
767 if self.selection_anchor.is_none() {
768 self.selection_anchor = Some(self.cursor_pos);
769 }
770
771 let bytes = self.input.as_bytes();
773 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
774
775 if new_pos == self.cursor_pos && new_pos < bytes.len() {
777 new_pos = (new_pos + 1).min(bytes.len());
778 new_pos = find_word_end_bytes(bytes, new_pos);
779 }
780
781 self.cursor_pos = new_pos;
782 }
783
784 pub fn move_word_left(&mut self) {
787 self.clear_selection();
788
789 let bytes = self.input.as_bytes();
790 if self.cursor_pos == 0 {
791 return;
792 }
793
794 let mut new_pos = self.cursor_pos.saturating_sub(1);
795
796 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
798 new_pos = new_pos.saturating_sub(1);
799 }
800
801 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
803 new_pos = new_pos.saturating_sub(1);
804 }
805
806 self.cursor_pos = new_pos;
807 }
808
809 pub fn move_word_right(&mut self) {
812 self.clear_selection();
813
814 let bytes = self.input.as_bytes();
815 if self.cursor_pos >= bytes.len() {
816 return;
817 }
818
819 let mut new_pos = self.cursor_pos;
820
821 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
823 new_pos += 1;
824 }
825
826 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
828 new_pos += 1;
829 }
830
831 self.cursor_pos = new_pos;
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 #[test]
840 fn test_delete_word_forward_basic() {
841 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
842 prompt.input = "hello world test".to_string();
843 prompt.cursor_pos = 0;
844
845 prompt.delete_word_forward();
846 assert_eq!(prompt.input, " world test");
847 assert_eq!(prompt.cursor_pos, 0);
848 }
849
850 #[test]
851 fn test_delete_word_forward_middle() {
852 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
853 prompt.input = "hello world test".to_string();
854 prompt.cursor_pos = 3; prompt.delete_word_forward();
857 assert_eq!(prompt.input, "hel world test");
858 assert_eq!(prompt.cursor_pos, 3);
859 }
860
861 #[test]
862 fn test_delete_word_forward_at_space() {
863 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
864 prompt.input = "hello world".to_string();
865 prompt.cursor_pos = 5; prompt.delete_word_forward();
868 assert_eq!(prompt.input, "hello");
869 assert_eq!(prompt.cursor_pos, 5);
870 }
871
872 #[test]
873 fn test_delete_word_backward_basic() {
874 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
875 prompt.input = "hello world test".to_string();
876 prompt.cursor_pos = 5; prompt.delete_word_backward();
879 assert_eq!(prompt.input, " world test");
880 assert_eq!(prompt.cursor_pos, 0);
881 }
882
883 #[test]
884 fn test_delete_word_backward_middle() {
885 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
886 prompt.input = "hello world test".to_string();
887 prompt.cursor_pos = 8; prompt.delete_word_backward();
890 assert_eq!(prompt.input, "hello rld test");
891 assert_eq!(prompt.cursor_pos, 6);
892 }
893
894 #[test]
895 fn test_delete_word_backward_at_end() {
896 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
897 prompt.input = "hello world".to_string();
898 prompt.cursor_pos = 11; prompt.delete_word_backward();
901 assert_eq!(prompt.input, "hello ");
902 assert_eq!(prompt.cursor_pos, 6);
903 }
904
905 #[test]
906 fn test_delete_word_with_special_chars() {
907 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
908 prompt.input = "save-file-as".to_string();
909 prompt.cursor_pos = 12; prompt.delete_word_backward();
913 assert_eq!(prompt.input, "save-file-");
914 assert_eq!(prompt.cursor_pos, 10);
915
916 prompt.delete_word_backward();
918 assert_eq!(prompt.input, "save-");
919 assert_eq!(prompt.cursor_pos, 5);
920 }
921
922 #[test]
923 fn test_get_text() {
924 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
925 prompt.input = "test content".to_string();
926
927 assert_eq!(prompt.get_text(), "test content");
928 }
929
930 #[test]
931 fn test_clear() {
932 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
933 prompt.input = "some text".to_string();
934 prompt.cursor_pos = 5;
935 prompt.selected_suggestion = Some(0);
936
937 prompt.clear();
938
939 assert_eq!(prompt.input, "");
940 assert_eq!(prompt.cursor_pos, 0);
941 assert_eq!(prompt.selected_suggestion, None);
942 }
943
944 #[test]
945 fn test_delete_forward_basic() {
946 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
947 prompt.input = "hello".to_string();
948 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
952
953 assert_eq!(prompt.input, "hllo");
954 assert_eq!(prompt.cursor_pos, 1);
955 }
956
957 #[test]
958 fn test_delete_at_end() {
959 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
960 prompt.input = "hello".to_string();
961 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
965 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
966 }
967
968 assert_eq!(prompt.input, "hello");
969 assert_eq!(prompt.cursor_pos, 5);
970 }
971
972 #[test]
973 fn test_insert_str_at_start() {
974 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
975 prompt.input = "world".to_string();
976 prompt.cursor_pos = 0;
977
978 prompt.insert_str("hello ");
979 assert_eq!(prompt.input, "hello world");
980 assert_eq!(prompt.cursor_pos, 6);
981 }
982
983 #[test]
984 fn test_insert_str_at_middle() {
985 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
986 prompt.input = "helloworld".to_string();
987 prompt.cursor_pos = 5;
988
989 prompt.insert_str(" ");
990 assert_eq!(prompt.input, "hello world");
991 assert_eq!(prompt.cursor_pos, 6);
992 }
993
994 #[test]
995 fn test_insert_str_at_end() {
996 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
997 prompt.input = "hello".to_string();
998 prompt.cursor_pos = 5;
999
1000 prompt.insert_str(" world");
1001 assert_eq!(prompt.input, "hello world");
1002 assert_eq!(prompt.cursor_pos, 11);
1003 }
1004
1005 #[test]
1006 fn test_delete_word_forward_empty() {
1007 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1008 prompt.input = "".to_string();
1009 prompt.cursor_pos = 0;
1010
1011 prompt.delete_word_forward();
1012 assert_eq!(prompt.input, "");
1013 assert_eq!(prompt.cursor_pos, 0);
1014 }
1015
1016 #[test]
1017 fn test_delete_word_backward_empty() {
1018 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1019 prompt.input = "".to_string();
1020 prompt.cursor_pos = 0;
1021
1022 prompt.delete_word_backward();
1023 assert_eq!(prompt.input, "");
1024 assert_eq!(prompt.cursor_pos, 0);
1025 }
1026
1027 #[test]
1028 fn test_delete_word_forward_only_spaces() {
1029 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1030 prompt.input = " ".to_string();
1031 prompt.cursor_pos = 0;
1032
1033 prompt.delete_word_forward();
1034 assert_eq!(prompt.input, "");
1035 assert_eq!(prompt.cursor_pos, 0);
1036 }
1037
1038 #[test]
1039 fn test_multiple_word_deletions() {
1040 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1041 prompt.input = "one two three four".to_string();
1042 prompt.cursor_pos = 18;
1043
1044 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1046
1047 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1049
1050 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1052 }
1053
1054 #[test]
1056 fn test_selection_with_shift_arrows() {
1057 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1058 prompt.input = "hello world".to_string();
1059 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1063 assert_eq!(prompt.selected_text(), None);
1064
1065 prompt.move_right_selecting();
1067 assert!(prompt.has_selection());
1068 assert_eq!(prompt.selection_range(), Some((5, 6)));
1069 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1070
1071 prompt.move_right_selecting();
1073 assert_eq!(prompt.selection_range(), Some((5, 7)));
1074 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1075
1076 prompt.move_left_selecting();
1078 assert_eq!(prompt.selection_range(), Some((5, 6)));
1079 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1080 }
1081
1082 #[test]
1083 fn test_selection_backward() {
1084 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1085 prompt.input = "abcdef".to_string();
1086 prompt.cursor_pos = 4; prompt.move_left_selecting();
1090 prompt.move_left_selecting();
1091 assert!(prompt.has_selection());
1092 assert_eq!(prompt.selection_range(), Some((2, 4)));
1093 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1094 }
1095
1096 #[test]
1097 fn test_selection_with_home_end() {
1098 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1099 prompt.input = "select this text".to_string();
1100 prompt.cursor_pos = 7; prompt.move_end_selecting();
1104 assert_eq!(prompt.selection_range(), Some((7, 16)));
1105 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1106
1107 prompt.clear_selection();
1109 prompt.move_home_selecting();
1110 assert_eq!(prompt.selection_range(), Some((0, 16)));
1111 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1112 }
1113
1114 #[test]
1115 fn test_word_selection() {
1116 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1117 prompt.input = "one two three".to_string();
1118 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1122 assert_eq!(prompt.selection_range(), Some((4, 7)));
1123 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1124
1125 prompt.move_word_right_selecting();
1127 assert_eq!(prompt.selection_range(), Some((4, 13)));
1128 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1129 }
1130
1131 #[test]
1132 fn test_word_selection_backward() {
1133 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1134 prompt.input = "one two three".to_string();
1135 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1139 assert_eq!(prompt.selection_range(), Some((8, 13)));
1140 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1141
1142 }
1147
1148 #[test]
1149 fn test_delete_selection() {
1150 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1151 prompt.input = "hello world".to_string();
1152 prompt.cursor_pos = 5;
1153
1154 prompt.move_end_selecting();
1156 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1157
1158 let deleted = prompt.delete_selection();
1160 assert_eq!(deleted, Some(" world".to_string()));
1161 assert_eq!(prompt.input, "hello");
1162 assert_eq!(prompt.cursor_pos, 5);
1163 assert!(!prompt.has_selection());
1164 }
1165
1166 #[test]
1167 fn test_insert_deletes_selection() {
1168 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1169 prompt.input = "hello world".to_string();
1170 prompt.cursor_pos = 0;
1171
1172 for _ in 0..5 {
1174 prompt.move_right_selecting();
1175 }
1176 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1177
1178 prompt.insert_str("goodbye");
1180 assert_eq!(prompt.input, "goodbye world");
1181 assert_eq!(prompt.cursor_pos, 7);
1182 assert!(!prompt.has_selection());
1183 }
1184
1185 #[test]
1186 fn test_clear_selection() {
1187 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1188 prompt.input = "test".to_string();
1189 prompt.cursor_pos = 0;
1190
1191 prompt.move_end_selecting();
1193 assert!(prompt.has_selection());
1194
1195 prompt.clear_selection();
1197 assert!(!prompt.has_selection());
1198 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1201
1202 #[test]
1203 fn test_selection_edge_cases() {
1204 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1205 prompt.input = "abc".to_string();
1206 prompt.cursor_pos = 3;
1207
1208 prompt.move_right_selecting();
1210 assert_eq!(prompt.cursor_pos, 3);
1211 assert_eq!(prompt.selection_range(), None);
1213 assert_eq!(prompt.selected_text(), None);
1214
1215 assert_eq!(prompt.delete_selection(), None);
1217 assert_eq!(prompt.input, "abc");
1218 }
1219
1220 #[test]
1221 fn test_selection_with_unicode() {
1222 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1223 prompt.input = "hello 世界 world".to_string();
1224 prompt.cursor_pos = 6; for _ in 0..2 {
1228 prompt.move_right_selecting();
1229 }
1230
1231 let selected = prompt.selected_text().unwrap();
1232 assert_eq!(selected, "世界");
1233
1234 prompt.delete_selection();
1236 assert_eq!(prompt.input, "hello world");
1237 }
1238
1239 #[test]
1243 fn test_word_selection_continues_across_words() {
1244 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1245 prompt.input = "one two three".to_string();
1246 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1250 assert_eq!(prompt.selection_range(), Some((8, 13)));
1251 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1252
1253 prompt.move_word_left_selecting();
1256
1257 assert_eq!(prompt.selection_range(), Some((4, 13)));
1259 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1260 }
1261
1262 #[cfg(test)]
1264 mod property_tests {
1265 use super::*;
1266 use proptest::prelude::*;
1267
1268 proptest! {
1269 #[test]
1271 fn prop_delete_word_backward_shrinks(
1272 input in "[a-zA-Z0-9_ ]{0,50}",
1273 cursor_pos in 0usize..50
1274 ) {
1275 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1276 prompt.input = input.clone();
1277 prompt.cursor_pos = cursor_pos.min(input.len());
1278
1279 let original_len = prompt.input.len();
1280 prompt.delete_word_backward();
1281
1282 prop_assert!(prompt.input.len() <= original_len);
1283 }
1284
1285 #[test]
1287 fn prop_delete_word_forward_shrinks(
1288 input in "[a-zA-Z0-9_ ]{0,50}",
1289 cursor_pos in 0usize..50
1290 ) {
1291 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1292 prompt.input = input.clone();
1293 prompt.cursor_pos = cursor_pos.min(input.len());
1294
1295 let original_len = prompt.input.len();
1296 prompt.delete_word_forward();
1297
1298 prop_assert!(prompt.input.len() <= original_len);
1299 }
1300
1301 #[test]
1303 fn prop_delete_word_backward_cursor_valid(
1304 input in "[a-zA-Z0-9_ ]{0,50}",
1305 cursor_pos in 0usize..50
1306 ) {
1307 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1308 prompt.input = input.clone();
1309 prompt.cursor_pos = cursor_pos.min(input.len());
1310
1311 prompt.delete_word_backward();
1312
1313 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1314 }
1315
1316 #[test]
1318 fn prop_delete_word_forward_cursor_valid(
1319 input in "[a-zA-Z0-9_ ]{0,50}",
1320 cursor_pos in 0usize..50
1321 ) {
1322 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1323 prompt.input = input.clone();
1324 prompt.cursor_pos = cursor_pos.min(input.len());
1325
1326 prompt.delete_word_forward();
1327
1328 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1329 }
1330
1331 #[test]
1333 fn prop_insert_str_length(
1334 input in "[a-zA-Z0-9_ ]{0,30}",
1335 insert in "[a-zA-Z0-9_ ]{0,20}",
1336 cursor_pos in 0usize..30
1337 ) {
1338 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1339 prompt.input = input.clone();
1340 prompt.cursor_pos = cursor_pos.min(input.len());
1341
1342 let original_len = prompt.input.len();
1343 prompt.insert_str(&insert);
1344
1345 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1346 }
1347
1348 #[test]
1350 fn prop_insert_str_cursor(
1351 input in "[a-zA-Z0-9_ ]{0,30}",
1352 insert in "[a-zA-Z0-9_ ]{0,20}",
1353 cursor_pos in 0usize..30
1354 ) {
1355 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1356 prompt.input = input.clone();
1357 let original_pos = cursor_pos.min(input.len());
1358 prompt.cursor_pos = original_pos;
1359
1360 prompt.insert_str(&insert);
1361
1362 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1363 }
1364
1365 #[test]
1367 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1368 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1369 prompt.input = input;
1370 prompt.cursor_pos = prompt.input.len();
1371
1372 prompt.clear();
1373
1374 prop_assert_eq!(prompt.input, "");
1375 prop_assert_eq!(prompt.cursor_pos, 0);
1376 }
1377 }
1378 }
1379}