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 Command,
38 QuickOpen,
41 GotoLine,
43 GotoByteOffset,
45 GotoLineScanConfirm,
47 SetBackgroundFile,
49 SetBackgroundBlend,
51 Plugin { custom_type: String },
54 LspRename {
57 original_text: String,
58 start_pos: usize,
59 end_pos: usize,
60 overlay_handle: crate::view::overlay::OverlayHandle,
61 },
62 RecordMacro,
64 PlayMacro,
66 SetBookmark,
68 JumpToBookmark,
70 SetComposeWidth,
72 AddRuler,
74 RemoveRuler,
76 SetTabSize,
78 SetLineEnding,
80 SetEncoding,
82 SetLanguage,
84 StopLspServer,
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 ConfirmCloseBuffer {
110 buffer_id: crate::model::event::BufferId,
111 },
112 ConfirmQuitWithModified,
114 FileExplorerRename {
117 original_path: std::path::PathBuf,
118 original_name: String,
119 is_new_file: bool,
122 },
123 ConfirmDeleteFile {
125 path: std::path::PathBuf,
126 is_dir: bool,
127 },
128 ConfirmLargeFileEncoding { path: std::path::PathBuf },
131 SwitchToTab,
133 ShellCommand { replace: bool },
137 AsyncPrompt,
140}
141
142#[derive(Debug, Clone)]
144pub struct Prompt {
145 pub message: String,
147 pub input: String,
149 pub cursor_pos: usize,
151 pub prompt_type: PromptType,
153 pub suggestions: Vec<Suggestion>,
155 pub original_suggestions: Option<Vec<Suggestion>>,
157 pub selected_suggestion: Option<usize>,
159 pub selection_anchor: Option<usize>,
162 pub suggestions_set_for_input: Option<String>,
165 pub sync_input_on_navigate: bool,
168}
169
170impl Prompt {
171 pub fn new(message: String, prompt_type: PromptType) -> Self {
173 Self {
174 message,
175 input: String::new(),
176 cursor_pos: 0,
177 prompt_type,
178 suggestions: Vec::new(),
179 original_suggestions: None,
180 selected_suggestion: None,
181 selection_anchor: None,
182 suggestions_set_for_input: None,
183 sync_input_on_navigate: false,
184 }
185 }
186
187 pub fn with_suggestions(
192 message: String,
193 prompt_type: PromptType,
194 suggestions: Vec<Suggestion>,
195 ) -> Self {
196 let selected_suggestion = if suggestions.is_empty() {
197 None
198 } else {
199 Some(0)
200 };
201 Self {
202 message,
203 input: String::new(),
204 cursor_pos: 0,
205 prompt_type,
206 original_suggestions: Some(suggestions.clone()),
207 suggestions,
208 selected_suggestion,
209 selection_anchor: None,
210 suggestions_set_for_input: None,
211 sync_input_on_navigate: false,
212 }
213 }
214
215 pub fn with_initial_text(
217 message: String,
218 prompt_type: PromptType,
219 initial_text: String,
220 ) -> Self {
221 let cursor_pos = initial_text.len();
222 let selection_anchor = if initial_text.is_empty() {
224 None
225 } else {
226 Some(0)
227 };
228 Self {
229 message,
230 input: initial_text,
231 cursor_pos,
232 prompt_type,
233 suggestions: Vec::new(),
234 original_suggestions: None,
235 selected_suggestion: None,
236 selection_anchor,
237 suggestions_set_for_input: None,
238 sync_input_on_navigate: false,
239 }
240 }
241
242 pub fn cursor_left(&mut self) {
247 if self.cursor_pos > 0 {
248 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
249 }
250 }
251
252 pub fn cursor_right(&mut self) {
257 if self.cursor_pos < self.input.len() {
258 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
259 }
260 }
261
262 pub fn insert_char(&mut self, ch: char) {
264 self.input.insert(self.cursor_pos, ch);
265 self.cursor_pos += ch.len_utf8();
266 }
267
268 pub fn backspace(&mut self) {
274 if self.cursor_pos > 0 {
275 let prev_boundary = self.input[..self.cursor_pos]
278 .char_indices()
279 .next_back()
280 .map(|(i, _)| i)
281 .unwrap_or(0);
282 self.input.drain(prev_boundary..self.cursor_pos);
283 self.cursor_pos = prev_boundary;
284 }
285 }
286
287 pub fn delete(&mut self) {
291 if self.cursor_pos < self.input.len() {
292 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
293 self.input.drain(self.cursor_pos..next_boundary);
294 }
295 }
296
297 pub fn move_to_start(&mut self) {
299 self.cursor_pos = 0;
300 }
301
302 pub fn move_to_end(&mut self) {
304 self.cursor_pos = self.input.len();
305 }
306
307 pub fn set_input(&mut self, text: String) {
324 self.cursor_pos = text.len();
325 self.input = text;
326 self.clear_selection();
327 }
328
329 pub fn select_next_suggestion(&mut self) {
331 if !self.suggestions.is_empty() {
332 self.selected_suggestion = Some(match self.selected_suggestion {
333 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
334 Some(_) => 0, None => 0,
336 });
337 }
338 }
339
340 pub fn select_prev_suggestion(&mut self) {
342 if !self.suggestions.is_empty() {
343 self.selected_suggestion = Some(match self.selected_suggestion {
344 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
346 None => 0,
347 });
348 }
349 }
350
351 pub fn selected_value(&self) -> Option<String> {
353 self.selected_suggestion
354 .and_then(|idx| self.suggestions.get(idx))
355 .map(|s| s.get_value().to_string())
356 }
357
358 pub fn get_final_input(&self) -> String {
360 self.selected_value().unwrap_or_else(|| self.input.clone())
361 }
362
363 pub fn filter_suggestions(&mut self, match_description: bool) {
368 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
369
370 if let Some(ref set_for_input) = self.suggestions_set_for_input {
374 if set_for_input == &self.input {
375 return;
376 }
377 }
378
379 let Some(original) = &self.original_suggestions else {
380 return;
381 };
382
383 let input = &self.input;
384 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
385 .iter()
386 .filter_map(|s| {
387 let text_result = fuzzy_match(input, &s.text);
388 let desc_result = if match_description {
389 s.description
390 .as_ref()
391 .map(|d| fuzzy_match(input, d))
392 .unwrap_or_else(FuzzyMatch::no_match)
393 } else {
394 FuzzyMatch::no_match()
395 };
396 if text_result.matched || desc_result.matched {
397 Some((s.clone(), text_result.score.max(desc_result.score)))
398 } else {
399 None
400 }
401 })
402 .collect();
403
404 filtered.sort_by(|a, b| b.1.cmp(&a.1));
405 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
406 self.selected_suggestion = if self.suggestions.is_empty() {
407 None
408 } else {
409 Some(0)
410 };
411 }
412
413 pub fn delete_word_forward(&mut self) {
443 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
444 if word_end > self.cursor_pos {
445 self.input.drain(self.cursor_pos..word_end);
446 }
448 }
449
450 pub fn delete_word_backward(&mut self) {
466 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
467 if word_start < self.cursor_pos {
468 self.input.drain(word_start..self.cursor_pos);
469 self.cursor_pos = word_start;
470 }
471 }
472
473 pub fn delete_to_end(&mut self) {
488 if self.cursor_pos < self.input.len() {
489 self.input.truncate(self.cursor_pos);
490 }
491 }
492
493 pub fn get_text(&self) -> String {
506 self.input.clone()
507 }
508
509 pub fn clear(&mut self) {
524 self.input.clear();
525 self.cursor_pos = 0;
526 self.selected_suggestion = None;
528 }
529
530 pub fn insert_str(&mut self, text: &str) {
546 if self.has_selection() {
548 self.delete_selection();
549 }
550 self.input.insert_str(self.cursor_pos, text);
551 self.cursor_pos += text.len();
552 }
553
554 pub fn has_selection(&self) -> bool {
560 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
561 }
562
563 pub fn selection_range(&self) -> Option<(usize, usize)> {
565 if let Some(anchor) = self.selection_anchor {
566 if anchor != self.cursor_pos {
567 let start = anchor.min(self.cursor_pos);
568 let end = anchor.max(self.cursor_pos);
569 return Some((start, end));
570 }
571 }
572 None
573 }
574
575 pub fn selected_text(&self) -> Option<String> {
577 self.selection_range()
578 .map(|(start, end)| self.input[start..end].to_string())
579 }
580
581 pub fn delete_selection(&mut self) -> Option<String> {
583 if let Some((start, end)) = self.selection_range() {
584 let deleted = self.input[start..end].to_string();
585 self.input.drain(start..end);
586 self.cursor_pos = start;
587 self.selection_anchor = None;
588 Some(deleted)
589 } else {
590 None
591 }
592 }
593
594 pub fn clear_selection(&mut self) {
596 self.selection_anchor = None;
597 }
598
599 pub fn move_left_selecting(&mut self) {
601 if self.selection_anchor.is_none() {
603 self.selection_anchor = Some(self.cursor_pos);
604 }
605
606 if self.cursor_pos > 0 {
608 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
609 }
610 }
611
612 pub fn move_right_selecting(&mut self) {
614 if self.selection_anchor.is_none() {
616 self.selection_anchor = Some(self.cursor_pos);
617 }
618
619 if self.cursor_pos < self.input.len() {
621 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
622 }
623 }
624
625 pub fn move_home_selecting(&mut self) {
627 if self.selection_anchor.is_none() {
628 self.selection_anchor = Some(self.cursor_pos);
629 }
630 self.cursor_pos = 0;
631 }
632
633 pub fn move_end_selecting(&mut self) {
635 if self.selection_anchor.is_none() {
636 self.selection_anchor = Some(self.cursor_pos);
637 }
638 self.cursor_pos = self.input.len();
639 }
640
641 pub fn move_word_left_selecting(&mut self) {
644 if self.selection_anchor.is_none() {
645 self.selection_anchor = Some(self.cursor_pos);
646 }
647
648 let bytes = self.input.as_bytes();
649 if self.cursor_pos == 0 {
650 return;
651 }
652
653 let mut new_pos = self.cursor_pos.saturating_sub(1);
654
655 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
657 new_pos = new_pos.saturating_sub(1);
658 }
659
660 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
662 new_pos = new_pos.saturating_sub(1);
663 }
664
665 self.cursor_pos = new_pos;
666 }
667
668 pub fn move_word_right_selecting(&mut self) {
671 if self.selection_anchor.is_none() {
672 self.selection_anchor = Some(self.cursor_pos);
673 }
674
675 let bytes = self.input.as_bytes();
677 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
678
679 if new_pos == self.cursor_pos && new_pos < bytes.len() {
681 new_pos = (new_pos + 1).min(bytes.len());
682 new_pos = find_word_end_bytes(bytes, new_pos);
683 }
684
685 self.cursor_pos = new_pos;
686 }
687
688 pub fn move_word_left(&mut self) {
691 self.clear_selection();
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(&mut self) {
716 self.clear_selection();
717
718 let bytes = self.input.as_bytes();
719 if self.cursor_pos >= bytes.len() {
720 return;
721 }
722
723 let mut new_pos = self.cursor_pos;
724
725 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
727 new_pos += 1;
728 }
729
730 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
732 new_pos += 1;
733 }
734
735 self.cursor_pos = new_pos;
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
744 fn test_delete_word_forward_basic() {
745 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
746 prompt.input = "hello world test".to_string();
747 prompt.cursor_pos = 0;
748
749 prompt.delete_word_forward();
750 assert_eq!(prompt.input, " world test");
751 assert_eq!(prompt.cursor_pos, 0);
752 }
753
754 #[test]
755 fn test_delete_word_forward_middle() {
756 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
757 prompt.input = "hello world test".to_string();
758 prompt.cursor_pos = 3; prompt.delete_word_forward();
761 assert_eq!(prompt.input, "hel world test");
762 assert_eq!(prompt.cursor_pos, 3);
763 }
764
765 #[test]
766 fn test_delete_word_forward_at_space() {
767 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
768 prompt.input = "hello world".to_string();
769 prompt.cursor_pos = 5; prompt.delete_word_forward();
772 assert_eq!(prompt.input, "hello");
773 assert_eq!(prompt.cursor_pos, 5);
774 }
775
776 #[test]
777 fn test_delete_word_backward_basic() {
778 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
779 prompt.input = "hello world test".to_string();
780 prompt.cursor_pos = 5; prompt.delete_word_backward();
783 assert_eq!(prompt.input, " world test");
784 assert_eq!(prompt.cursor_pos, 0);
785 }
786
787 #[test]
788 fn test_delete_word_backward_middle() {
789 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
790 prompt.input = "hello world test".to_string();
791 prompt.cursor_pos = 8; prompt.delete_word_backward();
794 assert_eq!(prompt.input, "hello rld test");
795 assert_eq!(prompt.cursor_pos, 6);
796 }
797
798 #[test]
799 fn test_delete_word_backward_at_end() {
800 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
801 prompt.input = "hello world".to_string();
802 prompt.cursor_pos = 11; prompt.delete_word_backward();
805 assert_eq!(prompt.input, "hello ");
806 assert_eq!(prompt.cursor_pos, 6);
807 }
808
809 #[test]
810 fn test_delete_word_with_special_chars() {
811 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
812 prompt.input = "save-file-as".to_string();
813 prompt.cursor_pos = 12; prompt.delete_word_backward();
817 assert_eq!(prompt.input, "save-file-");
818 assert_eq!(prompt.cursor_pos, 10);
819
820 prompt.delete_word_backward();
822 assert_eq!(prompt.input, "save-");
823 assert_eq!(prompt.cursor_pos, 5);
824 }
825
826 #[test]
827 fn test_get_text() {
828 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
829 prompt.input = "test content".to_string();
830
831 assert_eq!(prompt.get_text(), "test content");
832 }
833
834 #[test]
835 fn test_clear() {
836 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
837 prompt.input = "some text".to_string();
838 prompt.cursor_pos = 5;
839 prompt.selected_suggestion = Some(0);
840
841 prompt.clear();
842
843 assert_eq!(prompt.input, "");
844 assert_eq!(prompt.cursor_pos, 0);
845 assert_eq!(prompt.selected_suggestion, None);
846 }
847
848 #[test]
849 fn test_delete_forward_basic() {
850 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
851 prompt.input = "hello".to_string();
852 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
856
857 assert_eq!(prompt.input, "hllo");
858 assert_eq!(prompt.cursor_pos, 1);
859 }
860
861 #[test]
862 fn test_delete_at_end() {
863 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
864 prompt.input = "hello".to_string();
865 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
869 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
870 }
871
872 assert_eq!(prompt.input, "hello");
873 assert_eq!(prompt.cursor_pos, 5);
874 }
875
876 #[test]
877 fn test_insert_str_at_start() {
878 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
879 prompt.input = "world".to_string();
880 prompt.cursor_pos = 0;
881
882 prompt.insert_str("hello ");
883 assert_eq!(prompt.input, "hello world");
884 assert_eq!(prompt.cursor_pos, 6);
885 }
886
887 #[test]
888 fn test_insert_str_at_middle() {
889 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
890 prompt.input = "helloworld".to_string();
891 prompt.cursor_pos = 5;
892
893 prompt.insert_str(" ");
894 assert_eq!(prompt.input, "hello world");
895 assert_eq!(prompt.cursor_pos, 6);
896 }
897
898 #[test]
899 fn test_insert_str_at_end() {
900 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
901 prompt.input = "hello".to_string();
902 prompt.cursor_pos = 5;
903
904 prompt.insert_str(" world");
905 assert_eq!(prompt.input, "hello world");
906 assert_eq!(prompt.cursor_pos, 11);
907 }
908
909 #[test]
910 fn test_delete_word_forward_empty() {
911 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
912 prompt.input = "".to_string();
913 prompt.cursor_pos = 0;
914
915 prompt.delete_word_forward();
916 assert_eq!(prompt.input, "");
917 assert_eq!(prompt.cursor_pos, 0);
918 }
919
920 #[test]
921 fn test_delete_word_backward_empty() {
922 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
923 prompt.input = "".to_string();
924 prompt.cursor_pos = 0;
925
926 prompt.delete_word_backward();
927 assert_eq!(prompt.input, "");
928 assert_eq!(prompt.cursor_pos, 0);
929 }
930
931 #[test]
932 fn test_delete_word_forward_only_spaces() {
933 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
934 prompt.input = " ".to_string();
935 prompt.cursor_pos = 0;
936
937 prompt.delete_word_forward();
938 assert_eq!(prompt.input, "");
939 assert_eq!(prompt.cursor_pos, 0);
940 }
941
942 #[test]
943 fn test_multiple_word_deletions() {
944 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
945 prompt.input = "one two three four".to_string();
946 prompt.cursor_pos = 18;
947
948 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
950
951 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
953
954 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
956 }
957
958 #[test]
960 fn test_selection_with_shift_arrows() {
961 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::Command);
962 prompt.input = "hello world".to_string();
963 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
967 assert_eq!(prompt.selected_text(), None);
968
969 prompt.move_right_selecting();
971 assert!(prompt.has_selection());
972 assert_eq!(prompt.selection_range(), Some((5, 6)));
973 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
974
975 prompt.move_right_selecting();
977 assert_eq!(prompt.selection_range(), Some((5, 7)));
978 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
979
980 prompt.move_left_selecting();
982 assert_eq!(prompt.selection_range(), Some((5, 6)));
983 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
984 }
985
986 #[test]
987 fn test_selection_backward() {
988 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
989 prompt.input = "abcdef".to_string();
990 prompt.cursor_pos = 4; prompt.move_left_selecting();
994 prompt.move_left_selecting();
995 assert!(prompt.has_selection());
996 assert_eq!(prompt.selection_range(), Some((2, 4)));
997 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
998 }
999
1000 #[test]
1001 fn test_selection_with_home_end() {
1002 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::Command);
1003 prompt.input = "select this text".to_string();
1004 prompt.cursor_pos = 7; prompt.move_end_selecting();
1008 assert_eq!(prompt.selection_range(), Some((7, 16)));
1009 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1010
1011 prompt.clear_selection();
1013 prompt.move_home_selecting();
1014 assert_eq!(prompt.selection_range(), Some((0, 16)));
1015 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1016 }
1017
1018 #[test]
1019 fn test_word_selection() {
1020 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1021 prompt.input = "one two three".to_string();
1022 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1026 assert_eq!(prompt.selection_range(), Some((4, 7)));
1027 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1028
1029 prompt.move_word_right_selecting();
1031 assert_eq!(prompt.selection_range(), Some((4, 13)));
1032 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1033 }
1034
1035 #[test]
1036 fn test_word_selection_backward() {
1037 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1038 prompt.input = "one two three".to_string();
1039 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1043 assert_eq!(prompt.selection_range(), Some((8, 13)));
1044 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1045
1046 }
1051
1052 #[test]
1053 fn test_delete_selection() {
1054 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1055 prompt.input = "hello world".to_string();
1056 prompt.cursor_pos = 5;
1057
1058 prompt.move_end_selecting();
1060 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1061
1062 let deleted = prompt.delete_selection();
1064 assert_eq!(deleted, Some(" world".to_string()));
1065 assert_eq!(prompt.input, "hello");
1066 assert_eq!(prompt.cursor_pos, 5);
1067 assert!(!prompt.has_selection());
1068 }
1069
1070 #[test]
1071 fn test_insert_deletes_selection() {
1072 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1073 prompt.input = "hello world".to_string();
1074 prompt.cursor_pos = 0;
1075
1076 for _ in 0..5 {
1078 prompt.move_right_selecting();
1079 }
1080 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1081
1082 prompt.insert_str("goodbye");
1084 assert_eq!(prompt.input, "goodbye world");
1085 assert_eq!(prompt.cursor_pos, 7);
1086 assert!(!prompt.has_selection());
1087 }
1088
1089 #[test]
1090 fn test_clear_selection() {
1091 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1092 prompt.input = "test".to_string();
1093 prompt.cursor_pos = 0;
1094
1095 prompt.move_end_selecting();
1097 assert!(prompt.has_selection());
1098
1099 prompt.clear_selection();
1101 assert!(!prompt.has_selection());
1102 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1105
1106 #[test]
1107 fn test_selection_edge_cases() {
1108 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1109 prompt.input = "abc".to_string();
1110 prompt.cursor_pos = 3;
1111
1112 prompt.move_right_selecting();
1114 assert_eq!(prompt.cursor_pos, 3);
1115 assert_eq!(prompt.selection_range(), None);
1117 assert_eq!(prompt.selected_text(), None);
1118
1119 assert_eq!(prompt.delete_selection(), None);
1121 assert_eq!(prompt.input, "abc");
1122 }
1123
1124 #[test]
1125 fn test_selection_with_unicode() {
1126 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1127 prompt.input = "hello 世界 world".to_string();
1128 prompt.cursor_pos = 6; for _ in 0..2 {
1132 prompt.move_right_selecting();
1133 }
1134
1135 let selected = prompt.selected_text().unwrap();
1136 assert_eq!(selected, "世界");
1137
1138 prompt.delete_selection();
1140 assert_eq!(prompt.input, "hello world");
1141 }
1142
1143 #[test]
1147 fn test_word_selection_continues_across_words() {
1148 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1149 prompt.input = "one two three".to_string();
1150 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1154 assert_eq!(prompt.selection_range(), Some((8, 13)));
1155 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1156
1157 prompt.move_word_left_selecting();
1160
1161 assert_eq!(prompt.selection_range(), Some((4, 13)));
1163 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1164 }
1165
1166 #[cfg(test)]
1168 mod property_tests {
1169 use super::*;
1170 use proptest::prelude::*;
1171
1172 proptest! {
1173 #[test]
1175 fn prop_delete_word_backward_shrinks(
1176 input in "[a-zA-Z0-9_ ]{0,50}",
1177 cursor_pos in 0usize..50
1178 ) {
1179 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1180 prompt.input = input.clone();
1181 prompt.cursor_pos = cursor_pos.min(input.len());
1182
1183 let original_len = prompt.input.len();
1184 prompt.delete_word_backward();
1185
1186 prop_assert!(prompt.input.len() <= original_len);
1187 }
1188
1189 #[test]
1191 fn prop_delete_word_forward_shrinks(
1192 input in "[a-zA-Z0-9_ ]{0,50}",
1193 cursor_pos in 0usize..50
1194 ) {
1195 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1196 prompt.input = input.clone();
1197 prompt.cursor_pos = cursor_pos.min(input.len());
1198
1199 let original_len = prompt.input.len();
1200 prompt.delete_word_forward();
1201
1202 prop_assert!(prompt.input.len() <= original_len);
1203 }
1204
1205 #[test]
1207 fn prop_delete_word_backward_cursor_valid(
1208 input in "[a-zA-Z0-9_ ]{0,50}",
1209 cursor_pos in 0usize..50
1210 ) {
1211 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1212 prompt.input = input.clone();
1213 prompt.cursor_pos = cursor_pos.min(input.len());
1214
1215 prompt.delete_word_backward();
1216
1217 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1218 }
1219
1220 #[test]
1222 fn prop_delete_word_forward_cursor_valid(
1223 input in "[a-zA-Z0-9_ ]{0,50}",
1224 cursor_pos in 0usize..50
1225 ) {
1226 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1227 prompt.input = input.clone();
1228 prompt.cursor_pos = cursor_pos.min(input.len());
1229
1230 prompt.delete_word_forward();
1231
1232 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1233 }
1234
1235 #[test]
1237 fn prop_insert_str_length(
1238 input in "[a-zA-Z0-9_ ]{0,30}",
1239 insert in "[a-zA-Z0-9_ ]{0,20}",
1240 cursor_pos in 0usize..30
1241 ) {
1242 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1243 prompt.input = input.clone();
1244 prompt.cursor_pos = cursor_pos.min(input.len());
1245
1246 let original_len = prompt.input.len();
1247 prompt.insert_str(&insert);
1248
1249 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1250 }
1251
1252 #[test]
1254 fn prop_insert_str_cursor(
1255 input in "[a-zA-Z0-9_ ]{0,30}",
1256 insert in "[a-zA-Z0-9_ ]{0,20}",
1257 cursor_pos in 0usize..30
1258 ) {
1259 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1260 prompt.input = input.clone();
1261 let original_pos = cursor_pos.min(input.len());
1262 prompt.cursor_pos = original_pos;
1263
1264 prompt.insert_str(&insert);
1265
1266 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1267 }
1268
1269 #[test]
1271 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1272 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1273 prompt.input = input;
1274 prompt.cursor_pos = prompt.input.len();
1275
1276 prompt.clear();
1277
1278 prop_assert_eq!(prompt.input, "");
1279 prop_assert_eq!(prompt.cursor_pos, 0);
1280 }
1281 }
1282 }
1283}