use crate::config::Config;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use rust_i18n::t;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
static FORCE_LINUX_KEYBINDINGS: AtomicBool = AtomicBool::new(false);
pub fn set_force_linux_keybindings(force: bool) {
FORCE_LINUX_KEYBINDINGS.store(force, Ordering::SeqCst);
}
fn use_macos_symbols() -> bool {
if FORCE_LINUX_KEYBINDINGS.load(Ordering::SeqCst) {
return false;
}
cfg!(target_os = "macos")
}
pub fn format_keybinding(keycode: &KeyCode, modifiers: &KeyModifiers) -> String {
let mut result = String::new();
let (ctrl_label, alt_label, shift_label) = if use_macos_symbols() {
("Ctrl", "⌥", "⇧")
} else {
("Ctrl", "Alt", "Shift")
};
if modifiers.contains(KeyModifiers::CONTROL) {
result.push_str(ctrl_label);
result.push('+');
}
if modifiers.contains(KeyModifiers::ALT) {
result.push_str(alt_label);
result.push('+');
}
if modifiers.contains(KeyModifiers::SHIFT) {
result.push_str(shift_label);
result.push('+');
}
match keycode {
KeyCode::Enter => result.push_str("Enter"),
KeyCode::Backspace => result.push_str("Backspace"),
KeyCode::Delete => result.push_str("Del"),
KeyCode::Tab => result.push_str("Tab"),
KeyCode::Esc => result.push_str("Esc"),
KeyCode::Left => result.push_str("←"),
KeyCode::Right => result.push_str("→"),
KeyCode::Up => result.push_str("↑"),
KeyCode::Down => result.push_str("↓"),
KeyCode::Home => result.push_str("Home"),
KeyCode::End => result.push_str("End"),
KeyCode::PageUp => result.push_str("PgUp"),
KeyCode::PageDown => result.push_str("PgDn"),
KeyCode::Char(' ') => result.push_str("Space"),
KeyCode::Char(c) => result.push_str(&c.to_uppercase().to_string()),
KeyCode::F(n) => result.push_str(&format!("F{}", n)),
_ => return String::new(),
}
result
}
fn keybinding_priority_score(key: &KeyCode) -> u32 {
match key {
KeyCode::Char('@') => 100, KeyCode::Char('7') => 100, KeyCode::Char('_') => 100, _ => 0,
}
}
pub fn terminal_key_equivalents(
key: KeyCode,
modifiers: KeyModifiers,
) -> Vec<(KeyCode, KeyModifiers)> {
let mut equivalents = Vec::new();
if modifiers.contains(KeyModifiers::CONTROL) {
let base_modifiers = modifiers;
match key {
KeyCode::Char('/') => {
equivalents.push((KeyCode::Char('7'), base_modifiers));
}
KeyCode::Char('7') => {
equivalents.push((KeyCode::Char('/'), base_modifiers));
}
KeyCode::Backspace => {
equivalents.push((KeyCode::Char('h'), base_modifiers));
}
KeyCode::Char('h') if modifiers == KeyModifiers::CONTROL => {
equivalents.push((KeyCode::Backspace, base_modifiers));
}
KeyCode::Char(' ') => {
equivalents.push((KeyCode::Char('@'), base_modifiers));
}
KeyCode::Char('@') => {
equivalents.push((KeyCode::Char(' '), base_modifiers));
}
KeyCode::Char('-') => {
equivalents.push((KeyCode::Char('_'), base_modifiers));
}
KeyCode::Char('_') => {
equivalents.push((KeyCode::Char('-'), base_modifiers));
}
_ => {}
}
}
equivalents
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyContext {
Global,
Normal,
Prompt,
Popup,
FileExplorer,
Menu,
Terminal,
Settings,
}
impl KeyContext {
pub fn allows_text_input(&self) -> bool {
matches!(self, Self::Normal | Self::Prompt)
}
pub fn from_when_clause(when: &str) -> Option<Self> {
Some(match when.trim() {
"global" => Self::Global,
"prompt" => Self::Prompt,
"popup" => Self::Popup,
"fileExplorer" | "file_explorer" => Self::FileExplorer,
"normal" => Self::Normal,
"menu" => Self::Menu,
"terminal" => Self::Terminal,
"settings" => Self::Settings,
_ => return None,
})
}
pub fn to_when_clause(self) -> &'static str {
match self {
Self::Global => "global",
Self::Normal => "normal",
Self::Prompt => "prompt",
Self::Popup => "popup",
Self::FileExplorer => "fileExplorer",
Self::Menu => "menu",
Self::Terminal => "terminal",
Self::Settings => "settings",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Action {
InsertChar(char),
InsertNewline,
InsertTab,
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
MoveWordLeft,
MoveWordRight,
MoveLineStart,
MoveLineEnd,
MovePageUp,
MovePageDown,
MoveDocumentStart,
MoveDocumentEnd,
SelectLeft,
SelectRight,
SelectUp,
SelectDown,
SelectWordLeft,
SelectWordRight,
SelectLineStart,
SelectLineEnd,
SelectDocumentStart,
SelectDocumentEnd,
SelectPageUp,
SelectPageDown,
SelectAll,
SelectWord,
SelectLine,
ExpandSelection,
BlockSelectLeft,
BlockSelectRight,
BlockSelectUp,
BlockSelectDown,
DeleteBackward,
DeleteForward,
DeleteWordBackward,
DeleteWordForward,
DeleteLine,
DeleteToLineEnd,
DeleteToLineStart,
TransposeChars,
OpenLine,
Recenter,
SetMark,
Copy,
CopyWithTheme(String),
Cut,
Paste,
YankWordForward,
YankWordBackward,
YankToLineEnd,
YankToLineStart,
AddCursorAbove,
AddCursorBelow,
AddCursorNextMatch,
RemoveSecondaryCursors,
Save,
SaveAs,
Open,
SwitchProject,
New,
Close,
CloseTab,
Quit,
Revert,
ToggleAutoRevert,
FormatBuffer,
GotoLine,
GoToMatchingBracket,
JumpToNextError,
JumpToPreviousError,
SmartHome,
DedentSelection,
ToggleComment,
SetBookmark(char),
JumpToBookmark(char),
ClearBookmark(char),
ListBookmarks,
ToggleSearchCaseSensitive,
ToggleSearchWholeWord,
ToggleSearchRegex,
ToggleSearchConfirmEach,
StartMacroRecording,
StopMacroRecording,
PlayMacro(char),
ToggleMacroRecording(char),
ShowMacro(char),
ListMacros,
PromptRecordMacro,
PromptPlayMacro,
PlayLastMacro,
PromptSetBookmark,
PromptJumpToBookmark,
Undo,
Redo,
ScrollUp,
ScrollDown,
ShowHelp,
ShowKeyboardShortcuts,
ShowWarnings,
ShowLspStatus,
ClearWarnings,
CommandPalette,
ToggleLineWrap,
ToggleComposeMode,
SetComposeWidth,
SelectTheme,
SelectKeybindingMap,
SelectCursorStyle,
SelectLocale,
NextBuffer,
PrevBuffer,
SwitchToPreviousTab,
SwitchToTabByName,
ScrollTabsLeft,
ScrollTabsRight,
NavigateBack,
NavigateForward,
SplitHorizontal,
SplitVertical,
CloseSplit,
NextSplit,
PrevSplit,
IncreaseSplitSize,
DecreaseSplitSize,
ToggleMaximizeSplit,
PromptConfirm,
PromptCancel,
PromptBackspace,
PromptDelete,
PromptMoveLeft,
PromptMoveRight,
PromptMoveStart,
PromptMoveEnd,
PromptSelectPrev,
PromptSelectNext,
PromptPageUp,
PromptPageDown,
PromptAcceptSuggestion,
PromptMoveWordLeft,
PromptMoveWordRight,
PromptDeleteWordForward,
PromptDeleteWordBackward,
PromptDeleteToLineEnd,
PromptCopy,
PromptCut,
PromptPaste,
PromptMoveLeftSelecting,
PromptMoveRightSelecting,
PromptMoveHomeSelecting,
PromptMoveEndSelecting,
PromptSelectWordLeft,
PromptSelectWordRight,
PromptSelectAll,
FileBrowserToggleHidden,
PopupSelectNext,
PopupSelectPrev,
PopupPageUp,
PopupPageDown,
PopupConfirm,
PopupCancel,
ToggleFileExplorer,
ToggleMenuBar,
FocusFileExplorer,
FocusEditor,
FileExplorerUp,
FileExplorerDown,
FileExplorerPageUp,
FileExplorerPageDown,
FileExplorerExpand,
FileExplorerCollapse,
FileExplorerOpen,
FileExplorerRefresh,
FileExplorerNewFile,
FileExplorerNewDirectory,
FileExplorerDelete,
FileExplorerRename,
FileExplorerToggleHidden,
FileExplorerToggleGitignored,
LspCompletion,
LspGotoDefinition,
LspReferences,
LspRename,
LspHover,
LspSignatureHelp,
LspCodeActions,
LspRestart,
LspStop,
ToggleInlayHints,
ToggleMouseHover,
ToggleLineNumbers,
ToggleMouseCapture,
ToggleDebugHighlights, SetBackground,
SetBackgroundBlend,
SetTabSize,
SetLineEnding,
ToggleIndentationStyle,
ToggleTabIndicators,
ResetBufferSettings,
DumpConfig,
Search,
FindInSelection,
FindNext,
FindPrevious,
FindSelectionNext, FindSelectionPrevious, Replace,
QueryReplace,
MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String),
SwitchKeybindingMap(String),
PluginAction(String),
OpenSettings, CloseSettings, SettingsSave, SettingsReset, SettingsToggleFocus, SettingsActivate, SettingsSearch, SettingsHelp, SettingsIncrement, SettingsDecrement,
OpenTerminal, CloseTerminal, FocusTerminal, TerminalEscape, ToggleKeyboardCapture, TerminalPaste,
ShellCommand, ShellCommandReplace,
ToUpperCase, ToLowerCase,
CalibrateInput,
None,
}
impl Action {
fn with_char(
args: &HashMap<String, serde_json::Value>,
make_action: impl FnOnce(char) -> Self,
) -> Option<Self> {
if let Some(serde_json::Value::String(value)) = args.get("char") {
value.chars().next().map(make_action)
} else {
None
}
}
pub fn from_str(s: &str, args: &HashMap<String, serde_json::Value>) -> Option<Self> {
Some(match s {
"insert_char" => return Self::with_char(args, Self::InsertChar),
"insert_newline" => Self::InsertNewline,
"insert_tab" => Self::InsertTab,
"move_left" => Self::MoveLeft,
"move_right" => Self::MoveRight,
"move_up" => Self::MoveUp,
"move_down" => Self::MoveDown,
"move_word_left" => Self::MoveWordLeft,
"move_word_right" => Self::MoveWordRight,
"move_line_start" => Self::MoveLineStart,
"move_line_end" => Self::MoveLineEnd,
"move_page_up" => Self::MovePageUp,
"move_page_down" => Self::MovePageDown,
"move_document_start" => Self::MoveDocumentStart,
"move_document_end" => Self::MoveDocumentEnd,
"select_left" => Self::SelectLeft,
"select_right" => Self::SelectRight,
"select_up" => Self::SelectUp,
"select_down" => Self::SelectDown,
"select_word_left" => Self::SelectWordLeft,
"select_word_right" => Self::SelectWordRight,
"select_line_start" => Self::SelectLineStart,
"select_line_end" => Self::SelectLineEnd,
"select_document_start" => Self::SelectDocumentStart,
"select_document_end" => Self::SelectDocumentEnd,
"select_page_up" => Self::SelectPageUp,
"select_page_down" => Self::SelectPageDown,
"select_all" => Self::SelectAll,
"select_word" => Self::SelectWord,
"select_line" => Self::SelectLine,
"expand_selection" => Self::ExpandSelection,
"block_select_left" => Self::BlockSelectLeft,
"block_select_right" => Self::BlockSelectRight,
"block_select_up" => Self::BlockSelectUp,
"block_select_down" => Self::BlockSelectDown,
"delete_backward" => Self::DeleteBackward,
"delete_forward" => Self::DeleteForward,
"delete_word_backward" => Self::DeleteWordBackward,
"delete_word_forward" => Self::DeleteWordForward,
"delete_line" => Self::DeleteLine,
"delete_to_line_end" => Self::DeleteToLineEnd,
"delete_to_line_start" => Self::DeleteToLineStart,
"transpose_chars" => Self::TransposeChars,
"open_line" => Self::OpenLine,
"recenter" => Self::Recenter,
"set_mark" => Self::SetMark,
"copy" => Self::Copy,
"copy_with_theme" => {
let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
Self::CopyWithTheme(theme.to_string())
}
"cut" => Self::Cut,
"paste" => Self::Paste,
"yank_word_forward" => Self::YankWordForward,
"yank_word_backward" => Self::YankWordBackward,
"yank_to_line_end" => Self::YankToLineEnd,
"yank_to_line_start" => Self::YankToLineStart,
"add_cursor_above" => Self::AddCursorAbove,
"add_cursor_below" => Self::AddCursorBelow,
"add_cursor_next_match" => Self::AddCursorNextMatch,
"remove_secondary_cursors" => Self::RemoveSecondaryCursors,
"save" => Self::Save,
"save_as" => Self::SaveAs,
"open" => Self::Open,
"switch_project" => Self::SwitchProject,
"new" => Self::New,
"close" => Self::Close,
"close_tab" => Self::CloseTab,
"quit" => Self::Quit,
"revert" => Self::Revert,
"toggle_auto_revert" => Self::ToggleAutoRevert,
"format_buffer" => Self::FormatBuffer,
"goto_line" => Self::GotoLine,
"goto_matching_bracket" => Self::GoToMatchingBracket,
"jump_to_next_error" => Self::JumpToNextError,
"jump_to_previous_error" => Self::JumpToPreviousError,
"smart_home" => Self::SmartHome,
"dedent_selection" => Self::DedentSelection,
"toggle_comment" => Self::ToggleComment,
"set_bookmark" => return Self::with_char(args, Self::SetBookmark),
"jump_to_bookmark" => return Self::with_char(args, Self::JumpToBookmark),
"clear_bookmark" => return Self::with_char(args, Self::ClearBookmark),
"list_bookmarks" => Self::ListBookmarks,
"toggle_search_case_sensitive" => Self::ToggleSearchCaseSensitive,
"toggle_search_whole_word" => Self::ToggleSearchWholeWord,
"toggle_search_regex" => Self::ToggleSearchRegex,
"toggle_search_confirm_each" => Self::ToggleSearchConfirmEach,
"start_macro_recording" => Self::StartMacroRecording,
"stop_macro_recording" => Self::StopMacroRecording,
"play_macro" => return Self::with_char(args, Self::PlayMacro),
"toggle_macro_recording" => return Self::with_char(args, Self::ToggleMacroRecording),
"show_macro" => return Self::with_char(args, Self::ShowMacro),
"list_macros" => Self::ListMacros,
"prompt_record_macro" => Self::PromptRecordMacro,
"prompt_play_macro" => Self::PromptPlayMacro,
"play_last_macro" => Self::PlayLastMacro,
"prompt_set_bookmark" => Self::PromptSetBookmark,
"prompt_jump_to_bookmark" => Self::PromptJumpToBookmark,
"undo" => Self::Undo,
"redo" => Self::Redo,
"scroll_up" => Self::ScrollUp,
"scroll_down" => Self::ScrollDown,
"show_help" => Self::ShowHelp,
"keyboard_shortcuts" => Self::ShowKeyboardShortcuts,
"show_warnings" => Self::ShowWarnings,
"show_lsp_status" => Self::ShowLspStatus,
"clear_warnings" => Self::ClearWarnings,
"command_palette" => Self::CommandPalette,
"toggle_line_wrap" => Self::ToggleLineWrap,
"toggle_compose_mode" => Self::ToggleComposeMode,
"set_compose_width" => Self::SetComposeWidth,
"next_buffer" => Self::NextBuffer,
"prev_buffer" => Self::PrevBuffer,
"navigate_back" => Self::NavigateBack,
"navigate_forward" => Self::NavigateForward,
"split_horizontal" => Self::SplitHorizontal,
"split_vertical" => Self::SplitVertical,
"close_split" => Self::CloseSplit,
"next_split" => Self::NextSplit,
"prev_split" => Self::PrevSplit,
"increase_split_size" => Self::IncreaseSplitSize,
"decrease_split_size" => Self::DecreaseSplitSize,
"toggle_maximize_split" => Self::ToggleMaximizeSplit,
"prompt_confirm" => Self::PromptConfirm,
"prompt_cancel" => Self::PromptCancel,
"prompt_backspace" => Self::PromptBackspace,
"prompt_move_left" => Self::PromptMoveLeft,
"prompt_move_right" => Self::PromptMoveRight,
"prompt_move_start" => Self::PromptMoveStart,
"prompt_move_end" => Self::PromptMoveEnd,
"prompt_select_prev" => Self::PromptSelectPrev,
"prompt_select_next" => Self::PromptSelectNext,
"prompt_page_up" => Self::PromptPageUp,
"prompt_page_down" => Self::PromptPageDown,
"prompt_accept_suggestion" => Self::PromptAcceptSuggestion,
"prompt_delete_word_forward" => Self::PromptDeleteWordForward,
"prompt_delete_word_backward" => Self::PromptDeleteWordBackward,
"prompt_delete_to_line_end" => Self::PromptDeleteToLineEnd,
"prompt_copy" => Self::PromptCopy,
"prompt_cut" => Self::PromptCut,
"prompt_paste" => Self::PromptPaste,
"prompt_move_left_selecting" => Self::PromptMoveLeftSelecting,
"prompt_move_right_selecting" => Self::PromptMoveRightSelecting,
"prompt_move_home_selecting" => Self::PromptMoveHomeSelecting,
"prompt_move_end_selecting" => Self::PromptMoveEndSelecting,
"prompt_select_word_left" => Self::PromptSelectWordLeft,
"prompt_select_word_right" => Self::PromptSelectWordRight,
"prompt_select_all" => Self::PromptSelectAll,
"file_browser_toggle_hidden" => Self::FileBrowserToggleHidden,
"prompt_move_word_left" => Self::PromptMoveWordLeft,
"prompt_move_word_right" => Self::PromptMoveWordRight,
"prompt_delete" => Self::PromptDelete,
"popup_select_next" => Self::PopupSelectNext,
"popup_select_prev" => Self::PopupSelectPrev,
"popup_page_up" => Self::PopupPageUp,
"popup_page_down" => Self::PopupPageDown,
"popup_confirm" => Self::PopupConfirm,
"popup_cancel" => Self::PopupCancel,
"toggle_file_explorer" => Self::ToggleFileExplorer,
"toggle_menu_bar" => Self::ToggleMenuBar,
"focus_file_explorer" => Self::FocusFileExplorer,
"focus_editor" => Self::FocusEditor,
"file_explorer_up" => Self::FileExplorerUp,
"file_explorer_down" => Self::FileExplorerDown,
"file_explorer_page_up" => Self::FileExplorerPageUp,
"file_explorer_page_down" => Self::FileExplorerPageDown,
"file_explorer_expand" => Self::FileExplorerExpand,
"file_explorer_collapse" => Self::FileExplorerCollapse,
"file_explorer_open" => Self::FileExplorerOpen,
"file_explorer_refresh" => Self::FileExplorerRefresh,
"file_explorer_new_file" => Self::FileExplorerNewFile,
"file_explorer_new_directory" => Self::FileExplorerNewDirectory,
"file_explorer_delete" => Self::FileExplorerDelete,
"file_explorer_rename" => Self::FileExplorerRename,
"file_explorer_toggle_hidden" => Self::FileExplorerToggleHidden,
"file_explorer_toggle_gitignored" => Self::FileExplorerToggleGitignored,
"lsp_completion" => Self::LspCompletion,
"lsp_goto_definition" => Self::LspGotoDefinition,
"lsp_references" => Self::LspReferences,
"lsp_rename" => Self::LspRename,
"lsp_hover" => Self::LspHover,
"lsp_signature_help" => Self::LspSignatureHelp,
"lsp_code_actions" => Self::LspCodeActions,
"lsp_restart" => Self::LspRestart,
"lsp_stop" => Self::LspStop,
"toggle_inlay_hints" => Self::ToggleInlayHints,
"toggle_mouse_hover" => Self::ToggleMouseHover,
"toggle_line_numbers" => Self::ToggleLineNumbers,
"toggle_mouse_capture" => Self::ToggleMouseCapture,
"toggle_debug_highlights" => Self::ToggleDebugHighlights,
"set_background" => Self::SetBackground,
"set_background_blend" => Self::SetBackgroundBlend,
"select_theme" => Self::SelectTheme,
"select_keybinding_map" => Self::SelectKeybindingMap,
"select_locale" => Self::SelectLocale,
"set_tab_size" => Self::SetTabSize,
"set_line_ending" => Self::SetLineEnding,
"toggle_indentation_style" => Self::ToggleIndentationStyle,
"toggle_tab_indicators" => Self::ToggleTabIndicators,
"reset_buffer_settings" => Self::ResetBufferSettings,
"dump_config" => Self::DumpConfig,
"search" => Self::Search,
"find_in_selection" => Self::FindInSelection,
"find_next" => Self::FindNext,
"find_previous" => Self::FindPrevious,
"find_selection_next" => Self::FindSelectionNext,
"find_selection_previous" => Self::FindSelectionPrevious,
"replace" => Self::Replace,
"query_replace" => Self::QueryReplace,
"menu_activate" => Self::MenuActivate,
"menu_close" => Self::MenuClose,
"menu_left" => Self::MenuLeft,
"menu_right" => Self::MenuRight,
"menu_up" => Self::MenuUp,
"menu_down" => Self::MenuDown,
"menu_execute" => Self::MenuExecute,
"menu_open" => {
let name = args.get("name")?.as_str()?;
Self::MenuOpen(name.to_string())
}
"switch_keybinding_map" => {
let map_name = args.get("map")?.as_str()?;
Self::SwitchKeybindingMap(map_name.to_string())
}
"open_terminal" => Self::OpenTerminal,
"close_terminal" => Self::CloseTerminal,
"focus_terminal" => Self::FocusTerminal,
"terminal_escape" => Self::TerminalEscape,
"toggle_keyboard_capture" => Self::ToggleKeyboardCapture,
"terminal_paste" => Self::TerminalPaste,
"shell_command" => Self::ShellCommand,
"shell_command_replace" => Self::ShellCommandReplace,
"to_upper_case" => Self::ToUpperCase,
"to_lower_case" => Self::ToLowerCase,
"calibrate_input" => Self::CalibrateInput,
"open_settings" => Self::OpenSettings,
"close_settings" => Self::CloseSettings,
"settings_save" => Self::SettingsSave,
"settings_reset" => Self::SettingsReset,
"settings_toggle_focus" => Self::SettingsToggleFocus,
"settings_activate" => Self::SettingsActivate,
"settings_search" => Self::SettingsSearch,
"settings_help" => Self::SettingsHelp,
"settings_increment" => Self::SettingsIncrement,
"settings_decrement" => Self::SettingsDecrement,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ChordResolution {
Complete(Action),
Partial,
NoMatch,
}
#[derive(Clone)]
pub struct KeybindingResolver {
bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
}
impl KeybindingResolver {
pub fn new(config: &Config) -> Self {
let mut resolver = Self {
bindings: HashMap::new(),
default_bindings: HashMap::new(),
chord_bindings: HashMap::new(),
default_chord_bindings: HashMap::new(),
};
let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
resolver.load_default_bindings_from_vec(&map_bindings);
resolver.load_bindings_from_vec(&config.keybindings);
resolver
}
fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
for binding in bindings {
let context = if let Some(ref when) = binding.when {
KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
} else {
KeyContext::Normal
};
if let Some(action) = Action::from_str(&binding.action, &binding.args) {
if !binding.keys.is_empty() {
let mut sequence = Vec::new();
for key_press in &binding.keys {
if let Some(key_code) = Self::parse_key(&key_press.key) {
let modifiers = Self::parse_modifiers(&key_press.modifiers);
sequence.push((key_code, modifiers));
} else {
break;
}
}
if sequence.len() == binding.keys.len() && !sequence.is_empty() {
self.default_chord_bindings
.entry(context)
.or_insert_with(HashMap::new)
.insert(sequence, action);
}
} else if let Some(key_code) = Self::parse_key(&binding.key) {
let modifiers = Self::parse_modifiers(&binding.modifiers);
self.insert_binding_with_equivalents(
context,
key_code,
modifiers,
action,
&binding.key,
);
}
}
}
}
fn insert_binding_with_equivalents(
&mut self,
context: KeyContext,
key_code: KeyCode,
modifiers: KeyModifiers,
action: Action,
key_name: &str,
) {
let context_bindings = self
.default_bindings
.entry(context)
.or_insert_with(HashMap::new);
context_bindings.insert((key_code, modifiers), action.clone());
let equivalents = terminal_key_equivalents(key_code, modifiers);
for (equiv_key, equiv_mods) in equivalents {
if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
if existing_action != &action {
let equiv_name = format!("{:?}", equiv_key);
tracing::warn!(
"Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
is bound to {:?}, but {} is bound to {:?}. \
The explicit binding takes precedence.",
context,
equiv_name,
key_name,
existing_action,
key_name,
action
);
}
} else {
context_bindings.insert((equiv_key, equiv_mods), action.clone());
}
}
}
fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
for binding in bindings {
let context = if let Some(ref when) = binding.when {
KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
} else {
KeyContext::Normal
};
if let Some(action) = Action::from_str(&binding.action, &binding.args) {
if !binding.keys.is_empty() {
let mut sequence = Vec::new();
for key_press in &binding.keys {
if let Some(key_code) = Self::parse_key(&key_press.key) {
let modifiers = Self::parse_modifiers(&key_press.modifiers);
sequence.push((key_code, modifiers));
} else {
break;
}
}
if sequence.len() == binding.keys.len() && !sequence.is_empty() {
self.chord_bindings
.entry(context)
.or_insert_with(HashMap::new)
.insert(sequence, action);
}
} else if let Some(key_code) = Self::parse_key(&binding.key) {
let modifiers = Self::parse_modifiers(&binding.modifiers);
self.bindings
.entry(context)
.or_insert_with(HashMap::new)
.insert((key_code, modifiers), action);
}
}
}
}
fn is_application_wide_action(action: &Action) -> bool {
matches!(
action,
Action::Quit
| Action::Save
| Action::SaveAs
| Action::ShowHelp
| Action::ShowKeyboardShortcuts
| Action::PromptCancel | Action::PopupCancel )
}
pub fn is_terminal_ui_action(action: &Action) -> bool {
matches!(
action,
Action::CommandPalette
| Action::OpenSettings
| Action::MenuActivate
| Action::MenuOpen(_)
| Action::ShowHelp
| Action::ShowKeyboardShortcuts
| Action::Quit
| Action::NextSplit
| Action::PrevSplit
| Action::SplitHorizontal
| Action::SplitVertical
| Action::CloseSplit
| Action::ToggleMaximizeSplit
| Action::NextBuffer
| Action::PrevBuffer
| Action::Close
| Action::ScrollTabsLeft
| Action::ScrollTabsRight
| Action::TerminalEscape
| Action::ToggleKeyboardCapture
| Action::OpenTerminal
| Action::CloseTerminal
| Action::TerminalPaste
| Action::ToggleFileExplorer
| Action::ToggleMenuBar
)
}
pub fn resolve_chord(
&self,
chord_state: &[(KeyCode, KeyModifiers)],
event: &KeyEvent,
context: KeyContext,
) -> ChordResolution {
let mut full_sequence = chord_state.to_vec();
full_sequence.push((event.code, event.modifiers));
tracing::trace!(
"KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
full_sequence,
context
);
let search_order = vec![
(&self.chord_bindings, &KeyContext::Global, "custom global"),
(
&self.default_chord_bindings,
&KeyContext::Global,
"default global",
),
(&self.chord_bindings, &context, "custom context"),
(&self.default_chord_bindings, &context, "default context"),
];
let mut has_partial_match = false;
for (binding_map, bind_context, label) in search_order {
if let Some(context_chords) = binding_map.get(bind_context) {
if let Some(action) = context_chords.get(&full_sequence) {
tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
return ChordResolution::Complete(action.clone());
}
for (chord_seq, _) in context_chords.iter() {
if chord_seq.len() > full_sequence.len()
&& chord_seq[..full_sequence.len()] == full_sequence[..]
{
tracing::trace!(" -> Partial chord match in {}", label);
has_partial_match = true;
break;
}
}
}
}
if has_partial_match {
ChordResolution::Partial
} else {
tracing::trace!(" -> No chord match");
ChordResolution::NoMatch
}
}
pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
tracing::trace!(
"KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
event.code,
event.modifiers,
context
);
if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
if let Some(action) = global_bindings.get(&(event.code, event.modifiers)) {
tracing::trace!(" -> Found in custom global bindings: {:?}", action);
return action.clone();
}
}
if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
if let Some(action) = global_bindings.get(&(event.code, event.modifiers)) {
tracing::trace!(" -> Found in default global bindings: {:?}", action);
return action.clone();
}
}
if let Some(context_bindings) = self.bindings.get(&context) {
if let Some(action) = context_bindings.get(&(event.code, event.modifiers)) {
tracing::trace!(
" -> Found in custom {} bindings: {:?}",
context.to_when_clause(),
action
);
return action.clone();
}
}
if let Some(context_bindings) = self.default_bindings.get(&context) {
if let Some(action) = context_bindings.get(&(event.code, event.modifiers)) {
tracing::trace!(
" -> Found in default {} bindings: {:?}",
context.to_when_clause(),
action
);
return action.clone();
}
}
if context != KeyContext::Normal {
if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
if let Some(action) = normal_bindings.get(&(event.code, event.modifiers)) {
if Self::is_application_wide_action(action) {
tracing::trace!(
" -> Found application-wide action in custom normal bindings: {:?}",
action
);
return action.clone();
}
}
}
if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
if let Some(action) = normal_bindings.get(&(event.code, event.modifiers)) {
if Self::is_application_wide_action(action) {
tracing::trace!(
" -> Found application-wide action in default normal bindings: {:?}",
action
);
return action.clone();
}
}
}
}
if context.allows_text_input() {
if event.modifiers.is_empty() || event.modifiers == KeyModifiers::SHIFT {
if let KeyCode::Char(c) = event.code {
tracing::trace!(" -> Character input: '{}'", c);
return Action::InsertChar(c);
}
}
}
tracing::trace!(" -> No binding found, returning Action::None");
Action::None
}
pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
tracing::trace!(
"KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
event.code,
event.modifiers
);
for bindings in [&self.bindings, &self.default_bindings] {
if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
if let Some(action) = terminal_bindings.get(&(event.code, event.modifiers)) {
if Self::is_terminal_ui_action(action) {
tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
return action.clone();
}
}
}
}
for bindings in [&self.bindings, &self.default_bindings] {
if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
if let Some(action) = global_bindings.get(&(event.code, event.modifiers)) {
if Self::is_terminal_ui_action(action) {
tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
return action.clone();
}
}
}
}
for bindings in [&self.bindings, &self.default_bindings] {
if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
if let Some(action) = normal_bindings.get(&(event.code, event.modifiers)) {
if Self::is_terminal_ui_action(action) {
tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
return action.clone();
}
}
}
}
tracing::trace!(" -> No UI action found");
Action::None
}
pub fn find_keybinding_for_action(
&self,
action_name: &str,
context: KeyContext,
) -> Option<String> {
let target_action = Action::from_str(action_name, &HashMap::new())?;
let search_maps = vec![
self.bindings.get(&context),
self.bindings.get(&KeyContext::Global),
self.default_bindings.get(&context),
self.default_bindings.get(&KeyContext::Global),
];
for map in search_maps.into_iter().flatten() {
let mut matches: Vec<(KeyCode, KeyModifiers)> = map
.iter()
.filter(|(_, action)| {
std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
})
.map(|((key_code, modifiers), _)| (*key_code, *modifiers))
.collect();
if !matches.is_empty() {
matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
let mod_count_a = mod_a.bits().count_ones();
let mod_count_b = mod_b.bits().count_ones();
match mod_count_a.cmp(&mod_count_b) {
std::cmp::Ordering::Equal => {
match mod_a.bits().cmp(&mod_b.bits()) {
std::cmp::Ordering::Equal => {
Self::key_code_sort_key(key_a)
.cmp(&Self::key_code_sort_key(key_b))
}
other => other,
}
}
other => other,
}
});
let (key_code, modifiers) = matches[0];
return Some(Self::format_keybinding(key_code, modifiers));
}
}
None
}
fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
match key_code {
KeyCode::Char(c) => (0, *c as u32),
KeyCode::F(n) => (1, *n as u32),
KeyCode::Enter => (2, 0),
KeyCode::Tab => (2, 1),
KeyCode::Backspace => (2, 2),
KeyCode::Delete => (2, 3),
KeyCode::Esc => (2, 4),
KeyCode::Left => (3, 0),
KeyCode::Right => (3, 1),
KeyCode::Up => (3, 2),
KeyCode::Down => (3, 3),
KeyCode::Home => (3, 4),
KeyCode::End => (3, 5),
KeyCode::PageUp => (3, 6),
KeyCode::PageDown => (3, 7),
_ => (255, 0),
}
}
pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
let search_maps = vec![
self.bindings.get(&KeyContext::Normal),
self.bindings.get(&KeyContext::Global),
self.default_bindings.get(&KeyContext::Normal),
self.default_bindings.get(&KeyContext::Global),
];
for map in search_maps.into_iter().flatten() {
for ((key_code, modifiers), action) in map {
if let Action::MenuOpen(name) = action {
if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
if let KeyCode::Char(c) = key_code {
return Some(c.to_ascii_lowercase());
}
}
}
}
}
None
}
fn format_keybinding(key_code: KeyCode, modifiers: KeyModifiers) -> String {
let mut parts = Vec::new();
if modifiers.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if modifiers.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if modifiers.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
let key_str = match key_code {
KeyCode::Char(c) if c == ' ' => "Space".to_string(),
KeyCode::Char(c) => c.to_uppercase().to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Delete => "Delete".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Left => "Left".to_string(),
KeyCode::Right => "Right".to_string(),
KeyCode::Up => "Up".to_string(),
KeyCode::Down => "Down".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PageUp".to_string(),
KeyCode::PageDown => "PageDown".to_string(),
KeyCode::F(n) => format!("F{}", n),
_ => return String::new(),
};
parts.push(&key_str);
parts.join("+")
}
fn parse_key(key: &str) -> Option<KeyCode> {
let lower = key.to_lowercase();
match lower.as_str() {
"enter" => Some(KeyCode::Enter),
"backspace" => Some(KeyCode::Backspace),
"delete" | "del" => Some(KeyCode::Delete),
"tab" => Some(KeyCode::Tab),
"backtab" => Some(KeyCode::BackTab),
"esc" | "escape" => Some(KeyCode::Esc),
"space" => Some(KeyCode::Char(' ')),
"left" => Some(KeyCode::Left),
"right" => Some(KeyCode::Right),
"up" => Some(KeyCode::Up),
"down" => Some(KeyCode::Down),
"home" => Some(KeyCode::Home),
"end" => Some(KeyCode::End),
"pageup" => Some(KeyCode::PageUp),
"pagedown" => Some(KeyCode::PageDown),
s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
_ => None,
}
}
fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
let mut result = KeyModifiers::empty();
for m in modifiers {
match m.to_lowercase().as_str() {
"ctrl" | "control" => result |= KeyModifiers::CONTROL,
"shift" => result |= KeyModifiers::SHIFT,
"alt" => result |= KeyModifiers::ALT,
_ => {}
}
}
result
}
pub fn get_all_bindings(&self) -> Vec<(String, String)> {
let mut bindings = Vec::new();
for context in &[
KeyContext::Normal,
KeyContext::Prompt,
KeyContext::Popup,
KeyContext::FileExplorer,
KeyContext::Menu,
] {
let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
if let Some(context_defaults) = self.default_bindings.get(context) {
for (key, action) in context_defaults {
all_keys.insert(*key, action.clone());
}
}
if let Some(context_bindings) = self.bindings.get(context) {
for (key, action) in context_bindings {
all_keys.insert(*key, action.clone());
}
}
let context_str = if *context != KeyContext::Normal {
format!("[{}] ", context.to_when_clause())
} else {
String::new()
};
for ((key_code, modifiers), action) in all_keys {
let key_str = Self::format_key(key_code, modifiers);
let action_str = format!("{}{}", context_str, Self::format_action(&action));
bindings.push((key_str, action_str));
}
}
bindings.sort_by(|a, b| a.1.cmp(&b.1));
bindings
}
fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
format_keybinding(&key_code, &modifiers)
}
fn format_action(action: &Action) -> String {
match action {
Action::InsertChar(c) => t!("action.insert_char", char = c),
Action::InsertNewline => t!("action.insert_newline"),
Action::InsertTab => t!("action.insert_tab"),
Action::MoveLeft => t!("action.move_left"),
Action::MoveRight => t!("action.move_right"),
Action::MoveUp => t!("action.move_up"),
Action::MoveDown => t!("action.move_down"),
Action::MoveWordLeft => t!("action.move_word_left"),
Action::MoveWordRight => t!("action.move_word_right"),
Action::MoveLineStart => t!("action.move_line_start"),
Action::MoveLineEnd => t!("action.move_line_end"),
Action::MovePageUp => t!("action.move_page_up"),
Action::MovePageDown => t!("action.move_page_down"),
Action::MoveDocumentStart => t!("action.move_document_start"),
Action::MoveDocumentEnd => t!("action.move_document_end"),
Action::SelectLeft => t!("action.select_left"),
Action::SelectRight => t!("action.select_right"),
Action::SelectUp => t!("action.select_up"),
Action::SelectDown => t!("action.select_down"),
Action::SelectWordLeft => t!("action.select_word_left"),
Action::SelectWordRight => t!("action.select_word_right"),
Action::SelectLineStart => t!("action.select_line_start"),
Action::SelectLineEnd => t!("action.select_line_end"),
Action::SelectDocumentStart => t!("action.select_document_start"),
Action::SelectDocumentEnd => t!("action.select_document_end"),
Action::SelectPageUp => t!("action.select_page_up"),
Action::SelectPageDown => t!("action.select_page_down"),
Action::SelectAll => t!("action.select_all"),
Action::SelectWord => t!("action.select_word"),
Action::SelectLine => t!("action.select_line"),
Action::ExpandSelection => t!("action.expand_selection"),
Action::BlockSelectLeft => t!("action.block_select_left"),
Action::BlockSelectRight => t!("action.block_select_right"),
Action::BlockSelectUp => t!("action.block_select_up"),
Action::BlockSelectDown => t!("action.block_select_down"),
Action::DeleteBackward => t!("action.delete_backward"),
Action::DeleteForward => t!("action.delete_forward"),
Action::DeleteWordBackward => t!("action.delete_word_backward"),
Action::DeleteWordForward => t!("action.delete_word_forward"),
Action::DeleteLine => t!("action.delete_line"),
Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
Action::DeleteToLineStart => t!("action.delete_to_line_start"),
Action::TransposeChars => t!("action.transpose_chars"),
Action::OpenLine => t!("action.open_line"),
Action::Recenter => t!("action.recenter"),
Action::SetMark => t!("action.set_mark"),
Action::Copy => t!("action.copy"),
Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
Action::Cut => t!("action.cut"),
Action::Paste => t!("action.paste"),
Action::YankWordForward => t!("action.yank_word_forward"),
Action::YankWordBackward => t!("action.yank_word_backward"),
Action::YankToLineEnd => t!("action.yank_to_line_end"),
Action::YankToLineStart => t!("action.yank_to_line_start"),
Action::AddCursorAbove => t!("action.add_cursor_above"),
Action::AddCursorBelow => t!("action.add_cursor_below"),
Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
Action::Save => t!("action.save"),
Action::SaveAs => t!("action.save_as"),
Action::Open => t!("action.open"),
Action::SwitchProject => t!("action.switch_project"),
Action::New => t!("action.new"),
Action::Close => t!("action.close"),
Action::CloseTab => t!("action.close_tab"),
Action::Quit => t!("action.quit"),
Action::Revert => t!("action.revert"),
Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
Action::FormatBuffer => t!("action.format_buffer"),
Action::GotoLine => t!("action.goto_line"),
Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
Action::JumpToNextError => t!("action.jump_to_next_error"),
Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
Action::SmartHome => t!("action.smart_home"),
Action::DedentSelection => t!("action.dedent_selection"),
Action::ToggleComment => t!("action.toggle_comment"),
Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
Action::ListBookmarks => t!("action.list_bookmarks"),
Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
Action::StartMacroRecording => t!("action.start_macro_recording"),
Action::StopMacroRecording => t!("action.stop_macro_recording"),
Action::PlayMacro(c) => t!("action.play_macro", key = c),
Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
Action::ShowMacro(c) => t!("action.show_macro", key = c),
Action::ListMacros => t!("action.list_macros"),
Action::PromptRecordMacro => t!("action.prompt_record_macro"),
Action::PromptPlayMacro => t!("action.prompt_play_macro"),
Action::PlayLastMacro => t!("action.play_last_macro"),
Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
Action::Undo => t!("action.undo"),
Action::Redo => t!("action.redo"),
Action::ScrollUp => t!("action.scroll_up"),
Action::ScrollDown => t!("action.scroll_down"),
Action::ShowHelp => t!("action.show_help"),
Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
Action::ShowWarnings => t!("action.show_warnings"),
Action::ShowLspStatus => t!("action.show_lsp_status"),
Action::ClearWarnings => t!("action.clear_warnings"),
Action::CommandPalette => t!("action.command_palette"),
Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
Action::ToggleComposeMode => t!("action.toggle_compose_mode"),
Action::SetComposeWidth => t!("action.set_compose_width"),
Action::NextBuffer => t!("action.next_buffer"),
Action::PrevBuffer => t!("action.prev_buffer"),
Action::NavigateBack => t!("action.navigate_back"),
Action::NavigateForward => t!("action.navigate_forward"),
Action::SplitHorizontal => t!("action.split_horizontal"),
Action::SplitVertical => t!("action.split_vertical"),
Action::CloseSplit => t!("action.close_split"),
Action::NextSplit => t!("action.next_split"),
Action::PrevSplit => t!("action.prev_split"),
Action::IncreaseSplitSize => t!("action.increase_split_size"),
Action::DecreaseSplitSize => t!("action.decrease_split_size"),
Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
Action::PromptConfirm => t!("action.prompt_confirm"),
Action::PromptCancel => t!("action.prompt_cancel"),
Action::PromptBackspace => t!("action.prompt_backspace"),
Action::PromptDelete => t!("action.prompt_delete"),
Action::PromptMoveLeft => t!("action.prompt_move_left"),
Action::PromptMoveRight => t!("action.prompt_move_right"),
Action::PromptMoveStart => t!("action.prompt_move_start"),
Action::PromptMoveEnd => t!("action.prompt_move_end"),
Action::PromptSelectPrev => t!("action.prompt_select_prev"),
Action::PromptSelectNext => t!("action.prompt_select_next"),
Action::PromptPageUp => t!("action.prompt_page_up"),
Action::PromptPageDown => t!("action.prompt_page_down"),
Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
Action::PromptCopy => t!("action.prompt_copy"),
Action::PromptCut => t!("action.prompt_cut"),
Action::PromptPaste => t!("action.prompt_paste"),
Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
Action::PromptSelectAll => t!("action.prompt_select_all"),
Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
Action::PopupSelectNext => t!("action.popup_select_next"),
Action::PopupSelectPrev => t!("action.popup_select_prev"),
Action::PopupPageUp => t!("action.popup_page_up"),
Action::PopupPageDown => t!("action.popup_page_down"),
Action::PopupConfirm => t!("action.popup_confirm"),
Action::PopupCancel => t!("action.popup_cancel"),
Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
Action::FocusFileExplorer => t!("action.focus_file_explorer"),
Action::FocusEditor => t!("action.focus_editor"),
Action::FileExplorerUp => t!("action.file_explorer_up"),
Action::FileExplorerDown => t!("action.file_explorer_down"),
Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
Action::FileExplorerExpand => t!("action.file_explorer_expand"),
Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
Action::FileExplorerOpen => t!("action.file_explorer_open"),
Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
Action::FileExplorerDelete => t!("action.file_explorer_delete"),
Action::FileExplorerRename => t!("action.file_explorer_rename"),
Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
Action::LspCompletion => t!("action.lsp_completion"),
Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
Action::LspReferences => t!("action.lsp_references"),
Action::LspRename => t!("action.lsp_rename"),
Action::LspHover => t!("action.lsp_hover"),
Action::LspSignatureHelp => t!("action.lsp_signature_help"),
Action::LspCodeActions => t!("action.lsp_code_actions"),
Action::LspRestart => t!("action.lsp_restart"),
Action::LspStop => t!("action.lsp_stop"),
Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
Action::SetBackground => t!("action.set_background"),
Action::SetBackgroundBlend => t!("action.set_background_blend"),
Action::SetTabSize => t!("action.set_tab_size"),
Action::SetLineEnding => t!("action.set_line_ending"),
Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
Action::DumpConfig => t!("action.dump_config"),
Action::Search => t!("action.search"),
Action::FindInSelection => t!("action.find_in_selection"),
Action::FindNext => t!("action.find_next"),
Action::FindPrevious => t!("action.find_previous"),
Action::FindSelectionNext => t!("action.find_selection_next"),
Action::FindSelectionPrevious => t!("action.find_selection_previous"),
Action::Replace => t!("action.replace"),
Action::QueryReplace => t!("action.query_replace"),
Action::MenuActivate => t!("action.menu_activate"),
Action::MenuClose => t!("action.menu_close"),
Action::MenuLeft => t!("action.menu_left"),
Action::MenuRight => t!("action.menu_right"),
Action::MenuUp => t!("action.menu_up"),
Action::MenuDown => t!("action.menu_down"),
Action::MenuExecute => t!("action.menu_execute"),
Action::MenuOpen(name) => t!("action.menu_open", name = name),
Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
Action::PluginAction(name) => t!("action.plugin_action", name = name),
Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
Action::SelectTheme => t!("action.select_theme"),
Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
Action::SelectCursorStyle => t!("action.select_cursor_style"),
Action::SelectLocale => t!("action.select_locale"),
Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
Action::OpenTerminal => t!("action.open_terminal"),
Action::CloseTerminal => t!("action.close_terminal"),
Action::FocusTerminal => t!("action.focus_terminal"),
Action::TerminalEscape => t!("action.terminal_escape"),
Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
Action::TerminalPaste => t!("action.terminal_paste"),
Action::OpenSettings => t!("action.open_settings"),
Action::CloseSettings => t!("action.close_settings"),
Action::SettingsSave => t!("action.settings_save"),
Action::SettingsReset => t!("action.settings_reset"),
Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
Action::SettingsActivate => t!("action.settings_activate"),
Action::SettingsSearch => t!("action.settings_search"),
Action::SettingsHelp => t!("action.settings_help"),
Action::SettingsIncrement => t!("action.settings_increment"),
Action::SettingsDecrement => t!("action.settings_decrement"),
Action::ShellCommand => t!("action.shell_command"),
Action::ShellCommandReplace => t!("action.shell_command_replace"),
Action::ToUpperCase => t!("action.to_uppercase"),
Action::ToLowerCase => t!("action.to_lowercase"),
Action::CalibrateInput => t!("action.calibrate_input"),
Action::None => t!("action.none"),
}
.to_string()
}
pub fn get_keybinding_for_action(
&self,
action: &Action,
context: KeyContext,
) -> Option<String> {
fn find_best_keybinding(
bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
action: &Action,
) -> Option<(KeyCode, KeyModifiers)> {
let matches: Vec<_> = bindings
.iter()
.filter(|(_, a)| *a == action)
.map(|((k, m), _)| (*k, *m))
.collect();
if matches.is_empty() {
return None;
}
let mut sorted = matches;
sorted.sort_by(|(k1, m1), (k2, m2)| {
let score1 = keybinding_priority_score(k1);
let score2 = keybinding_priority_score(k2);
match score1.cmp(&score2) {
std::cmp::Ordering::Equal => {
let s1 = format_keybinding(k1, m1);
let s2 = format_keybinding(k2, m2);
s1.cmp(&s2)
}
other => other,
}
});
sorted.into_iter().next()
}
if let Some(context_bindings) = self.bindings.get(&context) {
if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
return Some(format_keybinding(&keycode, &modifiers));
}
}
if let Some(context_bindings) = self.default_bindings.get(&context) {
if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
return Some(format_keybinding(&keycode, &modifiers));
}
}
if context != KeyContext::Normal && Self::is_application_wide_action(action) {
if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
return Some(format_keybinding(&keycode, &modifiers));
}
}
if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
return Some(format_keybinding(&keycode, &modifiers));
}
}
}
None
}
pub fn reload(&mut self, config: &Config) {
self.bindings.clear();
for binding in &config.keybindings {
if let Some(key_code) = Self::parse_key(&binding.key) {
let modifiers = Self::parse_modifiers(&binding.modifiers);
if let Some(action) = Action::from_str(&binding.action, &binding.args) {
let context = if let Some(ref when) = binding.when {
KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
} else {
KeyContext::Normal
};
self.bindings
.entry(context)
.or_insert_with(HashMap::new)
.insert((key_code, modifiers), action);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key() {
assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
assert_eq!(
KeybindingResolver::parse_key("backspace"),
Some(KeyCode::Backspace)
);
assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
assert_eq!(
KeybindingResolver::parse_key("backtab"),
Some(KeyCode::BackTab)
);
assert_eq!(
KeybindingResolver::parse_key("BackTab"),
Some(KeyCode::BackTab)
);
assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
}
#[test]
fn test_parse_modifiers() {
let mods = vec!["ctrl".to_string()];
assert_eq!(
KeybindingResolver::parse_modifiers(&mods),
KeyModifiers::CONTROL
);
let mods = vec!["ctrl".to_string(), "shift".to_string()];
assert_eq!(
KeybindingResolver::parse_modifiers(&mods),
KeyModifiers::CONTROL | KeyModifiers::SHIFT
);
}
#[test]
fn test_resolve_basic() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
assert_eq!(
resolver.resolve(&event, KeyContext::Normal),
Action::MoveLeft
);
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
assert_eq!(
resolver.resolve(&event, KeyContext::Normal),
Action::InsertChar('a')
);
}
#[test]
fn test_action_from_str() {
let args = HashMap::new();
assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
assert_eq!(Action::from_str("save", &args), Some(Action::Save));
assert_eq!(Action::from_str("unknown", &args), None);
assert_eq!(
Action::from_str("keyboard_shortcuts", &args),
Some(Action::ShowKeyboardShortcuts)
);
assert_eq!(
Action::from_str("prompt_confirm", &args),
Some(Action::PromptConfirm)
);
assert_eq!(
Action::from_str("popup_cancel", &args),
Some(Action::PopupCancel)
);
assert_eq!(
Action::from_str("calibrate_input", &args),
Some(Action::CalibrateInput)
);
}
#[test]
fn test_key_context_from_when_clause() {
assert_eq!(
KeyContext::from_when_clause("normal"),
Some(KeyContext::Normal)
);
assert_eq!(
KeyContext::from_when_clause("prompt"),
Some(KeyContext::Prompt)
);
assert_eq!(
KeyContext::from_when_clause("popup"),
Some(KeyContext::Popup)
);
assert_eq!(KeyContext::from_when_clause("help"), None);
assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
assert_eq!(KeyContext::from_when_clause(""), None);
}
#[test]
fn test_key_context_to_when_clause() {
assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
}
#[test]
fn test_context_specific_bindings() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
assert_eq!(
resolver.resolve(&enter_event, KeyContext::Prompt),
Action::PromptConfirm
);
assert_eq!(
resolver.resolve(&enter_event, KeyContext::Normal),
Action::InsertNewline
);
let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
assert_eq!(
resolver.resolve(&up_event, KeyContext::Popup),
Action::PopupSelectPrev
);
assert_eq!(
resolver.resolve(&up_event, KeyContext::Normal),
Action::MoveUp
);
}
#[test]
fn test_context_fallback_to_normal() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&save_event, KeyContext::Normal),
Action::Save
);
assert_eq!(
resolver.resolve(&save_event, KeyContext::Popup),
Action::Save
);
}
#[test]
fn test_context_priority_resolution() {
use crate::config::Keybinding;
let mut config = Config::default();
config.keybindings.push(Keybinding {
key: "esc".to_string(),
modifiers: vec![],
keys: vec![],
action: "quit".to_string(), args: HashMap::new(),
when: Some("popup".to_string()),
});
let resolver = KeybindingResolver::new(&config);
let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
assert_eq!(
resolver.resolve(&esc_event, KeyContext::Popup),
Action::Quit
);
assert_eq!(
resolver.resolve(&esc_event, KeyContext::Normal),
Action::RemoveSecondaryCursors
);
}
#[test]
fn test_character_input_in_contexts() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
assert_eq!(
resolver.resolve(&char_event, KeyContext::Normal),
Action::InsertChar('a')
);
assert_eq!(
resolver.resolve(&char_event, KeyContext::Prompt),
Action::InsertChar('a')
);
assert_eq!(
resolver.resolve(&char_event, KeyContext::Popup),
Action::None
);
}
#[test]
fn test_custom_keybinding_loading() {
use crate::config::Keybinding;
let mut config = Config::default();
config.keybindings.push(Keybinding {
key: "f".to_string(),
modifiers: vec!["ctrl".to_string()],
keys: vec![],
action: "command_palette".to_string(),
args: HashMap::new(),
when: None, });
let resolver = KeybindingResolver::new(&config);
let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&ctrl_f, KeyContext::Normal),
Action::CommandPalette
);
let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&ctrl_k, KeyContext::Prompt),
Action::PromptDeleteToLineEnd
);
assert_eq!(
resolver.resolve(&ctrl_k, KeyContext::Normal),
Action::DeleteToLineEnd
);
}
#[test]
fn test_all_context_default_bindings_exist() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
assert!(resolver
.default_bindings
.contains_key(&KeyContext::FileExplorer));
assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
}
#[test]
fn test_resolve_determinism() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let test_cases = vec![
(KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
(
KeyCode::Esc,
KeyModifiers::empty(),
KeyContext::FileExplorer,
),
(KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
(KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
];
for (key_code, modifiers, context) in test_cases {
let event = KeyEvent::new(key_code, modifiers);
let action1 = resolver.resolve(&event, context);
let action2 = resolver.resolve(&event, context);
let action3 = resolver.resolve(&event, context);
assert_eq!(action1, action2, "Resolve should be deterministic");
assert_eq!(action2, action3, "Resolve should be deterministic");
}
}
#[test]
fn test_modifier_combinations() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let char_s = KeyCode::Char('s');
let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
let action_shift = resolver.resolve(&shift, KeyContext::Normal);
let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
assert_eq!(action_no_mod, Action::InsertChar('s'));
assert_eq!(action_ctrl, Action::Save);
assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
}
#[test]
fn test_scroll_keybindings() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&ctrl_up, KeyContext::Normal),
Action::ScrollUp,
"Ctrl+Up should resolve to ScrollUp"
);
let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&ctrl_down, KeyContext::Normal),
Action::ScrollDown,
"Ctrl+Down should resolve to ScrollDown"
);
}
#[test]
fn test_lsp_completion_keybinding() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
assert_eq!(
resolver.resolve(&ctrl_space, KeyContext::Normal),
Action::LspCompletion,
"Ctrl+Space should resolve to LspCompletion"
);
}
#[test]
fn test_terminal_key_equivalents() {
let ctrl = KeyModifiers::CONTROL;
let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
assert!(a_equivs.is_empty());
let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
assert!(slash_no_ctrl.is_empty());
}
#[test]
fn test_terminal_key_equivalents_auto_binding() {
let config = Config::default();
let resolver = KeybindingResolver::new(&config);
let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
assert_eq!(
action_slash,
Action::ToggleComment,
"Ctrl+/ should resolve to ToggleComment"
);
let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
assert_eq!(
action_7,
Action::ToggleComment,
"Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
);
}
#[test]
fn test_terminal_key_equivalents_normalization() {
let ctrl = KeyModifiers::CONTROL;
let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
assert_eq!(
slash_equivs,
vec![(KeyCode::Char('7'), ctrl)],
"Ctrl+/ should map to Ctrl+7"
);
let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
assert_eq!(
seven_equivs,
vec![(KeyCode::Char('/'), ctrl)],
"Ctrl+7 should map back to Ctrl+/"
);
let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
assert_eq!(
backspace_equivs,
vec![(KeyCode::Char('h'), ctrl)],
"Ctrl+Backspace should map to Ctrl+H"
);
let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
assert_eq!(
h_equivs,
vec![(KeyCode::Backspace, ctrl)],
"Ctrl+H should map back to Ctrl+Backspace"
);
let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
assert_eq!(
space_equivs,
vec![(KeyCode::Char('@'), ctrl)],
"Ctrl+Space should map to Ctrl+@"
);
let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
assert_eq!(
at_equivs,
vec![(KeyCode::Char(' '), ctrl)],
"Ctrl+@ should map back to Ctrl+Space"
);
let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
assert_eq!(
minus_equivs,
vec![(KeyCode::Char('_'), ctrl)],
"Ctrl+- should map to Ctrl+_"
);
let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
assert_eq!(
underscore_equivs,
vec![(KeyCode::Char('-'), ctrl)],
"Ctrl+_ should map back to Ctrl+-"
);
assert!(
terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
"Ctrl+A should have no terminal equivalents"
);
assert!(
terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
"Ctrl+Z should have no terminal equivalents"
);
assert!(
terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
"Ctrl+Enter should have no terminal equivalents"
);
assert!(
terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
"/ without Ctrl should have no equivalents"
);
assert!(
terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
"Shift+7 should have no equivalents"
);
assert!(
terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
"Alt+H should have no equivalents"
);
let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
assert!(
ctrl_shift_h_equivs.is_empty(),
"Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
);
}
}