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 ConfirmQuit,
127 FileExplorerRename {
130 original_path: std::path::PathBuf,
131 original_name: String,
132 is_new_file: bool,
135 },
136 ConfirmDeleteFile {
138 path: std::path::PathBuf,
139 is_dir: bool,
140 },
141 ConfirmPasteConflict {
143 src: std::path::PathBuf,
144 dst: std::path::PathBuf,
145 is_cut: bool,
146 },
147 FileExplorerPasteRename {
149 src: std::path::PathBuf,
150 dst_dir: std::path::PathBuf,
151 is_cut: bool,
152 },
153 ConfirmMultiDelete { paths: Vec<std::path::PathBuf> },
155 ConfirmMultiPasteConflict {
159 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
160 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
161 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
162 is_cut: bool,
163 },
164 ConfirmLargeFileEncoding { path: std::path::PathBuf },
167 SwitchToTab,
169 ShellCommand { replace: bool },
173 AsyncPrompt,
176}
177
178impl PromptType {
179 pub fn click_confirms(&self) -> bool {
186 !matches!(self, PromptType::ReloadWithEncoding)
187 }
188}
189
190#[derive(Debug, Clone)]
192pub struct Prompt {
193 pub message: String,
195 pub input: String,
197 pub cursor_pos: usize,
199 pub prompt_type: PromptType,
201 pub suggestions: Vec<Suggestion>,
203 pub original_suggestions: Option<Vec<Suggestion>>,
205 pub selected_suggestion: Option<usize>,
207 pub scroll_offset: usize,
212 pub selection_anchor: Option<usize>,
215 pub suggestions_set_for_input: Option<String>,
218 pub sync_input_on_navigate: bool,
221 pub overlay: bool,
228 pub title: Vec<fresh_core::api::StyledText>,
234 pub footer: Vec<fresh_core::api::StyledText>,
245}
246
247pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
251
252impl Prompt {
253 pub fn new(message: String, prompt_type: PromptType) -> Self {
255 Self {
256 message,
257 input: String::new(),
258 cursor_pos: 0,
259 prompt_type,
260 suggestions: Vec::new(),
261 original_suggestions: None,
262 selected_suggestion: None,
263 scroll_offset: 0,
264 selection_anchor: None,
265 suggestions_set_for_input: None,
266 sync_input_on_navigate: false,
267 overlay: false,
268 title: Vec::new(),
269 footer: Vec::new(),
270 }
271 }
272
273 pub fn with_suggestions(
278 message: String,
279 prompt_type: PromptType,
280 suggestions: Vec<Suggestion>,
281 ) -> Self {
282 let selected_suggestion = if suggestions.is_empty() {
283 None
284 } else {
285 Some(0)
286 };
287 Self {
288 message,
289 input: String::new(),
290 cursor_pos: 0,
291 prompt_type,
292 original_suggestions: Some(suggestions.clone()),
293 suggestions,
294 selected_suggestion,
295 scroll_offset: 0,
296 selection_anchor: None,
297 suggestions_set_for_input: None,
298 sync_input_on_navigate: false,
299 overlay: false,
300 title: Vec::new(),
301 footer: Vec::new(),
302 }
303 }
304
305 pub fn with_initial_text_for_edit(
310 message: String,
311 prompt_type: PromptType,
312 initial_text: String,
313 ) -> Self {
314 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
315 }
316
317 pub fn with_initial_text(
319 message: String,
320 prompt_type: PromptType,
321 initial_text: String,
322 ) -> Self {
323 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
324 }
325
326 fn with_initial_text_inner(
327 message: String,
328 prompt_type: PromptType,
329 initial_text: String,
330 select_all: bool,
331 ) -> Self {
332 let cursor_pos = initial_text.len();
333 let selection_anchor = if select_all && !initial_text.is_empty() {
334 Some(0)
335 } else {
336 None
337 };
338 Self {
339 message,
340 input: initial_text,
341 cursor_pos,
342 prompt_type,
343 suggestions: Vec::new(),
344 original_suggestions: None,
345 selected_suggestion: None,
346 scroll_offset: 0,
347 selection_anchor,
348 suggestions_set_for_input: None,
349 sync_input_on_navigate: false,
350 overlay: false,
351 title: Vec::new(),
352 footer: Vec::new(),
353 }
354 }
355
356 pub fn cursor_left(&mut self) {
361 if self.cursor_pos > 0 {
362 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
363 }
364 }
365
366 pub fn cursor_right(&mut self) {
371 if self.cursor_pos < self.input.len() {
372 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
373 }
374 }
375
376 pub fn insert_char(&mut self, ch: char) {
378 self.input.insert(self.cursor_pos, ch);
379 self.cursor_pos += ch.len_utf8();
380 }
381
382 pub fn backspace(&mut self) {
388 if self.cursor_pos > 0 {
389 let prev_boundary = self.input[..self.cursor_pos]
392 .char_indices()
393 .next_back()
394 .map(|(i, _)| i)
395 .unwrap_or(0);
396 self.input.drain(prev_boundary..self.cursor_pos);
397 self.cursor_pos = prev_boundary;
398 }
399 }
400
401 pub fn delete(&mut self) {
405 if self.cursor_pos < self.input.len() {
406 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
407 self.input.drain(self.cursor_pos..next_boundary);
408 }
409 }
410
411 pub fn move_to_start(&mut self) {
413 self.cursor_pos = 0;
414 }
415
416 pub fn move_to_end(&mut self) {
418 self.cursor_pos = self.input.len();
419 }
420
421 pub fn set_input(&mut self, text: String) {
438 self.cursor_pos = text.len();
439 self.input = text;
440 self.clear_selection();
441 }
442
443 pub fn select_next_suggestion(&mut self) {
445 if !self.suggestions.is_empty() {
446 self.selected_suggestion = Some(match self.selected_suggestion {
447 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
448 Some(_) => 0, None => 0,
450 });
451 }
452 }
453
454 pub fn select_prev_suggestion(&mut self) {
456 if !self.suggestions.is_empty() {
457 self.selected_suggestion = Some(match self.selected_suggestion {
458 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
460 None => 0,
461 });
462 }
463 }
464
465 pub fn selected_value(&self) -> Option<String> {
467 self.selected_suggestion
468 .and_then(|idx| self.suggestions.get(idx))
469 .map(|s| s.get_value().to_string())
470 }
471
472 pub fn get_final_input(&self) -> String {
474 self.selected_value().unwrap_or_else(|| self.input.clone())
475 }
476
477 pub fn filter_suggestions(&mut self, match_description: bool) {
482 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
483
484 if let Some(ref set_for_input) = self.suggestions_set_for_input {
488 if set_for_input == &self.input {
489 return;
490 }
491 }
492 self.suggestions_set_for_input = None;
496
497 let Some(original) = &self.original_suggestions else {
498 return;
499 };
500
501 let input = &self.input;
502 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
503 .iter()
504 .filter_map(|s| {
505 let text_result = fuzzy_match(input, &s.text);
506 let desc_result = if match_description {
507 s.description
508 .as_ref()
509 .map(|d| fuzzy_match(input, d))
510 .unwrap_or_else(FuzzyMatch::no_match)
511 } else {
512 FuzzyMatch::no_match()
513 };
514 if text_result.matched || desc_result.matched {
515 Some((s.clone(), text_result.score.max(desc_result.score)))
516 } else {
517 None
518 }
519 })
520 .collect();
521
522 filtered.sort_by(|a, b| b.1.cmp(&a.1));
523 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
524 self.selected_suggestion = if self.suggestions.is_empty() {
525 None
526 } else {
527 Some(0)
528 };
529 self.scroll_offset = 0;
530 }
531
532 pub fn ensure_selected_visible(&mut self) {
547 self.ensure_selected_visible_within(MAX_VISIBLE_SUGGESTIONS);
548 }
549
550 pub fn ensure_selected_visible_within(&mut self, visible_count: usize) {
555 let total = self.suggestions.len();
556 let visible = total.min(visible_count.max(1));
557 let max_offset = total.saturating_sub(visible);
558 if visible == 0 {
559 self.scroll_offset = 0;
560 return;
561 }
562 if let Some(selected) = self.selected_suggestion {
563 if selected < self.scroll_offset {
564 self.scroll_offset = selected;
565 } else if selected >= self.scroll_offset + visible {
566 self.scroll_offset = selected + 1 - visible;
567 }
568 }
569 if self.scroll_offset > max_offset {
570 self.scroll_offset = max_offset;
571 }
572 }
573
574 pub fn delete_word_forward(&mut self) {
604 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
605 if word_end > self.cursor_pos {
606 self.input.drain(self.cursor_pos..word_end);
607 }
609 }
610
611 pub fn delete_word_backward(&mut self) {
627 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
628 if word_start < self.cursor_pos {
629 self.input.drain(word_start..self.cursor_pos);
630 self.cursor_pos = word_start;
631 }
632 }
633
634 pub fn delete_to_end(&mut self) {
649 if self.cursor_pos < self.input.len() {
650 self.input.truncate(self.cursor_pos);
651 }
652 }
653
654 pub fn get_text(&self) -> String {
667 self.input.clone()
668 }
669
670 pub fn clear(&mut self) {
685 self.input.clear();
686 self.cursor_pos = 0;
687 self.selected_suggestion = None;
689 }
690
691 pub fn insert_str(&mut self, text: &str) {
707 if self.has_selection() {
709 self.delete_selection();
710 }
711 self.input.insert_str(self.cursor_pos, text);
712 self.cursor_pos += text.len();
713 }
714
715 pub fn has_selection(&self) -> bool {
721 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
722 }
723
724 pub fn selection_range(&self) -> Option<(usize, usize)> {
726 if let Some(anchor) = self.selection_anchor {
727 if anchor != self.cursor_pos {
728 let start = anchor.min(self.cursor_pos);
729 let end = anchor.max(self.cursor_pos);
730 return Some((start, end));
731 }
732 }
733 None
734 }
735
736 pub fn selected_text(&self) -> Option<String> {
738 self.selection_range()
739 .map(|(start, end)| self.input[start..end].to_string())
740 }
741
742 pub fn delete_selection(&mut self) -> Option<String> {
744 if let Some((start, end)) = self.selection_range() {
745 let deleted = self.input[start..end].to_string();
746 self.input.drain(start..end);
747 self.cursor_pos = start;
748 self.selection_anchor = None;
749 Some(deleted)
750 } else {
751 None
752 }
753 }
754
755 pub fn clear_selection(&mut self) {
757 self.selection_anchor = None;
758 }
759
760 pub fn move_left_selecting(&mut self) {
762 if self.selection_anchor.is_none() {
764 self.selection_anchor = Some(self.cursor_pos);
765 }
766
767 if self.cursor_pos > 0 {
769 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
770 }
771 }
772
773 pub fn move_right_selecting(&mut self) {
775 if self.selection_anchor.is_none() {
777 self.selection_anchor = Some(self.cursor_pos);
778 }
779
780 if self.cursor_pos < self.input.len() {
782 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
783 }
784 }
785
786 pub fn move_home_selecting(&mut self) {
788 if self.selection_anchor.is_none() {
789 self.selection_anchor = Some(self.cursor_pos);
790 }
791 self.cursor_pos = 0;
792 }
793
794 pub fn move_end_selecting(&mut self) {
796 if self.selection_anchor.is_none() {
797 self.selection_anchor = Some(self.cursor_pos);
798 }
799 self.cursor_pos = self.input.len();
800 }
801
802 pub fn move_word_left_selecting(&mut self) {
805 if self.selection_anchor.is_none() {
806 self.selection_anchor = Some(self.cursor_pos);
807 }
808
809 let bytes = self.input.as_bytes();
810 if self.cursor_pos == 0 {
811 return;
812 }
813
814 let mut new_pos = self.cursor_pos.saturating_sub(1);
815
816 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
818 new_pos = new_pos.saturating_sub(1);
819 }
820
821 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
823 new_pos = new_pos.saturating_sub(1);
824 }
825
826 self.cursor_pos = new_pos;
827 }
828
829 pub fn move_word_right_selecting(&mut self) {
832 if self.selection_anchor.is_none() {
833 self.selection_anchor = Some(self.cursor_pos);
834 }
835
836 let bytes = self.input.as_bytes();
838 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
839
840 if new_pos == self.cursor_pos && new_pos < bytes.len() {
842 new_pos = (new_pos + 1).min(bytes.len());
843 new_pos = find_word_end_bytes(bytes, new_pos);
844 }
845
846 self.cursor_pos = new_pos;
847 }
848
849 pub fn move_word_left(&mut self) {
852 self.clear_selection();
853
854 let bytes = self.input.as_bytes();
855 if self.cursor_pos == 0 {
856 return;
857 }
858
859 let mut new_pos = self.cursor_pos.saturating_sub(1);
860
861 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
863 new_pos = new_pos.saturating_sub(1);
864 }
865
866 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
868 new_pos = new_pos.saturating_sub(1);
869 }
870
871 self.cursor_pos = new_pos;
872 }
873
874 pub fn move_word_right(&mut self) {
877 self.clear_selection();
878
879 let bytes = self.input.as_bytes();
880 if self.cursor_pos >= bytes.len() {
881 return;
882 }
883
884 let mut new_pos = self.cursor_pos;
885
886 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
888 new_pos += 1;
889 }
890
891 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
893 new_pos += 1;
894 }
895
896 self.cursor_pos = new_pos;
897 }
898}
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903
904 #[test]
905 fn test_delete_word_forward_basic() {
906 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
907 prompt.input = "hello world test".to_string();
908 prompt.cursor_pos = 0;
909
910 prompt.delete_word_forward();
911 assert_eq!(prompt.input, " world test");
912 assert_eq!(prompt.cursor_pos, 0);
913 }
914
915 #[test]
916 fn test_delete_word_forward_middle() {
917 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
918 prompt.input = "hello world test".to_string();
919 prompt.cursor_pos = 3; prompt.delete_word_forward();
922 assert_eq!(prompt.input, "hel world test");
923 assert_eq!(prompt.cursor_pos, 3);
924 }
925
926 #[test]
927 fn test_delete_word_forward_at_space() {
928 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
929 prompt.input = "hello world".to_string();
930 prompt.cursor_pos = 5; prompt.delete_word_forward();
933 assert_eq!(prompt.input, "hello");
934 assert_eq!(prompt.cursor_pos, 5);
935 }
936
937 #[test]
938 fn test_delete_word_backward_basic() {
939 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
940 prompt.input = "hello world test".to_string();
941 prompt.cursor_pos = 5; prompt.delete_word_backward();
944 assert_eq!(prompt.input, " world test");
945 assert_eq!(prompt.cursor_pos, 0);
946 }
947
948 #[test]
949 fn test_delete_word_backward_middle() {
950 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
951 prompt.input = "hello world test".to_string();
952 prompt.cursor_pos = 8; prompt.delete_word_backward();
955 assert_eq!(prompt.input, "hello rld test");
956 assert_eq!(prompt.cursor_pos, 6);
957 }
958
959 #[test]
960 fn test_delete_word_backward_at_end() {
961 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
962 prompt.input = "hello world".to_string();
963 prompt.cursor_pos = 11; prompt.delete_word_backward();
966 assert_eq!(prompt.input, "hello ");
967 assert_eq!(prompt.cursor_pos, 6);
968 }
969
970 #[test]
971 fn test_delete_word_with_special_chars() {
972 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
973 prompt.input = "save-file-as".to_string();
974 prompt.cursor_pos = 12; prompt.delete_word_backward();
978 assert_eq!(prompt.input, "save-file-");
979 assert_eq!(prompt.cursor_pos, 10);
980
981 prompt.delete_word_backward();
983 assert_eq!(prompt.input, "save-");
984 assert_eq!(prompt.cursor_pos, 5);
985 }
986
987 #[test]
988 fn test_get_text() {
989 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
990 prompt.input = "test content".to_string();
991
992 assert_eq!(prompt.get_text(), "test content");
993 }
994
995 #[test]
996 fn test_clear() {
997 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
998 prompt.input = "some text".to_string();
999 prompt.cursor_pos = 5;
1000 prompt.selected_suggestion = Some(0);
1001
1002 prompt.clear();
1003
1004 assert_eq!(prompt.input, "");
1005 assert_eq!(prompt.cursor_pos, 0);
1006 assert_eq!(prompt.selected_suggestion, None);
1007 }
1008
1009 #[test]
1010 fn test_delete_forward_basic() {
1011 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1012 prompt.input = "hello".to_string();
1013 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1017
1018 assert_eq!(prompt.input, "hllo");
1019 assert_eq!(prompt.cursor_pos, 1);
1020 }
1021
1022 #[test]
1023 fn test_delete_at_end() {
1024 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1025 prompt.input = "hello".to_string();
1026 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
1030 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1031 }
1032
1033 assert_eq!(prompt.input, "hello");
1034 assert_eq!(prompt.cursor_pos, 5);
1035 }
1036
1037 #[test]
1038 fn test_insert_str_at_start() {
1039 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1040 prompt.input = "world".to_string();
1041 prompt.cursor_pos = 0;
1042
1043 prompt.insert_str("hello ");
1044 assert_eq!(prompt.input, "hello world");
1045 assert_eq!(prompt.cursor_pos, 6);
1046 }
1047
1048 #[test]
1049 fn test_insert_str_at_middle() {
1050 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1051 prompt.input = "helloworld".to_string();
1052 prompt.cursor_pos = 5;
1053
1054 prompt.insert_str(" ");
1055 assert_eq!(prompt.input, "hello world");
1056 assert_eq!(prompt.cursor_pos, 6);
1057 }
1058
1059 #[test]
1060 fn test_insert_str_at_end() {
1061 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1062 prompt.input = "hello".to_string();
1063 prompt.cursor_pos = 5;
1064
1065 prompt.insert_str(" world");
1066 assert_eq!(prompt.input, "hello world");
1067 assert_eq!(prompt.cursor_pos, 11);
1068 }
1069
1070 #[test]
1071 fn test_delete_word_forward_empty() {
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_delete_word_backward_empty() {
1083 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1084 prompt.input = "".to_string();
1085 prompt.cursor_pos = 0;
1086
1087 prompt.delete_word_backward();
1088 assert_eq!(prompt.input, "");
1089 assert_eq!(prompt.cursor_pos, 0);
1090 }
1091
1092 #[test]
1093 fn test_delete_word_forward_only_spaces() {
1094 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1095 prompt.input = " ".to_string();
1096 prompt.cursor_pos = 0;
1097
1098 prompt.delete_word_forward();
1099 assert_eq!(prompt.input, "");
1100 assert_eq!(prompt.cursor_pos, 0);
1101 }
1102
1103 #[test]
1104 fn test_multiple_word_deletions() {
1105 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1106 prompt.input = "one two three four".to_string();
1107 prompt.cursor_pos = 18;
1108
1109 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1111
1112 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1114
1115 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1117 }
1118
1119 #[test]
1121 fn test_selection_with_shift_arrows() {
1122 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1123 prompt.input = "hello world".to_string();
1124 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1128 assert_eq!(prompt.selected_text(), None);
1129
1130 prompt.move_right_selecting();
1132 assert!(prompt.has_selection());
1133 assert_eq!(prompt.selection_range(), Some((5, 6)));
1134 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1135
1136 prompt.move_right_selecting();
1138 assert_eq!(prompt.selection_range(), Some((5, 7)));
1139 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1140
1141 prompt.move_left_selecting();
1143 assert_eq!(prompt.selection_range(), Some((5, 6)));
1144 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1145 }
1146
1147 #[test]
1148 fn test_selection_backward() {
1149 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1150 prompt.input = "abcdef".to_string();
1151 prompt.cursor_pos = 4; prompt.move_left_selecting();
1155 prompt.move_left_selecting();
1156 assert!(prompt.has_selection());
1157 assert_eq!(prompt.selection_range(), Some((2, 4)));
1158 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1159 }
1160
1161 #[test]
1162 fn test_selection_with_home_end() {
1163 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1164 prompt.input = "select this text".to_string();
1165 prompt.cursor_pos = 7; prompt.move_end_selecting();
1169 assert_eq!(prompt.selection_range(), Some((7, 16)));
1170 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1171
1172 prompt.clear_selection();
1174 prompt.move_home_selecting();
1175 assert_eq!(prompt.selection_range(), Some((0, 16)));
1176 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1177 }
1178
1179 #[test]
1180 fn test_word_selection() {
1181 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1182 prompt.input = "one two three".to_string();
1183 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1187 assert_eq!(prompt.selection_range(), Some((4, 7)));
1188 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1189
1190 prompt.move_word_right_selecting();
1192 assert_eq!(prompt.selection_range(), Some((4, 13)));
1193 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1194 }
1195
1196 #[test]
1197 fn test_word_selection_backward() {
1198 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1199 prompt.input = "one two three".to_string();
1200 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1204 assert_eq!(prompt.selection_range(), Some((8, 13)));
1205 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1206
1207 }
1212
1213 #[test]
1214 fn test_delete_selection() {
1215 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1216 prompt.input = "hello world".to_string();
1217 prompt.cursor_pos = 5;
1218
1219 prompt.move_end_selecting();
1221 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1222
1223 let deleted = prompt.delete_selection();
1225 assert_eq!(deleted, Some(" world".to_string()));
1226 assert_eq!(prompt.input, "hello");
1227 assert_eq!(prompt.cursor_pos, 5);
1228 assert!(!prompt.has_selection());
1229 }
1230
1231 #[test]
1232 fn test_insert_deletes_selection() {
1233 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1234 prompt.input = "hello world".to_string();
1235 prompt.cursor_pos = 0;
1236
1237 for _ in 0..5 {
1239 prompt.move_right_selecting();
1240 }
1241 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1242
1243 prompt.insert_str("goodbye");
1245 assert_eq!(prompt.input, "goodbye world");
1246 assert_eq!(prompt.cursor_pos, 7);
1247 assert!(!prompt.has_selection());
1248 }
1249
1250 #[test]
1251 fn test_clear_selection() {
1252 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1253 prompt.input = "test".to_string();
1254 prompt.cursor_pos = 0;
1255
1256 prompt.move_end_selecting();
1258 assert!(prompt.has_selection());
1259
1260 prompt.clear_selection();
1262 assert!(!prompt.has_selection());
1263 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1266
1267 #[test]
1268 fn test_selection_edge_cases() {
1269 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1270 prompt.input = "abc".to_string();
1271 prompt.cursor_pos = 3;
1272
1273 prompt.move_right_selecting();
1275 assert_eq!(prompt.cursor_pos, 3);
1276 assert_eq!(prompt.selection_range(), None);
1278 assert_eq!(prompt.selected_text(), None);
1279
1280 assert_eq!(prompt.delete_selection(), None);
1282 assert_eq!(prompt.input, "abc");
1283 }
1284
1285 #[test]
1286 fn test_selection_with_unicode() {
1287 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1288 prompt.input = "hello 世界 world".to_string();
1289 prompt.cursor_pos = 6; for _ in 0..2 {
1293 prompt.move_right_selecting();
1294 }
1295
1296 let selected = prompt.selected_text().unwrap();
1297 assert_eq!(selected, "世界");
1298
1299 prompt.delete_selection();
1301 assert_eq!(prompt.input, "hello world");
1302 }
1303
1304 #[test]
1308 fn test_word_selection_continues_across_words() {
1309 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1310 prompt.input = "one two three".to_string();
1311 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1315 assert_eq!(prompt.selection_range(), Some((8, 13)));
1316 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1317
1318 prompt.move_word_left_selecting();
1321
1322 assert_eq!(prompt.selection_range(), Some((4, 13)));
1324 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1325 }
1326
1327 #[cfg(test)]
1329 mod property_tests {
1330 use super::*;
1331 use proptest::prelude::*;
1332
1333 proptest! {
1334 #[test]
1336 fn prop_delete_word_backward_shrinks(
1337 input in "[a-zA-Z0-9_ ]{0,50}",
1338 cursor_pos in 0usize..50
1339 ) {
1340 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1341 prompt.input = input.clone();
1342 prompt.cursor_pos = cursor_pos.min(input.len());
1343
1344 let original_len = prompt.input.len();
1345 prompt.delete_word_backward();
1346
1347 prop_assert!(prompt.input.len() <= original_len);
1348 }
1349
1350 #[test]
1352 fn prop_delete_word_forward_shrinks(
1353 input in "[a-zA-Z0-9_ ]{0,50}",
1354 cursor_pos in 0usize..50
1355 ) {
1356 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1357 prompt.input = input.clone();
1358 prompt.cursor_pos = cursor_pos.min(input.len());
1359
1360 let original_len = prompt.input.len();
1361 prompt.delete_word_forward();
1362
1363 prop_assert!(prompt.input.len() <= original_len);
1364 }
1365
1366 #[test]
1368 fn prop_delete_word_backward_cursor_valid(
1369 input in "[a-zA-Z0-9_ ]{0,50}",
1370 cursor_pos in 0usize..50
1371 ) {
1372 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1373 prompt.input = input.clone();
1374 prompt.cursor_pos = cursor_pos.min(input.len());
1375
1376 prompt.delete_word_backward();
1377
1378 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1379 }
1380
1381 #[test]
1383 fn prop_delete_word_forward_cursor_valid(
1384 input in "[a-zA-Z0-9_ ]{0,50}",
1385 cursor_pos in 0usize..50
1386 ) {
1387 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1388 prompt.input = input.clone();
1389 prompt.cursor_pos = cursor_pos.min(input.len());
1390
1391 prompt.delete_word_forward();
1392
1393 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1394 }
1395
1396 #[test]
1398 fn prop_insert_str_length(
1399 input in "[a-zA-Z0-9_ ]{0,30}",
1400 insert in "[a-zA-Z0-9_ ]{0,20}",
1401 cursor_pos in 0usize..30
1402 ) {
1403 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1404 prompt.input = input.clone();
1405 prompt.cursor_pos = cursor_pos.min(input.len());
1406
1407 let original_len = prompt.input.len();
1408 prompt.insert_str(&insert);
1409
1410 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1411 }
1412
1413 #[test]
1415 fn prop_insert_str_cursor(
1416 input in "[a-zA-Z0-9_ ]{0,30}",
1417 insert in "[a-zA-Z0-9_ ]{0,20}",
1418 cursor_pos in 0usize..30
1419 ) {
1420 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1421 prompt.input = input.clone();
1422 let original_pos = cursor_pos.min(input.len());
1423 prompt.cursor_pos = original_pos;
1424
1425 prompt.insert_str(&insert);
1426
1427 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1428 }
1429
1430 #[test]
1432 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1433 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1434 prompt.input = input;
1435 prompt.cursor_pos = prompt.input.len();
1436
1437 prompt.clear();
1438
1439 prop_assert_eq!(prompt.input, "");
1440 prop_assert_eq!(prompt.cursor_pos, 0);
1441 }
1442 }
1443 }
1444}