Add a sticky mode for keymaps (#635)

This commit is contained in:
Gokul Soumya 2021-09-05 09:25:13 +05:30 committed by GitHub
parent 99a753a579
commit 183dcce992
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 46 deletions

View File

@ -4,6 +4,7 @@
use helix_view::{document::Mode, info::Info, input::KeyEvent}; use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
borrow::Cow,
collections::HashMap, collections::HashMap,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -47,13 +48,13 @@ macro_rules! keymap {
}; };
(@trie (@trie
{ $label:literal $($($key:literal)|+ => $value:tt,)+ } { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => { ) => {
keymap!({ $label $($($key)|+ => $value,)+ }) keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
}; };
( (
{ $label:literal $($($key:literal)|+ => $value:tt,)+ } { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => { ) => {
// modified from the hashmap! macro // modified from the hashmap! macro
{ {
@ -70,7 +71,9 @@ macro_rules! keymap {
_order.push(_key); _order.push(_key);
)+ )+
)* )*
$crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order)) let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
$( _node.is_sticky = $sticky; )?
$crate::keymap::KeyTrie::Node(_node)
} }
}; };
} }
@ -84,6 +87,8 @@ pub struct KeyTrieNode {
map: HashMap<KeyEvent, KeyTrie>, map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)] #[serde(skip)]
order: Vec<KeyEvent>, order: Vec<KeyEvent>,
#[serde(skip)]
pub is_sticky: bool,
} }
impl KeyTrieNode { impl KeyTrieNode {
@ -92,6 +97,7 @@ pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) ->
name: name.to_string(), name: name.to_string(),
map, map,
order, order,
is_sticky: false,
} }
} }
@ -119,12 +125,10 @@ pub fn merge(&mut self, mut other: Self) {
} }
} }
} }
}
impl From<KeyTrieNode> for Info { pub fn infobox(&self) -> Info {
fn from(node: KeyTrieNode) -> Self { let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len()); for (&key, trie) in self.iter() {
for (&key, trie) in node.iter() {
let desc = match trie { let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(), KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Node(n) => n.name(), KeyTrie::Node(n) => n.name(),
@ -136,16 +140,16 @@ fn from(node: KeyTrieNode) -> Self {
} }
} }
body.sort_unstable_by_key(|(_, keys)| { body.sort_unstable_by_key(|(_, keys)| {
node.order.iter().position(|&k| k == keys[0]).unwrap() self.order.iter().position(|&k| k == keys[0]).unwrap()
}); });
let prefix = format!("{} ", node.name()); let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body body = body
.into_iter() .into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect(); .collect();
} }
Info::new(node.name(), body) Info::new(self.name(), body)
} }
} }
@ -218,7 +222,7 @@ pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum KeymapResult { pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke. /// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode), Pending(KeyTrieNode),
Matched(Command), Matched(Command),
@ -229,14 +233,31 @@ pub enum KeymapResult {
Cancelled(Vec<KeyEvent>), Cancelled(Vec<KeyEvent>),
} }
/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
/// reference to the sticky node if one is currently active.
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)] #[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Keymap { pub struct Keymap {
/// Always a Node /// Always a Node
#[serde(flatten)] #[serde(flatten)]
root: KeyTrie, root: KeyTrie,
/// Stores pending keys waiting for the next key /// Stores pending keys waiting for the next key. This is relative to a
/// sticky node if one is in use.
#[serde(skip)] #[serde(skip)]
state: Vec<KeyEvent>, state: Vec<KeyEvent>,
/// Stores the sticky node if one is activated.
#[serde(skip)]
sticky: Option<KeyTrieNode>,
} }
impl Keymap { impl Keymap {
@ -244,6 +265,7 @@ pub fn new(root: KeyTrie) -> Self {
Keymap { Keymap {
root, root,
state: Vec::new(), state: Vec::new(),
sticky: None,
} }
} }
@ -251,27 +273,60 @@ pub fn root(&self) -> &KeyTrie {
&self.root &self.root
} }
pub fn sticky(&self) -> Option<&KeyTrieNode> {
self.sticky.as_ref()
}
/// Returns list of keys waiting to be disambiguated. /// Returns list of keys waiting to be disambiguated.
pub fn pending(&self) -> &[KeyEvent] { pub fn pending(&self) -> &[KeyEvent] {
&self.state &self.state
} }
/// Lookup `key` in the keymap to try and find a command to execute /// 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 { pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
let &first = self.state.get(0).unwrap_or(&key); if let key!(Esc) = key {
let trie = match self.root.search(&[first]) { if self.state.is_empty() {
Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), self.sticky = None;
None => return KeymapResult::NotFound, }
return KeymapResult::new(
KeymapResultKind::Cancelled(self.state.drain(..).collect()),
self.sticky(),
);
}
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(cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
}
None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
Some(t) => t, Some(t) => t,
}; };
self.state.push(key); self.state.push(key);
match trie.search(&self.state[1..]) { match trie.search(&self.state[1..]) {
Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), Some(&KeyTrie::Node(ref map)) => {
Some(&KeyTrie::Leaf(command)) => { if map.is_sticky {
self.state.clear(); self.state.clear();
KeymapResult::Matched(command) self.sticky = Some(map.clone());
} }
None => KeymapResult::Cancelled(self.state.drain(..).collect()), KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
Some(&KeyTrie::Leaf(cmd)) => {
self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
}
None => KeymapResult::new(
KeymapResultKind::Cancelled(self.state.drain(..).collect()),
self.sticky(),
),
} }
} }
@ -602,19 +657,19 @@ fn merge_partial_keys() {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
keymap.get(key!('i')), keymap.get(key!('i')).kind,
KeymapResult::Matched(Command::normal_mode), KeymapResultKind::Matched(Command::normal_mode),
"Leaf should replace leaf" "Leaf should replace leaf"
); );
assert_eq!( assert_eq!(
keymap.get(key!('无')), keymap.get(key!('无')).kind,
KeymapResult::Matched(Command::insert_mode), KeymapResultKind::Matched(Command::insert_mode),
"New leaf should be present in merged keymap" "New leaf should be present in merged keymap"
); );
// Assumes that z is a node in the default keymap // Assumes that z is a node in the default keymap
assert_eq!( assert_eq!(
keymap.get(key!('z')), keymap.get(key!('z')).kind,
KeymapResult::Matched(Command::jump_backward), KeymapResultKind::Matched(Command::jump_backward),
"Leaf should replace node" "Leaf should replace node"
); );
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap

