diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 1cec5c139..f9d6966d6 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -224,6 +224,8 @@ mod tests { [keys.normal] o = { label = "Edit Config", command = ":open ~/.config" } c = ":buffer-close" + h = ["vsplit", "normal_mode", "swap_view_left"] + j = {command = ["hsplit", "normal_mode", {}], label = "split down"} "#; let config = Config::load_test(sample_keymaps); @@ -249,6 +251,32 @@ mod tests { } else { panic!(":buffer-close command did not parse to typable command"); } + + let split_left = node.get(&KeyEvent::from_str("h").unwrap()).unwrap(); + if let keymap::KeyTrie::Sequence(label, cmds) = split_left { + assert_eq!(label, KeyTrie::DEFAULT_SEQUENCE_LABEL); + assert_eq!( + *cmds, + vec![ + MappableCommand::vsplit, + MappableCommand::normal_mode, + MappableCommand::swap_view_left + ] + ); + } + + let split_down = node.get(&KeyEvent::from_str("j").unwrap()).unwrap(); + if let keymap::KeyTrie::Sequence(label, cmds) = split_down { + assert_eq!(label, "split down"); + assert_eq!( + *cmds, + vec![ + MappableCommand::hsplit, + MappableCommand::normal_mode, + MappableCommand::swap_view_down + ] + ); + } } else { panic!("Config did not parse to trie"); } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f4c1b9e7e..77a8bc586 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -12,6 +12,7 @@ use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, ops::{Deref, DerefMut}, + str::FromStr, sync::Arc, }; @@ -83,7 +84,7 @@ impl KeyTrieNode { cmd.doc() } KeyTrie::Node(n) => &n.name, - KeyTrie::Sequence(_) => "[Multiple commands]", + KeyTrie::Sequence(..) => KeyTrie::DEFAULT_SEQUENCE_LABEL, }; match body.iter().position(|(_, d)| d == &desc) { Some(pos) => { @@ -133,10 +134,18 @@ impl DerefMut for KeyTrieNode { #[derive(Debug, Clone, PartialEq)] pub enum KeyTrie { MappableCommand(MappableCommand), - Sequence(Vec), + Sequence(String, Vec), Node(KeyTrieNode), } +impl KeyTrie { + pub const DEFAULT_SEQUENCE_LABEL: &'static str = "[Multiple commands]"; + + pub fn sequence(commands: Vec) -> Self { + Self::Sequence(Self::DEFAULT_SEQUENCE_LABEL.to_string(), commands) + } +} + impl<'de> Deserialize<'de> for KeyTrie { fn deserialize(deserializer: D) -> Result where @@ -190,7 +199,10 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { )); } - Ok(KeyTrie::Sequence(commands)) + Ok(KeyTrie::Sequence( + KeyTrie::DEFAULT_SEQUENCE_LABEL.to_string(), + commands, + )) } fn visit_map(self, mut map: M) -> Result @@ -205,7 +217,35 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { while let Some(key) = map.next_key::()? { match &key as &str { "label" => label = map.next_value::()?, - "command" => command = Some(map.next_value::()?), + "command" => { + command = Some(match map.next_value::()? { + toml::Value::String(s) => { + vec![MappableCommand::from_str(&s).map_err(serde::de::Error::custom)?] + } + toml::Value::Array(arr) => { + let mut vec = Vec::with_capacity(arr.len()); + for value in arr { + let toml::Value::String(s) = value else { + return Err(serde::de::Error::invalid_type( + serde::de::Unexpected::Other(value.type_str()), + &"string", + )); + }; + vec.push( + MappableCommand::from_str(&s) + .map_err(serde::de::Error::custom)?, + ); + } + vec + } + value => { + return Err(serde::de::Error::invalid_type( + serde::de::Unexpected::Other(value.type_str()), + &"string or array", + )) + } + }); + } _ => { let key_event = key.parse::().map_err(serde::de::Error::custom)?; let key_trie = map.next_value::()?; @@ -220,17 +260,28 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { Some(_command) if !order.is_empty() => { Err(serde::de::Error::custom("ambiguous mapping: 'command' is only valid with 'label', but I found other keys")) } - 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(mut commands) if commands.len() == 1 => match commands.pop() { + None => Err(serde::de::Error::custom("UNREACHABLE!, vec is empty after checking len == 1")), + 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, + })) + } + Some(command) => Ok(KeyTrie::MappableCommand(command)), } - Some(command) => Ok(KeyTrie::MappableCommand(command)), + Some(commands) => { + let label = if label.is_empty() { + KeyTrie::DEFAULT_SEQUENCE_LABEL.to_string() + } else { + label + }; + Ok(KeyTrie::Sequence(label, commands)) + }, } } } @@ -254,7 +305,7 @@ impl KeyTrie { keys.pop(); } } - KeyTrie::Sequence(_) => {} + KeyTrie::Sequence(..) => {} }; } @@ -266,14 +317,14 @@ impl KeyTrie { pub fn node(&self) -> Option<&KeyTrieNode> { match *self { KeyTrie::Node(ref node) => Some(node), - KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None, } } pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { match *self { KeyTrie::Node(ref mut node) => Some(node), - KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None, } } @@ -290,7 +341,7 @@ impl KeyTrie { trie = match trie { KeyTrie::Node(map) => map.get(key), // leaf encountered while keys left to process - KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None, }? } Some(trie) @@ -380,7 +431,7 @@ impl Keymaps { Some(KeyTrie::MappableCommand(ref cmd)) => { return KeymapResult::Matched(cmd.clone()); } - Some(KeyTrie::Sequence(ref cmds)) => { + Some(KeyTrie::Sequence(_, ref cmds)) => { return KeymapResult::MatchedSequence(cmds.clone()); } None => return KeymapResult::NotFound, @@ -400,7 +451,7 @@ impl Keymaps { self.state.clear(); KeymapResult::Matched(cmd.clone()) } - Some(KeyTrie::Sequence(cmds)) => { + Some(KeyTrie::Sequence(_, cmds)) => { self.state.clear(); KeymapResult::MatchedSequence(cmds.clone()) } @@ -625,7 +676,7 @@ mod tests { let expectation = KeyTrie::Node(KeyTrieNode::new( "", hashmap! { - key => KeyTrie::Sequence(vec!{ + key => KeyTrie::sequence(vec!{ MappableCommand::select_all, MappableCommand::Typable { name: "pipe".to_string(),