mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-23 09:56:19 +04:00
Add a sticky mode for keymaps (#635)
This commit is contained in:
parent
99a753a579
commit
183dcce992
@ -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());
|
||||||
|
}
|
||||||
|
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
|
||||||
}
|
}
|
||||||
None => KeymapResult::Cancelled(self.state.drain(..).collect()),
|
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
|
||||||
|
@ -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,11 +979,12 @@ 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 {
|
||||||
// FIXME: insert mode can only be entered through single KeyCodes
|
KeymapResultKind::Matched(command) => command,
|
||||||
_ => unimplemented!(),
|
// FIXME: insert mode can only be entered through single KeyCodes
|
||||||
};
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
self.last_insert.1.clear();
|
self.last_insert.1.clear();
|
||||||
}
|
}
|
||||||
(Mode::Insert, Mode::Normal) => {
|
(Mode::Insert, Mode::Normal) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user