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 pub footer: Vec<fresh_core::api::StyledText>,
241}
242
243pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
247
248impl Prompt {
249 pub fn new(message: String, prompt_type: PromptType) -> Self {
251 Self {
252 message,
253 input: String::new(),
254 cursor_pos: 0,
255 prompt_type,
256 suggestions: Vec::new(),
257 original_suggestions: None,
258 selected_suggestion: None,
259 scroll_offset: 0,
260 selection_anchor: None,
261 suggestions_set_for_input: None,
262 sync_input_on_navigate: false,
263 overlay: false,
264 title: Vec::new(),
265 footer: Vec::new(),
266 }
267 }
268
269 pub fn with_suggestions(
274 message: String,
275 prompt_type: PromptType,
276 suggestions: Vec<Suggestion>,
277 ) -> Self {
278 let selected_suggestion = if suggestions.is_empty() {
279 None
280 } else {
281 Some(0)
282 };
283 Self {
284 message,
285 input: String::new(),
286 cursor_pos: 0,
287 prompt_type,
288 original_suggestions: Some(suggestions.clone()),
289 suggestions,
290 selected_suggestion,
291 scroll_offset: 0,
292 selection_anchor: None,
293 suggestions_set_for_input: None,
294 sync_input_on_navigate: false,
295 overlay: false,
296 title: Vec::new(),
297 footer: Vec::new(),
298 }
299 }
300
301 pub fn with_initial_text_for_edit(
306 message: String,
307 prompt_type: PromptType,
308 initial_text: String,
309 ) -> Self {
310 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
311 }
312
313 pub fn with_initial_text(
315 message: String,
316 prompt_type: PromptType,
317 initial_text: String,
318 ) -> Self {
319 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
320 }
321
322 fn with_initial_text_inner(
323 message: String,
324 prompt_type: PromptType,
325 initial_text: String,
326 select_all: bool,
327 ) -> Self {
328 let cursor_pos = initial_text.len();
329 let selection_anchor = if select_all && !initial_text.is_empty() {
330 Some(0)
331 } else {
332 None
333 };
334 Self {
335 message,
336 input: initial_text,
337 cursor_pos,
338 prompt_type,
339 suggestions: Vec::new(),
340 original_suggestions: None,
341 selected_suggestion: None,
342 scroll_offset: 0,
343 selection_anchor,
344 suggestions_set_for_input: None,
345 sync_input_on_navigate: false,
346 overlay: false,
347 title: Vec::new(),
348 footer: Vec::new(),
349 }
350 }
351
352 pub fn cursor_left(&mut self) {
357 if self.cursor_pos > 0 {
358 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
359 }
360 }
361
362 pub fn cursor_right(&mut self) {
367 if self.cursor_pos < self.input.len() {
368 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
369 }
370 }
371
372 pub fn insert_char(&mut self, ch: char) {
374 self.input.insert(self.cursor_pos, ch);
375 self.cursor_pos += ch.len_utf8();
376 }
377
378 pub fn backspace(&mut self) {
384 if self.cursor_pos > 0 {
385 let prev_boundary = self.input[..self.cursor_pos]
388 .char_indices()
389 .next_back()
390 .map(|(i, _)| i)
391 .unwrap_or(0);
392 self.input.drain(prev_boundary..self.cursor_pos);
393 self.cursor_pos = prev_boundary;
394 }
395 }
396
397 pub fn delete(&mut self) {
401 if self.cursor_pos < self.input.len() {
402 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
403 self.input.drain(self.cursor_pos..next_boundary);
404 }
405 }
406
407 pub fn move_to_start(&mut self) {
409 self.cursor_pos = 0;
410 }
411
412 pub fn move_to_end(&mut self) {
414 self.cursor_pos = self.input.len();
415 }
416
417 pub fn set_input(&mut self, text: String) {
434 self.cursor_pos = text.len();
435 self.input = text;
436 self.clear_selection();
437 }
438
439 pub fn select_next_suggestion(&mut self) {
441 if !self.suggestions.is_empty() {
442 self.selected_suggestion = Some(match self.selected_suggestion {
443 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
444 Some(_) => 0, None => 0,
446 });
447 }
448 }
449
450 pub fn select_prev_suggestion(&mut self) {
452 if !self.suggestions.is_empty() {
453 self.selected_suggestion = Some(match self.selected_suggestion {
454 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
456 None => 0,
457 });
458 }
459 }
460
461 pub fn selected_value(&self) -> Option<String> {
463 self.selected_suggestion
464 .and_then(|idx| self.suggestions.get(idx))
465 .map(|s| s.get_value().to_string())
466 }
467
468 pub fn get_final_input(&self) -> String {
470 self.selected_value().unwrap_or_else(|| self.input.clone())
471 }
472
473 pub fn filter_suggestions(&mut self, match_description: bool) {
478 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
479
480 if let Some(ref set_for_input) = self.suggestions_set_for_input {
484 if set_for_input == &self.input {
485 return;
486 }
487 }
488 self.suggestions_set_for_input = None;
492
493 let Some(original) = &self.original_suggestions else {
494 return;
495 };
496
497 let input = &self.input;
498 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
499 .iter()
500 .filter_map(|s| {
501 let text_result = fuzzy_match(input, &s.text);
502 let desc_result = if match_description {
503 s.description
504 .as_ref()
505 .map(|d| fuzzy_match(input, d))
506 .unwrap_or_else(FuzzyMatch::no_match)
507 } else {
508 FuzzyMatch::no_match()
509 };
510 if text_result.matched || desc_result.matched {
511 Some((s.clone(), text_result.score.max(desc_result.score)))
512 } else {
513 None
514 }
515 })
516 .collect();
517
518 filtered.sort_by(|a, b| b.1.cmp(&a.1));
519 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
520 self.selected_suggestion = if self.suggestions.is_empty() {
521 None
522 } else {
523 Some(0)
524 };
525 self.scroll_offset = 0;
526 }
527
528 pub fn ensure_selected_visible(&mut self) {
543 self.ensure_selected_visible_within(MAX_VISIBLE_SUGGESTIONS);
544 }
545
546 pub fn ensure_selected_visible_within(&mut self, visible_count: usize) {
551 let total = self.suggestions.len();
552 let visible = total.min(visible_count.max(1));
553 let max_offset = total.saturating_sub(visible);
554 if visible == 0 {
555 self.scroll_offset = 0;
556 return;
557 }
558 if let Some(selected) = self.selected_suggestion {
559 if selected < self.scroll_offset {
560 self.scroll_offset = selected;
561 } else if selected >= self.scroll_offset + visible {
562 self.scroll_offset = selected + 1 - visible;
563 }
564 }
565 if self.scroll_offset > max_offset {
566 self.scroll_offset = max_offset;
567 }
568 }
569
570 pub fn delete_word_forward(&mut self) {
600 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
601 if word_end > self.cursor_pos {
602 self.input.drain(self.cursor_pos..word_end);
603 }
605 }
606
607 pub fn delete_word_backward(&mut self) {
623 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
624 if word_start < self.cursor_pos {
625 self.input.drain(word_start..self.cursor_pos);
626 self.cursor_pos = word_start;
627 }
628 }
629
630 pub fn delete_to_end(&mut self) {
645 if self.cursor_pos < self.input.len() {
646 self.input.truncate(self.cursor_pos);
647 }
648 }
649
650 pub fn get_text(&self) -> String {
663 self.input.clone()
664 }
665
666 pub fn clear(&mut self) {
681 self.input.clear();
682 self.cursor_pos = 0;
683 self.selected_suggestion = None;
685 }
686
687 pub fn insert_str(&mut self, text: &str) {
703 if self.has_selection() {
705 self.delete_selection();
706 }
707 self.input.insert_str(self.cursor_pos, text);
708 self.cursor_pos += text.len();
709 }
710
711 pub fn has_selection(&self) -> bool {
717 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
718 }
719
720 pub fn selection_range(&self) -> Option<(usize, usize)> {
722 if let Some(anchor) = self.selection_anchor {
723 if anchor != self.cursor_pos {
724 let start = anchor.min(self.cursor_pos);
725 let end = anchor.max(self.cursor_pos);
726 return Some((start, end));
727 }
728 }
729 None
730 }
731
732 pub fn selected_text(&self) -> Option<String> {
734 self.selection_range()
735 .map(|(start, end)| self.input[start..end].to_string())
736 }
737
738 pub fn delete_selection(&mut self) -> Option<String> {
740 if let Some((start, end)) = self.selection_range() {
741 let deleted = self.input[start..end].to_string();
742 self.input.drain(start..end);
743 self.cursor_pos = start;
744 self.selection_anchor = None;
745 Some(deleted)
746 } else {
747 None
748 }
749 }
750
751 pub fn clear_selection(&mut self) {
753 self.selection_anchor = None;
754 }
755
756 pub fn move_left_selecting(&mut self) {
758 if self.selection_anchor.is_none() {
760 self.selection_anchor = Some(self.cursor_pos);
761 }
762
763 if self.cursor_pos > 0 {
765 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
766 }
767 }
768
769 pub fn move_right_selecting(&mut self) {
771 if self.selection_anchor.is_none() {
773 self.selection_anchor = Some(self.cursor_pos);
774 }
775
776 if self.cursor_pos < self.input.len() {
778 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
779 }
780 }
781
782 pub fn move_home_selecting(&mut self) {
784 if self.selection_anchor.is_none() {
785 self.selection_anchor = Some(self.cursor_pos);
786 }
787 self.cursor_pos = 0;
788 }
789
790 pub fn move_end_selecting(&mut self) {
792 if self.selection_anchor.is_none() {
793 self.selection_anchor = Some(self.cursor_pos);
794 }
795 self.cursor_pos = self.input.len();
796 }
797
798 pub fn move_word_left_selecting(&mut self) {
801 if self.selection_anchor.is_none() {
802 self.selection_anchor = Some(self.cursor_pos);
803 }
804
805 let bytes = self.input.as_bytes();
806 if self.cursor_pos == 0 {
807 return;
808 }
809
810 let mut new_pos = self.cursor_pos.saturating_sub(1);
811
812 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
814 new_pos = new_pos.saturating_sub(1);
815 }
816
817 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
819 new_pos = new_pos.saturating_sub(1);
820 }
821
822 self.cursor_pos = new_pos;
823 }
824
825 pub fn move_word_right_selecting(&mut self) {
828 if self.selection_anchor.is_none() {
829 self.selection_anchor = Some(self.cursor_pos);
830 }
831
832 let bytes = self.input.as_bytes();
834 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
835
836 if new_pos == self.cursor_pos && new_pos < bytes.len() {
838 new_pos = (new_pos + 1).min(bytes.len());
839 new_pos = find_word_end_bytes(bytes, new_pos);
840 }
841
842 self.cursor_pos = new_pos;
843 }
844
845 pub fn move_word_left(&mut self) {
848 self.clear_selection();
849
850 let bytes = self.input.as_bytes();
851 if self.cursor_pos == 0 {
852 return;
853 }
854
855 let mut new_pos = self.cursor_pos.saturating_sub(1);
856
857 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
859 new_pos = new_pos.saturating_sub(1);
860 }
861
862 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
864 new_pos = new_pos.saturating_sub(1);
865 }
866
867 self.cursor_pos = new_pos;
868 }
869
870 pub fn move_word_right(&mut self) {
873 self.clear_selection();
874
875 let bytes = self.input.as_bytes();
876 if self.cursor_pos >= bytes.len() {
877 return;
878 }
879
880 let mut new_pos = self.cursor_pos;
881
882 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
884 new_pos += 1;
885 }
886
887 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
889 new_pos += 1;
890 }
891
892 self.cursor_pos = new_pos;
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899
900 #[test]
901 fn test_delete_word_forward_basic() {
902 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
903 prompt.input = "hello world test".to_string();
904 prompt.cursor_pos = 0;
905
906 prompt.delete_word_forward();
907 assert_eq!(prompt.input, " world test");
908 assert_eq!(prompt.cursor_pos, 0);
909 }
910
911 #[test]
912 fn test_delete_word_forward_middle() {
913 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
914 prompt.input = "hello world test".to_string();
915 prompt.cursor_pos = 3; prompt.delete_word_forward();
918 assert_eq!(prompt.input, "hel world test");
919 assert_eq!(prompt.cursor_pos, 3);
920 }
921
922 #[test]
923 fn test_delete_word_forward_at_space() {
924 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
925 prompt.input = "hello world".to_string();
926 prompt.cursor_pos = 5; prompt.delete_word_forward();
929 assert_eq!(prompt.input, "hello");
930 assert_eq!(prompt.cursor_pos, 5);
931 }
932
933 #[test]
934 fn test_delete_word_backward_basic() {
935 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
936 prompt.input = "hello world test".to_string();
937 prompt.cursor_pos = 5; prompt.delete_word_backward();
940 assert_eq!(prompt.input, " world test");
941 assert_eq!(prompt.cursor_pos, 0);
942 }
943
944 #[test]
945 fn test_delete_word_backward_middle() {
946 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
947 prompt.input = "hello world test".to_string();
948 prompt.cursor_pos = 8; prompt.delete_word_backward();
951 assert_eq!(prompt.input, "hello rld test");
952 assert_eq!(prompt.cursor_pos, 6);
953 }
954
955 #[test]
956 fn test_delete_word_backward_at_end() {
957 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
958 prompt.input = "hello world".to_string();
959 prompt.cursor_pos = 11; prompt.delete_word_backward();
962 assert_eq!(prompt.input, "hello ");
963 assert_eq!(prompt.cursor_pos, 6);
964 }
965
966 #[test]
967 fn test_delete_word_with_special_chars() {
968 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
969 prompt.input = "save-file-as".to_string();
970 prompt.cursor_pos = 12; prompt.delete_word_backward();
974 assert_eq!(prompt.input, "save-file-");
975 assert_eq!(prompt.cursor_pos, 10);
976
977 prompt.delete_word_backward();
979 assert_eq!(prompt.input, "save-");
980 assert_eq!(prompt.cursor_pos, 5);
981 }
982
983 #[test]
984 fn test_get_text() {
985 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
986 prompt.input = "test content".to_string();
987
988 assert_eq!(prompt.get_text(), "test content");
989 }
990
991 #[test]
992 fn test_clear() {
993 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
994 prompt.input = "some text".to_string();
995 prompt.cursor_pos = 5;
996 prompt.selected_suggestion = Some(0);
997
998 prompt.clear();
999
1000 assert_eq!(prompt.input, "");
1001 assert_eq!(prompt.cursor_pos, 0);
1002 assert_eq!(prompt.selected_suggestion, None);
1003 }
1004
1005 #[test]
1006 fn test_delete_forward_basic() {
1007 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1008 prompt.input = "hello".to_string();
1009 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1013
1014 assert_eq!(prompt.input, "hllo");
1015 assert_eq!(prompt.cursor_pos, 1);
1016 }
1017
1018 #[test]
1019 fn test_delete_at_end() {
1020 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1021 prompt.input = "hello".to_string();
1022 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
1026 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1027 }
1028
1029 assert_eq!(prompt.input, "hello");
1030 assert_eq!(prompt.cursor_pos, 5);
1031 }
1032
1033 #[test]
1034 fn test_insert_str_at_start() {
1035 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1036 prompt.input = "world".to_string();
1037 prompt.cursor_pos = 0;
1038
1039 prompt.insert_str("hello ");
1040 assert_eq!(prompt.input, "hello world");
1041 assert_eq!(prompt.cursor_pos, 6);
1042 }
1043
1044 #[test]
1045 fn test_insert_str_at_middle() {
1046 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1047 prompt.input = "helloworld".to_string();
1048 prompt.cursor_pos = 5;
1049
1050 prompt.insert_str(" ");
1051 assert_eq!(prompt.input, "hello world");
1052 assert_eq!(prompt.cursor_pos, 6);
1053 }
1054
1055 #[test]
1056 fn test_insert_str_at_end() {
1057 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1058 prompt.input = "hello".to_string();
1059 prompt.cursor_pos = 5;
1060
1061 prompt.insert_str(" world");
1062 assert_eq!(prompt.input, "hello world");
1063 assert_eq!(prompt.cursor_pos, 11);
1064 }
1065
1066 #[test]
1067 fn test_delete_word_forward_empty() {
1068 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1069 prompt.input = "".to_string();
1070 prompt.cursor_pos = 0;
1071
1072 prompt.delete_word_forward();
1073 assert_eq!(prompt.input, "");
1074 assert_eq!(prompt.cursor_pos, 0);
1075 }
1076
1077 #[test]
1078 fn test_delete_word_backward_empty() {
1079 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1080 prompt.input = "".to_string();
1081 prompt.cursor_pos = 0;
1082
1083 prompt.delete_word_backward();
1084 assert_eq!(prompt.input, "");
1085 assert_eq!(prompt.cursor_pos, 0);
1086 }
1087
1088 #[test]
1089 fn test_delete_word_forward_only_spaces() {
1090 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1091 prompt.input = " ".to_string();
1092 prompt.cursor_pos = 0;
1093
1094 prompt.delete_word_forward();
1095 assert_eq!(prompt.input, "");
1096 assert_eq!(prompt.cursor_pos, 0);
1097 }
1098
1099 #[test]
1100 fn test_multiple_word_deletions() {
1101 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1102 prompt.input = "one two three four".to_string();
1103 prompt.cursor_pos = 18;
1104
1105 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1107
1108 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1110
1111 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1113 }
1114
1115 #[test]
1117 fn test_selection_with_shift_arrows() {
1118 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1119 prompt.input = "hello world".to_string();
1120 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1124 assert_eq!(prompt.selected_text(), None);
1125
1126 prompt.move_right_selecting();
1128 assert!(prompt.has_selection());
1129 assert_eq!(prompt.selection_range(), Some((5, 6)));
1130 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1131
1132 prompt.move_right_selecting();
1134 assert_eq!(prompt.selection_range(), Some((5, 7)));
1135 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1136
1137 prompt.move_left_selecting();
1139 assert_eq!(prompt.selection_range(), Some((5, 6)));
1140 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1141 }
1142
1143 #[test]
1144 fn test_selection_backward() {
1145 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1146 prompt.input = "abcdef".to_string();
1147 prompt.cursor_pos = 4; prompt.move_left_selecting();
1151 prompt.move_left_selecting();
1152 assert!(prompt.has_selection());
1153 assert_eq!(prompt.selection_range(), Some((2, 4)));
1154 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1155 }
1156
1157 #[test]
1158 fn test_selection_with_home_end() {
1159 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1160 prompt.input = "select this text".to_string();
1161 prompt.cursor_pos = 7; prompt.move_end_selecting();
1165 assert_eq!(prompt.selection_range(), Some((7, 16)));
1166 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1167
1168 prompt.clear_selection();
1170 prompt.move_home_selecting();
1171 assert_eq!(prompt.selection_range(), Some((0, 16)));
1172 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1173 }
1174
1175 #[test]
1176 fn test_word_selection() {
1177 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1178 prompt.input = "one two three".to_string();
1179 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1183 assert_eq!(prompt.selection_range(), Some((4, 7)));
1184 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1185
1186 prompt.move_word_right_selecting();
1188 assert_eq!(prompt.selection_range(), Some((4, 13)));
1189 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1190 }
1191
1192 #[test]
1193 fn test_word_selection_backward() {
1194 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1195 prompt.input = "one two three".to_string();
1196 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1200 assert_eq!(prompt.selection_range(), Some((8, 13)));
1201 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1202
1203 }
1208
1209 #[test]
1210 fn test_delete_selection() {
1211 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1212 prompt.input = "hello world".to_string();
1213 prompt.cursor_pos = 5;
1214
1215 prompt.move_end_selecting();
1217 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1218
1219 let deleted = prompt.delete_selection();
1221 assert_eq!(deleted, Some(" world".to_string()));
1222 assert_eq!(prompt.input, "hello");
1223 assert_eq!(prompt.cursor_pos, 5);
1224 assert!(!prompt.has_selection());
1225 }
1226
1227 #[test]
1228 fn test_insert_deletes_selection() {
1229 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1230 prompt.input = "hello world".to_string();
1231 prompt.cursor_pos = 0;
1232
1233 for _ in 0..5 {
1235 prompt.move_right_selecting();
1236 }
1237 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1238
1239 prompt.insert_str("goodbye");
1241 assert_eq!(prompt.input, "goodbye world");
1242 assert_eq!(prompt.cursor_pos, 7);
1243 assert!(!prompt.has_selection());
1244 }
1245
1246 #[test]
1247 fn test_clear_selection() {
1248 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1249 prompt.input = "test".to_string();
1250 prompt.cursor_pos = 0;
1251
1252 prompt.move_end_selecting();
1254 assert!(prompt.has_selection());
1255
1256 prompt.clear_selection();
1258 assert!(!prompt.has_selection());
1259 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1262
1263 #[test]
1264 fn test_selection_edge_cases() {
1265 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1266 prompt.input = "abc".to_string();
1267 prompt.cursor_pos = 3;
1268
1269 prompt.move_right_selecting();
1271 assert_eq!(prompt.cursor_pos, 3);
1272 assert_eq!(prompt.selection_range(), None);
1274 assert_eq!(prompt.selected_text(), None);
1275
1276 assert_eq!(prompt.delete_selection(), None);
1278 assert_eq!(prompt.input, "abc");
1279 }
1280
1281 #[test]
1282 fn test_selection_with_unicode() {
1283 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1284 prompt.input = "hello 世界 world".to_string();
1285 prompt.cursor_pos = 6; for _ in 0..2 {
1289 prompt.move_right_selecting();
1290 }
1291
1292 let selected = prompt.selected_text().unwrap();
1293 assert_eq!(selected, "世界");
1294
1295 prompt.delete_selection();
1297 assert_eq!(prompt.input, "hello world");
1298 }
1299
1300 #[test]
1304 fn test_word_selection_continues_across_words() {
1305 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1306 prompt.input = "one two three".to_string();
1307 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1311 assert_eq!(prompt.selection_range(), Some((8, 13)));
1312 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1313
1314 prompt.move_word_left_selecting();
1317
1318 assert_eq!(prompt.selection_range(), Some((4, 13)));
1320 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1321 }
1322
1323 #[cfg(test)]
1325 mod property_tests {
1326 use super::*;
1327 use proptest::prelude::*;
1328
1329 proptest! {
1330 #[test]
1332 fn prop_delete_word_backward_shrinks(
1333 input in "[a-zA-Z0-9_ ]{0,50}",
1334 cursor_pos in 0usize..50
1335 ) {
1336 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1337 prompt.input = input.clone();
1338 prompt.cursor_pos = cursor_pos.min(input.len());
1339
1340 let original_len = prompt.input.len();
1341 prompt.delete_word_backward();
1342
1343 prop_assert!(prompt.input.len() <= original_len);
1344 }
1345
1346 #[test]
1348 fn prop_delete_word_forward_shrinks(
1349 input in "[a-zA-Z0-9_ ]{0,50}",
1350 cursor_pos in 0usize..50
1351 ) {
1352 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1353 prompt.input = input.clone();
1354 prompt.cursor_pos = cursor_pos.min(input.len());
1355
1356 let original_len = prompt.input.len();
1357 prompt.delete_word_forward();
1358
1359 prop_assert!(prompt.input.len() <= original_len);
1360 }
1361
1362 #[test]
1364 fn prop_delete_word_backward_cursor_valid(
1365 input in "[a-zA-Z0-9_ ]{0,50}",
1366 cursor_pos in 0usize..50
1367 ) {
1368 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1369 prompt.input = input.clone();
1370 prompt.cursor_pos = cursor_pos.min(input.len());
1371
1372 prompt.delete_word_backward();
1373
1374 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1375 }
1376
1377 #[test]
1379 fn prop_delete_word_forward_cursor_valid(
1380 input in "[a-zA-Z0-9_ ]{0,50}",
1381 cursor_pos in 0usize..50
1382 ) {
1383 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1384 prompt.input = input.clone();
1385 prompt.cursor_pos = cursor_pos.min(input.len());
1386
1387 prompt.delete_word_forward();
1388
1389 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1390 }
1391
1392 #[test]
1394 fn prop_insert_str_length(
1395 input in "[a-zA-Z0-9_ ]{0,30}",
1396 insert in "[a-zA-Z0-9_ ]{0,20}",
1397 cursor_pos in 0usize..30
1398 ) {
1399 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1400 prompt.input = input.clone();
1401 prompt.cursor_pos = cursor_pos.min(input.len());
1402
1403 let original_len = prompt.input.len();
1404 prompt.insert_str(&insert);
1405
1406 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1407 }
1408
1409 #[test]
1411 fn prop_insert_str_cursor(
1412 input in "[a-zA-Z0-9_ ]{0,30}",
1413 insert in "[a-zA-Z0-9_ ]{0,20}",
1414 cursor_pos in 0usize..30
1415 ) {
1416 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1417 prompt.input = input.clone();
1418 let original_pos = cursor_pos.min(input.len());
1419 prompt.cursor_pos = original_pos;
1420
1421 prompt.insert_str(&insert);
1422
1423 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1424 }
1425
1426 #[test]
1428 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1429 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1430 prompt.input = input;
1431 prompt.cursor_pos = prompt.input.len();
1432
1433 prompt.clear();
1434
1435 prop_assert_eq!(prompt.input, "");
1436 prop_assert_eq!(prompt.cursor_pos, 0);
1437 }
1438 }
1439 }
1440}