1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use std::collections::HashMap;
6
7use crate::buffer::AppMode;
8use crate::ui::input::actions::{Action, ActionContext, CursorPosition, NavigateAction};
9
10pub struct KeyMapper {
12 global_mappings: HashMap<(KeyCode, KeyModifiers), Action>,
14
15 mode_mappings: HashMap<AppMode, HashMap<(KeyCode, KeyModifiers), Action>>,
17
18 count_buffer: String,
20
21 vim_command_buffer: String,
23}
24
25impl KeyMapper {
26 #[must_use]
27 pub fn new() -> Self {
28 let mut mapper = Self {
29 global_mappings: HashMap::new(),
30 mode_mappings: HashMap::new(),
31 count_buffer: String::new(),
32 vim_command_buffer: String::new(),
33 };
34
35 mapper.init_global_mappings();
36 mapper.init_mode_mappings();
37 mapper
38 }
39
40 fn init_global_mappings(&mut self) {
42 use KeyCode::{Char, F};
43 use KeyModifiers as Mod;
44
45 self.global_mappings
47 .insert((F(1), Mod::NONE), Action::ShowHelp);
48 self.global_mappings
49 .insert((F(3), Mod::NONE), Action::ShowPrettyQuery);
50 self.global_mappings
51 .insert((F(5), Mod::NONE), Action::ShowDebugInfo);
52 self.global_mappings
53 .insert((F(6), Mod::NONE), Action::ToggleRowNumbers);
54 self.global_mappings
55 .insert((F(7), Mod::NONE), Action::ToggleCompactMode);
56 self.global_mappings
57 .insert((F(8), Mod::NONE), Action::ToggleCaseInsensitive);
58 self.global_mappings
59 .insert((F(9), Mod::NONE), Action::KillLine);
60 self.global_mappings
61 .insert((F(10), Mod::NONE), Action::KillLineBackward);
62 self.global_mappings
63 .insert((F(12), Mod::NONE), Action::ToggleKeyIndicator);
64
65 self.global_mappings
67 .insert((Char('c'), Mod::CONTROL), Action::ForceQuit);
68 self.global_mappings
69 .insert((Char('C'), Mod::CONTROL), Action::ForceQuit);
70 }
71
72 fn init_mode_mappings(&mut self) {
74 self.init_results_mappings();
75 self.init_command_mappings();
76 }
78
79 fn init_results_mappings(&mut self) {
81 use crate::buffer::AppMode;
82 use KeyCode::{Char, Down, End, Esc, Home, Left, PageDown, PageUp, Right, Up, F};
83 use KeyModifiers as Mod;
84
85 let mut mappings = HashMap::new();
86
87 mappings.insert((Up, Mod::NONE), Action::Navigate(NavigateAction::Up(1)));
89 mappings.insert((Down, Mod::NONE), Action::Navigate(NavigateAction::Down(1)));
90 mappings.insert((Left, Mod::NONE), Action::Navigate(NavigateAction::Left(1)));
91 mappings.insert(
92 (Right, Mod::NONE),
93 Action::Navigate(NavigateAction::Right(1)),
94 );
95
96 mappings.insert(
97 (PageUp, Mod::NONE),
98 Action::Navigate(NavigateAction::PageUp),
99 );
100 mappings.insert(
101 (PageDown, Mod::NONE),
102 Action::Navigate(NavigateAction::PageDown),
103 );
104
105 mappings.insert(
107 (Char('f'), Mod::CONTROL),
108 Action::Navigate(NavigateAction::PageDown),
109 );
110 mappings.insert(
111 (Char('b'), Mod::CONTROL),
112 Action::Navigate(NavigateAction::PageUp),
113 );
114
115 mappings.insert((Home, Mod::NONE), Action::Navigate(NavigateAction::Home));
116 mappings.insert((End, Mod::NONE), Action::Navigate(NavigateAction::End));
117
118 mappings.insert(
120 (Char('h'), Mod::NONE),
121 Action::Navigate(NavigateAction::Left(1)),
122 );
123 mappings.insert(
124 (Char('j'), Mod::NONE),
125 Action::Navigate(NavigateAction::Down(1)),
126 );
127 mappings.insert(
128 (Char('k'), Mod::NONE),
129 Action::Navigate(NavigateAction::Up(1)),
130 );
131 mappings.insert(
132 (Char('l'), Mod::NONE),
133 Action::Navigate(NavigateAction::Right(1)),
134 );
135
136 mappings.insert((Left, Mod::NONE), Action::Navigate(NavigateAction::Left(1)));
138 mappings.insert(
139 (Right, Mod::NONE),
140 Action::Navigate(NavigateAction::Right(1)),
141 );
142 mappings.insert((Down, Mod::NONE), Action::Navigate(NavigateAction::Down(1)));
143 mappings.insert((Up, Mod::NONE), Action::Navigate(NavigateAction::Up(1))); mappings.insert(
147 (PageUp, Mod::NONE),
148 Action::Navigate(NavigateAction::PageUp),
149 );
150 mappings.insert(
151 (PageDown, Mod::NONE),
152 Action::Navigate(NavigateAction::PageDown),
153 );
154
155 mappings.insert(
159 (Char('G'), Mod::SHIFT),
160 Action::Navigate(NavigateAction::End),
161 );
162
163 mappings.insert(
165 (Char('0'), Mod::NONE),
166 Action::Navigate(NavigateAction::FirstColumn),
167 );
168 mappings.insert(
169 (Char('^'), Mod::NONE),
170 Action::Navigate(NavigateAction::FirstColumn),
171 );
172 mappings.insert(
173 (Char('$'), Mod::NONE),
174 Action::Navigate(NavigateAction::LastColumn),
175 );
176
177 mappings.insert((Char('H'), Mod::SHIFT), Action::NavigateToViewportTop);
179 mappings.insert((Char('M'), Mod::SHIFT), Action::NavigateToViewportMiddle);
180 mappings.insert((Char('L'), Mod::SHIFT), Action::NavigateToViewportBottom);
181
182 mappings.insert((Esc, Mod::NONE), Action::ExitCurrentMode);
184 mappings.insert((Char('q'), Mod::NONE), Action::Quit);
185 mappings.insert((Char('c'), Mod::CONTROL), Action::Quit); mappings.insert((F(2), Mod::NONE), Action::SwitchMode(AppMode::Command));
189
190 mappings.insert(
192 (Char('i'), Mod::NONE),
193 Action::SwitchModeWithCursor(AppMode::Command, CursorPosition::Current),
194 );
195
196 mappings.insert(
198 (Char('a'), Mod::NONE),
199 Action::SwitchModeWithCursor(AppMode::Command, CursorPosition::End),
200 );
201
202 mappings.insert((Char('p'), Mod::NONE), Action::ToggleColumnPin);
204 mappings.insert((Char('-'), Mod::NONE), Action::HideColumn); mappings.insert(
206 (Char('H'), Mod::CONTROL | Mod::SHIFT),
207 Action::UnhideAllColumns,
208 );
209 mappings.insert((Char('+'), Mod::NONE), Action::UnhideAllColumns); mappings.insert((Char('='), Mod::NONE), Action::UnhideAllColumns); mappings.insert((Char('e'), Mod::NONE), Action::HideEmptyColumns);
213 mappings.insert((Char('E'), Mod::SHIFT), Action::HideEmptyColumns);
214 mappings.insert((Left, Mod::SHIFT), Action::MoveColumnLeft);
215 mappings.insert((Right, Mod::SHIFT), Action::MoveColumnRight);
216 mappings.insert((Char('<'), Mod::NONE), Action::MoveColumnLeft);
218 mappings.insert((Char('>'), Mod::NONE), Action::MoveColumnRight);
219 mappings.insert((Char('/'), Mod::NONE), Action::StartSearch);
221 mappings.insert((Char('\\'), Mod::NONE), Action::StartColumnSearch);
222 mappings.insert((Char('f'), Mod::NONE), Action::StartFilter);
223 mappings.insert((Char('F'), Mod::SHIFT), Action::StartFuzzyFilter);
224
225 mappings.insert((Char('s'), Mod::NONE), Action::Sort(None));
227
228 mappings.insert((Char('N'), Mod::NONE), Action::ToggleRowNumbers);
230 mappings.insert((Char('C'), Mod::NONE), Action::ToggleCompactMode);
231
232 mappings.insert((Char('x'), Mod::CONTROL), Action::ExportToCsv);
234 mappings.insert((Char('j'), Mod::CONTROL), Action::ExportToJson);
235
236 mappings.insert((Char('C'), Mod::SHIFT), Action::ClearFilter);
238
239 mappings.insert((Char(':'), Mod::NONE), Action::StartJumpToRow);
241
242 mappings.insert((Char('n'), Mod::NONE), Action::NextSearchMatch);
244 mappings.insert((Char('N'), Mod::SHIFT), Action::PreviousSearchMatch);
245
246 mappings.insert((Char('v'), Mod::NONE), Action::ToggleSelectionMode);
248
249 mappings.insert((Char('S'), Mod::SHIFT), Action::ShowColumnStatistics);
251
252 mappings.insert((Char('s'), Mod::ALT), Action::CycleColumnPacking);
254
255 mappings.insert((Char(' '), Mod::NONE), Action::ToggleViewportLock);
257 mappings.insert((Char('x'), Mod::NONE), Action::ToggleCursorLock);
258 mappings.insert((Char('X'), Mod::SHIFT), Action::ToggleCursorLock);
259 mappings.insert((Char(' '), Mod::CONTROL), Action::ToggleViewportLock);
260
261 mappings.insert((Char('?'), Mod::NONE), Action::ShowHelp); mappings.insert((Char('P'), Mod::SHIFT), Action::ClearAllPins);
267
268 mappings.insert((Char('r'), Mod::CONTROL), Action::StartHistorySearch);
270
271 self.mode_mappings.insert(AppMode::Results, mappings);
272 }
273
274 fn init_command_mappings(&mut self) {
276 use crate::buffer::AppMode;
277 use KeyCode::{Backspace, Char, Delete, Down, End, Enter, Home, Left, Right, Up, F};
278 use KeyModifiers as Mod;
279
280 let mut mappings = HashMap::new();
281
282 mappings.insert((Enter, Mod::NONE), Action::ExecuteQuery);
284
285 mappings.insert((F(2), Mod::NONE), Action::SwitchMode(AppMode::Results));
287
288 mappings.insert((Char('u'), Mod::CONTROL), Action::ClearLine);
290
291 mappings.insert((Char('z'), Mod::CONTROL), Action::Undo);
293 mappings.insert((Char('y'), Mod::CONTROL), Action::Redo);
294
295 mappings.insert((Left, Mod::NONE), Action::MoveCursorLeft);
297 mappings.insert((Right, Mod::NONE), Action::MoveCursorRight);
298 mappings.insert((Down, Mod::NONE), Action::SwitchMode(AppMode::Results)); mappings.insert((Home, Mod::NONE), Action::MoveCursorHome);
300 mappings.insert((End, Mod::NONE), Action::MoveCursorEnd);
301 mappings.insert((Char('a'), Mod::CONTROL), Action::MoveCursorHome);
302 mappings.insert((Char('e'), Mod::CONTROL), Action::MoveCursorEnd);
303 mappings.insert((Left, Mod::CONTROL), Action::MoveCursorWordLeft);
304 mappings.insert((Right, Mod::CONTROL), Action::MoveCursorWordRight);
305 mappings.insert((Char('b'), Mod::ALT), Action::MoveCursorWordLeft);
306 mappings.insert((Char('f'), Mod::ALT), Action::MoveCursorWordRight);
307
308 mappings.insert((Backspace, Mod::NONE), Action::Backspace);
310 mappings.insert((Delete, Mod::NONE), Action::Delete);
311 mappings.insert((Char('w'), Mod::CONTROL), Action::DeleteWordBackward);
312 mappings.insert((Char('d'), Mod::ALT), Action::DeleteWordForward);
313 mappings.insert((Char('k'), Mod::CONTROL), Action::KillLine);
314 mappings.insert((Char('v'), Mod::CONTROL), Action::Paste);
318
319 mappings.insert((Char('p'), Mod::CONTROL), Action::PreviousHistoryCommand);
321 mappings.insert((Char('n'), Mod::CONTROL), Action::NextHistoryCommand);
322 mappings.insert((Up, Mod::ALT), Action::PreviousHistoryCommand);
323 mappings.insert((Down, Mod::ALT), Action::NextHistoryCommand);
324
325 mappings.insert((Char('*'), Mod::CONTROL), Action::ExpandAsterisk);
327 mappings.insert((Char('*'), Mod::ALT), Action::ExpandAsteriskVisible);
328
329 self.mode_mappings.insert(AppMode::Command, mappings);
332 }
333
334 pub fn map_key(&mut self, key: KeyEvent, context: &ActionContext) -> Option<Action> {
336 if context.mode == AppMode::Results {
338 if let KeyCode::Char(c) = key.code {
339 if key.modifiers.is_empty() {
340 if !self.vim_command_buffer.is_empty() {
342 let command = format!("{}{}", self.vim_command_buffer, c);
344 let action = if command.as_str() == "gg" {
345 self.vim_command_buffer.clear();
347 Some(Action::Navigate(NavigateAction::Home))
348 } else {
349 self.vim_command_buffer.clear();
351 None
352 };
353
354 if action.is_some() {
355 return action;
356 }
357 }
358
359 if c.is_ascii_digit() {
361 self.count_buffer.push(c);
362 return None; }
364
365 if c == 'g' {
369 let key_combo = (key.code, key.modifiers);
370 if let Some(mode_mappings) = self.mode_mappings.get(&context.mode) {
371 if mode_mappings.contains_key(&key_combo) {
372 tracing::debug!(
375 "Key '{}' has standalone mapping, not treating as vim command",
376 c
377 );
378 } else {
379 self.vim_command_buffer.push(c);
381 tracing::debug!("Starting vim command buffer with '{}'", c);
382 return None; }
384 }
385 }
386 }
387 }
388 }
389
390 let action = self.map_key_internal(key, context);
392
393 if !self.count_buffer.is_empty() {
395 if let Some(mut action) = action {
396 if let Ok(count) = self.count_buffer.parse::<usize>() {
397 action = self.apply_count_to_action(action, count);
398 }
399 self.count_buffer.clear();
400 return Some(action);
401 }
402 self.count_buffer.clear();
404 }
405
406 action
407 }
408
409 fn map_key_internal(&self, key: KeyEvent, context: &ActionContext) -> Option<Action> {
411 let key_combo = (key.code, key.modifiers);
412
413 if let Some(action) = self.global_mappings.get(&key_combo) {
415 return Some(action.clone());
416 }
417
418 if let Some(mode_mappings) = self.mode_mappings.get(&context.mode) {
420 if let Some(action) = mode_mappings.get(&key_combo) {
421 return Some(action.clone());
422 }
423 }
424
425 if context.mode == AppMode::Command {
427 if let KeyCode::Char(c) = key.code {
428 if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
429 return Some(Action::InsertChar(c));
431 }
432 }
433 }
434
435 None
437 }
438
439 fn apply_count_to_action(&self, action: Action, count: usize) -> Action {
441 match action {
442 Action::Navigate(NavigateAction::Up(_)) => Action::Navigate(NavigateAction::Up(count)),
443 Action::Navigate(NavigateAction::Down(_)) => {
444 Action::Navigate(NavigateAction::Down(count))
445 }
446 Action::Navigate(NavigateAction::Left(_)) => {
447 Action::Navigate(NavigateAction::Left(count))
448 }
449 Action::Navigate(NavigateAction::Right(_)) => {
450 Action::Navigate(NavigateAction::Right(count))
451 }
452 _ => action,
454 }
455 }
456
457 pub fn clear_pending(&mut self) {
459 self.count_buffer.clear();
460 self.vim_command_buffer.clear();
461 }
462
463 #[must_use]
465 pub fn is_collecting_count(&self) -> bool {
466 !self.count_buffer.is_empty()
467 }
468
469 #[must_use]
471 pub fn get_count_buffer(&self) -> &str {
472 &self.count_buffer
473 }
474}
475
476impl Default for KeyMapper {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::app_state_container::SelectionMode;
486
487 #[test]
488 fn test_basic_navigation_mapping() {
489 let mut mapper = KeyMapper::new();
490 let context = ActionContext {
491 mode: AppMode::Results,
492 selection_mode: SelectionMode::Row,
493 has_results: true,
494 has_filter: false,
495 has_search: false,
496 row_count: 100,
497 column_count: 10,
498 current_row: 0,
499 current_column: 0,
500 };
501
502 let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
504 let action = mapper.map_key(key, &context);
505 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(1))));
506
507 let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
509 let action = mapper.map_key(key, &context);
510 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(1))));
511 }
512
513 #[test]
514 fn test_vim_count_motion() {
515 let mut mapper = KeyMapper::new();
516 let context = ActionContext {
517 mode: AppMode::Results,
518 selection_mode: SelectionMode::Row,
519 has_results: true,
520 has_filter: false,
521 has_search: false,
522 row_count: 100,
523 column_count: 10,
524 current_row: 0,
525 current_column: 0,
526 };
527
528 let key = KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE);
530 let action = mapper.map_key(key, &context);
531 assert_eq!(action, None); assert_eq!(mapper.get_count_buffer(), "5");
533
534 let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
536 let action = mapper.map_key(key, &context);
537 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(5))));
538 assert_eq!(mapper.get_count_buffer(), ""); }
540
541 #[test]
542 fn test_global_mapping_override() {
543 let mut mapper = KeyMapper::new();
544 let context = ActionContext {
545 mode: AppMode::Results,
546 selection_mode: SelectionMode::Row,
547 has_results: true,
548 has_filter: false,
549 has_search: false,
550 row_count: 100,
551 column_count: 10,
552 current_row: 0,
553 current_column: 0,
554 };
555
556 let key = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
558 let action = mapper.map_key(key, &context);
559 assert_eq!(action, Some(Action::ShowHelp));
560 }
561
562 #[test]
563 fn test_command_mode_editing_actions() {
564 let mut mapper = KeyMapper::new();
565 let context = ActionContext {
566 mode: AppMode::Command,
567 selection_mode: SelectionMode::Row,
568 has_results: false,
569 has_filter: false,
570 has_search: false,
571 row_count: 0,
572 column_count: 0,
573 current_row: 0,
574 current_column: 0,
575 };
576
577 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
579 let action = mapper.map_key(key, &context);
580 assert_eq!(action, Some(Action::InsertChar('a')));
581
582 let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
584 let action = mapper.map_key(key, &context);
585 assert_eq!(action, Some(Action::InsertChar('A')));
586
587 let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
589 let action = mapper.map_key(key, &context);
590 assert_eq!(action, Some(Action::Backspace));
591
592 let key = KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE);
594 let action = mapper.map_key(key, &context);
595 assert_eq!(action, Some(Action::Delete));
596
597 let key = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE);
599 let action = mapper.map_key(key, &context);
600 assert_eq!(action, Some(Action::MoveCursorLeft));
601
602 let key = KeyEvent::new(KeyCode::Right, KeyModifiers::NONE);
604 let action = mapper.map_key(key, &context);
605 assert_eq!(action, Some(Action::MoveCursorRight));
606
607 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
609 let action = mapper.map_key(key, &context);
610 assert_eq!(action, Some(Action::MoveCursorHome));
611
612 let key = KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL);
614 let action = mapper.map_key(key, &context);
615 assert_eq!(action, Some(Action::MoveCursorEnd));
616
617 let key = KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL);
619 let action = mapper.map_key(key, &context);
620 assert_eq!(action, Some(Action::ClearLine));
621
622 let key = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL);
624 let action = mapper.map_key(key, &context);
625 assert_eq!(action, Some(Action::DeleteWordBackward));
626
627 let key = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL);
629 let action = mapper.map_key(key, &context);
630 assert_eq!(action, Some(Action::Undo));
631
632 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
634 let action = mapper.map_key(key, &context);
635 assert_eq!(action, Some(Action::ExecuteQuery));
636 }
637
638 #[test]
639 fn test_vim_style_append_modes() {
640 let mut mapper = KeyMapper::new();
641 let context = ActionContext {
642 mode: AppMode::Results,
643 selection_mode: SelectionMode::Row,
644 has_results: true,
645 has_filter: false,
646 has_search: false,
647 row_count: 100,
648 column_count: 10,
649 current_row: 0,
650 current_column: 0,
651 };
652
653 let key = KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE);
655 let action = mapper.map_key(key, &context);
656 assert_eq!(
657 action,
658 Some(Action::SwitchModeWithCursor(
659 AppMode::Command,
660 CursorPosition::Current
661 ))
662 );
663
664 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
666 let action = mapper.map_key(key, &context);
667 assert_eq!(
668 action,
669 Some(Action::SwitchModeWithCursor(
670 AppMode::Command,
671 CursorPosition::End
672 ))
673 );
674
675 }
679
680 #[test]
681 fn test_sort_key_mapping() {
682 let mut mapper = KeyMapper::new();
683 let context = ActionContext {
684 mode: AppMode::Results,
685 selection_mode: SelectionMode::Row,
686 has_results: true,
687 has_filter: false,
688 has_search: false,
689 row_count: 100,
690 column_count: 10,
691 current_row: 0,
692 current_column: 0,
693 };
694
695 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
697 let action = mapper.map_key(key, &context);
698 assert_eq!(action, Some(Action::Sort(None)));
699 }
700
701 #[test]
702 fn test_vim_go_to_top() {
703 let mut mapper = KeyMapper::new();
704 let context = ActionContext {
705 mode: AppMode::Results,
706 selection_mode: SelectionMode::Row,
707 has_results: true,
708 has_filter: false,
709 has_search: false,
710 row_count: 100,
711 column_count: 10,
712 current_row: 0,
713 current_column: 0,
714 };
715
716 let key_g1 = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
718 let action_g1 = mapper.map_key(key_g1, &context);
719 assert_eq!(action_g1, None); let key_g2 = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
722 let action_gg = mapper.map_key(key_g2, &context);
723 assert_eq!(action_gg, Some(Action::Navigate(NavigateAction::Home)));
724 }
725
726 #[test]
727 fn test_bug_reproduction_s_key_not_found() {
728 let mut mapper = KeyMapper::new();
730 let context = ActionContext {
731 mode: AppMode::Results,
732 selection_mode: SelectionMode::Row,
733 has_results: true,
734 has_filter: false,
735 has_search: false,
736 row_count: 100,
737 column_count: 10,
738 current_row: 0,
739 current_column: 0,
740 };
741
742 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
745 let action = mapper.map_key(key, &context);
746
747 assert!(
749 action.is_some(),
750 "Bug reproduction: 's' key should map to an action, not return None"
751 );
752 assert_eq!(
753 action,
754 Some(Action::Sort(None)),
755 "Bug reproduction: 's' key should map to Sort action"
756 );
757 }
758}