diff --git a/book/src/remapping.md b/book/src/remapping.md index c98bd41e..51ecbb6d 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -41,6 +41,13 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions "ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode "A-x" = "@x" # Maps Alt-x to a macro selecting the whole line and deleting it without yanking it +# You can create labeled sub-menus and provide friendly labels for typeable commands +[keys.normal.space.f] # Registering multiple mappings under a single entry creates a sub-menu (accessed by 'space', 'f' in this case) +label = "File" # The menu is called file and within it: +f = "file_picker" # 'f' opens the file picker +s = { label = "Save", command = ":write" } # 's' saves the current file +c = { label = "Edit Config", command = ":open ~/.config/helix/config.toml" } # 'c' opens the helix config file + [keys.insert] "A-x" = "normal_mode" # Maps Alt-X to enter normal mode j = { k = "normal_mode" } # Maps `jk` to exit insert mode diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3e61b801..42ec76f8 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -199,6 +199,86 @@ mod tests { ); } + #[test] + fn parsing_menus() { + use crate::keymap; + use helix_core::hashmap; + use helix_view::document::Mode; + + let sample_keymaps = r#" + [keys.normal] + f = { f = "file_picker", c = "wclose" } + b = { label = "buffer", b = "buffer_picker", n = "goto_next_buffer" } + "#; + + let mut keys = keymap::default(); + merge_keys( + &mut keys, + hashmap! { + Mode::Normal => keymap!({ "Normal mode" + "f" => { "" + "f" => file_picker, + "c" => wclose, + }, + "b" => { "buffer" + "b" => buffer_picker, + "n" => goto_next_buffer, + }, + }), + }, + ); + + assert_eq!( + Config::load_test(sample_keymaps), + Config { + keys, + ..Default::default() + } + ); + } + + #[test] + fn parsing_typable_commands() { + use crate::keymap; + use crate::keymap::MappableCommand; + use helix_view::document::Mode; + use helix_view::input::KeyEvent; + use std::str::FromStr; + + let sample_keymaps = r#" + [keys.normal] + o = { label = "Edit Config", command = ":open ~/.config" } + c = ":buffer-close" + "#; + + let config = Config::load_test(sample_keymaps); + + let tree = config.keys.get(&Mode::Normal).unwrap(); + + if let keymap::KeyTrie::Node(node) = tree { + let open_node = node.get(&KeyEvent::from_str("o").unwrap()).unwrap(); + + if let keymap::KeyTrie::MappableCommand(MappableCommand::Typable { doc, .. }) = + open_node + { + assert_eq!(doc, "Edit Config"); + } else { + panic!("Edit Config did not parse to typable command"); + } + + let close_node = node.get(&KeyEvent::from_str("c").unwrap()).unwrap(); + if let keymap::KeyTrie::MappableCommand(MappableCommand::Typable { doc, .. }) = + close_node + { + assert_eq!(doc, "Close the current buffer."); + } else { + panic!(":buffer-close command did not parse to typable command"); + } + } else { + panic!("Config did not parse to trie"); + } + } + #[test] fn keys_resolve_to_correct_defaults() { // From serde default diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a9013edc..14d264f1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -173,11 +173,36 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { where M: serde::de::MapAccess<'de>, { + let mut label = String::from(""); + let mut command = None; let mut mapping = IndexMap::new(); - while let Some((key, value)) = map.next_entry::()? { - mapping.insert(key, value); + + while let Some(key) = map.next_key::()? { + match &key as &str { + "label" => label = map.next_value::()?, + "command" => command = Some(map.next_value::()?), + _ => { + let key_event = key.parse::().map_err(serde::de::Error::custom)?; + let key_trie = map.next_value::()?; + mapping.insert(key_event, key_trie); + } + } + } + + match command { + None => Ok(KeyTrie::Node(KeyTrieNode::new(label.as_str(), mapping))), + Some(MappableCommand::Static { .. }) if !label.is_empty() => { + Err(serde::de::Error::custom("custom labels are only available for typable commands (the ones starting with ':')")) + } + Some(MappableCommand::Typable { name, args, .. }) if !label.is_empty() => { + Ok(KeyTrie::MappableCommand(MappableCommand::Typable { + name, + args, + doc: label.to_string(), + })) + } + Some(command) => Ok(KeyTrie::MappableCommand(command)), } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping))) } }