Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/keymap.rs')
-rw-r--r--helix-view/src/keymap.rs440
1 files changed, 440 insertions, 0 deletions
diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs
new file mode 100644
index 00000000..c9f26e17
--- /dev/null
+++ b/helix-view/src/keymap.rs
@@ -0,0 +1,440 @@
+pub mod default;
+pub mod macros;
+
+pub use crate::commands::MappableCommand;
+use crate::{document::Mode, info::Info, input::KeyEvent};
+use arc_swap::{
+ access::{DynAccess, DynGuard},
+ ArcSwap,
+};
+use serde::Deserialize;
+use std::{
+ borrow::Cow,
+ collections::{BTreeSet, HashMap},
+ ops::{Deref, DerefMut},
+ sync::Arc,
+};
+
+use default::default;
+use macros::key;
+
+#[derive(Debug, Clone)]
+pub struct KeyTrieNode {
+ /// A label for keys coming under this node, like "Goto mode"
+ name: String,
+ map: HashMap<KeyEvent, KeyTrie>,
+ order: Vec<KeyEvent>,
+ pub is_sticky: bool,
+}
+
+impl<'de> Deserialize<'de> for KeyTrieNode {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
+ let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
+ Ok(Self {
+ map,
+ order,
+ ..Default::default()
+ })
+ }
+}
+
+impl KeyTrieNode {
+ pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> 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<KeyEvent>)> = 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<KeyEvent, KeyTrie>;
+
+ 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<MappableCommand>),
+ 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 KeymapResult {
+ /// 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<MappableCommand>),
+ /// 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<KeyEvent>),
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[serde(transparent)]
+pub struct Keymap {
+ /// Always a Node
+ root: KeyTrie,
+}
+
+impl Keymap {
+ pub fn new(root: KeyTrie) -> Self {
+ Keymap { root }
+ }
+
+ pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
+ // recursively visit all nodes in keymap
+ fn map_node(
+ cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
+ node: &KeyTrie,
+ keys: &mut Vec<KeyEvent>,
+ ) {
+ 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 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()))
+ }
+}
+
+pub struct Keymaps {
+ pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>,
+ /// Stores pending keys waiting for the next key. This is relative to a
+ /// sticky node if one is in use.
+ state: Vec<KeyEvent>,
+ /// Stores the sticky node if one is activated.
+ pub sticky: Option<KeyTrieNode>,
+}
+
+impl Keymaps {
+ pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self {
+ Self {
+ map,
+ state: Vec::new(),
+ sticky: None,
+ }
+ }
+
+ pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> {
+ self.map.load()
+ }
+
+ /// Returns list of keys waiting to be disambiguated in current mode.
+ pub fn pending(&self) -> &[KeyEvent] {
+ &self.state
+ }
+
+ pub fn sticky(&self) -> Option<&KeyTrieNode> {
+ self.sticky.as_ref()
+ }
+
+ /// 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, mode: Mode, key: KeyEvent) -> KeymapResult {
+ // TODO: remove the sticky part and look up manually
+ let keymaps = &*self.map();
+ let keymap = &keymaps[&mode];
+
+ if key!(Esc) == key {
+ if !self.state.is_empty() {
+ // Note that Esc is not included here
+ return KeymapResult::Cancelled(self.state.drain(..).collect());
+ }
+ 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(&keymap.root),
+ };
+
+ let trie = match trie_node.search(&[*first]) {
+ Some(KeyTrie::Leaf(ref cmd)) => {
+ return KeymapResult::Matched(cmd.clone());
+ }
+ Some(KeyTrie::Sequence(ref cmds)) => {
+ return KeymapResult::MatchedSequence(cmds.clone());
+ }
+ None => return KeymapResult::NotFound,
+ 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::Pending(map.clone())
+ }
+ Some(&KeyTrie::Leaf(ref cmd)) => {
+ self.state.clear();
+ KeymapResult::Matched(cmd.clone())
+ }
+ Some(&KeyTrie::Sequence(ref cmds)) => {
+ self.state.clear();
+ KeymapResult::MatchedSequence(cmds.clone())
+ }
+ None => KeymapResult::Cancelled(self.state.drain(..).collect()),
+ }
+ }
+}
+
+impl Default for Keymaps {
+ fn default() -> Self {
+ Self::new(Box::new(ArcSwap::new(Arc::new(default()))))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::macros::keymap;
+ use super::*;
+ use helix_core::hashmap;
+
+ #[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 aliased_modes_are_same_in_default_keymap() {
+ let keymaps = Keymaps::default().map();
+ let root = keymaps.get(&Mode::Normal).unwrap().root();
+ assert_eq!(
+ root.search(&[key!(' '), key!('w')]).unwrap(),
+ root.search(&["C-w".parse::<KeyEvent>().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"
+ )
+ }
+}