You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helix/helix-term/src/keymap.rs

605 lines
18 KiB
Rust

pub mod default;
pub mod macros;
pub use crate::commands::MappableCommand;
use arc_swap::{
access::{DynAccess, DynGuard},
ArcSwap,
};
use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize;
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
ops::{Deref, DerefMut},
sync::Arc,
};
pub use default::default;
use macros::key;
#[derive(Debug, Clone, Default)]
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,
}
}
/// 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<(BTreeSet<KeyEvent>, &str)> = Vec::with_capacity(self.len());
for (&key, trie) in self.iter() {
let desc = match trie {
KeyTrie::MappableCommand(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].0.insert(key);
}
None => body.push((BTreeSet::from([key]), desc)),
}
}
body.sort_unstable_by_key(|(keys, _)| {
self.order
.iter()
.position(|&k| k == *keys.iter().next().unwrap())
.unwrap()
});
let body: Vec<_> = body
.into_iter()
.map(|(events, desc)| {
let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
(events.join(", "), desc)
})
.collect();
Info::new(&self.name, &body)
}
}
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
}
}
Improve keymap errors from command typos (#3931) * Improve keymap errors from command typos Currently, opening helix with a config containing a bad command mapping fails with a cryptic error. For example, say we have a config (bad.toml) with a command name that doesn't exist: [keys.normal] b = "buffer_close" # should be ":buffer-close" When we `hx -c bad.toml`, we get... > Bad config: data did not match any variant of untagged enum KeyTrie for key `keys.normal` at line 1 column 1 > Press <ENTER> to continue with default config This is because of the way that Serde tries to deserialize untagged enums such as `helix_term::keymap::KeyTrie`. From the Serde docs[^1]: > Serde will try to match the data against each variant in order and the > first one that deserializes successfully is the one returned. `MappableCommand::deserialize` fails (returns an Err variant) when a command does not exist. Serde interprets this as the `KeyTrie::Leaf` variant failing to match and declares that the input data doesn't "match any variant of untagged enum KeyTrie." Luckily the variants of KeyTrie are orthogonal in structure: we can tell them apart by the type hints from a `serde::de::Visitor`. This change uses a custom Deserialize implementation along with a Visitor that discerns which variant of the KeyTrie applies. With this change, the above failure becomes: > Bad config: No command named 'buffer_close' for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config We also provide more explicit information about the expectations on the field. A config with an unexpected type produces a message with that information and the expectation: [keys.normal] b = 1 > Bad config: invalid type: integer `1`, expected a command, list of commands, or sub-keymap for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config [^1]: https://serde.rs/enum-representations.html#untagged * Update helix-term/src/keymap.rs Co-authored-by: Ivan Tham <pickfire@riseup.net> Co-authored-by: Ivan Tham <pickfire@riseup.net>
2 years ago
#[derive(Debug, Clone, PartialEq)]
pub enum KeyTrie {
MappableCommand(MappableCommand),
Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
Improve keymap errors from command typos (#3931) * Improve keymap errors from command typos Currently, opening helix with a config containing a bad command mapping fails with a cryptic error. For example, say we have a config (bad.toml) with a command name that doesn't exist: [keys.normal] b = "buffer_close" # should be ":buffer-close" When we `hx -c bad.toml`, we get... > Bad config: data did not match any variant of untagged enum KeyTrie for key `keys.normal` at line 1 column 1 > Press <ENTER> to continue with default config This is because of the way that Serde tries to deserialize untagged enums such as `helix_term::keymap::KeyTrie`. From the Serde docs[^1]: > Serde will try to match the data against each variant in order and the > first one that deserializes successfully is the one returned. `MappableCommand::deserialize` fails (returns an Err variant) when a command does not exist. Serde interprets this as the `KeyTrie::Leaf` variant failing to match and declares that the input data doesn't "match any variant of untagged enum KeyTrie." Luckily the variants of KeyTrie are orthogonal in structure: we can tell them apart by the type hints from a `serde::de::Visitor`. This change uses a custom Deserialize implementation along with a Visitor that discerns which variant of the KeyTrie applies. With this change, the above failure becomes: > Bad config: No command named 'buffer_close' for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config We also provide more explicit information about the expectations on the field. A config with an unexpected type produces a message with that information and the expectation: [keys.normal] b = 1 > Bad config: invalid type: integer `1`, expected a command, list of commands, or sub-keymap for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config [^1]: https://serde.rs/enum-representations.html#untagged * Update helix-term/src/keymap.rs Co-authored-by: Ivan Tham <pickfire@riseup.net> Co-authored-by: Ivan Tham <pickfire@riseup.net>
2 years ago
impl<'de> Deserialize<'de> for KeyTrie {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(KeyTrieVisitor)
}
}
struct KeyTrieVisitor;
impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
type Value = KeyTrie;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a command, list of commands, or sub-keymap")
}
fn visit_str<E>(self, command: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
command
.parse::<MappableCommand>()
.map(KeyTrie::MappableCommand)
Improve keymap errors from command typos (#3931) * Improve keymap errors from command typos Currently, opening helix with a config containing a bad command mapping fails with a cryptic error. For example, say we have a config (bad.toml) with a command name that doesn't exist: [keys.normal] b = "buffer_close" # should be ":buffer-close" When we `hx -c bad.toml`, we get... > Bad config: data did not match any variant of untagged enum KeyTrie for key `keys.normal` at line 1 column 1 > Press <ENTER> to continue with default config This is because of the way that Serde tries to deserialize untagged enums such as `helix_term::keymap::KeyTrie`. From the Serde docs[^1]: > Serde will try to match the data against each variant in order and the > first one that deserializes successfully is the one returned. `MappableCommand::deserialize` fails (returns an Err variant) when a command does not exist. Serde interprets this as the `KeyTrie::Leaf` variant failing to match and declares that the input data doesn't "match any variant of untagged enum KeyTrie." Luckily the variants of KeyTrie are orthogonal in structure: we can tell them apart by the type hints from a `serde::de::Visitor`. This change uses a custom Deserialize implementation along with a Visitor that discerns which variant of the KeyTrie applies. With this change, the above failure becomes: > Bad config: No command named 'buffer_close' for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config We also provide more explicit information about the expectations on the field. A config with an unexpected type produces a message with that information and the expectation: [keys.normal] b = 1 > Bad config: invalid type: integer `1`, expected a command, list of commands, or sub-keymap for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config [^1]: https://serde.rs/enum-representations.html#untagged * Update helix-term/src/keymap.rs Co-authored-by: Ivan Tham <pickfire@riseup.net> Co-authored-by: Ivan Tham <pickfire@riseup.net>
2 years ago
.map_err(E::custom)
}
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
let mut commands = Vec::new();
while let Some(command) = seq.next_element::<String>()? {
Improve keymap errors from command typos (#3931) * Improve keymap errors from command typos Currently, opening helix with a config containing a bad command mapping fails with a cryptic error. For example, say we have a config (bad.toml) with a command name that doesn't exist: [keys.normal] b = "buffer_close" # should be ":buffer-close" When we `hx -c bad.toml`, we get... > Bad config: data did not match any variant of untagged enum KeyTrie for key `keys.normal` at line 1 column 1 > Press <ENTER> to continue with default config This is because of the way that Serde tries to deserialize untagged enums such as `helix_term::keymap::KeyTrie`. From the Serde docs[^1]: > Serde will try to match the data against each variant in order and the > first one that deserializes successfully is the one returned. `MappableCommand::deserialize` fails (returns an Err variant) when a command does not exist. Serde interprets this as the `KeyTrie::Leaf` variant failing to match and declares that the input data doesn't "match any variant of untagged enum KeyTrie." Luckily the variants of KeyTrie are orthogonal in structure: we can tell them apart by the type hints from a `serde::de::Visitor`. This change uses a custom Deserialize implementation along with a Visitor that discerns which variant of the KeyTrie applies. With this change, the above failure becomes: > Bad config: No command named 'buffer_close' for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config We also provide more explicit information about the expectations on the field. A config with an unexpected type produces a message with that information and the expectation: [keys.normal] b = 1 > Bad config: invalid type: integer `1`, expected a command, list of commands, or sub-keymap for key `keys.normal.b` at line 2 column 5 > Press <ENTER> to continue with default config [^1]: https://serde.rs/enum-representations.html#untagged * Update helix-term/src/keymap.rs Co-authored-by: Ivan Tham <pickfire@riseup.net> Co-authored-by: Ivan Tham <pickfire@riseup.net>
2 years ago
commands.push(
command
.parse::<MappableCommand>()
.map_err(serde::de::Error::custom)?,
)
}
Ok(KeyTrie::Sequence(commands))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut mapping = HashMap::new();
let mut order = Vec::new();
while let Some((key, value)) = map.next_entry::<KeyEvent, KeyTrie>()? {
mapping.insert(key, value);
order.push(key);
}
Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order)))
}
}
impl KeyTrie {
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::MappableCommand(cmd) => {
let name = cmd.name();
if name != "no_op" {
cmd_map.entry(name.into()).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, &mut Vec::new());
res
}
pub fn node(&self) -> Option<&KeyTrieNode> {
match *self {
KeyTrie::Node(ref node) => Some(node),
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,
}
}
/// 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::MappableCommand(_) | 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>),
}
/// A map of command names to keybinds that will execute the command.
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
pub struct Keymaps {
pub map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>,
/// 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, KeyTrie>>>) -> Self {
Self {
map,
state: Vec::new(),
sticky: None,
}
}
pub fn map(&self) -> DynGuard<HashMap<Mode, KeyTrie>> {
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()
}
pub fn contains_key(&self, mode: Mode, key: KeyEvent) -> bool {
let keymaps = &*self.map();
let keymap = &keymaps[&mode];
keymap
.search(self.pending())
.and_then(KeyTrie::node)
.is_some_and(|node| node.contains_key(&key))
}
/// 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.first().unwrap_or(&key);
let trie_node = match self.sticky {
Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
None => Cow::Borrowed(keymap),
};
let trie = match trie_node.search(&[*first]) {
Some(KeyTrie::MappableCommand(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(map)) => {
if map.is_sticky {
self.state.clear();
self.sticky = Some(map.clone());
}
KeymapResult::Pending(map.clone())
}
Some(KeyTrie::MappableCommand(cmd)) => {
self.state.clear();
KeymapResult::Matched(cmd.clone())
}
Some(KeyTrie::Sequence(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()))))
}
}
/// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
for (mode, keys) in dst {
keys.merge_nodes(
delta
.remove(mode)
.unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())),
)
}
}
#[cfg(test)]
mod tests {
use super::macros::keymap;
use super::*;
use arc_swap::access::Constant;
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 merge_partial_keys() {
let keymap = hashmap! {
Mode::Normal => keymap!({ "Normal mode"
"i" => normal_mode,
"无" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
assert_eq!(
keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(Mode::Normal, key!('无')),
KeymapResult::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(Mode::Normal, key!('z')),
KeymapResult::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
assert!(
merged_keyamp
.get(&Mode::Normal)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 1
);
assert!(
merged_keyamp
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 0
);
}
#[test]
fn order_should_be_set() {
let keymap = hashmap! {
Mode::Normal => keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order.as_slice().is_empty())
}
#[test]
fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default().map();
let root = keymaps.get(&Mode::Normal).unwrap();
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 = 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')]]
),
]),
"Mismatch"
)
}
#[test]
fn escaped_keymap() {
use crate::commands::MappableCommand;
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
let keys = r#"
"+" = [
"select_all",
":pipe sed -E 's/\\s+$//g'",
]
"#;
let key = KeyEvent {
code: KeyCode::Char('+'),
modifiers: KeyModifiers::NONE,
};
let expectation = KeyTrie::Node(KeyTrieNode::new(
"",
hashmap! {
key => KeyTrie::Sequence(vec!{
MappableCommand::select_all,
MappableCommand::Typable {
name: "pipe".to_string(),
args: vec!{
"sed".to_string(),
"-E".to_string(),
"'s/\\s+$//g'".to_string()
},
doc: "".to_string(),
},
})
},
vec![key],
));
assert_eq!(toml::from_str(keys), Ok(expectation));
}
}