use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::app::InputMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
CursorDown(usize),
CursorUp(usize),
HalfPageDown,
HalfPageUp,
PageDown,
PageUp,
GoToTop,
GoToBottom,
Digit(u8),
NextFile,
PrevFile,
PendingZCommand,
PendingShiftZCommand,
PendingRBracketCommand,
PendingLBracketCommand,
PendingGCommand,
ReviewComment,
ToggleFileList,
ShrinkFileList,
GrowFileList,
TourGranularityCoarser,
TourGranularityFiner,
ScrollLeft(usize),
ScrollRight(usize),
GoToLineStart,
GoToLineEnd,
WordForward,
WordBackward,
CursorToTopOfScreen,
CursorToBottomOfScreen,
CenterOnCursor,
ScrollViewUp,
ScrollViewDown,
ToggleFocus,
ToggleFocusReverse,
SelectFile,
ToggleReviewed,
AddLineComment,
EditComment,
PendingDCommand,
SearchNext,
SearchPrev,
SparringAccept,
ToggleResolveThread,
EnterVisualMode,
AddRangeComment,
Quit,
ExportToClipboard,
ReturnToCommits,
EnterCommandMode,
EnterSearchMode,
ExitMode,
ToggleHelp,
ToggleViewer,
ToggleViewerRender,
ToggleAiSummary,
ToggleViewportPin,
JumpToAgentGhost,
InsertChar(char),
DeleteChar,
DeleteWord,
ClearLine,
SubmitInput,
TextCursorLeft,
TextCursorRight,
TextCursorLineStart,
TextCursorLineEnd,
TextCursorWordLeft,
TextCursorWordRight,
InsertSuggestion,
OpenExternalEditor,
CycleCommentType,
CycleCommentTypeReverse,
ConfirmYes,
ConfirmNo,
CommitSelectUp,
CommitSelectDown,
ToggleCommitSelect,
ConfirmCommitSelect,
CycleCommitNext,
CycleCommitPrev,
ToggleExpand,
CollapseAll,
ToggleFileCollapse,
SelectFileFull,
SubmitReview,
MergePR,
ReanchorOrphan,
OpenInBrowser,
OpenInEditor,
OpenReactionPicker,
ReactionPickerCancel,
ReactionPickerCursorLeft,
ReactionPickerCursorRight,
ReactionPickerSelect,
ReactionPickerSelectAt(usize),
OpenCommentTemplatePicker,
CommentTemplatePickerSelect,
CommentTemplatePickerCancel,
MentalModelOpen,
MentalModelSave,
MentalModelCancel,
MentalModelNextField,
MentalModelPrevField,
MentalModelInsertChar(char),
MentalModelBackspace,
None,
}
pub fn map_key_to_action(key: KeyEvent, mode: InputMode) -> Action {
match mode {
InputMode::Normal => map_normal_mode(key),
InputMode::Command => map_command_mode(key),
InputMode::Search => map_search_mode(key),
InputMode::Comment => map_comment_mode(key),
InputMode::Help => map_help_mode(key),
InputMode::Confirm => map_confirm_mode(key),
InputMode::CommitSelect => map_commit_select_mode(key),
InputMode::VisualSelect => map_visual_mode(key),
InputMode::ReviewSubmit => map_review_submit_mode(key),
InputMode::CommandPalette => map_command_palette_mode(key),
InputMode::ReactionPicker => map_reaction_picker_mode(key),
InputMode::CommentTemplatePicker => map_comment_template_picker_mode(key),
InputMode::MentalModelEdit => map_mental_model_edit_mode(key),
}
}
fn map_mental_model_edit_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) => Action::MentalModelCancel,
(KeyCode::Char('s'), KeyModifiers::CONTROL) => Action::MentalModelSave,
(KeyCode::Tab, KeyModifiers::NONE) => Action::MentalModelNextField,
(KeyCode::BackTab, _) => Action::MentalModelPrevField,
(KeyCode::Enter, KeyModifiers::NONE) => Action::MentalModelInsertChar('\n'),
(KeyCode::Backspace, _) => Action::MentalModelBackspace,
(KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
Action::MentalModelInsertChar(c)
}
_ => Action::None,
}
}
fn map_normal_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Char('j') | KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Char('k') | KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Char('d'), KeyModifiers::CONTROL) => Action::HalfPageDown,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::HalfPageUp,
(KeyCode::Char('f'), KeyModifiers::CONTROL) => Action::PageDown,
(KeyCode::Char('b'), KeyModifiers::CONTROL) => Action::PageUp,
(KeyCode::PageDown, KeyModifiers::NONE) => Action::PageDown,
(KeyCode::PageUp, KeyModifiers::NONE) => Action::PageUp,
(KeyCode::Char('g'), KeyModifiers::NONE) => Action::PendingGCommand,
(KeyCode::Char('G'), _) => Action::GoToBottom,
(KeyCode::Char('z'), KeyModifiers::NONE) => Action::PendingZCommand,
(KeyCode::Char('Z'), _) => Action::PendingShiftZCommand,
(KeyCode::Char('}'), _) => Action::NextFile,
(KeyCode::Char('{'), _) => Action::PrevFile,
(KeyCode::Char(']'), _) => Action::PendingRBracketCommand,
(KeyCode::Char('['), _) => Action::PendingLBracketCommand,
(KeyCode::Char(')'), _) => Action::CycleCommitNext,
(KeyCode::Char('('), _) => Action::CycleCommitPrev,
(KeyCode::Tab, KeyModifiers::NONE) => Action::ToggleFocus,
(KeyCode::BackTab, _) => Action::ToggleFocusReverse,
(KeyCode::Enter, KeyModifiers::NONE) => Action::SelectFile,
(KeyCode::Enter, KeyModifiers::SHIFT) => Action::SelectFileFull,
(KeyCode::Char('h') | KeyCode::Left, KeyModifiers::NONE) => Action::ScrollLeft(4),
(KeyCode::Char('l') | KeyCode::Right, KeyModifiers::NONE) => Action::ScrollRight(4),
(KeyCode::Char('$'), _) => Action::GoToLineEnd,
(KeyCode::Char('w'), KeyModifiers::NONE) => Action::WordForward,
(KeyCode::Char('b'), KeyModifiers::NONE) => Action::WordBackward,
(KeyCode::Char('H'), _) => Action::CursorToTopOfScreen,
(KeyCode::Char('L'), _) => Action::CursorToBottomOfScreen,
(KeyCode::Char('y'), KeyModifiers::CONTROL) => Action::ScrollViewUp,
(KeyCode::Char('e'), KeyModifiers::CONTROL) => Action::ScrollViewDown,
(KeyCode::Char('r'), KeyModifiers::NONE) => Action::ToggleReviewed,
(KeyCode::Char('a'), KeyModifiers::NONE) => Action::SparringAccept,
(KeyCode::Char('c'), KeyModifiers::NONE) => Action::AddLineComment,
(KeyCode::Char('C'), _) => Action::ReviewComment,
(KeyCode::Char('\\'), _) => Action::ToggleFileList,
(KeyCode::Char('t'), KeyModifiers::NONE) => Action::ToggleViewer,
(KeyCode::Char('T'), _) => Action::ToggleViewerRender,
(KeyCode::Char('<'), _) => Action::ShrinkFileList,
(KeyCode::Char('>'), _) => Action::GrowFileList,
(KeyCode::Char('='), KeyModifiers::NONE) => Action::TourGranularityCoarser,
(KeyCode::Char('-'), KeyModifiers::NONE) => Action::TourGranularityFiner,
(KeyCode::Char('i'), KeyModifiers::NONE) => Action::EditComment,
(KeyCode::Char('e'), KeyModifiers::NONE) => Action::OpenReactionPicker,
(KeyCode::Char('d'), KeyModifiers::NONE) => Action::PendingDCommand,
(KeyCode::Char('v' | 'V'), _) => Action::EnterVisualMode,
(KeyCode::Char('y'), KeyModifiers::NONE) => Action::ExportToClipboard,
(KeyCode::Char('n'), KeyModifiers::NONE) => Action::SearchNext,
(KeyCode::Char('N'), _) => Action::SearchPrev,
(KeyCode::Char('x'), KeyModifiers::NONE) => Action::ToggleResolveThread,
(KeyCode::Char(':'), _) => Action::EnterCommandMode,
(KeyCode::Char('/'), _) => Action::EnterSearchMode,
(KeyCode::Char('?'), _) => Action::ToggleHelp,
(KeyCode::Char('a'), KeyModifiers::CONTROL) => Action::ToggleAiSummary,
(KeyCode::Char('p'), KeyModifiers::CONTROL) => Action::ToggleViewportPin,
(KeyCode::Char('g'), KeyModifiers::CONTROL) => Action::JumpToAgentGhost,
(KeyCode::Esc, KeyModifiers::NONE) => Action::ReturnToCommits,
(KeyCode::Char('q'), KeyModifiers::NONE) => Action::Quit,
(KeyCode::Char(' '), KeyModifiers::NONE) => Action::ToggleExpand,
(KeyCode::Char('o'), KeyModifiers::NONE) => Action::OpenInBrowser,
(KeyCode::Char('O'), _) => Action::CollapseAll,
(KeyCode::Char('R'), _) => Action::SubmitReview,
(KeyCode::Char('M'), _) => Action::MergePR,
(KeyCode::Char('m'), KeyModifiers::NONE) => Action::MentalModelOpen,
(KeyCode::Char('A'), _) => Action::ReanchorOrphan,
(KeyCode::Char(c @ '0'..='9'), KeyModifiers::NONE) => Action::Digit(c as u8 - b'0'),
_ => Action::None,
}
}
fn map_command_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Enter, KeyModifiers::NONE) => Action::SubmitInput,
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char('w'), KeyModifiers::CONTROL) => Action::DeleteWord,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::ClearLine,
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => Action::InsertChar(c),
_ => Action::None,
}
}
fn map_search_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Enter, KeyModifiers::NONE) => Action::SubmitInput,
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char('w'), KeyModifiers::CONTROL) => Action::DeleteWord,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::ClearLine,
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => Action::InsertChar(c),
_ => Action::None,
}
}
fn map_comment_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Char('c'), KeyModifiers::CONTROL) => Action::ExitMode,
(KeyCode::Enter, KeyModifiers::NONE) => Action::SubmitInput,
(KeyCode::Enter, KeyModifiers::CONTROL) => Action::SubmitInput,
(KeyCode::Char('s'), KeyModifiers::CONTROL) => Action::SubmitInput,
(KeyCode::Enter, mods) if mods.contains(KeyModifiers::SHIFT) => Action::InsertChar('\n'),
(KeyCode::Char('j'), KeyModifiers::CONTROL) => Action::InsertChar('\n'),
(KeyCode::Char('g'), KeyModifiers::CONTROL) => Action::InsertSuggestion,
(KeyCode::Tab, KeyModifiers::NONE) => Action::CycleCommentType,
(KeyCode::BackTab, _) => Action::CycleCommentTypeReverse,
(KeyCode::Char('e'), KeyModifiers::CONTROL) => Action::OpenExternalEditor,
(KeyCode::Char('t'), KeyModifiers::CONTROL) => Action::OpenCommentTemplatePicker,
(KeyCode::Char('a'), KeyModifiers::CONTROL) => Action::TextCursorLineStart,
(KeyCode::Left, mods)
if mods.contains(KeyModifiers::ALT) || mods.contains(KeyModifiers::CONTROL) =>
{
Action::TextCursorWordLeft
}
(KeyCode::Right, mods)
if mods.contains(KeyModifiers::ALT) || mods.contains(KeyModifiers::CONTROL) =>
{
Action::TextCursorWordRight
}
(KeyCode::Home, _) => Action::TextCursorLineStart,
(KeyCode::End, _) => Action::TextCursorLineEnd,
(KeyCode::Left, mods)
if mods.contains(KeyModifiers::SUPER) || mods.contains(KeyModifiers::META) =>
{
Action::TextCursorLineStart
}
(KeyCode::Right, mods)
if mods.contains(KeyModifiers::SUPER) || mods.contains(KeyModifiers::META) =>
{
Action::TextCursorLineEnd
}
(KeyCode::Left, KeyModifiers::NONE) => Action::TextCursorLeft,
(KeyCode::Right, KeyModifiers::NONE) => Action::TextCursorRight,
(KeyCode::Backspace, mods)
if mods.contains(KeyModifiers::SUPER) || mods.contains(KeyModifiers::META) =>
{
Action::DeleteWord
}
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char('w'), KeyModifiers::CONTROL) => Action::DeleteWord,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::ClearLine,
(KeyCode::Char(c), _) => Action::InsertChar(c),
_ => Action::None,
}
}
fn map_help_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc | KeyCode::Char('q'), KeyModifiers::NONE) | (KeyCode::Char('?'), _) => {
Action::ToggleHelp
}
(KeyCode::Char('j') | KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Char('k') | KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Char('d'), KeyModifiers::CONTROL) => Action::HalfPageDown,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::HalfPageUp,
(KeyCode::Char('f'), KeyModifiers::CONTROL) => Action::PageDown,
(KeyCode::Char('b'), KeyModifiers::CONTROL) => Action::PageUp,
(KeyCode::PageDown, KeyModifiers::NONE) => Action::PageDown,
(KeyCode::PageUp, KeyModifiers::NONE) => Action::PageUp,
(KeyCode::Char('g'), KeyModifiers::NONE) => Action::GoToTop,
(KeyCode::Char('G'), _) => Action::GoToBottom,
_ => Action::None,
}
}
fn map_confirm_mode(key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y' | 'Y') | KeyCode::Enter => Action::ConfirmYes,
KeyCode::Char('n' | 'N') | KeyCode::Esc => Action::ConfirmNo,
_ => Action::None,
}
}
fn map_commit_select_mode(key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('j') | KeyCode::Down => Action::CommitSelectDown,
KeyCode::Char('k') | KeyCode::Up => Action::CommitSelectUp,
KeyCode::Char(' ') => Action::ToggleCommitSelect,
KeyCode::Enter => Action::ConfirmCommitSelect,
KeyCode::Char(':') => Action::EnterCommandMode,
KeyCode::Esc => Action::ExitMode,
KeyCode::Char('q') => Action::Quit,
_ => Action::None,
}
}
fn map_visual_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Char('j') | KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Char('k') | KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Char('c'), KeyModifiers::NONE) => Action::AddRangeComment,
(KeyCode::Enter, KeyModifiers::NONE) => Action::AddRangeComment,
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Char('v' | 'V'), _) => Action::ExitMode,
(KeyCode::Char('q'), KeyModifiers::NONE) => Action::Quit,
_ => Action::None,
}
}
fn map_command_palette_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Enter, KeyModifiers::NONE) => Action::SubmitInput,
(KeyCode::Char('j') | KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Char('k') | KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char('w'), KeyModifiers::CONTROL) => Action::DeleteWord,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::ClearLine,
(KeyCode::Char('n'), KeyModifiers::CONTROL) => Action::CursorDown(1),
(KeyCode::Char('p'), KeyModifiers::CONTROL) => Action::CursorUp(1),
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => Action::InsertChar(c),
_ => Action::None,
}
}
fn map_comment_template_picker_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::CommentTemplatePickerCancel,
(KeyCode::Char('c'), KeyModifiers::CONTROL) => Action::CommentTemplatePickerCancel,
(KeyCode::Enter, KeyModifiers::NONE) => Action::CommentTemplatePickerSelect,
(KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Char('n'), KeyModifiers::CONTROL) => Action::CursorDown(1),
(KeyCode::Char('p'), KeyModifiers::CONTROL) => Action::CursorUp(1),
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char('w'), KeyModifiers::CONTROL) => Action::DeleteWord,
(KeyCode::Char('u'), KeyModifiers::CONTROL) => Action::ClearLine,
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => Action::InsertChar(c),
_ => Action::None,
}
}
fn map_reaction_picker_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => Action::ReactionPickerCancel,
(KeyCode::Char('q'), KeyModifiers::NONE) => Action::ReactionPickerCancel,
(KeyCode::Enter, KeyModifiers::NONE) => Action::ReactionPickerSelect,
(KeyCode::Char('h') | KeyCode::Left, KeyModifiers::NONE) => {
Action::ReactionPickerCursorLeft
}
(KeyCode::Char('l') | KeyCode::Right, KeyModifiers::NONE) => {
Action::ReactionPickerCursorRight
}
(KeyCode::Char(c @ '1'..='8'), KeyModifiers::NONE) => {
Action::ReactionPickerSelectAt((c as u8 - b'1') as usize)
}
_ => Action::None,
}
}
fn map_review_submit_mode(key: KeyEvent) -> Action {
match (key.code, key.modifiers) {
(KeyCode::Char('j') | KeyCode::Down, KeyModifiers::NONE) => Action::CursorDown(1),
(KeyCode::Char('k') | KeyCode::Up, KeyModifiers::NONE) => Action::CursorUp(1),
(KeyCode::Enter, KeyModifiers::NONE) => Action::SelectFile,
(KeyCode::Char('s'), KeyModifiers::CONTROL) => Action::SubmitInput,
(KeyCode::Esc, KeyModifiers::NONE) => Action::ExitMode,
(KeyCode::Backspace, KeyModifiers::NONE) => Action::DeleteChar,
(KeyCode::Char(c), _) => Action::InsertChar(c),
_ => Action::None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn key_shift(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::SHIFT)
}
#[test]
fn should_map_digit_keys_to_digit_action_in_normal_mode() {
for d in 0..=9u8 {
let c = (b'0' + d) as char;
let action = map_normal_mode(key(KeyCode::Char(c)));
assert_eq!(
action,
Action::Digit(d),
"digit key '{c}' should map to Digit({d})"
);
}
}
#[test]
fn should_map_uppercase_g_to_go_to_bottom_in_normal_mode() {
let action = map_normal_mode(key_shift('G'));
assert_eq!(action, Action::GoToBottom);
}
#[test]
fn should_map_lowercase_g_to_pending_chord_in_normal_mode() {
let action = map_normal_mode(key(KeyCode::Char('g')));
assert_eq!(action, Action::PendingGCommand);
}
#[test]
fn should_not_map_digits_in_command_mode() {
for d in 0..=9u8 {
let c = (b'0' + d) as char;
let action = map_command_mode(key(KeyCode::Char(c)));
assert_eq!(
action,
Action::InsertChar(c),
"digit '{c}' in command mode should be InsertChar"
);
}
}
#[test]
fn should_not_map_digits_in_search_mode() {
for d in 0..=9u8 {
let c = (b'0' + d) as char;
let action = map_search_mode(key(KeyCode::Char(c)));
assert_eq!(
action,
Action::InsertChar(c),
"digit '{c}' in search mode should be InsertChar"
);
}
}
#[test]
fn should_not_map_shifted_digits_to_digit_action() {
for d in 0..=9u8 {
let c = (b'0' + d) as char;
let action = map_normal_mode(key_shift(c));
assert_ne!(
action,
Action::Digit(d),
"Shift+'{c}' in normal mode must not produce Digit({d})"
);
}
}
#[test]
fn should_map_backtab_to_reverse_focus_in_normal_mode() {
let action = map_normal_mode(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(action, Action::ToggleFocusReverse);
}
#[test]
fn should_map_backtab_to_reverse_comment_type_in_comment_mode() {
let action = map_comment_mode(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(action, Action::CycleCommentTypeReverse);
}
#[test]
fn ctrl_e_in_comment_mode_maps_to_open_external_editor() {
let action = map_comment_mode(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert_eq!(action, Action::OpenExternalEditor);
}
#[test]
fn paste_inserts_text_at_cursor_position() {
let mut buffer = String::from("hello world");
let mut cursor = 5; let paste_text = " beautiful";
buffer.insert_str(cursor, paste_text);
cursor += paste_text.len();
assert_eq!(buffer, "hello beautiful world");
assert_eq!(cursor, 15); }
#[test]
fn paste_at_empty_buffer() {
let mut buffer = String::new();
let mut cursor = 0;
let paste_text = "pasted content";
buffer.insert_str(cursor, paste_text);
cursor += paste_text.len();
assert_eq!(buffer, "pasted content");
assert_eq!(cursor, 14);
}
#[test]
fn plain_e_maps_to_open_reaction_picker() {
let action = map_normal_mode(key(KeyCode::Char('e')));
assert_eq!(action, Action::OpenReactionPicker);
}
#[test]
fn ctrl_e_still_maps_to_external_editor() {
let action = map_comment_mode(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert_eq!(action, Action::OpenExternalEditor);
}
#[test]
fn dollar_key_maps_to_line_end() {
assert_eq!(map_normal_mode(key_shift('$')), Action::GoToLineEnd);
}
#[test]
fn shift_h_and_l_map_to_cursor_vertical() {
assert_eq!(map_normal_mode(key_shift('H')), Action::CursorToTopOfScreen);
assert_eq!(
map_normal_mode(key_shift('L')),
Action::CursorToBottomOfScreen
);
}
#[test]
fn w_and_b_navigate_hunks() {
assert_eq!(
map_normal_mode(key(KeyCode::Char('w'))),
Action::WordForward
);
assert_eq!(
map_normal_mode(key(KeyCode::Char('b'))),
Action::WordBackward
);
}
#[test]
fn ctrl_e_and_ctrl_y_scroll_view_without_cursor() {
assert_eq!(
map_normal_mode(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL)),
Action::ScrollViewDown
);
assert_eq!(
map_normal_mode(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL)),
Action::ScrollViewUp
);
}
#[test]
fn reaction_picker_mode_has_expected_bindings() {
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Esc)),
Action::ReactionPickerCancel
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Char('q'))),
Action::ReactionPickerCancel
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Enter)),
Action::ReactionPickerSelect
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Char('h'))),
Action::ReactionPickerCursorLeft
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Left)),
Action::ReactionPickerCursorLeft
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Char('l'))),
Action::ReactionPickerCursorRight
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Right)),
Action::ReactionPickerCursorRight
);
for i in 0..8u8 {
let c = (b'1' + i) as char;
let action = map_reaction_picker_mode(key(KeyCode::Char(c)));
assert_eq!(action, Action::ReactionPickerSelectAt(i as usize));
}
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Char('9'))),
Action::None
);
assert_eq!(
map_reaction_picker_mode(key(KeyCode::Char('0'))),
Action::None
);
}
#[test]
fn template_picker_j_k_insert_as_filter_chars_not_nav() {
assert_eq!(
map_comment_template_picker_mode(key(KeyCode::Char('j'))),
Action::InsertChar('j'),
"lowercase j must reach the filter buffer"
);
assert_eq!(
map_comment_template_picker_mode(key(KeyCode::Char('k'))),
Action::InsertChar('k'),
"lowercase k must reach the filter buffer"
);
}
#[test]
fn esc_in_normal_mode_maps_to_return_to_commits() {
assert_eq!(map_normal_mode(key(KeyCode::Esc)), Action::ReturnToCommits);
}
#[test]
fn colon_in_commit_picker_enters_command_mode() {
assert_eq!(
map_commit_select_mode(key(KeyCode::Char(':'))),
Action::EnterCommandMode
);
assert_eq!(
map_commit_select_mode(key(KeyCode::Char(' '))),
Action::ToggleCommitSelect
);
assert_eq!(map_commit_select_mode(key(KeyCode::Esc)), Action::ExitMode);
}
#[test]
fn template_picker_arrow_keys_still_navigate() {
assert_eq!(
map_comment_template_picker_mode(key(KeyCode::Down)),
Action::CursorDown(1)
);
assert_eq!(
map_comment_template_picker_mode(key(KeyCode::Up)),
Action::CursorUp(1)
);
assert_eq!(
map_comment_template_picker_mode(KeyEvent::new(
KeyCode::Char('n'),
KeyModifiers::CONTROL
)),
Action::CursorDown(1)
);
assert_eq!(
map_comment_template_picker_mode(KeyEvent::new(
KeyCode::Char('p'),
KeyModifiers::CONTROL
)),
Action::CursorUp(1)
);
}
}