View File

@ -2,7 +2,7 @@
commands, commands,
compositor::{Component, Context, EventResult}, compositor::{Component, Context, EventResult},
key, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, KeymapResultKind, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{Completion, ProgressSpinners},
}; };
@ -638,7 +638,7 @@ pub fn render_statusline(
/// Handle events by looking them up in `self.keymaps`. Returns None /// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was /// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
/// otherwise. /// otherwise.
fn handle_keymap_event( fn handle_keymap_event(
&mut self, &mut self,
@ -647,29 +647,32 @@ fn handle_keymap_event(
event: KeyEvent, event: KeyEvent,
) -> Option<KeymapResult> { ) -> Option<KeymapResult> {
self.autoinfo = None; self.autoinfo = None;
match self.keymaps.get_mut(&mode).unwrap().get(event) { let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
KeymapResult::Matched(command) => command.execute(cxt), self.autoinfo = key_result.sticky.map(|node| node.infobox());
KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt),
KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
} }
None None
} }
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
match keyresult { match keyresult.kind {
KeymapResult::NotFound => { KeymapResultKind::NotFound => {
if let Some(ch) = event.char() { if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch) commands::insert::insert_char(cx, ch)
} }
} }
KeymapResult::Cancelled(pending) => { KeymapResultKind::Cancelled(pending) => {
for ev in pending { for ev in pending {
match ev.char() { match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch), Some(ch) => commands::insert::insert_char(cx, ch),
None => { None => {
if let KeymapResult::Matched(command) = if let KeymapResultKind::Matched(command) =
self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
{ {
command.execute(cx); command.execute(cx);
} }
@ -976,8 +979,9 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
// how we entered insert mode is important, and we should track that so // how we entered insert mode is important, and we should track that so
// we can repeat the side effect. // we can repeat the side effect.
self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { self.last_insert.0 =
KeymapResult::Matched(command) => command, match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
KeymapResultKind::Matched(command) => command,
// FIXME: insert mode can only be entered through single KeyCodes // FIXME: insert mode can only be entered through single KeyCodes
_ => unimplemented!(), _ => unimplemented!(),
}; };