pub use crate::commands::MappableCommand; use crate::config::Config; use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, ops::{Deref, DerefMut}, }; #[macro_export] macro_rules! key { ($key:ident) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; ($($ch:tt)*) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; } #[macro_export] macro_rules! shift { ($key:ident) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, } }; ($($ch:tt)*) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, } }; } #[macro_export] macro_rules! ctrl { ($key:ident) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; ($($ch:tt)*) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; } #[macro_export] macro_rules! alt { ($key:ident) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; ($($ch:tt)*) => { ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; } /// Macro for defining the root of a `Keymap` object. Example: /// /// ``` /// # use helix_core::hashmap; /// # use helix_term::keymap; /// # use helix_term::keymap::Keymap; /// let normal_mode = keymap!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" /// "g" => goto_file_start, /// "e" => goto_file_end, /// }, /// "j" | "down" => move_line_down, /// }); /// let keymap = Keymap::new(normal_mode); /// ``` #[macro_export] macro_rules! keymap { (@trie $cmd:ident) => { $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) }; (@trie { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) }; (@trie [$($cmd:ident),* $(,)?]) => { $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*]) }; ( { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { // modified from the hashmap! macro { let _cap = hashmap!(@count $($($key),+),*); let mut _map = ::std::collections::HashMap::with_capacity(_cap); let mut _order = ::std::vec::Vec::with_capacity(_cap); $( $( let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); let _duplicate = _map.insert( _key, keymap!(@trie $value) ); assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); _order.push(_key); )+ )* let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); $( _node.is_sticky = $sticky; )? $crate::keymap::KeyTrie::Node(_node) } }; } #[derive(Debug, Clone)] pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" name: String, map: HashMap, order: Vec, pub is_sticky: bool, } impl<'de> Deserialize<'de> for KeyTrieNode { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let map = HashMap::::deserialize(deserializer)?; let order = map.keys().copied().collect::>(); // NOTE: map.keys() has arbitrary order Ok(Self { map, order, ..Default::default() }) } } impl KeyTrieNode { pub fn new(name: &str, map: HashMap, order: Vec) -> Self { Self { name: name.to_string(), map, order, is_sticky: false, } } pub fn name(&self) -> &str { &self.name } /// Merge another Node in. Leaves and subnodes from the other node replace /// corresponding keyevent in self, except when both other and self have /// subnodes for same key. In that case the merge is recursive. pub fn merge(&mut self, mut other: Self) { for (key, trie) in std::mem::take(&mut other.map) { if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) { if let KeyTrie::Node(other_node) = trie { node.merge(other_node); continue; } } self.map.insert(key, trie); } for &key in self.map.keys() { if !self.order.contains(&key) { self.order.push(key); } } } pub fn infobox(&self) -> Info { let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => { if cmd.name() == "no_op" { continue; } cmd.doc() } KeyTrie::Node(n) => n.name(), KeyTrie::Sequence(_) => "[Multiple commands]", }; match body.iter().position(|(d, _)| d == &desc) { Some(pos) => { body[pos].1.insert(key); } None => body.push((desc, BTreeSet::from([key]))), } } body.sort_unstable_by_key(|(_, keys)| { self.order .iter() .position(|&k| k == *keys.iter().next().unwrap()) .unwrap() }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body .into_iter() .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .collect(); } Info::from_keymap(self.name(), body) } /// Get a reference to the key trie node's order. pub fn order(&self) -> &[KeyEvent] { self.order.as_slice() } } impl Default for KeyTrieNode { fn default() -> Self { Self::new("", HashMap::new(), Vec::new()) } } impl PartialEq for KeyTrieNode { fn eq(&self, other: &Self) -> bool { self.map == other.map } } impl Deref for KeyTrieNode { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.map } } impl DerefMut for KeyTrieNode { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.map } } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(untagged)] pub enum KeyTrie { Leaf(MappableCommand), Sequence(Vec), Node(KeyTrieNode), } impl KeyTrie { pub fn node(&self) -> Option<&KeyTrieNode> { match *self { KeyTrie::Node(ref node) => Some(node), KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, } } pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { match *self { KeyTrie::Node(ref mut node) => Some(node), KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, } } /// Merge another KeyTrie in, assuming that this KeyTrie and the other /// are both Nodes. Panics otherwise. pub fn merge_nodes(&mut self, mut other: Self) { let node = std::mem::take(other.node_mut().unwrap()); self.node_mut().unwrap().merge(node); } pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> { let mut trie = self; for key in keys { trie = match trie { KeyTrie::Node(map) => map.get(key), // leaf encountered while keys left to process KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, }? } Some(trie) } } #[derive(Debug, Clone, PartialEq)] pub enum KeymapResultKind { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), Matched(MappableCommand), /// Matched a sequence of commands to execute. MatchedSequence(Vec), /// Key was not found in the root keymap NotFound, /// Key is invalid in combination with previous keys. Contains keys leading upto /// and including current (invalid) key. Cancelled(Vec), } /// Returned after looking up a key in [`Keymap`]. The `sticky` field has a /// reference to the sticky node if one is currently active. #[derive(Debug)] pub struct KeymapResult<'a> { pub kind: KeymapResultKind, pub sticky: Option<&'a KeyTrieNode>, } impl<'a> KeymapResult<'a> { pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self { Self { kind, sticky } } } #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Keymap { /// Always a Node #[serde(flatten)] root: KeyTrie, /// Stores pending keys waiting for the next key. This is relative to a /// sticky node if one is in use. #[serde(skip)] state: Vec, /// Stores the sticky node if one is activated. #[serde(skip)] sticky: Option, } impl Keymap { pub fn new(root: KeyTrie) -> Self { Keymap { root, state: Vec::new(), sticky: None, } } pub fn reverse_map(&self) -> HashMap>> { // recursively visit all nodes in keymap fn map_node( cmd_map: &mut HashMap>>, node: &KeyTrie, keys: &mut Vec, ) { match node { KeyTrie::Leaf(cmd) => match cmd { MappableCommand::Typable { name, .. } => { cmd_map.entry(name.into()).or_default().push(keys.clone()) } MappableCommand::Static { name, .. } => cmd_map .entry(name.to_string()) .or_default() .push(keys.clone()), }, KeyTrie::Node(next) => { for (key, trie) in &next.map { keys.push(*key); map_node(cmd_map, trie, keys); keys.pop(); } } KeyTrie::Sequence(_) => {} }; } let mut res = HashMap::new(); map_node(&mut res, &self.root, &mut Vec::new()); res } pub fn root(&self) -> &KeyTrie { &self.root } pub fn sticky(&self) -> Option<&KeyTrieNode> { self.sticky.as_ref() } /// Returns list of keys waiting to be disambiguated. pub fn pending(&self) -> &[KeyEvent] { &self.state } /// Lookup `key` in the keymap to try and find a command to execute. Escape /// key cancels pending keystrokes. If there are no pending keystrokes but a /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { if key!(Esc) == key { if !self.state.is_empty() { return KeymapResult::new( // Note that Esc is not included here KeymapResultKind::Cancelled(self.state.drain(..).collect()), self.sticky(), ); } self.sticky = None; } let first = self.state.get(0).unwrap_or(&key); let trie_node = match self.sticky { Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), None => Cow::Borrowed(&self.root), }; let trie = match trie_node.search(&[*first]) { Some(KeyTrie::Leaf(ref cmd)) => { return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky()) } Some(KeyTrie::Sequence(ref cmds)) => { return KeymapResult::new( KeymapResultKind::MatchedSequence(cmds.clone()), self.sticky(), ) } None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), Some(t) => t, }; self.state.push(key); match trie.search(&self.state[1..]) { Some(&KeyTrie::Node(ref map)) => { if map.is_sticky { self.state.clear(); self.sticky = Some(map.clone()); } KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) } Some(&KeyTrie::Leaf(ref cmd)) => { self.state.clear(); return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky()); } Some(&KeyTrie::Sequence(ref cmds)) => { self.state.clear(); KeymapResult::new( KeymapResultKind::MatchedSequence(cmds.clone()), self.sticky(), ) } None => KeymapResult::new( KeymapResultKind::Cancelled(self.state.drain(..).collect()), self.sticky(), ), } } pub fn merge(&mut self, other: Self) { self.root.merge_nodes(other.root); } } impl Deref for Keymap { type Target = KeyTrieNode; fn deref(&self) -> &Self::Target { self.root.node().unwrap() } } impl Default for Keymap { fn default() -> Self { Self::new(KeyTrie::Node(KeyTrieNode::default())) } } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] pub struct Keymaps(pub HashMap); impl Keymaps { /// Returns list of keys waiting to be disambiguated in current mode. pub fn pending(&self) -> &[KeyEvent] { self.0 .values() .find_map(|keymap| match keymap.pending().is_empty() { true => None, false => Some(keymap.pending()), }) .unwrap_or_default() } } impl Deref for Keymaps { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Keymaps { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Default for Keymaps { fn default() -> Self { let normal = keymap!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, "k" | "up" => move_line_up, "l" | "right" => move_char_right, "t" => find_till_char, "f" => find_next_char, "T" => till_prev_char, "F" => find_prev_char, "r" => replace, "R" => replace_with_yanked, "A-." => repeat_last_motion, "~" => switch_case, "`" => switch_to_lowercase, "A-`" => switch_to_uppercase, "home" => goto_line_start, "end" => goto_line_end, "w" => move_next_word_start, "b" => move_prev_word_start, "e" => move_next_word_end, "W" => move_next_long_word_start, "B" => move_prev_long_word_start, "E" => move_next_long_word_end, "v" => select_mode, "G" => goto_line, "g" => { "Goto" "g" => goto_file_start, "e" => goto_last_line, "f" => goto_file, "h" => goto_line_start, "l" => goto_line_end, "s" => goto_first_nonwhitespace, "d" => goto_definition, "y" => goto_type_definition, "r" => goto_reference, "i" => goto_implementation, "t" => goto_window_top, "c" => goto_window_center, "b" => goto_window_bottom, "a" => goto_last_accessed_file, "m" => goto_last_modified_file, "n" => goto_next_buffer, "p" => goto_previous_buffer, "." => goto_last_modification, }, ":" => command_mode, "i" => insert_mode, "I" => prepend_to_line, "a" => append_mode, "A" => append_to_line, "o" => open_below, "O" => open_above, "d" => delete_selection, "A-d" => delete_selection_noyank, "c" => change_selection, "A-c" => change_selection_noyank, "C" => copy_selection_on_next_line, "A-C" => copy_selection_on_prev_line, "s" => select_regex, "A-s" => split_selection_on_newline, "S" => split_selection, ";" => collapse_selection, "A-;" => flip_selections, "A-k" => expand_selection, "A-j" => shrink_selection, "A-h" => select_prev_sibling, "A-l" => select_next_sibling, "%" => select_all, "x" => extend_line, "X" => extend_to_line_bounds, // crop_to_whole_line "m" => { "Match" "m" => match_brackets, "s" => surround_add, "r" => surround_replace, "d" => surround_delete, "a" => select_textobject_around, "i" => select_textobject_inner, }, "[" => { "Left bracket" "d" => goto_prev_diag, "D" => goto_first_diag, "f" => goto_prev_function, "c" => goto_prev_class, "p" => goto_prev_parameter, "space" => add_newline_above, }, "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, "f" => goto_next_function, "c" => goto_next_class, "p" => goto_next_parameter, "space" => add_newline_below, }, "/" => search, "?" => rsearch, "n" => search_next, "N" => search_prev, "*" => search_selection, "u" => undo, "U" => redo, "A-u" => earlier, "A-U" => later, "y" => yank, // yank_all "p" => paste_after, // paste_all "P" => paste_before, "Q" => record_macro, "q" => replay_macro, ">" => indent, "<" => unindent, "=" => format_selections, "J" => join_selections, "K" => keep_selections, "A-K" => remove_selections, "," => keep_primary_selection, "A-," => remove_primary_selection, // "q" => record_macro, // "Q" => replay_macro, "&" => align_selections, "_" => trim_selections, "(" => rotate_selections_backward, ")" => rotate_selections_forward, "A-(" => rotate_selection_contents_backward, "A-)" => rotate_selection_contents_forward, "A-:" => ensure_selections_forward, "esc" => normal_mode, "C-b" | "pageup" => page_up, "C-f" | "pagedown" => page_down, "C-u" => half_page_up, "C-d" => half_page_down, "C-w" => { "Window" "C-w" | "w" => rotate_view, "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "f" => goto_file_hsplit, "F" => goto_file_vsplit, "C-q" | "q" => wclose, "C-o" | "o" => wonly, "C-h" | "h" | "left" => jump_view_left, "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, }, // move under c "C-c" => toggle_comments, // z family for save/restore/combine from/to sels from register "tab" => jump_forward, // tab == "C-o" => jump_backward, "C-s" => save_selection, "space" => { "Space" "f" => file_picker, "b" => buffer_picker, "s" => symbol_picker, "S" => workspace_symbol_picker, "a" => code_action, "'" => last_picker, "d" => { "Debug (experimental)" sticky=true "l" => dap_launch, "b" => dap_toggle_breakpoint, "c" => dap_continue, "h" => dap_pause, "i" => dap_step_in, "o" => dap_step_out, "n" => dap_next, "v" => dap_variables, "t" => dap_terminate, "C-c" => dap_edit_condition, "C-l" => dap_edit_log, "s" => { "Switch" "t" => dap_switch_thread, "f" => dap_switch_stack_frame, // sl, sb }, "e" => dap_enable_exceptions, "E" => dap_disable_exceptions, }, "w" => { "Window" "C-w" | "w" => rotate_view, "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "f" => goto_file_hsplit, "F" => goto_file_vsplit, "C-q" | "q" => wclose, "C-o" | "o" => wonly, "C-h" | "h" | "left" => jump_view_left, "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, }, "y" => yank_joined_to_clipboard, "Y" => yank_main_selection_to_clipboard, "p" => paste_clipboard_after, "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, "/" => global_search, "k" => hover, "r" => rename_symbol, "?" => command_palette, }, "z" => { "View" "z" | "c" => align_view_center, "t" => align_view_top, "b" => align_view_bottom, "m" => align_view_middle, "k" | "up" => scroll_up, "j" | "down" => scroll_down, "C-b" | "pageup" => page_up, "C-f" | "pagedown" => page_down, "C-u" => half_page_up, "C-d" => half_page_down, }, "Z" => { "View" sticky=true "z" | "c" => align_view_center, "t" => align_view_top, "b" => align_view_bottom, "m" => align_view_middle, "k" | "up" => scroll_up, "j" | "down" => scroll_down, "C-b" | "pageup" => page_up, "C-f" | "pagedown" => page_down, "C-u" => half_page_up, "C-d" => half_page_down, }, "\"" => select_register, "|" => shell_pipe, "A-|" => shell_pipe_to, "!" => shell_insert_output, "A-!" => shell_append_output, "$" => shell_keep_pipe, "C-z" => suspend, "C-a" => increment, "C-x" => decrement, }); let mut select = normal.clone(); select.merge_nodes(keymap!({ "Select mode" "h" | "left" => extend_char_left, "j" | "down" => extend_line_down, "k" | "up" => extend_line_up, "l" | "right" => extend_char_right, "w" => extend_next_word_start, "b" => extend_prev_word_start, "e" => extend_next_word_end, "W" => extend_next_long_word_start, "B" => extend_prev_long_word_start, "E" => extend_next_long_word_end, "n" => extend_search_next, "N" => extend_search_prev, "t" => extend_till_char, "f" => extend_next_char, "T" => extend_till_prev_char, "F" => extend_prev_char, "home" => extend_to_line_start, "end" => extend_to_line_end, "esc" => exit_select_mode, "v" => normal_mode, })); let insert = keymap!({ "Insert mode" "esc" => normal_mode, "backspace" => delete_char_backward, "C-h" => delete_char_backward, "del" => delete_char_forward, "C-d" => delete_char_forward, "ret" => insert_newline, "C-j" => insert_newline, "tab" => insert_tab, "C-w" => delete_word_backward, "A-backspace" => delete_word_backward, "A-d" => delete_word_forward, "left" => move_char_left, "C-b" => move_char_left, "down" => move_line_down, "C-n" => move_line_down, "up" => move_line_up, "C-p" => move_line_up, "right" => move_char_right, "C-f" => move_char_right, "A-b" => move_prev_word_end, "A-left" => move_prev_word_end, "A-f" => move_next_word_start, "A-right" => move_next_word_start, "A-<" => goto_file_start, "A->" => goto_file_end, "pageup" => page_up, "pagedown" => page_down, "home" => goto_line_start, "C-a" => goto_line_start, "end" => goto_line_end_newline, "C-e" => goto_line_end_newline, "C-k" => kill_to_line_end, "C-u" => kill_to_line_start, "C-x" => completion, "C-r" => insert_register, }); Self(hashmap!( Mode::Normal => Keymap::new(normal), Mode::Select => Keymap::new(select), Mode::Insert => Keymap::new(insert), )) } } /// Merge default config keys with user overwritten keys for custom user config. pub fn merge_keys(mut config: Config) -> Config { let mut delta = std::mem::take(&mut config.keys); for (mode, keys) in &mut *config.keys { keys.merge(delta.remove(mode).unwrap_or_default()) } config } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn duplicate_keys_should_panic() { keymap!({ "Normal mode" "i" => normal_mode, "i" => goto_definition, }); } #[test] fn check_duplicate_keys_in_default_keymap() { // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro Keymaps::default(); } #[test] fn merge_partial_keys() { let config = Config { keys: Keymaps(hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, "g" => { "Merge into goto mode" "$" => goto_line_end, "g" => delete_char_forward, }, }) ) }), ..Default::default() }; let mut merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( keymap.get(key!('i')).kind, KeymapResultKind::Matched(MappableCommand::normal_mode), "Leaf should replace leaf" ); assert_eq!( keymap.get(key!('无')).kind, KeymapResultKind::Matched(MappableCommand::insert_mode), "New leaf should be present in merged keymap" ); // Assumes that z is a node in the default keymap assert_eq!( keymap.get(key!('z')).kind, KeymapResultKind::Matched(MappableCommand::jump_backward), "Leaf should replace node" ); // Assumes that `g` is a node in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('$')]).unwrap(), &KeyTrie::Leaf(MappableCommand::goto_line_end), "Leaf should be present in merged subnode" ); // Assumes that `gg` is in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('g')]).unwrap(), &KeyTrie::Leaf(MappableCommand::delete_char_forward), "Leaf should replace old leaf in merged subnode" ); // Assumes that `ge` is in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('e')]).unwrap(), &KeyTrie::Leaf(MappableCommand::goto_last_line), "Old leaves in subnode should be present in merged node" ); assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); } #[test] fn order_should_be_set() { let config = Config { keys: Keymaps(hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" "space" => { "" "s" => { "" "v" => vsplit, "c" => hsplit, }, }, }) ) }), ..Default::default() }; let mut merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( keymap .root() .search(&[key!(' '), key!('s'), key!('v')]) .unwrap(), &KeyTrie::Leaf(MappableCommand::vsplit), "Leaf should be present in merged subnode" ); // Make sure an order was set during merge let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); assert!(!node.node().unwrap().order().is_empty()) } #[test] fn aliased_modes_are_same_in_default_keymap() { let keymaps = Keymaps::default(); let root = keymaps.get(&Mode::Normal).unwrap().root(); assert_eq!( root.search(&[key!(' '), key!('w')]).unwrap(), root.search(&["C-w".parse::().unwrap()]).unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`" ); assert_eq!( root.search(&[key!('z')]).unwrap(), root.search(&[key!('Z')]).unwrap(), "Mismatch for view mode on `z` and `Z`" ); } #[test] fn reverse_map() { let normal_mode = keymap!({ "Normal mode" "i" => insert_mode, "g" => { "Goto" "g" => goto_file_start, "e" => goto_file_end, }, "j" | "k" => move_line_down, }); let keymap = Keymap::new(normal_mode); let mut reverse_map = keymap.reverse_map(); // sort keybindings in order to have consistent tests // HashMaps can be compared but we can still get different ordering of bindings // for commands that have multiple bindings assigned for v in reverse_map.values_mut() { v.sort() } assert_eq!( reverse_map, HashMap::from([ ("insert_mode".to_string(), vec![vec![key!('i')]]), ( "goto_file_start".to_string(), vec![vec![key!('g'), key!('g')]] ), ( "goto_file_end".to_string(), vec![vec![key!('g'), key!('e')]] ), ( "move_line_down".to_string(), vec![vec![key!('j')], vec![key!('k')]] ), ]), "Mistmatch" ) } }