216a11b9db
Plug'n'play modal keybinding system inspired by Doom Emacs + Evil mode.
Generic over consumer Action type. Core: Key parser ("C-d", "SPC"),
trie-based sequence matching with conflict detection, count prefix (5j),
timeout tracking, which-key introspection, and multi-mode dispatch.
78 unit tests covering key parsing, trie conflicts, dispatch state machine,
count accumulation, timeout expiry, and which-key generation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
112 lines
3.4 KiB
Rust
112 lines
3.4 KiB
Rust
use crate::trie::{KeyTrie, KeyTrieNode};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct WhichKeyEntry {
|
|
pub key: String,
|
|
pub description: String,
|
|
pub is_group: bool,
|
|
}
|
|
|
|
impl<A> KeyTrieNode<A> {
|
|
pub fn which_key_entries(&self) -> Vec<WhichKeyEntry> {
|
|
let mut groups = Vec::new();
|
|
let mut leaves = Vec::new();
|
|
|
|
for (key, trie) in &self.map {
|
|
match trie {
|
|
KeyTrie::Node(node) => {
|
|
groups.push(WhichKeyEntry {
|
|
key: key.to_string(),
|
|
description: node.name.clone(),
|
|
is_group: true,
|
|
});
|
|
}
|
|
KeyTrie::Leaf(leaf) => {
|
|
leaves.push(WhichKeyEntry {
|
|
key: key.to_string(),
|
|
description: leaf.description.clone().unwrap_or_default(),
|
|
is_group: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
groups.sort_by(|a, b| a.key.cmp(&b.key));
|
|
leaves.sort_by(|a, b| a.key.cmp(&b.key));
|
|
groups.extend(leaves);
|
|
groups
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::trie::KeyTrie;
|
|
|
|
#[test]
|
|
fn test_empty_node() {
|
|
let trie: KeyTrie<&str> = KeyTrie::new("root");
|
|
if let KeyTrie::Node(node) = trie {
|
|
let entries = node.which_key_entries();
|
|
assert!(entries.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_leaves_only_sorted() {
|
|
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
|
|
trie.bind_desc("c", "action_c", "C action").unwrap();
|
|
trie.bind_desc("a", "action_a", "A action").unwrap();
|
|
trie.bind_desc("b", "action_b", "B action").unwrap();
|
|
|
|
if let KeyTrie::Node(node) = trie {
|
|
let entries = node.which_key_entries();
|
|
assert_eq!(entries.len(), 3);
|
|
assert_eq!(entries[0].key, "a");
|
|
assert_eq!(entries[1].key, "b");
|
|
assert_eq!(entries[2].key, "c");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_groups_before_leaves() {
|
|
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
|
|
trie.bind_desc("z", "action", "Z action").unwrap();
|
|
trie.group("g", "goto", |node| {
|
|
node.bind("g", "goto_top")?;
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
trie.bind_desc("a", "action", "A action").unwrap();
|
|
|
|
if let KeyTrie::Node(node) = trie {
|
|
let entries = node.which_key_entries();
|
|
assert_eq!(entries.len(), 3);
|
|
assert!(entries[0].is_group);
|
|
assert_eq!(entries[0].key, "g");
|
|
assert!(!entries[1].is_group);
|
|
assert_eq!(entries[1].key, "a");
|
|
assert!(!entries[2].is_group);
|
|
assert_eq!(entries[2].key, "z");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_descriptions_populated() {
|
|
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
|
|
trie.bind_desc("j", "down", "Move down").unwrap();
|
|
trie.group("g", "goto", |_| Ok(())).unwrap();
|
|
|
|
if let KeyTrie::Node(node) = trie {
|
|
let entries = node.which_key_entries();
|
|
|
|
let group = entries.iter().find(|e| e.key == "g").unwrap();
|
|
assert_eq!(group.description, "goto");
|
|
assert!(group.is_group);
|
|
|
|
let leaf = entries.iter().find(|e| e.key == "j").unwrap();
|
|
assert_eq!(leaf.description, "Move down");
|
|
assert!(!leaf.is_group);
|
|
}
|
|
}
|
|
}
|