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
167#[derive(Debug, Clone)]
169pub struct Prompt {
170 pub message: String,
172 pub input: String,
174 pub cursor_pos: usize,
176 pub prompt_type: PromptType,
178 pub suggestions: Vec<Suggestion>,
180 pub original_suggestions: Option<Vec<Suggestion>>,
182 pub selected_suggestion: Option<usize>,
184 pub selection_anchor: Option<usize>,
187 pub suggestions_set_for_input: Option<String>,
190 pub sync_input_on_navigate: bool,
193}
194
195impl Prompt {
196 pub fn new(message: String, prompt_type: PromptType) -> Self {
198 Self {
199 message,
200 input: String::new(),
201 cursor_pos: 0,
202 prompt_type,
203 suggestions: Vec::new(),
204 original_suggestions: None,
205 selected_suggestion: None,
206 selection_anchor: None,
207 suggestions_set_for_input: None,
208 sync_input_on_navigate: false,
209 }
210 }
211
212 pub fn with_suggestions(
217 message: String,
218 prompt_type: PromptType,
219 suggestions: Vec<Suggestion>,
220 ) -> Self {
221 let selected_suggestion = if suggestions.is_empty() {
222 None
223 } else {
224 Some(0)
225 };
226 Self {
227 message,
228 input: String::new(),
229 cursor_pos: 0,
230 prompt_type,
231 original_suggestions: Some(suggestions.clone()),
232 suggestions,
233 selected_suggestion,
234 selection_anchor: None,
235 suggestions_set_for_input: None,
236 sync_input_on_navigate: false,
237 }
238 }
239
240 pub fn with_initial_text_for_edit(
245 message: String,
246 prompt_type: PromptType,
247 initial_text: String,
248 ) -> Self {
249 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
250 }
251
252 pub fn with_initial_text(
254 message: String,
255 prompt_type: PromptType,
256 initial_text: String,
257 ) -> Self {
258 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
259 }
260
261 fn with_initial_text_inner(
262 message: String,
263 prompt_type: PromptType,
264 initial_text: String,
265 select_all: bool,
266 ) -> Self {
267 let cursor_pos = initial_text.len();
268 let selection_anchor = if select_all && !initial_text.is_empty() {
269 Some(0)
270 } else {
271 None
272 };
273 Self {
274 message,
275 input: initial_text,
276 cursor_pos,
277 prompt_type,
278 suggestions: Vec::new(),
279 original_suggestions: None,
280 selected_suggestion: None,
281 selection_anchor,
282 suggestions_set_for_input: None,
283 sync_input_on_navigate: false,
284 }
285 }
286
287 pub fn cursor_left(&mut self) {
292 if self.cursor_pos > 0 {
293 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
294 }
295 }
296
297 pub fn cursor_right(&mut self) {
302 if self.cursor_pos < self.input.len() {
303 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
304 }
305 }
306
307 pub fn insert_char(&mut self, ch: char) {
309 self.input.insert(self.cursor_pos, ch);
310 self.cursor_pos += ch.len_utf8();
311 }
312
313 pub fn backspace(&mut self) {
319 if self.cursor_pos > 0 {
320 let prev_boundary = self.input[..self.cursor_pos]
323 .char_indices()
324 .next_back()
325 .map(|(i, _)| i)
326 .unwrap_or(0);
327 self.input.drain(prev_boundary..self.cursor_pos);
328 self.cursor_pos = prev_boundary;
329 }
330 }
331
332 pub fn delete(&mut self) {
336 if self.cursor_pos < self.input.len() {
337 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
338 self.input.drain(self.cursor_pos..next_boundary);
339 }
340 }
341
342 pub fn move_to_start(&mut self) {
344 self.cursor_pos = 0;
345 }
346
347 pub fn move_to_end(&mut self) {
349 self.cursor_pos = self.input.len();
350 }
351
352 pub fn set_input(&mut self, text: String) {
369 self.cursor_pos = text.len();
370 self.input = text;
371 self.clear_selection();
372 }
373
374 pub fn select_next_suggestion(&mut self) {
376 if !self.suggestions.is_empty() {
377 self.selected_suggestion = Some(match self.selected_suggestion {
378 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
379 Some(_) => 0, None => 0,
381 });
382 }
383 }
384
385 pub fn select_prev_suggestion(&mut self) {
387 if !self.suggestions.is_empty() {
388 self.selected_suggestion = Some(match self.selected_suggestion {
389 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
391 None => 0,
392 });
393 }
394 }
395
396 pub fn selected_value(&self) -> Option<String> {
398 self.selected_suggestion
399 .and_then(|idx| self.suggestions.get(idx))
400 .map(|s| s.get_value().to_string())
401 }
402
403 pub fn get_final_input(&self) -> String {
405 self.selected_value().unwrap_or_else(|| self.input.clone())
406 }
407
408 pub fn filter_suggestions(&mut self, match_description: bool) {
413 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
414
415 if let Some(ref set_for_input) = self.suggestions_set_for_input {
419 if set_for_input == &self.input {
420 return;
421 }
422 }
423
424 let Some(original) = &self.original_suggestions else {
425 return;
426 };
427
428 let input = &self.input;
429 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
430 .iter()
431 .filter_map(|s| {
432 let text_result = fuzzy_match(input, &s.text);
433 let desc_result = if match_description {
434 s.description
435 .as_ref()
436 .map(|d| fuzzy_match(input, d))
437 .unwrap_or_else(FuzzyMatch::no_match)
438 } else {
439 FuzzyMatch::no_match()
440 };
441 if text_result.matched || desc_result.matched {
442 Some((s.clone(), text_result.score.max(desc_result.score)))
443 } else {
444 None
445 }
446 })
447 .collect();
448
449 filtered.sort_by(|a, b| b.1.cmp(&a.1));
450 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
451 self.selected_suggestion = if self.suggestions.is_empty() {
452 None
453 } else {
454 Some(0)
455 };
456 }
457
458 pub fn delete_word_forward(&mut self) {
488 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
489 if word_end > self.cursor_pos {
490 self.input.drain(self.cursor_pos..word_end);
491 }
493 }
494
495 pub fn delete_word_backward(&mut self) {
511 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
512 if word_start < self.cursor_pos {
513 self.input.drain(word_start..self.cursor_pos);
514 self.cursor_pos = word_start;
515 }
516 }
517
518 pub fn delete_to_end(&mut self) {
533 if self.cursor_pos < self.input.len() {
534 self.input.truncate(self.cursor_pos);
535 }
536 }
537
538 pub fn get_text(&self) -> String {
551 self.input.clone()
552 }
553
554 pub fn clear(&mut self) {
569 self.input.clear();
570 self.cursor_pos = 0;
571 self.selected_suggestion = None;
573 }
574
575 pub fn insert_str(&mut self, text: &str) {
591 if self.has_selection() {
593 self.delete_selection();
594 }
595 self.input.insert_str(self.cursor_pos, text);
596 self.cursor_pos += text.len();
597 }
598
599 pub fn has_selection(&self) -> bool {
605 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
606 }
607
608 pub fn selection_range(&self) -> Option<(usize, usize)> {
610 if let Some(anchor) = self.selection_anchor {
611 if anchor != self.cursor_pos {
612 let start = anchor.min(self.cursor_pos);
613 let end = anchor.max(self.cursor_pos);
614 return Some((start, end));
615 }
616 }
617 None
618 }
619
620 pub fn selected_text(&self) -> Option<String> {
622 self.selection_range()
623 .map(|(start, end)| self.input[start..end].to_string())
624 }
625
626 pub fn delete_selection(&mut self) -> Option<String> {
628 if let Some((start, end)) = self.selection_range() {
629 let deleted = self.input[start..end].to_string();
630 self.input.drain(start..end);
631 self.cursor_pos = start;
632 self.selection_anchor = None;
633 Some(deleted)
634 } else {
635 None
636 }
637 }
638
639 pub fn clear_selection(&mut self) {
641 self.selection_anchor = None;
642 }
643
644 pub fn move_left_selecting(&mut self) {
646 if self.selection_anchor.is_none() {
648 self.selection_anchor = Some(self.cursor_pos);
649 }
650
651 if self.cursor_pos > 0 {
653 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
654 }
655 }
656
657 pub fn move_right_selecting(&mut self) {
659 if self.selection_anchor.is_none() {
661 self.selection_anchor = Some(self.cursor_pos);
662 }
663
664 if self.cursor_pos < self.input.len() {
666 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
667 }
668 }
669
670 pub fn move_home_selecting(&mut self) {
672 if self.selection_anchor.is_none() {
673 self.selection_anchor = Some(self.cursor_pos);
674 }
675 self.cursor_pos = 0;
676 }
677
678 pub fn move_end_selecting(&mut self) {
680 if self.selection_anchor.is_none() {
681 self.selection_anchor = Some(self.cursor_pos);
682 }
683 self.cursor_pos = self.input.len();
684 }
685
686 pub fn move_word_left_selecting(&mut self) {
689 if self.selection_anchor.is_none() {
690 self.selection_anchor = Some(self.cursor_pos);
691 }
692
693 let bytes = self.input.as_bytes();
694 if self.cursor_pos == 0 {
695 return;
696 }
697
698 let mut new_pos = self.cursor_pos.saturating_sub(1);
699
700 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
702 new_pos = new_pos.saturating_sub(1);
703 }
704
705 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
707 new_pos = new_pos.saturating_sub(1);
708 }
709
710 self.cursor_pos = new_pos;
711 }
712
713 pub fn move_word_right_selecting(&mut self) {
716 if self.selection_anchor.is_none() {
717 self.selection_anchor = Some(self.cursor_pos);
718 }
719
720 let bytes = self.input.as_bytes();
722 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
723
724 if new_pos == self.cursor_pos && new_pos < bytes.len() {
726 new_pos = (new_pos + 1).min(bytes.len());
727 new_pos = find_word_end_bytes(bytes, new_pos);
728 }
729
730 self.cursor_pos = new_pos;
731 }
732
733 pub fn move_word_left(&mut self) {
736 self.clear_selection();
737
738 let bytes = self.input.as_bytes();
739 if self.cursor_pos == 0 {
740 return;
741 }
742
743 let mut new_pos = self.cursor_pos.saturating_sub(1);
744
745 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
747 new_pos = new_pos.saturating_sub(1);
748 }
749
750 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
752 new_pos = new_pos.saturating_sub(1);
753 }
754
755 self.cursor_pos = new_pos;
756 }
757
758 pub fn move_word_right(&mut self) {
761 self.clear_selection();
762
763 let bytes = self.input.as_bytes();
764 if self.cursor_pos >= bytes.len() {
765 return;
766 }
767
768 let mut new_pos = self.cursor_pos;
769
770 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
772 new_pos += 1;
773 }
774
775 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
777 new_pos += 1;
778 }
779
780 self.cursor_pos = new_pos;
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
789 fn test_delete_word_forward_basic() {
790 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
791 prompt.input = "hello world test".to_string();
792 prompt.cursor_pos = 0;
793
794 prompt.delete_word_forward();
795 assert_eq!(prompt.input, " world test");
796 assert_eq!(prompt.cursor_pos, 0);
797 }
798
799 #[test]
800 fn test_delete_word_forward_middle() {
801 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
802 prompt.input = "hello world test".to_string();
803 prompt.cursor_pos = 3; prompt.delete_word_forward();
806 assert_eq!(prompt.input, "hel world test");
807 assert_eq!(prompt.cursor_pos, 3);
808 }
809
810 #[test]
811 fn test_delete_word_forward_at_space() {
812 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
813 prompt.input = "hello world".to_string();
814 prompt.cursor_pos = 5; prompt.delete_word_forward();
817 assert_eq!(prompt.input, "hello");
818 assert_eq!(prompt.cursor_pos, 5);
819 }
820
821 #[test]
822 fn test_delete_word_backward_basic() {
823 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
824 prompt.input = "hello world test".to_string();
825 prompt.cursor_pos = 5; prompt.delete_word_backward();
828 assert_eq!(prompt.input, " world test");
829 assert_eq!(prompt.cursor_pos, 0);
830 }
831
832 #[test]
833 fn test_delete_word_backward_middle() {
834 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
835 prompt.input = "hello world test".to_string();
836 prompt.cursor_pos = 8; prompt.delete_word_backward();
839 assert_eq!(prompt.input, "hello rld test");
840 assert_eq!(prompt.cursor_pos, 6);
841 }
842
843 #[test]
844 fn test_delete_word_backward_at_end() {
845 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
846 prompt.input = "hello world".to_string();
847 prompt.cursor_pos = 11; prompt.delete_word_backward();
850 assert_eq!(prompt.input, "hello ");
851 assert_eq!(prompt.cursor_pos, 6);
852 }
853
854 #[test]
855 fn test_delete_word_with_special_chars() {
856 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
857 prompt.input = "save-file-as".to_string();
858 prompt.cursor_pos = 12; prompt.delete_word_backward();
862 assert_eq!(prompt.input, "save-file-");
863 assert_eq!(prompt.cursor_pos, 10);
864
865 prompt.delete_word_backward();
867 assert_eq!(prompt.input, "save-");
868 assert_eq!(prompt.cursor_pos, 5);
869 }
870
871 #[test]
872 fn test_get_text() {
873 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
874 prompt.input = "test content".to_string();
875
876 assert_eq!(prompt.get_text(), "test content");
877 }
878
879 #[test]
880 fn test_clear() {
881 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
882 prompt.input = "some text".to_string();
883 prompt.cursor_pos = 5;
884 prompt.selected_suggestion = Some(0);
885
886 prompt.clear();
887
888 assert_eq!(prompt.input, "");
889 assert_eq!(prompt.cursor_pos, 0);
890 assert_eq!(prompt.selected_suggestion, None);
891 }
892
893 #[test]
894 fn test_delete_forward_basic() {
895 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
896 prompt.input = "hello".to_string();
897 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
901
902 assert_eq!(prompt.input, "hllo");
903 assert_eq!(prompt.cursor_pos, 1);
904 }
905
906 #[test]
907 fn test_delete_at_end() {
908 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
909 prompt.input = "hello".to_string();
910 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
914 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
915 }
916
917 assert_eq!(prompt.input, "hello");
918 assert_eq!(prompt.cursor_pos, 5);
919 }
920
921 #[test]
922 fn test_insert_str_at_start() {
923 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
924 prompt.input = "world".to_string();
925 prompt.cursor_pos = 0;
926
927 prompt.insert_str("hello ");
928 assert_eq!(prompt.input, "hello world");
929 assert_eq!(prompt.cursor_pos, 6);
930 }
931
932 #[test]
933 fn test_insert_str_at_middle() {
934 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
935 prompt.input = "helloworld".to_string();
936 prompt.cursor_pos = 5;
937
938 prompt.insert_str(" ");
939 assert_eq!(prompt.input, "hello world");
940 assert_eq!(prompt.cursor_pos, 6);
941 }
942
943 #[test]
944 fn test_insert_str_at_end() {
945 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
946 prompt.input = "hello".to_string();
947 prompt.cursor_pos = 5;
948
949 prompt.insert_str(" world");
950 assert_eq!(prompt.input, "hello world");
951 assert_eq!(prompt.cursor_pos, 11);
952 }
953
954 #[test]
955 fn test_delete_word_forward_empty() {
956 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
957 prompt.input = "".to_string();
958 prompt.cursor_pos = 0;
959
960 prompt.delete_word_forward();
961 assert_eq!(prompt.input, "");
962 assert_eq!(prompt.cursor_pos, 0);
963 }
964
965 #[test]
966 fn test_delete_word_backward_empty() {
967 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
968 prompt.input = "".to_string();
969 prompt.cursor_pos = 0;
970
971 prompt.delete_word_backward();
972 assert_eq!(prompt.input, "");
973 assert_eq!(prompt.cursor_pos, 0);
974 }
975
976 #[test]
977 fn test_delete_word_forward_only_spaces() {
978 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
979 prompt.input = " ".to_string();
980 prompt.cursor_pos = 0;
981
982 prompt.delete_word_forward();
983 assert_eq!(prompt.input, "");
984 assert_eq!(prompt.cursor_pos, 0);
985 }
986
987 #[test]
988 fn test_multiple_word_deletions() {
989 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
990 prompt.input = "one two three four".to_string();
991 prompt.cursor_pos = 18;
992
993 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
995
996 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
998
999 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1001 }
1002
1003 #[test]
1005 fn test_selection_with_shift_arrows() {
1006 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1007 prompt.input = "hello world".to_string();
1008 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1012 assert_eq!(prompt.selected_text(), None);
1013
1014 prompt.move_right_selecting();
1016 assert!(prompt.has_selection());
1017 assert_eq!(prompt.selection_range(), Some((5, 6)));
1018 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1019
1020 prompt.move_right_selecting();
1022 assert_eq!(prompt.selection_range(), Some((5, 7)));
1023 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1024
1025 prompt.move_left_selecting();
1027 assert_eq!(prompt.selection_range(), Some((5, 6)));
1028 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1029 }
1030
1031 #[test]
1032 fn test_selection_backward() {
1033 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1034 prompt.input = "abcdef".to_string();
1035 prompt.cursor_pos = 4; prompt.move_left_selecting();
1039 prompt.move_left_selecting();
1040 assert!(prompt.has_selection());
1041 assert_eq!(prompt.selection_range(), Some((2, 4)));
1042 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1043 }
1044
1045 #[test]
1046 fn test_selection_with_home_end() {
1047 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1048 prompt.input = "select this text".to_string();
1049 prompt.cursor_pos = 7; prompt.move_end_selecting();
1053 assert_eq!(prompt.selection_range(), Some((7, 16)));
1054 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1055
1056 prompt.clear_selection();
1058 prompt.move_home_selecting();
1059 assert_eq!(prompt.selection_range(), Some((0, 16)));
1060 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1061 }
1062
1063 #[test]
1064 fn test_word_selection() {
1065 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1066 prompt.input = "one two three".to_string();
1067 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1071 assert_eq!(prompt.selection_range(), Some((4, 7)));
1072 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1073
1074 prompt.move_word_right_selecting();
1076 assert_eq!(prompt.selection_range(), Some((4, 13)));
1077 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1078 }
1079
1080 #[test]
1081 fn test_word_selection_backward() {
1082 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1083 prompt.input = "one two three".to_string();
1084 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1088 assert_eq!(prompt.selection_range(), Some((8, 13)));
1089 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1090
1091 }
1096
1097 #[test]
1098 fn test_delete_selection() {
1099 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1100 prompt.input = "hello world".to_string();
1101 prompt.cursor_pos = 5;
1102
1103 prompt.move_end_selecting();
1105 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1106
1107 let deleted = prompt.delete_selection();
1109 assert_eq!(deleted, Some(" world".to_string()));
1110 assert_eq!(prompt.input, "hello");
1111 assert_eq!(prompt.cursor_pos, 5);
1112 assert!(!prompt.has_selection());
1113 }
1114
1115 #[test]
1116 fn test_insert_deletes_selection() {
1117 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1118 prompt.input = "hello world".to_string();
1119 prompt.cursor_pos = 0;
1120
1121 for _ in 0..5 {
1123 prompt.move_right_selecting();
1124 }
1125 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1126
1127 prompt.insert_str("goodbye");
1129 assert_eq!(prompt.input, "goodbye world");
1130 assert_eq!(prompt.cursor_pos, 7);
1131 assert!(!prompt.has_selection());
1132 }
1133
1134 #[test]
1135 fn test_clear_selection() {
1136 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1137 prompt.input = "test".to_string();
1138 prompt.cursor_pos = 0;
1139
1140 prompt.move_end_selecting();
1142 assert!(prompt.has_selection());
1143
1144 prompt.clear_selection();
1146 assert!(!prompt.has_selection());
1147 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1150
1151 #[test]
1152 fn test_selection_edge_cases() {
1153 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1154 prompt.input = "abc".to_string();
1155 prompt.cursor_pos = 3;
1156
1157 prompt.move_right_selecting();
1159 assert_eq!(prompt.cursor_pos, 3);
1160 assert_eq!(prompt.selection_range(), None);
1162 assert_eq!(prompt.selected_text(), None);
1163
1164 assert_eq!(prompt.delete_selection(), None);
1166 assert_eq!(prompt.input, "abc");
1167 }
1168
1169 #[test]
1170 fn test_selection_with_unicode() {
1171 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1172 prompt.input = "hello 世界 world".to_string();
1173 prompt.cursor_pos = 6; for _ in 0..2 {
1177 prompt.move_right_selecting();
1178 }
1179
1180 let selected = prompt.selected_text().unwrap();
1181 assert_eq!(selected, "世界");
1182
1183 prompt.delete_selection();
1185 assert_eq!(prompt.input, "hello world");
1186 }
1187
1188 #[test]
1192 fn test_word_selection_continues_across_words() {
1193 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1194 prompt.input = "one two three".to_string();
1195 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1199 assert_eq!(prompt.selection_range(), Some((8, 13)));
1200 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1201
1202 prompt.move_word_left_selecting();
1205
1206 assert_eq!(prompt.selection_range(), Some((4, 13)));
1208 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1209 }
1210
1211 #[cfg(test)]
1213 mod property_tests {
1214 use super::*;
1215 use proptest::prelude::*;
1216
1217 proptest! {
1218 #[test]
1220 fn prop_delete_word_backward_shrinks(
1221 input in "[a-zA-Z0-9_ ]{0,50}",
1222 cursor_pos in 0usize..50
1223 ) {
1224 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1225 prompt.input = input.clone();
1226 prompt.cursor_pos = cursor_pos.min(input.len());
1227
1228 let original_len = prompt.input.len();
1229 prompt.delete_word_backward();
1230
1231 prop_assert!(prompt.input.len() <= original_len);
1232 }
1233
1234 #[test]
1236 fn prop_delete_word_forward_shrinks(
1237 input in "[a-zA-Z0-9_ ]{0,50}",
1238 cursor_pos in 0usize..50
1239 ) {
1240 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1241 prompt.input = input.clone();
1242 prompt.cursor_pos = cursor_pos.min(input.len());
1243
1244 let original_len = prompt.input.len();
1245 prompt.delete_word_forward();
1246
1247 prop_assert!(prompt.input.len() <= original_len);
1248 }
1249
1250 #[test]
1252 fn prop_delete_word_backward_cursor_valid(
1253 input in "[a-zA-Z0-9_ ]{0,50}",
1254 cursor_pos in 0usize..50
1255 ) {
1256 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1257 prompt.input = input.clone();
1258 prompt.cursor_pos = cursor_pos.min(input.len());
1259
1260 prompt.delete_word_backward();
1261
1262 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1263 }
1264
1265 #[test]
1267 fn prop_delete_word_forward_cursor_valid(
1268 input in "[a-zA-Z0-9_ ]{0,50}",
1269 cursor_pos in 0usize..50
1270 ) {
1271 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1272 prompt.input = input.clone();
1273 prompt.cursor_pos = cursor_pos.min(input.len());
1274
1275 prompt.delete_word_forward();
1276
1277 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1278 }
1279
1280 #[test]
1282 fn prop_insert_str_length(
1283 input in "[a-zA-Z0-9_ ]{0,30}",
1284 insert in "[a-zA-Z0-9_ ]{0,20}",
1285 cursor_pos in 0usize..30
1286 ) {
1287 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1288 prompt.input = input.clone();
1289 prompt.cursor_pos = cursor_pos.min(input.len());
1290
1291 let original_len = prompt.input.len();
1292 prompt.insert_str(&insert);
1293
1294 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1295 }
1296
1297 #[test]
1299 fn prop_insert_str_cursor(
1300 input in "[a-zA-Z0-9_ ]{0,30}",
1301 insert in "[a-zA-Z0-9_ ]{0,20}",
1302 cursor_pos in 0usize..30
1303 ) {
1304 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1305 prompt.input = input.clone();
1306 let original_pos = cursor_pos.min(input.len());
1307 prompt.cursor_pos = original_pos;
1308
1309 prompt.insert_str(&insert);
1310
1311 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1312 }
1313
1314 #[test]
1316 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1317 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1318 prompt.input = input;
1319 prompt.cursor_pos = prompt.input.len();
1320
1321 prompt.clear();
1322
1323 prop_assert_eq!(prompt.input, "");
1324 prop_assert_eq!(prompt.cursor_pos, 0);
1325 }
1326 }
1327 }
1328}