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 manual_scroll: bool,
218 pub selection_anchor: Option<usize>,
221 pub suggestions_set_for_input: Option<String>,
224 pub sync_input_on_navigate: bool,
227 pub overlay: bool,
234 pub title: Vec<fresh_core::api::StyledText>,
240 pub footer: Vec<fresh_core::api::StyledText>,
251 undo_stack: Vec<(String, usize)>,
256 redo_stack: Vec<(String, usize)>,
258 pub toolbar_widget: Option<fresh_core::api::WidgetSpec>,
264 pub toolbar_focus: Option<String>,
269 pub status: String,
273}
274
275pub const MAX_VISIBLE_SUGGESTIONS: usize = 10;
279
280impl Prompt {
281 pub fn new(message: String, prompt_type: PromptType) -> Self {
283 Self {
284 message,
285 input: String::new(),
286 cursor_pos: 0,
287 prompt_type,
288 suggestions: Vec::new(),
289 original_suggestions: None,
290 selected_suggestion: None,
291 scroll_offset: 0,
292 manual_scroll: false,
293 selection_anchor: None,
294 suggestions_set_for_input: None,
295 sync_input_on_navigate: false,
296 overlay: false,
297 title: Vec::new(),
298 footer: Vec::new(),
299 undo_stack: Vec::new(),
300 redo_stack: Vec::new(),
301 toolbar_widget: None,
302 toolbar_focus: None,
303 status: String::new(),
304 }
305 }
306
307 pub fn with_suggestions(
312 message: String,
313 prompt_type: PromptType,
314 suggestions: Vec<Suggestion>,
315 ) -> Self {
316 let selected_suggestion = if suggestions.is_empty() {
317 None
318 } else {
319 Some(0)
320 };
321 Self {
322 message,
323 input: String::new(),
324 cursor_pos: 0,
325 prompt_type,
326 original_suggestions: Some(suggestions.clone()),
327 suggestions,
328 selected_suggestion,
329 scroll_offset: 0,
330 manual_scroll: false,
331 selection_anchor: None,
332 suggestions_set_for_input: None,
333 sync_input_on_navigate: false,
334 overlay: false,
335 title: Vec::new(),
336 footer: Vec::new(),
337 undo_stack: Vec::new(),
338 redo_stack: Vec::new(),
339 toolbar_widget: None,
340 toolbar_focus: None,
341 status: String::new(),
342 }
343 }
344
345 pub fn with_initial_text_for_edit(
350 message: String,
351 prompt_type: PromptType,
352 initial_text: String,
353 ) -> Self {
354 Self::with_initial_text_inner(message, prompt_type, initial_text, false)
355 }
356
357 pub fn with_initial_text(
359 message: String,
360 prompt_type: PromptType,
361 initial_text: String,
362 ) -> Self {
363 Self::with_initial_text_inner(message, prompt_type, initial_text, true)
364 }
365
366 fn with_initial_text_inner(
367 message: String,
368 prompt_type: PromptType,
369 initial_text: String,
370 select_all: bool,
371 ) -> Self {
372 let cursor_pos = initial_text.len();
373 let selection_anchor = if select_all && !initial_text.is_empty() {
374 Some(0)
375 } else {
376 None
377 };
378 Self {
379 message,
380 input: initial_text,
381 cursor_pos,
382 prompt_type,
383 suggestions: Vec::new(),
384 original_suggestions: None,
385 selected_suggestion: None,
386 scroll_offset: 0,
387 manual_scroll: false,
388 selection_anchor,
389 suggestions_set_for_input: None,
390 sync_input_on_navigate: false,
391 overlay: false,
392 title: Vec::new(),
393 footer: Vec::new(),
394 undo_stack: Vec::new(),
395 redo_stack: Vec::new(),
396 toolbar_widget: None,
397 toolbar_focus: None,
398 status: String::new(),
399 }
400 }
401
402 pub fn cursor_left(&mut self) {
407 if self.cursor_pos > 0 {
408 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
409 }
410 }
411
412 pub fn cursor_right(&mut self) {
417 if self.cursor_pos < self.input.len() {
418 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
419 }
420 }
421
422 fn push_undo_snapshot(&mut self) {
427 if self
428 .undo_stack
429 .last()
430 .is_some_and(|(text, _)| *text == self.input)
431 {
432 return;
433 }
434 const MAX_UNDO: usize = 500;
437 if self.undo_stack.len() >= MAX_UNDO {
438 self.undo_stack.remove(0);
439 }
440 self.undo_stack.push((self.input.clone(), self.cursor_pos));
441 self.redo_stack.clear();
442 }
443
444 pub fn undo_input(&mut self) -> bool {
446 if let Some((text, cursor)) = self.undo_stack.pop() {
447 self.redo_stack.push((self.input.clone(), self.cursor_pos));
448 self.input = text;
449 self.cursor_pos = cursor.min(self.input.len());
450 self.selection_anchor = None;
451 true
452 } else {
453 false
454 }
455 }
456
457 pub fn redo_input(&mut self) -> bool {
459 if let Some((text, cursor)) = self.redo_stack.pop() {
460 self.undo_stack.push((self.input.clone(), self.cursor_pos));
461 self.input = text;
462 self.cursor_pos = cursor.min(self.input.len());
463 self.selection_anchor = None;
464 true
465 } else {
466 false
467 }
468 }
469
470 pub fn insert_char(&mut self, ch: char) {
472 self.push_undo_snapshot();
473 self.input.insert(self.cursor_pos, ch);
474 self.cursor_pos += ch.len_utf8();
475 }
476
477 pub fn backspace(&mut self) {
483 if self.cursor_pos > 0 {
484 self.push_undo_snapshot();
485 let prev_boundary = self.input[..self.cursor_pos]
488 .char_indices()
489 .next_back()
490 .map(|(i, _)| i)
491 .unwrap_or(0);
492 self.input.drain(prev_boundary..self.cursor_pos);
493 self.cursor_pos = prev_boundary;
494 }
495 }
496
497 pub fn delete(&mut self) {
501 if self.cursor_pos < self.input.len() {
502 self.push_undo_snapshot();
503 let next_boundary = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
504 self.input.drain(self.cursor_pos..next_boundary);
505 }
506 }
507
508 pub fn move_to_start(&mut self) {
510 self.cursor_pos = 0;
511 }
512
513 pub fn move_to_end(&mut self) {
515 self.cursor_pos = self.input.len();
516 }
517
518 pub fn set_input(&mut self, text: String) {
535 self.push_undo_snapshot();
536 self.cursor_pos = text.len();
537 self.input = text;
538 self.clear_selection();
539 }
540
541 pub fn select_next_suggestion(&mut self) {
543 if !self.suggestions.is_empty() {
544 self.manual_scroll = false;
546 self.selected_suggestion = Some(match self.selected_suggestion {
547 Some(idx) if idx + 1 < self.suggestions.len() => idx + 1,
548 Some(_) => 0, None => 0,
550 });
551 }
552 }
553
554 pub fn select_prev_suggestion(&mut self) {
556 if !self.suggestions.is_empty() {
557 self.manual_scroll = false;
558 self.selected_suggestion = Some(match self.selected_suggestion {
559 Some(0) => self.suggestions.len() - 1, Some(idx) => idx - 1,
561 None => 0,
562 });
563 }
564 }
565
566 pub fn scroll_results(&mut self, delta: i32, visible: usize) {
571 let total = self.suggestions.len();
572 if total == 0 {
573 return;
574 }
575 let max_offset = total.saturating_sub(visible.max(1));
576 let next = (self.scroll_offset as i32 + delta).clamp(0, max_offset as i32) as usize;
577 if next != self.scroll_offset {
578 self.scroll_offset = next;
579 }
580 self.manual_scroll = true;
583 }
584
585 pub fn selected_value(&self) -> Option<String> {
587 self.selected_suggestion
588 .and_then(|idx| self.suggestions.get(idx))
589 .map(|s| s.get_value().to_string())
590 }
591
592 pub fn get_final_input(&self) -> String {
594 self.selected_value().unwrap_or_else(|| self.input.clone())
595 }
596
597 pub fn filter_suggestions(&mut self, match_description: bool) {
602 use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
603
604 if let Some(ref set_for_input) = self.suggestions_set_for_input {
608 if set_for_input == &self.input {
609 return;
610 }
611 }
612 self.suggestions_set_for_input = None;
616
617 let Some(original) = &self.original_suggestions else {
618 return;
619 };
620
621 let input = &self.input;
622 let mut filtered: Vec<(crate::input::commands::Suggestion, i32)> = original
623 .iter()
624 .filter_map(|s| {
625 let text_result = fuzzy_match(input, &s.text);
626 let desc_result = if match_description {
627 s.description
628 .as_ref()
629 .map(|d| fuzzy_match(input, d))
630 .unwrap_or_else(FuzzyMatch::no_match)
631 } else {
632 FuzzyMatch::no_match()
633 };
634 if text_result.matched || desc_result.matched {
635 Some((s.clone(), text_result.score.max(desc_result.score)))
636 } else {
637 None
638 }
639 })
640 .collect();
641
642 filtered.sort_by(|a, b| b.1.cmp(&a.1));
643 self.suggestions = filtered.into_iter().map(|(s, _)| s).collect();
644 self.selected_suggestion = if self.suggestions.is_empty() {
645 None
646 } else {
647 Some(0)
648 };
649 self.scroll_offset = 0;
650 self.manual_scroll = false;
651 }
652
653 pub fn ensure_selected_visible(&mut self) {
668 self.ensure_selected_visible_within(MAX_VISIBLE_SUGGESTIONS);
669 }
670
671 pub fn ensure_selected_visible_within(&mut self, visible_count: usize) {
676 let total = self.suggestions.len();
677 let visible = total.min(visible_count.max(1));
678 let max_offset = total.saturating_sub(visible);
679 if visible == 0 {
680 self.scroll_offset = 0;
681 return;
682 }
683 if let Some(selected) = self.selected_suggestion {
684 if selected < self.scroll_offset {
685 self.scroll_offset = selected;
686 } else if selected >= self.scroll_offset + visible {
687 self.scroll_offset = selected + 1 - visible;
688 }
689 }
690 if self.scroll_offset > max_offset {
691 self.scroll_offset = max_offset;
692 }
693 }
694
695 pub fn delete_word_forward(&mut self) {
725 let word_end = find_word_end_bytes(self.input.as_bytes(), self.cursor_pos);
726 if word_end > self.cursor_pos {
727 self.push_undo_snapshot();
728 self.input.drain(self.cursor_pos..word_end);
729 }
731 }
732
733 pub fn delete_word_backward(&mut self) {
749 let word_start = find_word_start_bytes(self.input.as_bytes(), self.cursor_pos);
750 if word_start < self.cursor_pos {
751 self.push_undo_snapshot();
752 self.input.drain(word_start..self.cursor_pos);
753 self.cursor_pos = word_start;
754 }
755 }
756
757 pub fn delete_to_end(&mut self) {
772 if self.cursor_pos < self.input.len() {
773 self.push_undo_snapshot();
774 self.input.truncate(self.cursor_pos);
775 }
776 }
777
778 pub fn delete_to_start(&mut self) {
783 if self.cursor_pos > 0 {
784 self.push_undo_snapshot();
785 self.input.drain(..self.cursor_pos);
786 self.cursor_pos = 0;
787 }
788 }
789
790 pub fn get_text(&self) -> String {
803 self.input.clone()
804 }
805
806 pub fn clear(&mut self) {
821 self.input.clear();
822 self.cursor_pos = 0;
823 self.selected_suggestion = None;
825 }
826
827 pub fn insert_str(&mut self, text: &str) {
843 if self.has_selection() {
845 self.delete_selection();
846 }
847 self.input.insert_str(self.cursor_pos, text);
848 self.cursor_pos += text.len();
849 }
850
851 pub fn has_selection(&self) -> bool {
857 self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor_pos)
858 }
859
860 pub fn selection_range(&self) -> Option<(usize, usize)> {
862 if let Some(anchor) = self.selection_anchor {
863 if anchor != self.cursor_pos {
864 let start = anchor.min(self.cursor_pos);
865 let end = anchor.max(self.cursor_pos);
866 return Some((start, end));
867 }
868 }
869 None
870 }
871
872 pub fn selected_text(&self) -> Option<String> {
874 self.selection_range()
875 .map(|(start, end)| self.input[start..end].to_string())
876 }
877
878 pub fn delete_selection(&mut self) -> Option<String> {
880 if let Some((start, end)) = self.selection_range() {
881 self.push_undo_snapshot();
882 let deleted = self.input[start..end].to_string();
883 self.input.drain(start..end);
884 self.cursor_pos = start;
885 self.selection_anchor = None;
886 Some(deleted)
887 } else {
888 None
889 }
890 }
891
892 pub fn clear_selection(&mut self) {
894 self.selection_anchor = None;
895 }
896
897 pub fn move_left_selecting(&mut self) {
899 if self.selection_anchor.is_none() {
901 self.selection_anchor = Some(self.cursor_pos);
902 }
903
904 if self.cursor_pos > 0 {
906 self.cursor_pos = grapheme::prev_grapheme_boundary(&self.input, self.cursor_pos);
907 }
908 }
909
910 pub fn move_right_selecting(&mut self) {
912 if self.selection_anchor.is_none() {
914 self.selection_anchor = Some(self.cursor_pos);
915 }
916
917 if self.cursor_pos < self.input.len() {
919 self.cursor_pos = grapheme::next_grapheme_boundary(&self.input, self.cursor_pos);
920 }
921 }
922
923 pub fn move_home_selecting(&mut self) {
925 if self.selection_anchor.is_none() {
926 self.selection_anchor = Some(self.cursor_pos);
927 }
928 self.cursor_pos = 0;
929 }
930
931 pub fn move_end_selecting(&mut self) {
933 if self.selection_anchor.is_none() {
934 self.selection_anchor = Some(self.cursor_pos);
935 }
936 self.cursor_pos = self.input.len();
937 }
938
939 pub fn move_word_left_selecting(&mut self) {
942 if self.selection_anchor.is_none() {
943 self.selection_anchor = Some(self.cursor_pos);
944 }
945
946 let bytes = self.input.as_bytes();
947 if self.cursor_pos == 0 {
948 return;
949 }
950
951 let mut new_pos = self.cursor_pos.saturating_sub(1);
952
953 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
955 new_pos = new_pos.saturating_sub(1);
956 }
957
958 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
960 new_pos = new_pos.saturating_sub(1);
961 }
962
963 self.cursor_pos = new_pos;
964 }
965
966 pub fn move_word_right_selecting(&mut self) {
969 if self.selection_anchor.is_none() {
970 self.selection_anchor = Some(self.cursor_pos);
971 }
972
973 let bytes = self.input.as_bytes();
975 let mut new_pos = find_word_end_bytes(bytes, self.cursor_pos);
976
977 if new_pos == self.cursor_pos && new_pos < bytes.len() {
979 new_pos = (new_pos + 1).min(bytes.len());
980 new_pos = find_word_end_bytes(bytes, new_pos);
981 }
982
983 self.cursor_pos = new_pos;
984 }
985
986 pub fn move_word_left(&mut self) {
989 self.clear_selection();
990
991 let bytes = self.input.as_bytes();
992 if self.cursor_pos == 0 {
993 return;
994 }
995
996 let mut new_pos = self.cursor_pos.saturating_sub(1);
997
998 while new_pos > 0 && !is_word_char(bytes[new_pos]) {
1000 new_pos = new_pos.saturating_sub(1);
1001 }
1002
1003 while new_pos > 0 && is_word_char(bytes[new_pos.saturating_sub(1)]) {
1005 new_pos = new_pos.saturating_sub(1);
1006 }
1007
1008 self.cursor_pos = new_pos;
1009 }
1010
1011 pub fn move_word_right(&mut self) {
1014 self.clear_selection();
1015
1016 let bytes = self.input.as_bytes();
1017 if self.cursor_pos >= bytes.len() {
1018 return;
1019 }
1020
1021 let mut new_pos = self.cursor_pos;
1022
1023 while new_pos < bytes.len() && is_word_char(bytes[new_pos]) {
1025 new_pos += 1;
1026 }
1027
1028 while new_pos < bytes.len() && !is_word_char(bytes[new_pos]) {
1030 new_pos += 1;
1031 }
1032
1033 self.cursor_pos = new_pos;
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1042 fn test_delete_word_forward_basic() {
1043 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1044 prompt.input = "hello world test".to_string();
1045 prompt.cursor_pos = 0;
1046
1047 prompt.delete_word_forward();
1048 assert_eq!(prompt.input, " world test");
1049 assert_eq!(prompt.cursor_pos, 0);
1050 }
1051
1052 #[test]
1053 fn test_delete_word_forward_middle() {
1054 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1055 prompt.input = "hello world test".to_string();
1056 prompt.cursor_pos = 3; prompt.delete_word_forward();
1059 assert_eq!(prompt.input, "hel world test");
1060 assert_eq!(prompt.cursor_pos, 3);
1061 }
1062
1063 #[test]
1064 fn test_delete_word_forward_at_space() {
1065 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1066 prompt.input = "hello world".to_string();
1067 prompt.cursor_pos = 5; prompt.delete_word_forward();
1070 assert_eq!(prompt.input, "hello");
1071 assert_eq!(prompt.cursor_pos, 5);
1072 }
1073
1074 #[test]
1075 fn test_delete_word_backward_basic() {
1076 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1077 prompt.input = "hello world test".to_string();
1078 prompt.cursor_pos = 5; prompt.delete_word_backward();
1081 assert_eq!(prompt.input, " world test");
1082 assert_eq!(prompt.cursor_pos, 0);
1083 }
1084
1085 #[test]
1086 fn test_delete_word_backward_middle() {
1087 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1088 prompt.input = "hello world test".to_string();
1089 prompt.cursor_pos = 8; prompt.delete_word_backward();
1092 assert_eq!(prompt.input, "hello rld test");
1093 assert_eq!(prompt.cursor_pos, 6);
1094 }
1095
1096 #[test]
1097 fn test_delete_word_backward_at_end() {
1098 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1099 prompt.input = "hello world".to_string();
1100 prompt.cursor_pos = 11; prompt.delete_word_backward();
1103 assert_eq!(prompt.input, "hello ");
1104 assert_eq!(prompt.cursor_pos, 6);
1105 }
1106
1107 #[test]
1108 fn test_delete_word_with_special_chars() {
1109 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1110 prompt.input = "save-file-as".to_string();
1111 prompt.cursor_pos = 12; prompt.delete_word_backward();
1115 assert_eq!(prompt.input, "save-file-");
1116 assert_eq!(prompt.cursor_pos, 10);
1117
1118 prompt.delete_word_backward();
1120 assert_eq!(prompt.input, "save-");
1121 assert_eq!(prompt.cursor_pos, 5);
1122 }
1123
1124 #[test]
1125 fn test_get_text() {
1126 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
1127 prompt.input = "test content".to_string();
1128
1129 assert_eq!(prompt.get_text(), "test content");
1130 }
1131
1132 #[test]
1133 fn test_clear() {
1134 let mut prompt = Prompt::new("Find: ".to_string(), PromptType::OpenFile);
1135 prompt.input = "some text".to_string();
1136 prompt.cursor_pos = 5;
1137 prompt.selected_suggestion = Some(0);
1138
1139 prompt.clear();
1140
1141 assert_eq!(prompt.input, "");
1142 assert_eq!(prompt.cursor_pos, 0);
1143 assert_eq!(prompt.selected_suggestion, None);
1144 }
1145
1146 #[test]
1147 fn test_delete_forward_basic() {
1148 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1149 prompt.input = "hello".to_string();
1150 prompt.cursor_pos = 1; prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1154
1155 assert_eq!(prompt.input, "hllo");
1156 assert_eq!(prompt.cursor_pos, 1);
1157 }
1158
1159 #[test]
1160 fn test_delete_at_end() {
1161 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1162 prompt.input = "hello".to_string();
1163 prompt.cursor_pos = 5; if prompt.cursor_pos < prompt.input.len() {
1167 prompt.input.drain(prompt.cursor_pos..prompt.cursor_pos + 1);
1168 }
1169
1170 assert_eq!(prompt.input, "hello");
1171 assert_eq!(prompt.cursor_pos, 5);
1172 }
1173
1174 #[test]
1175 fn test_insert_str_at_start() {
1176 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1177 prompt.input = "world".to_string();
1178 prompt.cursor_pos = 0;
1179
1180 prompt.insert_str("hello ");
1181 assert_eq!(prompt.input, "hello world");
1182 assert_eq!(prompt.cursor_pos, 6);
1183 }
1184
1185 #[test]
1186 fn test_insert_str_at_middle() {
1187 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1188 prompt.input = "helloworld".to_string();
1189 prompt.cursor_pos = 5;
1190
1191 prompt.insert_str(" ");
1192 assert_eq!(prompt.input, "hello world");
1193 assert_eq!(prompt.cursor_pos, 6);
1194 }
1195
1196 #[test]
1197 fn test_insert_str_at_end() {
1198 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1199 prompt.input = "hello".to_string();
1200 prompt.cursor_pos = 5;
1201
1202 prompt.insert_str(" world");
1203 assert_eq!(prompt.input, "hello world");
1204 assert_eq!(prompt.cursor_pos, 11);
1205 }
1206
1207 #[test]
1208 fn test_delete_word_forward_empty() {
1209 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1210 prompt.input = "".to_string();
1211 prompt.cursor_pos = 0;
1212
1213 prompt.delete_word_forward();
1214 assert_eq!(prompt.input, "");
1215 assert_eq!(prompt.cursor_pos, 0);
1216 }
1217
1218 #[test]
1219 fn test_delete_word_backward_empty() {
1220 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1221 prompt.input = "".to_string();
1222 prompt.cursor_pos = 0;
1223
1224 prompt.delete_word_backward();
1225 assert_eq!(prompt.input, "");
1226 assert_eq!(prompt.cursor_pos, 0);
1227 }
1228
1229 #[test]
1230 fn test_delete_word_forward_only_spaces() {
1231 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1232 prompt.input = " ".to_string();
1233 prompt.cursor_pos = 0;
1234
1235 prompt.delete_word_forward();
1236 assert_eq!(prompt.input, "");
1237 assert_eq!(prompt.cursor_pos, 0);
1238 }
1239
1240 #[test]
1241 fn test_multiple_word_deletions() {
1242 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1243 prompt.input = "one two three four".to_string();
1244 prompt.cursor_pos = 18;
1245
1246 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two three ");
1248
1249 prompt.delete_word_backward(); assert_eq!(prompt.input, "one two ");
1251
1252 prompt.delete_word_backward(); assert_eq!(prompt.input, "one ");
1254 }
1255
1256 #[test]
1258 fn test_selection_with_shift_arrows() {
1259 let mut prompt = Prompt::new("Command: ".to_string(), PromptType::QuickOpen);
1260 prompt.input = "hello world".to_string();
1261 prompt.cursor_pos = 5; assert!(!prompt.has_selection());
1265 assert_eq!(prompt.selected_text(), None);
1266
1267 prompt.move_right_selecting();
1269 assert!(prompt.has_selection());
1270 assert_eq!(prompt.selection_range(), Some((5, 6)));
1271 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1272
1273 prompt.move_right_selecting();
1275 assert_eq!(prompt.selection_range(), Some((5, 7)));
1276 assert_eq!(prompt.selected_text(), Some(" w".to_string()));
1277
1278 prompt.move_left_selecting();
1280 assert_eq!(prompt.selection_range(), Some((5, 6)));
1281 assert_eq!(prompt.selected_text(), Some(" ".to_string()));
1282 }
1283
1284 #[test]
1285 fn test_selection_backward() {
1286 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1287 prompt.input = "abcdef".to_string();
1288 prompt.cursor_pos = 4; prompt.move_left_selecting();
1292 prompt.move_left_selecting();
1293 assert!(prompt.has_selection());
1294 assert_eq!(prompt.selection_range(), Some((2, 4)));
1295 assert_eq!(prompt.selected_text(), Some("cd".to_string()));
1296 }
1297
1298 #[test]
1299 fn test_selection_with_home_end() {
1300 let mut prompt = Prompt::new("Prompt: ".to_string(), PromptType::QuickOpen);
1301 prompt.input = "select this text".to_string();
1302 prompt.cursor_pos = 7; prompt.move_end_selecting();
1306 assert_eq!(prompt.selection_range(), Some((7, 16)));
1307 assert_eq!(prompt.selected_text(), Some("this text".to_string()));
1308
1309 prompt.clear_selection();
1311 prompt.move_home_selecting();
1312 assert_eq!(prompt.selection_range(), Some((0, 16)));
1313 assert_eq!(prompt.selected_text(), Some("select this text".to_string()));
1314 }
1315
1316 #[test]
1317 fn test_word_selection() {
1318 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1319 prompt.input = "one two three".to_string();
1320 prompt.cursor_pos = 4; prompt.move_word_right_selecting();
1324 assert_eq!(prompt.selection_range(), Some((4, 7)));
1325 assert_eq!(prompt.selected_text(), Some("two".to_string()));
1326
1327 prompt.move_word_right_selecting();
1329 assert_eq!(prompt.selection_range(), Some((4, 13)));
1330 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1331 }
1332
1333 #[test]
1334 fn test_word_selection_backward() {
1335 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1336 prompt.input = "one two three".to_string();
1337 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1341 assert_eq!(prompt.selection_range(), Some((8, 13)));
1342 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1343
1344 }
1349
1350 #[test]
1351 fn test_delete_selection() {
1352 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1353 prompt.input = "hello world".to_string();
1354 prompt.cursor_pos = 5;
1355
1356 prompt.move_end_selecting();
1358 assert_eq!(prompt.selected_text(), Some(" world".to_string()));
1359
1360 let deleted = prompt.delete_selection();
1362 assert_eq!(deleted, Some(" world".to_string()));
1363 assert_eq!(prompt.input, "hello");
1364 assert_eq!(prompt.cursor_pos, 5);
1365 assert!(!prompt.has_selection());
1366 }
1367
1368 #[test]
1369 fn test_insert_deletes_selection() {
1370 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1371 prompt.input = "hello world".to_string();
1372 prompt.cursor_pos = 0;
1373
1374 for _ in 0..5 {
1376 prompt.move_right_selecting();
1377 }
1378 assert_eq!(prompt.selected_text(), Some("hello".to_string()));
1379
1380 prompt.insert_str("goodbye");
1382 assert_eq!(prompt.input, "goodbye world");
1383 assert_eq!(prompt.cursor_pos, 7);
1384 assert!(!prompt.has_selection());
1385 }
1386
1387 #[test]
1388 fn test_clear_selection() {
1389 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1390 prompt.input = "test".to_string();
1391 prompt.cursor_pos = 0;
1392
1393 prompt.move_end_selecting();
1395 assert!(prompt.has_selection());
1396
1397 prompt.clear_selection();
1399 assert!(!prompt.has_selection());
1400 assert_eq!(prompt.cursor_pos, 4); assert_eq!(prompt.input, "test"); }
1403
1404 #[test]
1405 fn test_selection_edge_cases() {
1406 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1407 prompt.input = "abc".to_string();
1408 prompt.cursor_pos = 3;
1409
1410 prompt.move_right_selecting();
1412 assert_eq!(prompt.cursor_pos, 3);
1413 assert_eq!(prompt.selection_range(), None);
1415 assert_eq!(prompt.selected_text(), None);
1416
1417 assert_eq!(prompt.delete_selection(), None);
1419 assert_eq!(prompt.input, "abc");
1420 }
1421
1422 #[test]
1423 fn test_selection_with_unicode() {
1424 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1425 prompt.input = "hello 世界 world".to_string();
1426 prompt.cursor_pos = 6; for _ in 0..2 {
1430 prompt.move_right_selecting();
1431 }
1432
1433 let selected = prompt.selected_text().unwrap();
1434 assert_eq!(selected, "世界");
1435
1436 prompt.delete_selection();
1438 assert_eq!(prompt.input, "hello world");
1439 }
1440
1441 #[test]
1445 fn test_word_selection_continues_across_words() {
1446 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1447 prompt.input = "one two three".to_string();
1448 prompt.cursor_pos = 13; prompt.move_word_left_selecting();
1452 assert_eq!(prompt.selection_range(), Some((8, 13)));
1453 assert_eq!(prompt.selected_text(), Some("three".to_string()));
1454
1455 prompt.move_word_left_selecting();
1458
1459 assert_eq!(prompt.selection_range(), Some((4, 13)));
1461 assert_eq!(prompt.selected_text(), Some("two three".to_string()));
1462 }
1463
1464 #[cfg(test)]
1466 mod property_tests {
1467 use super::*;
1468 use proptest::prelude::*;
1469
1470 proptest! {
1471 #[test]
1473 fn prop_delete_word_backward_shrinks(
1474 input in "[a-zA-Z0-9_ ]{0,50}",
1475 cursor_pos in 0usize..50
1476 ) {
1477 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1478 prompt.input = input.clone();
1479 prompt.cursor_pos = cursor_pos.min(input.len());
1480
1481 let original_len = prompt.input.len();
1482 prompt.delete_word_backward();
1483
1484 prop_assert!(prompt.input.len() <= original_len);
1485 }
1486
1487 #[test]
1489 fn prop_delete_word_forward_shrinks(
1490 input in "[a-zA-Z0-9_ ]{0,50}",
1491 cursor_pos in 0usize..50
1492 ) {
1493 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1494 prompt.input = input.clone();
1495 prompt.cursor_pos = cursor_pos.min(input.len());
1496
1497 let original_len = prompt.input.len();
1498 prompt.delete_word_forward();
1499
1500 prop_assert!(prompt.input.len() <= original_len);
1501 }
1502
1503 #[test]
1505 fn prop_delete_word_backward_cursor_valid(
1506 input in "[a-zA-Z0-9_ ]{0,50}",
1507 cursor_pos in 0usize..50
1508 ) {
1509 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1510 prompt.input = input.clone();
1511 prompt.cursor_pos = cursor_pos.min(input.len());
1512
1513 prompt.delete_word_backward();
1514
1515 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1516 }
1517
1518 #[test]
1520 fn prop_delete_word_forward_cursor_valid(
1521 input in "[a-zA-Z0-9_ ]{0,50}",
1522 cursor_pos in 0usize..50
1523 ) {
1524 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1525 prompt.input = input.clone();
1526 prompt.cursor_pos = cursor_pos.min(input.len());
1527
1528 prompt.delete_word_forward();
1529
1530 prop_assert!(prompt.cursor_pos <= prompt.input.len());
1531 }
1532
1533 #[test]
1535 fn prop_insert_str_length(
1536 input in "[a-zA-Z0-9_ ]{0,30}",
1537 insert in "[a-zA-Z0-9_ ]{0,20}",
1538 cursor_pos in 0usize..30
1539 ) {
1540 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1541 prompt.input = input.clone();
1542 prompt.cursor_pos = cursor_pos.min(input.len());
1543
1544 let original_len = prompt.input.len();
1545 prompt.insert_str(&insert);
1546
1547 prop_assert_eq!(prompt.input.len(), original_len + insert.len());
1548 }
1549
1550 #[test]
1552 fn prop_insert_str_cursor(
1553 input in "[a-zA-Z0-9_ ]{0,30}",
1554 insert in "[a-zA-Z0-9_ ]{0,20}",
1555 cursor_pos in 0usize..30
1556 ) {
1557 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1558 prompt.input = input.clone();
1559 let original_pos = cursor_pos.min(input.len());
1560 prompt.cursor_pos = original_pos;
1561
1562 prompt.insert_str(&insert);
1563
1564 prop_assert_eq!(prompt.cursor_pos, original_pos + insert.len());
1565 }
1566
1567 #[test]
1569 fn prop_clear_resets(input in "[a-zA-Z0-9_ ]{0,50}") {
1570 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
1571 prompt.input = input;
1572 prompt.cursor_pos = prompt.input.len();
1573
1574 prompt.clear();
1575
1576 prop_assert_eq!(prompt.input, "");
1577 prop_assert_eq!(prompt.cursor_pos, 0);
1578 }
1579 }
1580 }
1581}