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 undo_stack: Vec<(String, usize)>,
250 redo_stack: Vec<(String, usize)>,
252 pub toolbar_widget: Option<fresh_core::api::WidgetSpec>,
258 pub toolbar_focus: Option<String>,
263 pub status: String,
267}
268
269pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
273
274impl Prompt {
275 pub fn new(message: String, prompt_type: PromptType) -> Self {
277 Self {
278 message,
279 input: String::new(),
280 cursor_pos: 0,
281 prompt_type,
282 suggestions: Vec::new(),
283 original_suggestions: None,
284 selected_suggestion: None,
285 scroll_offset: 0,
286 selection_anchor: None,
287 suggestions_set_for_input: None,
288 sync_input_on_navigate: false,
289 overlay: false,
290 title: Vec::new(),
291 footer: Vec::new(),
292 undo_stack: Vec::new(),
293 redo_stack: Vec::new(),
294 toolbar_widget: None,
295 toolbar_focus: None,
296 status: String::new(),
297 }
298 }
299
300 pub fn with_suggestions(
305 message: String,
306 prompt_type: PromptType,
307 suggestions: Vec<Suggestion>,
308 ) -> Self {
309 let selected_suggestion = if suggestions.is_empty() {
310 None
311 } else {
312 Some(0)
313 };
314 Self {
315 message,
316 input: String::new(),
317 cursor_pos: 0,
318 prompt_type,
319 original_suggestions: Some(suggestions.clone()),
320 suggestions,
321 selected_suggestion,
322 scroll_offset: 0,
323 selection_anchor: None,
324 suggestions_set_for_input: None,
325 sync_input_on_navigate: false,
326 overlay: false,
327 title: Vec::new(),
328 footer: Vec::new(),
329 undo_stack: Vec::new(),
330 redo_stack: Vec::new(),
331 toolbar_widget: None,
332 toolbar_focus: None,
333 status: String::new(),
334 }
335 }
336
337 pub fn with_initial_text_for_edit(
342 message: String,
343 prompt_type: PromptType,
344 initial_text: String,
345 ) -> Self {
346 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
347 }
348
349 pub fn with_initial_text(
351 message: String,
352 prompt_type: PromptType,
353 initial_text: String,
354 ) -> Self {
355 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
356 }
357
358 fn with_initial_text_inner(
359 message: String,
360 prompt_type: PromptType,
361 initial_text: String,
362 select_all: bool,
363 ) -> Self {
364 let cursor_pos = initial_text.len();
365 let selection_anchor = if select_all && !initial_text.is_empty() {
366 Some(0)
367 } else {
368 None
369 };
370 Self {
371 message,
372 input: initial_text,
373 cursor_pos,
374 prompt_type,
375 suggestions: Vec::new(),
376 original_suggestions: None,
377 selected_suggestion: None,
378 scroll_offset: 0,
379 selection_anchor,
380 suggestions_set_for_input: None,
381 sync_input_on_navigate: false,
382 overlay: false,
383 title: Vec::new(),
384 footer: Vec::new(),
385 undo_stack: Vec::new(),
386 redo_stack: Vec::new(),
387 toolbar_widget: None,
388 toolbar_focus: None,
389 status: String::new(),
390 }
391 }
392
393 pub fn cursor_left(&mut self) {
398 if self.cursor_pos > 0 {
399 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
400 }
401 }
402
403 pub fn cursor_right(&mut self) {
408 if self.cursor_pos < self.input.len() {
409 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
410 }
411 }
412
413 fn push_undo_snapshot(&mut self) {
418 if self
419 .undo_stack
420 .last()
421 .is_some_and(|(text, _)| *text == self.input)
422 {
423 return;
424 }
425 const MAX_UNDO: usize = 500;
428 if self.undo_stack.len() >= MAX_UNDO {
429 self.undo_stack.remove(0);
430 }
431 self.undo_stack.push((self.input.clone(), self.cursor_pos));
432 self.redo_stack.clear();
433 }
434
435 pub fn undo_input(&mut self) -> bool {
437 if let Some((text, cursor)) = self.undo_stack.pop() {
438 self.redo_stack.push((self.input.clone(), self.cursor_pos));
439 self.input = text;
440 self.cursor_pos = cursor.min(self.input.len());
441 self.selection_anchor = None;
442 true
443 } else {
444 false
445 }
446 }
447
448 pub fn redo_input(&mut self) -> bool {
450 if let Some((text, cursor)) = self.redo_stack.pop() {
451 self.undo_stack.push((self.input.clone(), self.cursor_pos));
452 self.input = text;
453 self.cursor_pos = cursor.min(self.input.len());
454 self.selection_anchor = None;
455 true
456 } else {
457 false
458 }
459 }
460
461 pub fn insert_char(&mut self, ch: char) {
463 self.push_undo_snapshot();
464 self.input.insert(self.cursor_pos, ch);
465 self.cursor_pos += ch.len_utf8();
466 }
467
468 pub fn backspace(&mut self) {
474 if self.cursor_pos > 0 {
475 self.push_undo_snapshot();
476 let prev_boundary = self.input[..self.cursor_pos]
479 .char_indices()
480 .next_back()
481 .map(|(i, _)| i)
482 .unwrap_or(0);
483 self.input.drain(prev_boundary..self.cursor_pos);
484 self.cursor_pos = prev_boundary;
485 }
486 }
487
488 pub fn delete(&mut self) {
492 if self.cursor_pos < self.input.len() {
493 self.push_undo_snapshot();
494 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
495 self.input.drain(self.cursor_pos..next_boundary);
496 }
497 }
498
499 pub fn move_to_start(&mut self) {
501 self.cursor_pos = 0;
502 }
503
504 pub fn move_to_end(&mut self) {
506 self.cursor_pos = self.input.len();
507 }
508
509 pub fn set_input(&mut self, text: String) {
526 self.push_undo_snapshot();
527 self.cursor_pos = text.len();
528 self.input = text;
529 self.clear_selection();
530 }
531
532 pub fn select_next_suggestion(&mut self) {
534 if !self.suggestions.is_empty() {
535 self.selected_suggestion = Some(match self.selected_suggestion {
536 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
537 Some(_) => 0, None => 0,
539 });
540 }
541 }
542
543 pub fn select_prev_suggestion(&mut self) {
545 if !self.suggestions.is_empty() {
546 self.selected_suggestion = Some(match self.selected_suggestion {
547 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
549 None => 0,
550 });
551 }
552 }
553
554 pub fn selected_value(&self) -> Option<String> {
556 self.selected_suggestion
557 .and_then(|idx| self.suggestions.get(idx))
558 .map(|s| s.get_value().to_string())
559 }
560
561 pub fn get_final_input(&self) -> String {
563 self.selected_value().unwrap_or_else(|| self.input.clone())
564 }
565
566 pub fn filter_suggestions(&mut self, match_description: bool) {
571 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
572
573 if let Some(ref set_for_input) = self.suggestions_set_for_input {
577 if set_for_input == &self.input {
578 return;
579 }
580 }
581 self.suggestions_set_for_input = None;
585
586 let Some(original) = &self.original_suggestions else {
587 return;
588 };
589
590 let input = &self.input;
591 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
592 .iter()
593 .filter_map(|s| {
594 let text_result = fuzzy_match(input, &s.text);
595 let desc_result = if match_description {
596 s.description
597 .as_ref()
598 .map(|d| fuzzy_match(input, d))
599 .unwrap_or_else(FuzzyMatch::no_match)
600 } else {
601 FuzzyMatch::no_match()
602 };
603 if text_result.matched || desc_result.matched {
604 Some((s.clone(), text_result.score.max(desc_result.score)))
605 } else {
606 None
607 }
608 })
609 .collect();
610
611 filtered.sort_by(|a, b| b.1.cmp(&a.1));
612 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
613 self.selected_suggestion = if self.suggestions.is_empty() {
614 None
615 } else {
616 Some(0)
617 };
618 self.scroll_offset = 0;
619 }
620
621 pub fn ensure_selected_visible(&mut self) {
636 self.ensure_selected_visible_within(MAX_VISIBLE_SUGGESTIONS);
637 }
638
639 pub fn ensure_selected_visible_within(&mut self, visible_count: usize) {
644 let total = self.suggestions.len();
645 let visible = total.min(visible_count.max(1));
646 let max_offset = total.saturating_sub(visible);
647 if visible == 0 {
648 self.scroll_offset = 0;
649 return;
650 }
651 if let Some(selected) = self.selected_suggestion {
652 if selected < self.scroll_offset {
653 self.scroll_offset = selected;
654 } else if selected >= self.scroll_offset + visible {
655 self.scroll_offset = selected + 1 - visible;
656 }
657 }
658 if self.scroll_offset > max_offset {
659 self.scroll_offset = max_offset;
660 }
661 }
662
663 pub fn delete_word_forward(&mut self) {
693 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
694 if word_end > self.cursor_pos {
695 self.push_undo_snapshot();
696 self.input.drain(self.cursor_pos..word_end);
697 }
699 }
700
701 pub fn delete_word_backward(&mut self) {
717 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
718 if word_start < self.cursor_pos {
719 self.push_undo_snapshot();
720 self.input.drain(word_start..self.cursor_pos);
721 self.cursor_pos = word_start;
722 }
723 }
724
725 pub fn delete_to_end(&mut self) {
740 if self.cursor_pos < self.input.len() {
741 self.push_undo_snapshot();
742 self.input.truncate(self.cursor_pos);
743 }
744 }
745
746 pub fn get_text(&self) -> String {
759 self.input.clone()
760 }
761
762 pub fn clear(&mut self) {
777 self.input.clear();
778 self.cursor_pos = 0;
779 self.selected_suggestion = None;
781 }
782
783 pub fn insert_str(&mut self, text: &str) {
799 if self.has_selection() {
801 self.delete_selection();
802 }
803 self.input.insert_str(self.cursor_pos, text);
804 self.cursor_pos += text.len();
805 }
806
807 pub fn has_selection(&self) -> bool {
813 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
814 }
815
816 pub fn selection_range(&self) -> Option<(usize, usize)> {
818 if let Some(anchor) = self.selection_anchor {
819 if anchor != self.cursor_pos {
820 let start = anchor.min(self.cursor_pos);
821 let end = anchor.max(self.cursor_pos);
822 return Some((start, end));
823 }
824 }
825 None
826 }
827
828 pub fn selected_text(&self) -> Option<String> {
830 self.selection_range()
831 .map(|(start, end)| self.input[start..end].to_string())
832 }
833
834 pub fn delete_selection(&mut self) -> Option<String> {
836 if let Some((start, end)) = self.selection_range() {
837 self.push_undo_snapshot();
838 let deleted = self.input[start..end].to_string();
839 self.input.drain(start..end);
840 self.cursor_pos = start;
841 self.selection_anchor = None;
842 Some(deleted)
843 } else {
844 None
845 }
846 }
847
848 pub fn clear_selection(&mut self) {
850 self.selection_anchor = None;
851 }
852
853 pub fn move_left_selecting(&mut self) {
855 if self.selection_anchor.is_none() {
857 self.selection_anchor = Some(self.cursor_pos);
858 }
859
860 if self.cursor_pos > 0 {
862 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
863 }
864 }
865
866 pub fn move_right_selecting(&mut self) {
868 if self.selection_anchor.is_none() {
870 self.selection_anchor = Some(self.cursor_pos);
871 }
872
873 if self.cursor_pos < self.input.len() {
875 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
876 }
877 }
878
879 pub fn move_home_selecting(&mut self) {
881 if self.selection_anchor.is_none() {
882 self.selection_anchor = Some(self.cursor_pos);
883 }
884 self.cursor_pos = 0;
885 }
886
887 pub fn move_end_selecting(&mut self) {
889 if self.selection_anchor.is_none() {
890 self.selection_anchor = Some(self.cursor_pos);
891 }
892 self.cursor_pos = self.input.len();
893 }
894
895 pub fn move_word_left_selecting(&mut self) {
898 if self.selection_anchor.is_none() {
899 self.selection_anchor = Some(self.cursor_pos);
900 }
901
902 let bytes = self.input.as_bytes();
903 if self.cursor_pos == 0 {
904 return;
905 }
906
907 let mut new_pos = self.cursor_pos.saturating_sub(1);
908
909 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
911 new_pos = new_pos.saturating_sub(1);
912 }
913
914 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
916 new_pos = new_pos.saturating_sub(1);
917 }
918
919 self.cursor_pos = new_pos;
920 }
921
922 pub fn move_word_right_selecting(&mut self) {
925 if self.selection_anchor.is_none() {
926 self.selection_anchor = Some(self.cursor_pos);
927 }
928
929 let bytes = self.input.as_bytes();
931 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
932
933 if new_pos == self.cursor_pos && new_pos < bytes.len() {
935 new_pos = (new_pos + 1).min(bytes.len());
936 new_pos = find_word_end_bytes(bytes, new_pos);
937 }
938
939 self.cursor_pos = new_pos;
940 }
941
942 pub fn move_word_left(&mut self) {
945 self.clear_selection();
946
947 let bytes = self.input.as_bytes();
948 if self.cursor_pos == 0 {
949 return;
950 }
951
952 let mut new_pos = self.cursor_pos.saturating_sub(1);
953
954 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
956 new_pos = new_pos.saturating_sub(1);
957 }
958
959 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
961 new_pos = new_pos.saturating_sub(1);
962 }
963
964 self.cursor_pos = new_pos;
965 }
966
967 pub fn move_word_right(&mut self) {
970 self.clear_selection();
971
972 let bytes = self.input.as_bytes();
973 if self.cursor_pos >= bytes.len() {
974 return;
975 }
976
977 let mut new_pos = self.cursor_pos;
978
979 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
981 new_pos += 1;
982 }
983
984 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
986 new_pos += 1;
987 }
988
989 self.cursor_pos = new_pos;
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 #[test]
998 fn test_delete_word_forward_basic() {
999 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1000 prompt.input = "hello world test".to_string();
1001 prompt.cursor_pos = 0;
1002
1003 prompt.delete_word_forward();
1004 assert_eq!(prompt.input, " world test");
1005 assert_eq!(prompt.cursor_pos, 0);
1006 }
1007
1008 #[test]
1009 fn test_delete_word_forward_middle() {
1010 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1011 prompt.input = "hello world test".to_string();
1012 prompt.cursor_pos = 3; prompt.delete_word_forward();
1015 assert_eq!(prompt.input, "hel world test");
1016 assert_eq!(prompt.cursor_pos, 3);
1017 }
1018
1019 #[test]
1020 fn test_delete_word_forward_at_space() {
1021 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1022 prompt.input = "hello world".to_string();
1023 prompt.cursor_pos = 5; prompt.delete_word_forward();
1026 assert_eq!(prompt.input, "hello");
1027 assert_eq!(prompt.cursor_pos, 5);
1028 }
1029
1030 #[test]
1031 fn test_delete_word_backward_basic() {
1032 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1033 prompt.input = "hello world test".to_string();
1034 prompt.cursor_pos = 5; prompt.delete_word_backward();
1037 assert_eq!(prompt.input, " world test");
1038 assert_eq!(prompt.cursor_pos, 0);
1039 }
1040
1041 #[test]
1042 fn test_delete_word_backward_middle() {
1043 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1044 prompt.input = "hello world test".to_string();
1045 prompt.cursor_pos = 8; prompt.delete_word_backward();
1048 assert_eq!(prompt.input, "hello rld test");
1049 assert_eq!(prompt.cursor_pos, 6);
1050 }
1051
1052 #[test]
1053 fn test_delete_word_backward_at_end() {
1054 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1055 prompt.input = "hello world".to_string();
1056 prompt.cursor_pos = 11; prompt.delete_word_backward();
1059 assert_eq!(prompt.input, "hello ");
1060 assert_eq!(prompt.cursor_pos, 6);
1061 }
1062
1063 #[test]
1064 fn test_delete_word_with_special_chars() {
1065 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1066 prompt.input = "save-file-as".to_string();
1067 prompt.cursor_pos = 12; prompt.delete_word_backward();
1071 assert_eq!(prompt.input, "save-file-");
1072 assert_eq!(prompt.cursor_pos, 10);
1073
1074 prompt.delete_word_backward();
1076 assert_eq!(prompt.input, "save-");
1077 assert_eq!(prompt.cursor_pos, 5);
1078 }
1079
1080 #[test]
1081 fn test_get_text() {
1082 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
1083 prompt.input = "test content".to_string();
1084
1085 assert_eq!(prompt.get_text(), "test content");
1086 }
1087
1088 #[test]
1089 fn test_clear() {
1090 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
1091 prompt.input = "some text".to_string();
1092 prompt.cursor_pos = 5;
1093 prompt.selected_suggestion = Some(0);
1094
1095 prompt.clear();
1096
1097 assert_eq!(prompt.input, "");
1098 assert_eq!(prompt.cursor_pos, 0);
1099 assert_eq!(prompt.selected_suggestion, None);
1100 }
1101
1102 #[test]
1103 fn test_delete_forward_basic() {
1104 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1105 prompt.input = "hello".to_string();
1106 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1110
1111 assert_eq!(prompt.input, "hllo");
1112 assert_eq!(prompt.cursor_pos, 1);
1113 }
1114
1115 #[test]
1116 fn test_delete_at_end() {
1117 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1118 prompt.input = "hello".to_string();
1119 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
1123 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1124 }
1125
1126 assert_eq!(prompt.input, "hello");
1127 assert_eq!(prompt.cursor_pos, 5);
1128 }
1129
1130 #[test]
1131 fn test_insert_str_at_start() {
1132 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1133 prompt.input = "world".to_string();
1134 prompt.cursor_pos = 0;
1135
1136 prompt.insert_str("hello ");
1137 assert_eq!(prompt.input, "hello world");
1138 assert_eq!(prompt.cursor_pos, 6);
1139 }
1140
1141 #[test]
1142 fn test_insert_str_at_middle() {
1143 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1144 prompt.input = "helloworld".to_string();
1145 prompt.cursor_pos = 5;
1146
1147 prompt.insert_str(" ");
1148 assert_eq!(prompt.input, "hello world");
1149 assert_eq!(prompt.cursor_pos, 6);
1150 }
1151
1152 #[test]
1153 fn test_insert_str_at_end() {
1154 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1155 prompt.input = "hello".to_string();
1156 prompt.cursor_pos = 5;
1157
1158 prompt.insert_str(" world");
1159 assert_eq!(prompt.input, "hello world");
1160 assert_eq!(prompt.cursor_pos, 11);
1161 }
1162
1163 #[test]
1164 fn test_delete_word_forward_empty() {
1165 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1166 prompt.input = "".to_string();
1167 prompt.cursor_pos = 0;
1168
1169 prompt.delete_word_forward();
1170 assert_eq!(prompt.input, "");
1171 assert_eq!(prompt.cursor_pos, 0);
1172 }
1173
1174 #[test]
1175 fn test_delete_word_backward_empty() {
1176 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1177 prompt.input = "".to_string();
1178 prompt.cursor_pos = 0;
1179
1180 prompt.delete_word_backward();
1181 assert_eq!(prompt.input, "");
1182 assert_eq!(prompt.cursor_pos, 0);
1183 }
1184
1185 #[test]
1186 fn test_delete_word_forward_only_spaces() {
1187 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1188 prompt.input = " ".to_string();
1189 prompt.cursor_pos = 0;
1190
1191 prompt.delete_word_forward();
1192 assert_eq!(prompt.input, "");
1193 assert_eq!(prompt.cursor_pos, 0);
1194 }
1195
1196 #[test]
1197 fn test_multiple_word_deletions() {
1198 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1199 prompt.input = "one two three four".to_string();
1200 prompt.cursor_pos = 18;
1201
1202 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1204
1205 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1207
1208 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1210 }
1211
1212 #[test]
1214 fn test_selection_with_shift_arrows() {
1215 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1216 prompt.input = "hello world".to_string();
1217 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1221 assert_eq!(prompt.selected_text(), None);
1222
1223 prompt.move_right_selecting();
1225 assert!(prompt.has_selection());
1226 assert_eq!(prompt.selection_range(), Some((5, 6)));
1227 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1228
1229 prompt.move_right_selecting();
1231 assert_eq!(prompt.selection_range(), Some((5, 7)));
1232 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1233
1234 prompt.move_left_selecting();
1236 assert_eq!(prompt.selection_range(), Some((5, 6)));
1237 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1238 }
1239
1240 #[test]
1241 fn test_selection_backward() {
1242 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1243 prompt.input = "abcdef".to_string();
1244 prompt.cursor_pos = 4; prompt.move_left_selecting();
1248 prompt.move_left_selecting();
1249 assert!(prompt.has_selection());
1250 assert_eq!(prompt.selection_range(), Some((2, 4)));
1251 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1252 }
1253
1254 #[test]
1255 fn test_selection_with_home_end() {
1256 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1257 prompt.input = "select this text".to_string();
1258 prompt.cursor_pos = 7; prompt.move_end_selecting();
1262 assert_eq!(prompt.selection_range(), Some((7, 16)));
1263 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1264
1265 prompt.clear_selection();
1267 prompt.move_home_selecting();
1268 assert_eq!(prompt.selection_range(), Some((0, 16)));
1269 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1270 }
1271
1272 #[test]
1273 fn test_word_selection() {
1274 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1275 prompt.input = "one two three".to_string();
1276 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1280 assert_eq!(prompt.selection_range(), Some((4, 7)));
1281 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1282
1283 prompt.move_word_right_selecting();
1285 assert_eq!(prompt.selection_range(), Some((4, 13)));
1286 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1287 }
1288
1289 #[test]
1290 fn test_word_selection_backward() {
1291 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1292 prompt.input = "one two three".to_string();
1293 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1297 assert_eq!(prompt.selection_range(), Some((8, 13)));
1298 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1299
1300 }
1305
1306 #[test]
1307 fn test_delete_selection() {
1308 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1309 prompt.input = "hello world".to_string();
1310 prompt.cursor_pos = 5;
1311
1312 prompt.move_end_selecting();
1314 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1315
1316 let deleted = prompt.delete_selection();
1318 assert_eq!(deleted, Some(" world".to_string()));
1319 assert_eq!(prompt.input, "hello");
1320 assert_eq!(prompt.cursor_pos, 5);
1321 assert!(!prompt.has_selection());
1322 }
1323
1324 #[test]
1325 fn test_insert_deletes_selection() {
1326 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1327 prompt.input = "hello world".to_string();
1328 prompt.cursor_pos = 0;
1329
1330 for _ in 0..5 {
1332 prompt.move_right_selecting();
1333 }
1334 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1335
1336 prompt.insert_str("goodbye");
1338 assert_eq!(prompt.input, "goodbye world");
1339 assert_eq!(prompt.cursor_pos, 7);
1340 assert!(!prompt.has_selection());
1341 }
1342
1343 #[test]
1344 fn test_clear_selection() {
1345 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1346 prompt.input = "test".to_string();
1347 prompt.cursor_pos = 0;
1348
1349 prompt.move_end_selecting();
1351 assert!(prompt.has_selection());
1352
1353 prompt.clear_selection();
1355 assert!(!prompt.has_selection());
1356 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1359
1360 #[test]
1361 fn test_selection_edge_cases() {
1362 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1363 prompt.input = "abc".to_string();
1364 prompt.cursor_pos = 3;
1365
1366 prompt.move_right_selecting();
1368 assert_eq!(prompt.cursor_pos, 3);
1369 assert_eq!(prompt.selection_range(), None);
1371 assert_eq!(prompt.selected_text(), None);
1372
1373 assert_eq!(prompt.delete_selection(), None);
1375 assert_eq!(prompt.input, "abc");
1376 }
1377
1378 #[test]
1379 fn test_selection_with_unicode() {
1380 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1381 prompt.input = "hello 世界 world".to_string();
1382 prompt.cursor_pos = 6; for _ in 0..2 {
1386 prompt.move_right_selecting();
1387 }
1388
1389 let selected = prompt.selected_text().unwrap();
1390 assert_eq!(selected, "世界");
1391
1392 prompt.delete_selection();
1394 assert_eq!(prompt.input, "hello world");
1395 }
1396
1397 #[test]
1401 fn test_word_selection_continues_across_words() {
1402 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1403 prompt.input = "one two three".to_string();
1404 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1408 assert_eq!(prompt.selection_range(), Some((8, 13)));
1409 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1410
1411 prompt.move_word_left_selecting();
1414
1415 assert_eq!(prompt.selection_range(), Some((4, 13)));
1417 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1418 }
1419
1420 #[cfg(test)]
1422 mod property_tests {
1423 use super::*;
1424 use proptest::prelude::*;
1425
1426 proptest! {
1427 #[test]
1429 fn prop_delete_word_backward_shrinks(
1430 input in "[a-zA-Z0-9_ ]{0,50}",
1431 cursor_pos in 0usize..50
1432 ) {
1433 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1434 prompt.input = input.clone();
1435 prompt.cursor_pos = cursor_pos.min(input.len());
1436
1437 let original_len = prompt.input.len();
1438 prompt.delete_word_backward();
1439
1440 prop_assert!(prompt.input.len() <= original_len);
1441 }
1442
1443 #[test]
1445 fn prop_delete_word_forward_shrinks(
1446 input in "[a-zA-Z0-9_ ]{0,50}",
1447 cursor_pos in 0usize..50
1448 ) {
1449 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1450 prompt.input = input.clone();
1451 prompt.cursor_pos = cursor_pos.min(input.len());
1452
1453 let original_len = prompt.input.len();
1454 prompt.delete_word_forward();
1455
1456 prop_assert!(prompt.input.len() <= original_len);
1457 }
1458
1459 #[test]
1461 fn prop_delete_word_backward_cursor_valid(
1462 input in "[a-zA-Z0-9_ ]{0,50}",
1463 cursor_pos in 0usize..50
1464 ) {
1465 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1466 prompt.input = input.clone();
1467 prompt.cursor_pos = cursor_pos.min(input.len());
1468
1469 prompt.delete_word_backward();
1470
1471 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1472 }
1473
1474 #[test]
1476 fn prop_delete_word_forward_cursor_valid(
1477 input in "[a-zA-Z0-9_ ]{0,50}",
1478 cursor_pos in 0usize..50
1479 ) {
1480 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1481 prompt.input = input.clone();
1482 prompt.cursor_pos = cursor_pos.min(input.len());
1483
1484 prompt.delete_word_forward();
1485
1486 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1487 }
1488
1489 #[test]
1491 fn prop_insert_str_length(
1492 input in "[a-zA-Z0-9_ ]{0,30}",
1493 insert in "[a-zA-Z0-9_ ]{0,20}",
1494 cursor_pos in 0usize..30
1495 ) {
1496 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1497 prompt.input = input.clone();
1498 prompt.cursor_pos = cursor_pos.min(input.len());
1499
1500 let original_len = prompt.input.len();
1501 prompt.insert_str(&insert);
1502
1503 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1504 }
1505
1506 #[test]
1508 fn prop_insert_str_cursor(
1509 input in "[a-zA-Z0-9_ ]{0,30}",
1510 insert in "[a-zA-Z0-9_ ]{0,20}",
1511 cursor_pos in 0usize..30
1512 ) {
1513 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1514 prompt.input = input.clone();
1515 let original_pos = cursor_pos.min(input.len());
1516 prompt.cursor_pos = original_pos;
1517
1518 prompt.insert_str(&insert);
1519
1520 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1521 }
1522
1523 #[test]
1525 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1526 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1527 prompt.input = input;
1528 prompt.cursor_pos = prompt.input.len();
1529
1530 prompt.clear();
1531
1532 prop_assert_eq!(prompt.input, "");
1533 prop_assert_eq!(prompt.cursor_pos, 0);
1534 }
1535 }
1536 }
1537}