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 LiveGrep,
46 GotoLine,
48 GotoByteOffset,
50 GotoLineScanConfirm,
52 SetBackgroundFile,
54 SetBackgroundBlend,
56 Plugin { custom_type: String },
59 LspRename {
62 original_text: String,
63 start_pos: usize,
64 end_pos: usize,
65 overlay_handle: crate::view::overlay::OverlayHandle,
66 },
67 RecordMacro,
69 PlayMacro,
71 SetBookmark,
73 JumpToBookmark,
75 SetPageWidth,
77 AddRuler,
79 RemoveRuler,
81 SetTabSize,
83 SetLineEnding,
85 SetEncoding,
87 SetLanguage,
89 StopLspServer,
91 RestartLspServer,
93 SelectTheme { original_theme: String },
96 SelectKeybindingMap,
98 SelectCursorStyle,
100 SelectLocale,
102 CopyWithFormattingTheme,
104 ConfirmRevert,
106 ConfirmSaveConflict,
108 ConfirmSudoSave {
110 info: crate::model::buffer::SudoSaveRequired,
111 },
112 ConfirmOverwriteFile { path: std::path::PathBuf },
114 ConfirmCreateDirectory { path: std::path::PathBuf },
116 ConfirmCloseBuffer {
119 buffer_id: crate::model::event::BufferId,
120 },
121 ConfirmQuitWithModified,
123 FileExplorerRename {
126 original_path: std::path::PathBuf,
127 original_name: String,
128 is_new_file: bool,
131 },
132 ConfirmDeleteFile {
134 path: std::path::PathBuf,
135 is_dir: bool,
136 },
137 ConfirmPasteConflict {
139 src: std::path::PathBuf,
140 dst: std::path::PathBuf,
141 is_cut: bool,
142 },
143 FileExplorerPasteRename {
145 src: std::path::PathBuf,
146 dst_dir: std::path::PathBuf,
147 is_cut: bool,
148 },
149 ConfirmMultiDelete { paths: Vec<std::path::PathBuf> },
151 ConfirmMultiPasteConflict {
155 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
156 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
157 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
158 is_cut: bool,
159 },
160 ConfirmLargeFileEncoding { path: std::path::PathBuf },
163 SwitchToTab,
165 ShellCommand { replace: bool },
169 AsyncPrompt,
172}
173
174impl PromptType {
175 pub fn click_confirms(&self) -> bool {
182 !matches!(self, PromptType::ReloadWithEncoding)
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct Prompt {
189 pub message: String,
191 pub input: String,
193 pub cursor_pos: usize,
195 pub prompt_type: PromptType,
197 pub suggestions: Vec<Suggestion>,
199 pub original_suggestions: Option<Vec<Suggestion>>,
201 pub selected_suggestion: Option<usize>,
203 pub scroll_offset: usize,
208 pub selection_anchor: Option<usize>,
211 pub suggestions_set_for_input: Option<String>,
214 pub sync_input_on_navigate: bool,
217 pub overlay: bool,
224 pub title: Vec<fresh_core::api::StyledText>,
230}
231
232pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
236
237impl Prompt {
238 pub fn new(message: String, prompt_type: PromptType) -> Self {
240 Self {
241 message,
242 input: String::new(),
243 cursor_pos: 0,
244 prompt_type,
245 suggestions: Vec::new(),
246 original_suggestions: None,
247 selected_suggestion: None,
248 scroll_offset: 0,
249 selection_anchor: None,
250 suggestions_set_for_input: None,
251 sync_input_on_navigate: false,
252 overlay: false,
253 title: Vec::new(),
254 }
255 }
256
257 pub fn with_suggestions(
262 message: String,
263 prompt_type: PromptType,
264 suggestions: Vec<Suggestion>,
265 ) -> Self {
266 let selected_suggestion = if suggestions.is_empty() {
267 None
268 } else {
269 Some(0)
270 };
271 Self {
272 message,
273 input: String::new(),
274 cursor_pos: 0,
275 prompt_type,
276 original_suggestions: Some(suggestions.clone()),
277 suggestions,
278 selected_suggestion,
279 scroll_offset: 0,
280 selection_anchor: None,
281 suggestions_set_for_input: None,
282 sync_input_on_navigate: false,
283 overlay: false,
284 title: Vec::new(),
285 }
286 }
287
288 pub fn with_initial_text_for_edit(
293 message: String,
294 prompt_type: PromptType,
295 initial_text: String,
296 ) -> Self {
297 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
298 }
299
300 pub fn with_initial_text(
302 message: String,
303 prompt_type: PromptType,
304 initial_text: String,
305 ) -> Self {
306 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
307 }
308
309 fn with_initial_text_inner(
310 message: String,
311 prompt_type: PromptType,
312 initial_text: String,
313 select_all: bool,
314 ) -> Self {
315 let cursor_pos = initial_text.len();
316 let selection_anchor = if select_all && !initial_text.is_empty() {
317 Some(0)
318 } else {
319 None
320 };
321 Self {
322 message,
323 input: initial_text,
324 cursor_pos,
325 prompt_type,
326 suggestions: Vec::new(),
327 original_suggestions: None,
328 selected_suggestion: None,
329 scroll_offset: 0,
330 selection_anchor,
331 suggestions_set_for_input: None,
332 sync_input_on_navigate: false,
333 overlay: false,
334 title: Vec::new(),
335 }
336 }
337
338 pub fn cursor_left(&mut self) {
343 if self.cursor_pos > 0 {
344 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
345 }
346 }
347
348 pub fn cursor_right(&mut self) {
353 if self.cursor_pos < self.input.len() {
354 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
355 }
356 }
357
358 pub fn insert_char(&mut self, ch: char) {
360 self.input.insert(self.cursor_pos, ch);
361 self.cursor_pos += ch.len_utf8();
362 }
363
364 pub fn backspace(&mut self) {
370 if self.cursor_pos > 0 {
371 let prev_boundary = self.input[..self.cursor_pos]
374 .char_indices()
375 .next_back()
376 .map(|(i, _)| i)
377 .unwrap_or(0);
378 self.input.drain(prev_boundary..self.cursor_pos);
379 self.cursor_pos = prev_boundary;
380 }
381 }
382
383 pub fn delete(&mut self) {
387 if self.cursor_pos < self.input.len() {
388 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
389 self.input.drain(self.cursor_pos..next_boundary);
390 }
391 }
392
393 pub fn move_to_start(&mut self) {
395 self.cursor_pos = 0;
396 }
397
398 pub fn move_to_end(&mut self) {
400 self.cursor_pos = self.input.len();
401 }
402
403 pub fn set_input(&mut self, text: String) {
420 self.cursor_pos = text.len();
421 self.input = text;
422 self.clear_selection();
423 }
424
425 pub fn select_next_suggestion(&mut self) {
427 if !self.suggestions.is_empty() {
428 self.selected_suggestion = Some(match self.selected_suggestion {
429 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
430 Some(_) => 0, None => 0,
432 });
433 }
434 }
435
436 pub fn select_prev_suggestion(&mut self) {
438 if !self.suggestions.is_empty() {
439 self.selected_suggestion = Some(match self.selected_suggestion {
440 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
442 None => 0,
443 });
444 }
445 }
446
447 pub fn selected_value(&self) -> Option<String> {
449 self.selected_suggestion
450 .and_then(|idx| self.suggestions.get(idx))
451 .map(|s| s.get_value().to_string())
452 }
453
454 pub fn get_final_input(&self) -> String {
456 self.selected_value().unwrap_or_else(|| self.input.clone())
457 }
458
459 pub fn filter_suggestions(&mut self, match_description: bool) {
464 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
465
466 if let Some(ref set_for_input) = self.suggestions_set_for_input {
470 if set_for_input == &self.input {
471 return;
472 }
473 }
474
475 let Some(original) = &self.original_suggestions else {
476 return;
477 };
478
479 let input = &self.input;
480 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
481 .iter()
482 .filter_map(|s| {
483 let text_result = fuzzy_match(input, &s.text);
484 let desc_result = if match_description {
485 s.description
486 .as_ref()
487 .map(|d| fuzzy_match(input, d))
488 .unwrap_or_else(FuzzyMatch::no_match)
489 } else {
490 FuzzyMatch::no_match()
491 };
492 if text_result.matched || desc_result.matched {
493 Some((s.clone(), text_result.score.max(desc_result.score)))
494 } else {
495 None
496 }
497 })
498 .collect();
499
500 filtered.sort_by(|a, b| b.1.cmp(&a.1));
501 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
502 self.selected_suggestion = if self.suggestions.is_empty() {
503 None
504 } else {
505 Some(0)
506 };
507 self.scroll_offset = 0;
508 }
509
510 pub fn ensure_selected_visible(&mut self) {
525 self.ensure_selected_visible_within(MAX_VISIBLE_SUGGESTIONS);
526 }
527
528 pub fn ensure_selected_visible_within(&mut self, visible_count: usize) {
533 let total = self.suggestions.len();
534 let visible = total.min(visible_count.max(1));
535 let max_offset = total.saturating_sub(visible);
536 if visible == 0 {
537 self.scroll_offset = 0;
538 return;
539 }
540 if let Some(selected) = self.selected_suggestion {
541 if selected < self.scroll_offset {
542 self.scroll_offset = selected;
543 } else if selected >= self.scroll_offset + visible {
544 self.scroll_offset = selected + 1 - visible;
545 }
546 }
547 if self.scroll_offset > max_offset {
548 self.scroll_offset = max_offset;
549 }
550 }
551
552 pub fn delete_word_forward(&mut self) {
582 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
583 if word_end > self.cursor_pos {
584 self.input.drain(self.cursor_pos..word_end);
585 }
587 }
588
589 pub fn delete_word_backward(&mut self) {
605 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
606 if word_start < self.cursor_pos {
607 self.input.drain(word_start..self.cursor_pos);
608 self.cursor_pos = word_start;
609 }
610 }
611
612 pub fn delete_to_end(&mut self) {
627 if self.cursor_pos < self.input.len() {
628 self.input.truncate(self.cursor_pos);
629 }
630 }
631
632 pub fn get_text(&self) -> String {
645 self.input.clone()
646 }
647
648 pub fn clear(&mut self) {
663 self.input.clear();
664 self.cursor_pos = 0;
665 self.selected_suggestion = None;
667 }
668
669 pub fn insert_str(&mut self, text: &str) {
685 if self.has_selection() {
687 self.delete_selection();
688 }
689 self.input.insert_str(self.cursor_pos, text);
690 self.cursor_pos += text.len();
691 }
692
693 pub fn has_selection(&self) -> bool {
699 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
700 }
701
702 pub fn selection_range(&self) -> Option<(usize, usize)> {
704 if let Some(anchor) = self.selection_anchor {
705 if anchor != self.cursor_pos {
706 let start = anchor.min(self.cursor_pos);
707 let end = anchor.max(self.cursor_pos);
708 return Some((start, end));
709 }
710 }
711 None
712 }
713
714 pub fn selected_text(&self) -> Option<String> {
716 self.selection_range()
717 .map(|(start, end)| self.input[start..end].to_string())
718 }
719
720 pub fn delete_selection(&mut self) -> Option<String> {
722 if let Some((start, end)) = self.selection_range() {
723 let deleted = self.input[start..end].to_string();
724 self.input.drain(start..end);
725 self.cursor_pos = start;
726 self.selection_anchor = None;
727 Some(deleted)
728 } else {
729 None
730 }
731 }
732
733 pub fn clear_selection(&mut self) {
735 self.selection_anchor = None;
736 }
737
738 pub fn move_left_selecting(&mut self) {
740 if self.selection_anchor.is_none() {
742 self.selection_anchor = Some(self.cursor_pos);
743 }
744
745 if self.cursor_pos > 0 {
747 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
748 }
749 }
750
751 pub fn move_right_selecting(&mut self) {
753 if self.selection_anchor.is_none() {
755 self.selection_anchor = Some(self.cursor_pos);
756 }
757
758 if self.cursor_pos < self.input.len() {
760 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
761 }
762 }
763
764 pub fn move_home_selecting(&mut self) {
766 if self.selection_anchor.is_none() {
767 self.selection_anchor = Some(self.cursor_pos);
768 }
769 self.cursor_pos = 0;
770 }
771
772 pub fn move_end_selecting(&mut self) {
774 if self.selection_anchor.is_none() {
775 self.selection_anchor = Some(self.cursor_pos);
776 }
777 self.cursor_pos = self.input.len();
778 }
779
780 pub fn move_word_left_selecting(&mut self) {
783 if self.selection_anchor.is_none() {
784 self.selection_anchor = Some(self.cursor_pos);
785 }
786
787 let bytes = self.input.as_bytes();
788 if self.cursor_pos == 0 {
789 return;
790 }
791
792 let mut new_pos = self.cursor_pos.saturating_sub(1);
793
794 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
796 new_pos = new_pos.saturating_sub(1);
797 }
798
799 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
801 new_pos = new_pos.saturating_sub(1);
802 }
803
804 self.cursor_pos = new_pos;
805 }
806
807 pub fn move_word_right_selecting(&mut self) {
810 if self.selection_anchor.is_none() {
811 self.selection_anchor = Some(self.cursor_pos);
812 }
813
814 let bytes = self.input.as_bytes();
816 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
817
818 if new_pos == self.cursor_pos && new_pos < bytes.len() {
820 new_pos = (new_pos + 1).min(bytes.len());
821 new_pos = find_word_end_bytes(bytes, new_pos);
822 }
823
824 self.cursor_pos = new_pos;
825 }
826
827 pub fn move_word_left(&mut self) {
830 self.clear_selection();
831
832 let bytes = self.input.as_bytes();
833 if self.cursor_pos == 0 {
834 return;
835 }
836
837 let mut new_pos = self.cursor_pos.saturating_sub(1);
838
839 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
841 new_pos = new_pos.saturating_sub(1);
842 }
843
844 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
846 new_pos = new_pos.saturating_sub(1);
847 }
848
849 self.cursor_pos = new_pos;
850 }
851
852 pub fn move_word_right(&mut self) {
855 self.clear_selection();
856
857 let bytes = self.input.as_bytes();
858 if self.cursor_pos >= bytes.len() {
859 return;
860 }
861
862 let mut new_pos = self.cursor_pos;
863
864 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
866 new_pos += 1;
867 }
868
869 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
871 new_pos += 1;
872 }
873
874 self.cursor_pos = new_pos;
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 #[test]
883 fn test_delete_word_forward_basic() {
884 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
885 prompt.input = "hello world test".to_string();
886 prompt.cursor_pos = 0;
887
888 prompt.delete_word_forward();
889 assert_eq!(prompt.input, " world test");
890 assert_eq!(prompt.cursor_pos, 0);
891 }
892
893 #[test]
894 fn test_delete_word_forward_middle() {
895 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
896 prompt.input = "hello world test".to_string();
897 prompt.cursor_pos = 3; prompt.delete_word_forward();
900 assert_eq!(prompt.input, "hel world test");
901 assert_eq!(prompt.cursor_pos, 3);
902 }
903
904 #[test]
905 fn test_delete_word_forward_at_space() {
906 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
907 prompt.input = "hello world".to_string();
908 prompt.cursor_pos = 5; prompt.delete_word_forward();
911 assert_eq!(prompt.input, "hello");
912 assert_eq!(prompt.cursor_pos, 5);
913 }
914
915 #[test]
916 fn test_delete_word_backward_basic() {
917 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
918 prompt.input = "hello world test".to_string();
919 prompt.cursor_pos = 5; prompt.delete_word_backward();
922 assert_eq!(prompt.input, " world test");
923 assert_eq!(prompt.cursor_pos, 0);
924 }
925
926 #[test]
927 fn test_delete_word_backward_middle() {
928 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
929 prompt.input = "hello world test".to_string();
930 prompt.cursor_pos = 8; prompt.delete_word_backward();
933 assert_eq!(prompt.input, "hello rld test");
934 assert_eq!(prompt.cursor_pos, 6);
935 }
936
937 #[test]
938 fn test_delete_word_backward_at_end() {
939 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
940 prompt.input = "hello world".to_string();
941 prompt.cursor_pos = 11; prompt.delete_word_backward();
944 assert_eq!(prompt.input, "hello ");
945 assert_eq!(prompt.cursor_pos, 6);
946 }
947
948 #[test]
949 fn test_delete_word_with_special_chars() {
950 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
951 prompt.input = "save-file-as".to_string();
952 prompt.cursor_pos = 12; prompt.delete_word_backward();
956 assert_eq!(prompt.input, "save-file-");
957 assert_eq!(prompt.cursor_pos, 10);
958
959 prompt.delete_word_backward();
961 assert_eq!(prompt.input, "save-");
962 assert_eq!(prompt.cursor_pos, 5);
963 }
964
965 #[test]
966 fn test_get_text() {
967 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
968 prompt.input = "test content".to_string();
969
970 assert_eq!(prompt.get_text(), "test content");
971 }
972
973 #[test]
974 fn test_clear() {
975 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
976 prompt.input = "some text".to_string();
977 prompt.cursor_pos = 5;
978 prompt.selected_suggestion = Some(0);
979
980 prompt.clear();
981
982 assert_eq!(prompt.input, "");
983 assert_eq!(prompt.cursor_pos, 0);
984 assert_eq!(prompt.selected_suggestion, None);
985 }
986
987 #[test]
988 fn test_delete_forward_basic() {
989 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
990 prompt.input = "hello".to_string();
991 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
995
996 assert_eq!(prompt.input, "hllo");
997 assert_eq!(prompt.cursor_pos, 1);
998 }
999
1000 #[test]
1001 fn test_delete_at_end() {
1002 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1003 prompt.input = "hello".to_string();
1004 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
1008 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1009 }
1010
1011 assert_eq!(prompt.input, "hello");
1012 assert_eq!(prompt.cursor_pos, 5);
1013 }
1014
1015 #[test]
1016 fn test_insert_str_at_start() {
1017 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1018 prompt.input = "world".to_string();
1019 prompt.cursor_pos = 0;
1020
1021 prompt.insert_str("hello ");
1022 assert_eq!(prompt.input, "hello world");
1023 assert_eq!(prompt.cursor_pos, 6);
1024 }
1025
1026 #[test]
1027 fn test_insert_str_at_middle() {
1028 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1029 prompt.input = "helloworld".to_string();
1030 prompt.cursor_pos = 5;
1031
1032 prompt.insert_str(" ");
1033 assert_eq!(prompt.input, "hello world");
1034 assert_eq!(prompt.cursor_pos, 6);
1035 }
1036
1037 #[test]
1038 fn test_insert_str_at_end() {
1039 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1040 prompt.input = "hello".to_string();
1041 prompt.cursor_pos = 5;
1042
1043 prompt.insert_str(" world");
1044 assert_eq!(prompt.input, "hello world");
1045 assert_eq!(prompt.cursor_pos, 11);
1046 }
1047
1048 #[test]
1049 fn test_delete_word_forward_empty() {
1050 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1051 prompt.input = "".to_string();
1052 prompt.cursor_pos = 0;
1053
1054 prompt.delete_word_forward();
1055 assert_eq!(prompt.input, "");
1056 assert_eq!(prompt.cursor_pos, 0);
1057 }
1058
1059 #[test]
1060 fn test_delete_word_backward_empty() {
1061 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1062 prompt.input = "".to_string();
1063 prompt.cursor_pos = 0;
1064
1065 prompt.delete_word_backward();
1066 assert_eq!(prompt.input, "");
1067 assert_eq!(prompt.cursor_pos, 0);
1068 }
1069
1070 #[test]
1071 fn test_delete_word_forward_only_spaces() {
1072 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1073 prompt.input = " ".to_string();
1074 prompt.cursor_pos = 0;
1075
1076 prompt.delete_word_forward();
1077 assert_eq!(prompt.input, "");
1078 assert_eq!(prompt.cursor_pos, 0);
1079 }
1080
1081 #[test]
1082 fn test_multiple_word_deletions() {
1083 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1084 prompt.input = "one two three four".to_string();
1085 prompt.cursor_pos = 18;
1086
1087 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1089
1090 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1092
1093 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1095 }
1096
1097 #[test]
1099 fn test_selection_with_shift_arrows() {
1100 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1101 prompt.input = "hello world".to_string();
1102 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1106 assert_eq!(prompt.selected_text(), None);
1107
1108 prompt.move_right_selecting();
1110 assert!(prompt.has_selection());
1111 assert_eq!(prompt.selection_range(), Some((5, 6)));
1112 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1113
1114 prompt.move_right_selecting();
1116 assert_eq!(prompt.selection_range(), Some((5, 7)));
1117 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1118
1119 prompt.move_left_selecting();
1121 assert_eq!(prompt.selection_range(), Some((5, 6)));
1122 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1123 }
1124
1125 #[test]
1126 fn test_selection_backward() {
1127 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1128 prompt.input = "abcdef".to_string();
1129 prompt.cursor_pos = 4; prompt.move_left_selecting();
1133 prompt.move_left_selecting();
1134 assert!(prompt.has_selection());
1135 assert_eq!(prompt.selection_range(), Some((2, 4)));
1136 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1137 }
1138
1139 #[test]
1140 fn test_selection_with_home_end() {
1141 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1142 prompt.input = "select this text".to_string();
1143 prompt.cursor_pos = 7; prompt.move_end_selecting();
1147 assert_eq!(prompt.selection_range(), Some((7, 16)));
1148 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1149
1150 prompt.clear_selection();
1152 prompt.move_home_selecting();
1153 assert_eq!(prompt.selection_range(), Some((0, 16)));
1154 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1155 }
1156
1157 #[test]
1158 fn test_word_selection() {
1159 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1160 prompt.input = "one two three".to_string();
1161 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1165 assert_eq!(prompt.selection_range(), Some((4, 7)));
1166 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1167
1168 prompt.move_word_right_selecting();
1170 assert_eq!(prompt.selection_range(), Some((4, 13)));
1171 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1172 }
1173
1174 #[test]
1175 fn test_word_selection_backward() {
1176 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1177 prompt.input = "one two three".to_string();
1178 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1182 assert_eq!(prompt.selection_range(), Some((8, 13)));
1183 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1184
1185 }
1190
1191 #[test]
1192 fn test_delete_selection() {
1193 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1194 prompt.input = "hello world".to_string();
1195 prompt.cursor_pos = 5;
1196
1197 prompt.move_end_selecting();
1199 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1200
1201 let deleted = prompt.delete_selection();
1203 assert_eq!(deleted, Some(" world".to_string()));
1204 assert_eq!(prompt.input, "hello");
1205 assert_eq!(prompt.cursor_pos, 5);
1206 assert!(!prompt.has_selection());
1207 }
1208
1209 #[test]
1210 fn test_insert_deletes_selection() {
1211 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1212 prompt.input = "hello world".to_string();
1213 prompt.cursor_pos = 0;
1214
1215 for _ in 0..5 {
1217 prompt.move_right_selecting();
1218 }
1219 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1220
1221 prompt.insert_str("goodbye");
1223 assert_eq!(prompt.input, "goodbye world");
1224 assert_eq!(prompt.cursor_pos, 7);
1225 assert!(!prompt.has_selection());
1226 }
1227
1228 #[test]
1229 fn test_clear_selection() {
1230 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1231 prompt.input = "test".to_string();
1232 prompt.cursor_pos = 0;
1233
1234 prompt.move_end_selecting();
1236 assert!(prompt.has_selection());
1237
1238 prompt.clear_selection();
1240 assert!(!prompt.has_selection());
1241 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1244
1245 #[test]
1246 fn test_selection_edge_cases() {
1247 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1248 prompt.input = "abc".to_string();
1249 prompt.cursor_pos = 3;
1250
1251 prompt.move_right_selecting();
1253 assert_eq!(prompt.cursor_pos, 3);
1254 assert_eq!(prompt.selection_range(), None);
1256 assert_eq!(prompt.selected_text(), None);
1257
1258 assert_eq!(prompt.delete_selection(), None);
1260 assert_eq!(prompt.input, "abc");
1261 }
1262
1263 #[test]
1264 fn test_selection_with_unicode() {
1265 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1266 prompt.input = "hello 世界 world".to_string();
1267 prompt.cursor_pos = 6; for _ in 0..2 {
1271 prompt.move_right_selecting();
1272 }
1273
1274 let selected = prompt.selected_text().unwrap();
1275 assert_eq!(selected, "世界");
1276
1277 prompt.delete_selection();
1279 assert_eq!(prompt.input, "hello world");
1280 }
1281
1282 #[test]
1286 fn test_word_selection_continues_across_words() {
1287 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1288 prompt.input = "one two three".to_string();
1289 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1293 assert_eq!(prompt.selection_range(), Some((8, 13)));
1294 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1295
1296 prompt.move_word_left_selecting();
1299
1300 assert_eq!(prompt.selection_range(), Some((4, 13)));
1302 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1303 }
1304
1305 #[cfg(test)]
1307 mod property_tests {
1308 use super::*;
1309 use proptest::prelude::*;
1310
1311 proptest! {
1312 #[test]
1314 fn prop_delete_word_backward_shrinks(
1315 input in "[a-zA-Z0-9_ ]{0,50}",
1316 cursor_pos in 0usize..50
1317 ) {
1318 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1319 prompt.input = input.clone();
1320 prompt.cursor_pos = cursor_pos.min(input.len());
1321
1322 let original_len = prompt.input.len();
1323 prompt.delete_word_backward();
1324
1325 prop_assert!(prompt.input.len() <= original_len);
1326 }
1327
1328 #[test]
1330 fn prop_delete_word_forward_shrinks(
1331 input in "[a-zA-Z0-9_ ]{0,50}",
1332 cursor_pos in 0usize..50
1333 ) {
1334 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1335 prompt.input = input.clone();
1336 prompt.cursor_pos = cursor_pos.min(input.len());
1337
1338 let original_len = prompt.input.len();
1339 prompt.delete_word_forward();
1340
1341 prop_assert!(prompt.input.len() <= original_len);
1342 }
1343
1344 #[test]
1346 fn prop_delete_word_backward_cursor_valid(
1347 input in "[a-zA-Z0-9_ ]{0,50}",
1348 cursor_pos in 0usize..50
1349 ) {
1350 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1351 prompt.input = input.clone();
1352 prompt.cursor_pos = cursor_pos.min(input.len());
1353
1354 prompt.delete_word_backward();
1355
1356 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1357 }
1358
1359 #[test]
1361 fn prop_delete_word_forward_cursor_valid(
1362 input in "[a-zA-Z0-9_ ]{0,50}",
1363 cursor_pos in 0usize..50
1364 ) {
1365 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1366 prompt.input = input.clone();
1367 prompt.cursor_pos = cursor_pos.min(input.len());
1368
1369 prompt.delete_word_forward();
1370
1371 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1372 }
1373
1374 #[test]
1376 fn prop_insert_str_length(
1377 input in "[a-zA-Z0-9_ ]{0,30}",
1378 insert in "[a-zA-Z0-9_ ]{0,20}",
1379 cursor_pos in 0usize..30
1380 ) {
1381 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1382 prompt.input = input.clone();
1383 prompt.cursor_pos = cursor_pos.min(input.len());
1384
1385 let original_len = prompt.input.len();
1386 prompt.insert_str(&insert);
1387
1388 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1389 }
1390
1391 #[test]
1393 fn prop_insert_str_cursor(
1394 input in "[a-zA-Z0-9_ ]{0,30}",
1395 insert in "[a-zA-Z0-9_ ]{0,20}",
1396 cursor_pos in 0usize..30
1397 ) {
1398 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1399 prompt.input = input.clone();
1400 let original_pos = cursor_pos.min(input.len());
1401 prompt.cursor_pos = original_pos;
1402
1403 prompt.insert_str(&insert);
1404
1405 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1406 }
1407
1408 #[test]
1410 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1411 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1412 prompt.input = input;
1413 prompt.cursor_pos = prompt.input.len();
1414
1415 prompt.clear();
1416
1417 prop_assert_eq!(prompt.input, "");
1418 prop_assert_eq!(prompt.cursor_pos, 0);
1419 }
1420 }
1421 }
1422}