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 self.manual_scroll = false;
139 if !self.suggestions.is_empty() {
140 if let Some(selected) = self.selected_suggestion {
142 let new_selected = if selected == 0 { 0 } else { selected - 1 };
143 self.selected_suggestion = Some(new_selected);
144 let should_sync = self.sync_input_on_navigate
147 || !matches!(
148 self.prompt_type,
149 crate::view::prompt::PromptType::Plugin { .. }
150 | crate::view::prompt::PromptType::QuickOpen
151 | crate::view::prompt::PromptType::LiveGrep
152 );
153 if should_sync {
154 if let Some(suggestion) = self.suggestions.get(new_selected) {
155 self.input = suggestion.get_value().to_string();
156 self.cursor_pos = self.input.len();
157 self.selection_anchor = Some(0);
158 }
159 }
160 if matches!(
162 self.prompt_type,
163 crate::view::prompt::PromptType::SelectTheme { .. }
164 ) {
165 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
166 }
167 if matches!(
169 self.prompt_type,
170 crate::view::prompt::PromptType::Plugin { .. }
171 ) {
172 ctx.defer(DeferredAction::PromptSelectionChanged {
173 selected_index: new_selected,
174 });
175 }
176 }
177 } else {
178 ctx.defer(DeferredAction::PromptHistoryPrev);
180 }
181 InputResult::Consumed
182 }
183 KeyCode::Down => {
184 self.manual_scroll = false;
185 if !self.suggestions.is_empty() {
186 if let Some(selected) = self.selected_suggestion {
188 let new_selected = (selected + 1).min(self.suggestions.len() - 1);
189 self.selected_suggestion = Some(new_selected);
190 let should_sync = self.sync_input_on_navigate
193 || !matches!(
194 self.prompt_type,
195 crate::view::prompt::PromptType::Plugin { .. }
196 | crate::view::prompt::PromptType::QuickOpen
197 | crate::view::prompt::PromptType::LiveGrep
198 );
199 if should_sync {
200 if let Some(suggestion) = self.suggestions.get(new_selected) {
201 self.input = suggestion.get_value().to_string();
202 self.cursor_pos = self.input.len();
203 self.selection_anchor = Some(0);
204 }
205 }
206 if matches!(
208 self.prompt_type,
209 crate::view::prompt::PromptType::SelectTheme { .. }
210 ) {
211 ctx.defer(DeferredAction::PreviewThemeFromPrompt);
212 }
213 if matches!(
215 self.prompt_type,
216 crate::view::prompt::PromptType::Plugin { .. }
217 ) {
218 ctx.defer(DeferredAction::PromptSelectionChanged {
219 selected_index: new_selected,
220 });
221 }
222 }
223 } else {
224 ctx.defer(DeferredAction::PromptHistoryNext);
226 }
227 InputResult::Consumed
228 }
229 KeyCode::PageUp => {
230 self.manual_scroll = false;
231 if let Some(selected) = self.selected_suggestion {
232 self.selected_suggestion = Some(selected.saturating_sub(10));
233 }
234 InputResult::Consumed
235 }
236 KeyCode::PageDown => {
237 self.manual_scroll = false;
238 if let Some(selected) = self.selected_suggestion {
239 let len = self.suggestions.len();
240 let new_pos = selected + 10;
241 self.selected_suggestion = Some(new_pos.min(len.saturating_sub(1)));
242 }
243 InputResult::Consumed
244 }
245
246 KeyCode::Tab => {
248 if self.overlay {
255 return InputResult::Consumed;
256 }
257 if let Some(selected) = self.selected_suggestion {
258 if let Some(suggestion) = self.suggestions.get(selected) {
259 if !suggestion.disabled {
260 let value = suggestion.get_value().to_string();
261 if matches!(
263 self.prompt_type,
264 crate::view::prompt::PromptType::QuickOpen
265 ) {
266 let prefix = self
267 .input
268 .chars()
269 .next()
270 .filter(|c| *c == '>' || *c == '#' || *c == ':');
271 if let Some(p) = prefix {
272 self.input = format!("{}{}", p, value);
273 } else {
274 self.input = value;
275 }
276 } else {
277 self.input = value;
278 }
279 self.cursor_pos = self.input.len();
280 self.clear_selection();
281 }
282 }
283 }
284 ctx.defer(DeferredAction::UpdatePromptSuggestions);
285 InputResult::Consumed
286 }
287
288 _ => InputResult::Consumed, }
290 }
291
292 fn is_modal(&self) -> bool {
293 true
294 }
295}
296
297impl Prompt {
298 fn handle_ctrl_key(&mut self, c: char, ctx: &mut InputContext) -> InputResult {
299 match c {
300 'a' => {
301 self.selection_anchor = Some(0);
303 self.cursor_pos = self.input.len();
304 InputResult::Consumed
305 }
306 'c' => {
307 ctx.defer(DeferredAction::ExecuteAction(
309 crate::input::keybindings::Action::PromptCopy,
310 ));
311 InputResult::Consumed
312 }
313 'x' => {
314 ctx.defer(DeferredAction::ExecuteAction(
316 crate::input::keybindings::Action::PromptCut,
317 ));
318 InputResult::Consumed
319 }
320 'v' => {
321 ctx.defer(DeferredAction::ExecuteAction(
323 crate::input::keybindings::Action::PromptPaste,
324 ));
325 InputResult::Consumed
326 }
327 'k' => {
328 self.delete_to_end();
330 ctx.defer(DeferredAction::UpdatePromptSuggestions);
331 InputResult::Consumed
332 }
333 'u' => {
334 self.clear_selection();
339 self.delete_to_start();
340 ctx.defer(DeferredAction::UpdatePromptSuggestions);
341 InputResult::Consumed
342 }
343 'z' => {
344 if self.undo_input() {
349 ctx.defer(DeferredAction::UpdatePromptSuggestions);
350 }
351 InputResult::Consumed
352 }
353 'y' => {
354 if self.redo_input() {
356 ctx.defer(DeferredAction::UpdatePromptSuggestions);
357 }
358 InputResult::Consumed
359 }
360 _ => InputResult::Ignored,
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::view::prompt::PromptType;
370
371 fn key(code: KeyCode) -> KeyEvent {
372 KeyEvent::new(code, KeyModifiers::NONE)
373 }
374
375 fn key_with_ctrl(c: char) -> KeyEvent {
376 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
377 }
378
379 fn key_with_shift(code: KeyCode) -> KeyEvent {
380 KeyEvent::new(code, KeyModifiers::SHIFT)
381 }
382
383 #[test]
384 fn test_prompt_character_input() {
385 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
386 let mut ctx = InputContext::new();
387
388 prompt.handle_key_event(
389 &KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
390 &mut ctx,
391 );
392 prompt.handle_key_event(
393 &KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
394 &mut ctx,
395 );
396
397 assert_eq!(prompt.input, "hi");
398 assert_eq!(prompt.cursor_pos, 2);
399 }
400
401 #[test]
402 fn test_prompt_backspace() {
403 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
404 prompt.input = "hello".to_string();
405 prompt.cursor_pos = 5;
406 let mut ctx = InputContext::new();
407
408 prompt.handle_key_event(&key(KeyCode::Backspace), &mut ctx);
409 assert_eq!(prompt.input, "hell");
410 assert_eq!(prompt.cursor_pos, 4);
411 }
412
413 #[test]
414 fn test_prompt_cursor_movement() {
415 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
416 prompt.input = "hello".to_string();
417 prompt.cursor_pos = 5;
418 let mut ctx = InputContext::new();
419
420 prompt.handle_key_event(&key(KeyCode::Home), &mut ctx);
422 assert_eq!(prompt.cursor_pos, 0);
423
424 prompt.handle_key_event(&key(KeyCode::End), &mut ctx);
426 assert_eq!(prompt.cursor_pos, 5);
427
428 prompt.handle_key_event(&key(KeyCode::Left), &mut ctx);
430 assert_eq!(prompt.cursor_pos, 4);
431
432 prompt.handle_key_event(&key(KeyCode::Right), &mut ctx);
434 assert_eq!(prompt.cursor_pos, 5);
435 }
436
437 #[test]
438 fn test_prompt_selection() {
439 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
440 prompt.input = "hello world".to_string();
441 prompt.cursor_pos = 0;
442 let mut ctx = InputContext::new();
443
444 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
446 prompt.handle_key_event(&key_with_shift(KeyCode::Right), &mut ctx);
447 assert!(prompt.has_selection());
448 assert_eq!(prompt.selected_text(), Some("he".to_string()));
449
450 prompt.handle_key_event(&key_with_ctrl('a'), &mut ctx);
452 assert_eq!(prompt.selected_text(), Some("hello world".to_string()));
453 }
454
455 #[test]
456 fn test_prompt_enter_confirms() {
457 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
458 let mut ctx = InputContext::new();
459
460 prompt.handle_key_event(&key(KeyCode::Enter), &mut ctx);
461 assert!(ctx
462 .deferred_actions
463 .iter()
464 .any(|a| matches!(a, DeferredAction::ConfirmPrompt)));
465 }
466
467 #[test]
468 fn test_prompt_escape_cancels() {
469 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
470 let mut ctx = InputContext::new();
471
472 prompt.handle_key_event(&key(KeyCode::Esc), &mut ctx);
473 assert!(ctx
474 .deferred_actions
475 .iter()
476 .any(|a| matches!(a, DeferredAction::ClosePrompt)));
477 }
478
479 #[test]
480 fn test_prompt_is_modal() {
481 let prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
482 assert!(prompt.is_modal());
483 }
484
485 #[test]
486 fn test_prompt_ctrl_p_returns_ignored() {
487 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
488 let mut ctx = InputContext::new();
489
490 let result = prompt.handle_key_event(&key_with_ctrl('p'), &mut ctx);
492 assert_eq!(result, InputResult::Ignored, "Ctrl+P should return Ignored");
493 }
494
495 #[test]
496 fn test_prompt_ctrl_p_dispatch_returns_ignored() {
497 let mut prompt = Prompt::new("Test: ".to_string(), PromptType::Search);
498 let mut ctx = InputContext::new();
499
500 let result = prompt.dispatch_input(&key_with_ctrl('p'), &mut ctx);
502 assert_eq!(
503 result,
504 InputResult::Ignored,
505 "dispatch_input should return Ignored for Ctrl+P"
506 );
507 }
508}