fresh/view/
prompt_input.rs1use super::prompt::Prompt;
7use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10impl InputHandler for Prompt {
11 fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
12 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
13 let alt = event.modifiers.contains(KeyModifiers::ALT);
14 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
15
16 match event.code {
17 KeyCode::Enter => {
19 ctx.defer(DeferredAction::ConfirmPrompt);
20 InputResult::Consumed
21 }
22 KeyCode::Esc => {
23 ctx.defer(DeferredAction::ClosePrompt);
24 InputResult::Consumed
25 }
26
27 KeyCode::Char(_) if alt => InputResult::Ignored,
29
30 KeyCode::Char(c) if !ctrl => {
32 if self.has_selection() {
34 self.delete_selection();
35 }
36 if shift {
37 self.insert_char(c.to_ascii_uppercase());
38 } else {
39 self.insert_char(c);
40 }
41 ctx.defer(DeferredAction::UpdatePromptSuggestions);
42 InputResult::Consumed
43 }
44 KeyCode::Char(c) if ctrl => self.handle_ctrl_key(c, ctx),
45
46 KeyCode::Backspace if ctrl => {
48 self.delete_word_backward();
49 ctx.defer(DeferredAction::UpdatePromptSuggestions);
50 InputResult::Consumed
51 }
52 KeyCode::Backspace => {
53 if self.has_selection() {
54 self.delete_selection();
55 } else {
56 self.backspace();
57 }
58 ctx.defer(DeferredAction::UpdatePromptSuggestions);
59 InputResult::Consumed
60 }
61 KeyCode::Delete if ctrl => {
62 self.delete_word_forward();
63 ctx.defer(DeferredAction::UpdatePromptSuggestions);
64 InputResult::Consumed
65 }
66 KeyCode::Delete => {
67 if self.has_selection() {
68 self.delete_selection();
69 } else {
70 self.delete();
71 }
72 ctx.defer(DeferredAction::UpdatePromptSuggestions);
73 InputResult::Consumed
74 }
75
76 KeyCode::Left if ctrl && shift => {
78 self.move_word_left_selecting();
79 InputResult::Consumed
80 }
81 KeyCode::Left if ctrl => {
82 self.move_word_left();
83 InputResult::Consumed
84 }
85 KeyCode::Left if shift => {
86 self.move_left_selecting();
87 InputResult::Consumed
88 }
89 KeyCode::Left => {
90 self.clear_selection();
91 self.cursor_left();
92 InputResult::Consumed
93 }
94 KeyCode::Right if ctrl && shift => {
95 self.move_word_right_selecting();
96 InputResult::Consumed
97 }
98 KeyCode::Right if ctrl => {
99 self.move_word_right();
100 InputResult::Consumed
101 }
102 KeyCode::Right if shift => {
103 self.move_right_selecting();
104 InputResult::Consumed
105 }
106 KeyCode::Right => {
107 self.clear_selection();
108 self.cursor_right();
109 InputResult::Consumed
110 }
111 KeyCode::Home if shift => {
112 self.move_home_selecting();
113 InputResult::Consumed
114 }
115 KeyCode::Home => {
116 self.clear_selection();
117 self.move_to_start();
118 InputResult::Consumed
119 }
120 KeyCode::End if shift => {
121 self.move_end_selecting();
122 InputResult::Consumed
123 }
124 KeyCode::End => {
125 self.clear_selection();
126 self.move_to_end();
127 InputResult::Consumed
128 }
129
130 KeyCode::Up => {
136 if !self.suggestions.is_empty() {
137 if let Some(selected) = self.selected_suggestion {
139 let new_selected = if selected == 0 { 0 } else { selected - 1 };
140 self.selected_suggestion = Some(new_selected);
141 let should_sync = self.sync_input_on_navigate
144 || !matches!(
145 self.prompt_type,
146 crate::view::prompt::PromptType::Plugin { .. }
147 | crate::view::prompt::PromptType::QuickOpen
148 | crate::view::prompt::PromptType::LiveGrep
149 );
150 if should_sync {
151 if let Some(suggestion) = self.suggestions.get(new_selected) {
152 self.input = suggestion.get_value().to_string();
153 self.cursor_pos = self.input.len();
154 self.selection_anchor = Some(0);
155 }
156 }
157 if matches!(
159 self.prompt_type,
160 crate::view::prompt::PromptType::SelectTheme { .. }
161 ) {
162 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
163 }
164 if matches!(
166 self.prompt_type,
167 crate::view::prompt::PromptType::Plugin { .. }
168 ) {
169 ctx.defer(DeferredAction::PromptSelectionChanged {
170 selected_index: new_selected,
171 });
172 }
173 }
174 } else {
175 ctx.defer(DeferredAction::PromptHistoryPrev);
177 }
178 InputResult::Consumed
179 }
180 KeyCode::Down => {
181 if !self.suggestions.is_empty() {
182 if let Some(selected) = self.selected_suggestion {
184 let new_selected = (selected + 1).min(self.suggestions.len() - 1);
185 self.selected_suggestion = Some(new_selected);
186 let should_sync = self.sync_input_on_navigate
189 || !matches!(
190 self.prompt_type,
191 crate::view::prompt::PromptType::Plugin { .. }
192 | crate::view::prompt::PromptType::QuickOpen
193 | crate::view::prompt::PromptType::LiveGrep
194 );
195 if should_sync {
196 if let Some(suggestion) = self.suggestions.get(new_selected) {
197 self.input = suggestion.get_value().to_string();
198 self.cursor_pos = self.input.len();
199 self.selection_anchor = Some(0);
200 }
201 }
202 if matches!(
204 self.prompt_type,
205 crate::view::prompt::PromptType::SelectTheme { .. }
206 ) {
207 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
208 }
209 if matches!(
211 self.prompt_type,
212 crate::view::prompt::PromptType::Plugin { .. }
213 ) {
214 ctx.defer(DeferredAction::PromptSelectionChanged {
215 selected_index: new_selected,
216 });
217 }
218 }
219 } else {
220 ctx.defer(DeferredAction::PromptHistoryNext);
222 }
223 InputResult::Consumed
224 }
225 KeyCode::PageUp => {
226 if let Some(selected) = self.selected_suggestion {
227 self.selected_suggestion = Some(selected.saturating_sub(10));
228 }
229 InputResult::Consumed
230 }
231 KeyCode::PageDown => {
232 if let Some(selected) = self.selected_suggestion {
233 let len = self.suggestions.len();
234 let new_pos = selected + 10;
235 self.selected_suggestion = Some(new_pos.min(len.saturating_sub(1)));
236 }
237 InputResult::Consumed
238 }
239
240 KeyCode::Tab => {
242 if self.overlay {
249 return InputResult::Consumed;
250 }
251 if let Some(selected) = self.selected_suggestion {
252 if let Some(suggestion) = self.suggestions.get(selected) {
253 if !suggestion.disabled {
254 let value = suggestion.get_value().to_string();
255 if matches!(
257 self.prompt_type,
258 crate::view::prompt::PromptType::QuickOpen
259 ) {
260 let prefix = self
261 .input
262 .chars()
263 .next()
264 .filter(|c| *c == '>' || *c == '#' || *c == ':');
265 if let Some(p) = prefix {
266 self.input = format!("{}{}", p, value);
267 } else {
268 self.input = value;
269 }
270 } else {
271 self.input = value;
272 }
273 self.cursor_pos = self.input.len();
274 self.clear_selection();
275 }
276 }
277 }
278 ctx.defer(DeferredAction::UpdatePromptSuggestions);
279 InputResult::Consumed
280 }
281
282 _ => InputResult::Consumed, }
284 }
285
286 fn is_modal(&self) -> bool {
287 true
288 }
289}
290
291impl Prompt {
292 fn handle_ctrl_key(&mut self, c: char, ctx: &mut InputContext) -> InputResult {
293 match c {
294 'a' => {
295 self.selection_anchor = Some(0);
297 self.cursor_pos = self.input.len();
298 InputResult::Consumed
299 }
300 'c' => {
301 ctx.defer(DeferredAction::ExecuteAction(
303 crate::input::keybindings::Action::PromptCopy,
304 ));
305 InputResult::Consumed
306 }
307 'x' => {
308 ctx.defer(DeferredAction::ExecuteAction(
310 crate::input::keybindings::Action::PromptCut,
311 ));
312 InputResult::Consumed
313 }
314 'v' => {
315 ctx.defer(DeferredAction::ExecuteAction(
317 crate::input::keybindings::Action::PromptPaste,
318 ));
319 InputResult::Consumed
320 }
321 'k' => {
322 self.delete_to_end();
324 ctx.defer(DeferredAction::UpdatePromptSuggestions);
325 InputResult::Consumed
326 }
327 'z' => {
328 if self.undo_input() {
333 ctx.defer(DeferredAction::UpdatePromptSuggestions);
334 }
335 InputResult::Consumed
336 }
337 'y' => {
338 if self.redo_input() {
340 ctx.defer(DeferredAction::UpdatePromptSuggestions);
341 }
342 InputResult::Consumed
343 }
344 _ => InputResult::Ignored,
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::view::prompt::PromptType;
354
355 fn key(code: KeyCode) -> KeyEvent {
356 KeyEvent::new(code, KeyModifiers::NONE)
357 }
358
359 fn key_with_ctrl(c: char) -> KeyEvent {
360 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
361 }
362
363 fn key_with_shift(code: KeyCode) -> KeyEvent {
364 KeyEvent::new(code, KeyModifiers::SHIFT)
365 }
366
367 #[test]
368 fn test_prompt_character_input() {
369 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
370 let mut ctx = InputContext::new();
371
372 prompt.handle_key_event(
373 &KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
374 &mut ctx,
375 );
376 prompt.handle_key_event(
377 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
378 &mut ctx,
379 );
380
381 assert_eq!(prompt.input, "hi");
382 assert_eq!(prompt.cursor_pos, 2);
383 }
384
385 #[test]
386 fn test_prompt_backspace() {
387 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
388 prompt.input = "hello".to_string();
389 prompt.cursor_pos = 5;
390 let mut ctx = InputContext::new();
391
392 prompt.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
393 assert_eq!(prompt.input, "hell");
394 assert_eq!(prompt.cursor_pos, 4);
395 }
396
397 #[test]
398 fn test_prompt_cursor_movement() {
399 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
400 prompt.input = "hello".to_string();
401 prompt.cursor_pos = 5;
402 let mut ctx = InputContext::new();
403
404 prompt.handle_key_event(&key(KeyCode::Home), &mut ctx);
406 assert_eq!(prompt.cursor_pos, 0);
407
408 prompt.handle_key_event(&key(KeyCode::End), &mut ctx);
410 assert_eq!(prompt.cursor_pos, 5);
411
412 prompt.handle_key_event(&key(KeyCode::Left), &mut ctx);
414 assert_eq!(prompt.cursor_pos, 4);
415
416 prompt.handle_key_event(&key(KeyCode::Right), &mut ctx);
418 assert_eq!(prompt.cursor_pos, 5);
419 }
420
421 #[test]
422 fn test_prompt_selection() {
423 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
424 prompt.input = "hello world".to_string();
425 prompt.cursor_pos = 0;
426 let mut ctx = InputContext::new();
427
428 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
430 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
431 assert!(prompt.has_selection());
432 assert_eq!(prompt.selected_text(), Some("he".to_string()));
433
434 prompt.handle_key_event(&key_with_ctrl('a'), &mut ctx);
436 assert_eq!(prompt.selected_text(), Some("hello world".to_string()));
437 }
438
439 #[test]
440 fn test_prompt_enter_confirms() {
441 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
442 let mut ctx = InputContext::new();
443
444 prompt.handle_key_event(&key(KeyCode::Enter), &mut ctx);
445 assert!(ctx
446 .deferred_actions
447 .iter()
448 .any(|a| matches!(a, DeferredAction::ConfirmPrompt)));
449 }
450
451 #[test]
452 fn test_prompt_escape_cancels() {
453 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
454 let mut ctx = InputContext::new();
455
456 prompt.handle_key_event(&key(KeyCode::Esc), &mut ctx);
457 assert!(ctx
458 .deferred_actions
459 .iter()
460 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
461 }
462
463 #[test]
464 fn test_prompt_is_modal() {
465 let prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
466 assert!(prompt.is_modal());
467 }
468
469 #[test]
470 fn test_prompt_ctrl_p_returns_ignored() {
471 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
472 let mut ctx = InputContext::new();
473
474 let result = prompt.handle_key_event(&key_with_ctrl('p'), &mut ctx);
476 assert_eq!(result, InputResult::Ignored, "Ctrl+P should return Ignored");
477 }
478
479 #[test]
480 fn test_prompt_ctrl_p_dispatch_returns_ignored() {
481 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
482 let mut ctx = InputContext::new();
483
484 let result = prompt.dispatch_input(&key_with_ctrl('p'), &mut ctx);
486 assert_eq!(
487 result,
488 InputResult::Ignored,
489 "dispatch_input should return Ignored for Ctrl+P"
490 );
491 }
492}