feat(evil-keys): add keybinding crate with trie dispatch, count prefix, and timeout

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>
This commit is contained in:
Alexander
2026-05-10 10:59:43 +02:00
parent 5a34fafd3f
commit 216a11b9db
9 changed files with 1900 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "evil-keys"
version = "0.1.0"
edition = "2021"
[dependencies]
crossterm = "0.28"
indexmap = "2"
[dev-dependencies]
proptest = "1.4"
+114
View File
@@ -0,0 +1,114 @@
pub struct CountState {
digits: String,
}
impl CountState {
pub fn new() -> Self {
Self {
digits: String::new(),
}
}
pub fn push_digit(&mut self, d: char) {
self.digits.push(d);
}
pub fn take(&mut self) -> usize {
if self.digits.is_empty() {
return 1;
}
let val = self.digits.parse::<usize>().unwrap_or(usize::MAX);
self.digits.clear();
val
}
pub fn is_active(&self) -> bool {
!self.digits.is_empty()
}
pub fn display(&self) -> &str {
&self.digits
}
pub fn reset(&mut self) {
self.digits.clear();
}
}
impl Default for CountState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_take() {
let mut count = CountState::new();
assert_eq!(count.take(), 1);
}
#[test]
fn test_push_take() {
let mut count = CountState::new();
count.push_digit('5');
assert_eq!(count.take(), 5);
assert_eq!(count.take(), 1);
}
#[test]
fn test_multi_digit() {
let mut count = CountState::new();
count.push_digit('1');
count.push_digit('2');
count.push_digit('3');
assert_eq!(count.take(), 123);
}
#[test]
fn test_leading_one() {
let mut count = CountState::new();
count.push_digit('1');
count.push_digit('0');
assert_eq!(count.take(), 10);
}
#[test]
fn test_saturate() {
let mut count = CountState::new();
for _ in 0..20 {
count.push_digit('9');
}
assert_eq!(count.take(), usize::MAX);
}
#[test]
fn test_is_active() {
let mut count = CountState::new();
assert!(!count.is_active());
count.push_digit('5');
assert!(count.is_active());
count.take();
assert!(!count.is_active());
}
#[test]
fn test_display() {
let mut count = CountState::new();
assert_eq!(count.display(), "");
count.push_digit('5');
assert_eq!(count.display(), "5");
}
#[test]
fn test_reset() {
let mut count = CountState::new();
count.push_digit('5');
count.reset();
assert!(!count.is_active());
assert_eq!(count.display(), "");
}
}
+615
View File
@@ -0,0 +1,615 @@
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::collections::HashMap;
use std::time::Duration;
use crate::count::CountState;
use crate::error::ModeError;
use crate::key::Key;
use crate::timeout::TimeoutTracker;
use crate::trie::{KeyTrie, SearchResult};
use crate::which_key::WhichKeyEntry;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispatchResult<A> {
Matched { action: A, count: usize },
Pending,
Cancelled,
CountAccumulated,
Ignored,
NotFound,
}
pub struct Dispatcher<A: Clone> {
modes: HashMap<String, KeyTrie<A>>,
active_mode: String,
pending: Vec<Key>,
count: CountState,
timeout: TimeoutTracker,
}
impl<A: Clone> Dispatcher<A> {
pub fn new() -> Self {
Self {
modes: HashMap::new(),
active_mode: String::new(),
pending: Vec::new(),
count: CountState::new(),
timeout: TimeoutTracker::new(Duration::from_secs(1)),
}
}
pub fn add_mode(&mut self, name: &str, keymap: KeyTrie<A>) -> Result<(), ModeError> {
self.modes.insert(name.to_string(), keymap);
Ok(())
}
pub fn set_active(&mut self, mode: &str) -> Result<(), ModeError> {
if !self.modes.contains_key(mode) {
return Err(ModeError::UnknownMode(mode.to_string()));
}
self.active_mode = mode.to_string();
self.pending.clear();
self.count.reset();
self.timeout.reset();
Ok(())
}
pub fn active_mode(&self) -> &str {
&self.active_mode
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = TimeoutTracker::new(timeout);
}
pub fn dispatch(&mut self, event: KeyEvent) -> DispatchResult<A> {
if event.kind != KeyEventKind::Press {
return DispatchResult::Ignored;
}
let key = Key::from(event);
let is_escape = key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE;
if is_escape && (!self.pending.is_empty() || self.count.is_active()) {
self.pending.clear();
self.count.reset();
self.timeout.reset();
return DispatchResult::Cancelled;
}
if let KeyCode::Char(c) = key.code {
if key.modifiers == KeyModifiers::NONE && self.pending.is_empty() {
let is_count_digit = c.is_ascii_digit() && (c != '0' || self.count.is_active());
if is_count_digit {
self.count.push_digit(c);
return DispatchResult::CountAccumulated;
}
}
}
self.pending.push(key);
let Some(trie) = self.modes.get(&self.active_mode) else {
self.pending.clear();
self.count.reset();
self.timeout.reset();
return DispatchResult::NotFound;
};
match trie.search(&self.pending) {
SearchResult::Found(leaf) => {
let action = leaf.action.clone();
let count = self.count.take();
self.pending.clear();
self.timeout.reset();
DispatchResult::Matched { action, count }
}
SearchResult::Prefix(_) => {
if !self.timeout.is_active() {
self.timeout.start();
}
DispatchResult::Pending
}
SearchResult::NotFound => {
self.pending.clear();
self.count.reset();
self.timeout.reset();
DispatchResult::NotFound
}
}
}
pub fn check_timeout(&mut self) -> bool {
if self.timeout.check() {
self.pending.clear();
self.count.reset();
return true;
}
false
}
pub fn pending_keys(&self) -> &[Key] {
&self.pending
}
pub fn pending_display(&self) -> String {
self.pending
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
.join(" ")
}
pub fn pending_elapsed(&self) -> Duration {
self.timeout.elapsed()
}
pub fn clear_pending(&mut self) {
self.pending.clear();
self.count.reset();
self.timeout.reset();
}
pub fn which_key_entries(&self) -> Option<Vec<WhichKeyEntry>> {
if self.pending.is_empty() {
return None;
}
let trie = self.modes.get(&self.active_mode)?;
if let SearchResult::Prefix(node) = trie.search(&self.pending) {
Some(node.which_key_entries())
} else {
None
}
}
pub fn count_display(&self) -> Option<&str> {
if self.count.is_active() {
Some(self.count.display())
} else {
None
}
}
}
impl<A: Clone> Default for Dispatcher<A> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trie::KeyTrie;
use crossterm::event::KeyEventState;
fn press(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
fn release(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Release,
state: KeyEventState::NONE,
}
}
#[test]
fn test_dispatch_non_press() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(release(KeyCode::Char('j'))),
DispatchResult::Ignored
);
}
#[test]
fn test_dispatch_no_modes() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::NotFound
);
}
#[test]
fn test_simple_match() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::Matched {
action: "down",
count: 1
}
);
}
#[test]
fn test_sequence_match() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g g", "goto_top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Matched {
action: "goto_top",
count: 1
}
);
}
#[test]
fn test_wrong_key_mid_sequence() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g g", "goto_top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('h'))),
DispatchResult::NotFound
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
}
#[test]
fn test_escape_nothing_pending_bound() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("Esc", "escape_action").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Matched {
action: "escape_action",
count: 1
}
);
}
#[test]
fn test_escape_clears_pending() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g g", "goto_top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
}
#[test]
fn test_escape_clears_count() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('5'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
}
#[test]
fn test_escape_clears_both() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g g", "goto_top").unwrap();
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('3'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::Matched {
action: "down",
count: 1
}
);
}
#[test]
fn test_count_zero_as_binding() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("0", "start_of_line").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('0'))),
DispatchResult::Matched {
action: "start_of_line",
count: 1
}
);
}
#[test]
fn test_count_10() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('1'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('0'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::Matched {
action: "down",
count: 10
}
);
}
#[test]
fn test_count_through_prefix() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g g", "goto_top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('5'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Matched {
action: "goto_top",
count: 5
}
);
}
#[test]
fn test_digits_during_pending() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("g 3", "some_action").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('3'))),
DispatchResult::Matched {
action: "some_action",
count: 1
}
);
}
#[test]
fn test_mode_switch_clears() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut normal = KeyTrie::new("normal");
normal.bind("g g", "goto_top").unwrap();
let insert = KeyTrie::new("insert");
disp.add_mode("normal", normal).unwrap();
disp.add_mode("insert", insert).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
disp.set_active("insert").unwrap();
assert!(disp.pending_keys().is_empty());
}
#[test]
fn test_which_key_entries_after_spc() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.group("SPC", "leader", |node| {
node.bind_desc("b", "buffers", "Buffers")?;
node.bind_desc("f", "files", "Files")?;
Ok(())
})
.unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char(' '))),
DispatchResult::Pending
);
let entries = disp.which_key_entries().unwrap();
assert!(!entries.is_empty());
}
#[test]
fn test_which_key_entries_nothing_pending() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let trie = KeyTrie::new("normal");
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert!(disp.which_key_entries().is_none());
}
#[test]
fn test_pending_display() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("SPC b l", "list_buffers").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
disp.dispatch(press(KeyCode::Char(' ')));
assert_eq!(disp.pending_display(), "SPC");
disp.dispatch(press(KeyCode::Char('b')));
assert_eq!(disp.pending_display(), "SPC b");
}
#[test]
fn test_count_display() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert!(disp.count_display().is_none());
disp.dispatch(press(KeyCode::Char('5')));
assert_eq!(disp.count_display(), Some("5"));
}
#[test]
fn test_timeout_clears_count() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
trie.bind("g g", "top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
disp.set_timeout(std::time::Duration::from_millis(50));
assert_eq!(
disp.dispatch(press(KeyCode::Char('5'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
std::thread::sleep(std::time::Duration::from_millis(80));
assert!(disp.check_timeout());
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::Matched {
action: "down",
count: 1
}
);
}
#[test]
fn test_escape_full_progression() {
let mut disp: Dispatcher<&str> = Dispatcher::new();
let mut trie = KeyTrie::new("normal");
trie.bind("j", "down").unwrap();
trie.bind("g g", "top").unwrap();
disp.add_mode("normal", trie).unwrap();
disp.set_active("normal").unwrap();
assert_eq!(
disp.dispatch(press(KeyCode::Char('5'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('3'))),
DispatchResult::CountAccumulated
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('g'))),
DispatchResult::Pending
);
assert_eq!(
disp.dispatch(press(KeyCode::Esc)),
DispatchResult::Cancelled
);
assert_eq!(
disp.dispatch(press(KeyCode::Char('j'))),
DispatchResult::Matched {
action: "down",
count: 1
}
);
}
}
+78
View File
@@ -0,0 +1,78 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
EmptyInput,
UnknownKey(String),
DanglingModifier,
DuplicateModifier,
RedundantShift,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyInput => write!(f, "empty input"),
Self::UnknownKey(k) => write!(f, "unknown key: {k}"),
Self::DanglingModifier => write!(f, "dangling modifier (e.g. \"C-\")"),
Self::DuplicateModifier => write!(f, "duplicate modifier"),
Self::RedundantShift => write!(
f,
"redundant shift on uppercase char (use \"G\" not \"S-G\")"
),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindError {
EmptySequence,
ConflictWithLeaf { existing_keys: String },
ConflictWithPrefix { existing_keys: String },
Parse(ParseError),
}
impl fmt::Display for BindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptySequence => write!(f, "empty key sequence"),
Self::ConflictWithLeaf { existing_keys } => {
write!(
f,
"conflicts with existing leaf binding at \"{existing_keys}\""
)
}
Self::ConflictWithPrefix { existing_keys } => {
write!(f, "conflicts with existing prefix at \"{existing_keys}\"")
}
Self::Parse(e) => write!(f, "parse error: {e}"),
}
}
}
impl std::error::Error for BindError {}
impl From<ParseError> for BindError {
fn from(e: ParseError) -> Self {
Self::Parse(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModeError {
UnknownMode(String),
DuplicateMode(String),
}
impl fmt::Display for ModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownMode(m) => write!(f, "unknown mode: \"{m}\""),
Self::DuplicateMode(m) => write!(f, "mode already exists: \"{m}\""),
}
}
}
impl std::error::Error for ModeError {}
+443
View File
@@ -0,0 +1,443 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::fmt;
use std::hash::{Hash, Hasher};
use crate::error::ParseError;
#[derive(Clone, Copy, Debug)]
pub struct Key {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.code == other.code && self.modifiers == other.modifiers
}
}
impl Eq for Key {}
impl Hash for Key {
fn hash<H: Hasher>(&self, state: &mut H) {
hash_key_code(&self.code, state);
self.modifiers.bits().hash(state);
}
}
fn hash_key_code<H: Hasher>(code: &KeyCode, state: &mut H) {
match code {
KeyCode::Backspace => (0u8).hash(state),
KeyCode::Enter => (1u8).hash(state),
KeyCode::Left => (2u8).hash(state),
KeyCode::Right => (3u8).hash(state),
KeyCode::Up => (4u8).hash(state),
KeyCode::Down => (5u8).hash(state),
KeyCode::Home => (6u8).hash(state),
KeyCode::End => (7u8).hash(state),
KeyCode::PageUp => (8u8).hash(state),
KeyCode::PageDown => (9u8).hash(state),
KeyCode::Tab => (10u8).hash(state),
KeyCode::BackTab => (11u8).hash(state),
KeyCode::Delete => (12u8).hash(state),
KeyCode::Insert => (13u8).hash(state),
KeyCode::F(n) => {
(14u8).hash(state);
n.hash(state);
}
KeyCode::Char(c) => {
(15u8).hash(state);
c.hash(state);
}
KeyCode::Null => (16u8).hash(state),
KeyCode::Esc => (17u8).hash(state),
KeyCode::CapsLock => (18u8).hash(state),
KeyCode::ScrollLock => (19u8).hash(state),
KeyCode::NumLock => (20u8).hash(state),
KeyCode::PrintScreen => (21u8).hash(state),
KeyCode::Pause => (22u8).hash(state),
KeyCode::Menu => (23u8).hash(state),
KeyCode::KeypadBegin => (24u8).hash(state),
KeyCode::Media(m) => {
(25u8).hash(state);
(*m as u8).hash(state);
}
KeyCode::Modifier(m) => {
(26u8).hash(state);
(*m as u8).hash(state);
}
}
}
impl From<KeyEvent> for Key {
fn from(e: KeyEvent) -> Self {
Key {
code: e.code,
modifiers: e.modifiers,
}
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.modifiers.contains(KeyModifiers::CONTROL) {
write!(f, "C-")?;
}
if self.modifiers.contains(KeyModifiers::SHIFT) {
write!(f, "S-")?;
}
if self.modifiers.contains(KeyModifiers::ALT) {
write!(f, "A-")?;
}
match self.code {
KeyCode::Char(' ') => write!(f, "SPC"),
KeyCode::Char(c) => write!(f, "{c}"),
KeyCode::Esc => write!(f, "Esc"),
KeyCode::Tab => write!(f, "Tab"),
KeyCode::Enter => write!(f, "Enter"),
KeyCode::Backspace => write!(f, "Backspace"),
KeyCode::F(n) => write!(f, "F{n}"),
KeyCode::Left => write!(f, "Left"),
KeyCode::Right => write!(f, "Right"),
KeyCode::Up => write!(f, "Up"),
KeyCode::Down => write!(f, "Down"),
KeyCode::Home => write!(f, "Home"),
KeyCode::End => write!(f, "End"),
KeyCode::PageUp => write!(f, "PageUp"),
KeyCode::PageDown => write!(f, "PageDown"),
KeyCode::Delete => write!(f, "Delete"),
KeyCode::Insert => write!(f, "Insert"),
_ => write!(f, "?"),
}
}
}
pub fn parse_key(input: &str) -> Result<Key, ParseError> {
if input.trim().is_empty() {
return Err(ParseError::EmptyInput);
}
if input.contains(' ') {
return Err(ParseError::UnknownKey(input.to_string()));
}
let trimmed = input;
let mut modifiers = KeyModifiers::NONE;
let mut remaining = trimmed;
let mut has_ctrl = false;
let mut has_shift = false;
let mut has_alt = false;
loop {
if remaining.starts_with("C-") {
if has_ctrl {
return Err(ParseError::DuplicateModifier);
}
has_ctrl = true;
modifiers |= KeyModifiers::CONTROL;
remaining = &remaining[2..];
} else if remaining.starts_with("S-") {
if has_shift {
return Err(ParseError::DuplicateModifier);
}
has_shift = true;
modifiers |= KeyModifiers::SHIFT;
remaining = &remaining[2..];
} else if remaining.starts_with("A-") {
if has_alt {
return Err(ParseError::DuplicateModifier);
}
has_alt = true;
modifiers |= KeyModifiers::ALT;
remaining = &remaining[2..];
} else {
break;
}
}
if remaining.is_empty() {
return Err(ParseError::DanglingModifier);
}
let code = match remaining {
"SPC" => KeyCode::Char(' '),
"Esc" => KeyCode::Esc,
"Tab" => KeyCode::Tab,
"Enter" => KeyCode::Enter,
"Backspace" => KeyCode::Backspace,
"Left" => KeyCode::Left,
"Right" => KeyCode::Right,
"Up" => KeyCode::Up,
"Down" => KeyCode::Down,
"Home" => KeyCode::Home,
"End" => KeyCode::End,
"PageUp" => KeyCode::PageUp,
"PageDown" => KeyCode::PageDown,
"Delete" => KeyCode::Delete,
"Insert" => KeyCode::Insert,
"-" => KeyCode::Char('-'),
s if s.starts_with('F') && s.len() > 1 => {
let num_str = &s[1..];
match num_str.parse::<u8>() {
Ok(n) if (1..=12).contains(&n) => KeyCode::F(n),
_ => return Err(ParseError::UnknownKey(input.to_string())),
}
}
s if s.len() == 1 => {
let c = s.chars().next().expect("non-empty string");
if !c.is_ascii() {
return Err(ParseError::UnknownKey(input.to_string()));
}
if has_shift && c.is_ascii_uppercase() {
return Err(ParseError::RedundantShift);
}
KeyCode::Char(c)
}
_ => return Err(ParseError::UnknownKey(input.to_string())),
};
Ok(Key { code, modifiers })
}
pub fn parse_sequence(input: &str) -> Result<Vec<Key>, ParseError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ParseError::EmptyInput);
}
trimmed.split_whitespace().map(parse_key).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_input() {
assert_eq!(parse_key(""), Err(ParseError::EmptyInput));
assert_eq!(parse_key(" "), Err(ParseError::EmptyInput));
}
#[test]
fn test_space_in_key() {
assert!(matches!(parse_key(" j"), Err(ParseError::UnknownKey(_))));
}
#[test]
fn test_dangling_modifier() {
assert_eq!(parse_key("C-"), Err(ParseError::DanglingModifier));
}
#[test]
fn test_duplicate_modifier() {
assert_eq!(parse_key("C-C-d"), Err(ParseError::DuplicateModifier));
}
#[test]
fn test_lowercase_modifier() {
assert!(matches!(parse_key("c-d"), Err(ParseError::UnknownKey(_))));
}
#[test]
fn test_ctrl_d() {
let key = parse_key("C-d").unwrap();
assert_eq!(key.code, KeyCode::Char('d'));
assert_eq!(key.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn test_ctrl_shift_d() {
let key = parse_key("C-S-d").unwrap();
assert_eq!(key.code, KeyCode::Char('d'));
assert_eq!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
}
#[test]
fn test_modifier_order_normalized() {
let key1 = parse_key("S-C-d").unwrap();
let key2 = parse_key("C-S-d").unwrap();
assert_eq!(key1, key2);
}
#[test]
fn test_uppercase_no_shift() {
let key = parse_key("G").unwrap();
assert_eq!(key.code, KeyCode::Char('G'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_shift_lowercase() {
let key = parse_key("S-g").unwrap();
assert_eq!(key.code, KeyCode::Char('g'));
assert_eq!(key.modifiers, KeyModifiers::SHIFT);
}
#[test]
fn test_redundant_shift() {
assert_eq!(parse_key("S-G"), Err(ParseError::RedundantShift));
}
#[test]
fn test_bare_modifier_letters() {
let key = parse_key("C").unwrap();
assert_eq!(key.code, KeyCode::Char('C'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("S").unwrap();
assert_eq!(key.code, KeyCode::Char('S'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_special_keys() {
let key = parse_key("SPC").unwrap();
assert_eq!(key.code, KeyCode::Char(' '));
assert_eq!(key.modifiers, KeyModifiers::NONE);
assert!(matches!(parse_key("spc"), Err(ParseError::UnknownKey(_))));
let key = parse_key("Esc").unwrap();
assert_eq!(key.code, KeyCode::Esc);
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("Tab").unwrap();
assert_eq!(key.code, KeyCode::Tab);
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("Enter").unwrap();
assert_eq!(key.code, KeyCode::Enter);
assert_eq!(key.modifiers, KeyModifiers::NONE);
// Tab != 't'
assert_ne!(parse_key("Tab").unwrap(), parse_key("t").unwrap());
}
#[test]
fn test_function_keys() {
let key = parse_key("F1").unwrap();
assert_eq!(key.code, KeyCode::F(1));
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("F12").unwrap();
assert_eq!(key.code, KeyCode::F(12));
assert_eq!(key.modifiers, KeyModifiers::NONE);
assert!(matches!(parse_key("F0"), Err(ParseError::UnknownKey(_))));
assert!(matches!(parse_key("F13"), Err(ParseError::UnknownKey(_))));
let key = parse_key("F").unwrap();
assert_eq!(key.code, KeyCode::Char('F'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("C-F1").unwrap();
assert_eq!(key.code, KeyCode::F(1));
assert_eq!(key.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn test_symbols() {
let key = parse_key("-").unwrap();
assert_eq!(key.code, KeyCode::Char('-'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("C--").unwrap();
assert_eq!(key.code, KeyCode::Char('-'));
assert_eq!(key.modifiers, KeyModifiers::CONTROL);
let key = parse_key("[").unwrap();
assert_eq!(key.code, KeyCode::Char('['));
assert_eq!(key.modifiers, KeyModifiers::NONE);
let key = parse_key("?").unwrap();
assert_eq!(key.code, KeyCode::Char('?'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_digits() {
for c in '0'..='9' {
let key = parse_key(&c.to_string()).unwrap();
assert_eq!(key.code, KeyCode::Char(c));
assert_eq!(key.modifiers, KeyModifiers::NONE);
}
}
#[test]
fn test_invalid_non_ascii() {
assert!(matches!(parse_key("é"), Err(ParseError::UnknownKey(_))));
}
#[test]
fn test_space_is_sequence() {
assert!(matches!(parse_key("g g"), Err(ParseError::UnknownKey(_))));
}
#[test]
fn test_display_roundtrip() {
let test_cases = [
"j", "G", "C-d", "C-S-d", "SPC", "Esc", "Tab", "Enter", "F1", "-", "C--", "[",
];
for input in test_cases {
let key = parse_key(input).unwrap();
let displayed = key.to_string();
let reparsed = parse_key(&displayed).unwrap();
assert_eq!(key, reparsed, "roundtrip failed for {input}");
}
}
#[test]
fn test_display_specific() {
let key = Key {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
};
assert_eq!(key.to_string(), "SPC");
let key = Key {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
};
assert_eq!(key.to_string(), "C-S-d");
}
#[test]
fn test_sequence_parsing() {
let seq = parse_sequence("g g").unwrap();
assert_eq!(seq.len(), 2);
assert_eq!(seq[0].code, KeyCode::Char('g'));
assert_eq!(seq[1].code, KeyCode::Char('g'));
let seq = parse_sequence("SPC b l").unwrap();
assert_eq!(seq.len(), 3);
assert_eq!(seq[0].code, KeyCode::Char(' '));
assert_eq!(seq[1].code, KeyCode::Char('b'));
assert_eq!(seq[2].code, KeyCode::Char('l'));
assert_eq!(parse_sequence(""), Err(ParseError::EmptyInput));
assert_eq!(parse_sequence(" "), Err(ParseError::EmptyInput));
}
#[test]
fn test_from_key_event() {
let event = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
let key = Key::from(event);
assert_eq!(key.code, KeyCode::Char('j'));
assert_eq!(key.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_key_hash() {
use std::collections::HashMap;
let mut map = HashMap::new();
let key1 = parse_key("C-d").unwrap();
let key2 = parse_key("C-d").unwrap();
map.insert(key1, "action");
assert_eq!(map.get(&key2), Some(&"action"));
}
}
+13
View File
@@ -0,0 +1,13 @@
pub mod count;
pub mod dispatch;
pub mod error;
pub mod key;
pub mod timeout;
pub mod trie;
pub mod which_key;
pub use dispatch::{DispatchResult, Dispatcher};
pub use error::{BindError, ModeError, ParseError};
pub use key::Key;
pub use trie::{KeyTrie, KeyTrieNode, LeafBinding, SearchResult};
pub use which_key::WhichKeyEntry;
+101
View File
@@ -0,0 +1,101 @@
use std::time::{Duration, Instant};
pub struct TimeoutTracker {
timeout: Duration,
started_at: Option<Instant>,
}
impl TimeoutTracker {
pub fn new(timeout: Duration) -> Self {
Self {
timeout,
started_at: None,
}
}
pub fn start(&mut self) {
self.started_at = Some(Instant::now());
}
pub fn check(&mut self) -> bool {
if let Some(started) = self.started_at {
if started.elapsed() >= self.timeout {
self.started_at = None;
return true;
}
}
false
}
pub fn elapsed(&self) -> Duration {
self.started_at
.map(|s| s.elapsed())
.unwrap_or(Duration::ZERO)
}
pub fn reset(&mut self) {
self.started_at = None;
}
pub fn is_active(&self) -> bool {
self.started_at.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_check_nothing_started() {
let mut tracker = TimeoutTracker::new(Duration::from_millis(100));
assert!(!tracker.check());
}
#[test]
fn test_check_expired() {
let mut tracker = TimeoutTracker::new(Duration::from_millis(100));
tracker.start();
thread::sleep(Duration::from_millis(150));
assert!(tracker.check());
}
#[test]
fn test_check_not_expired() {
let mut tracker = TimeoutTracker::new(Duration::from_millis(100));
tracker.start();
assert!(!tracker.check());
}
#[test]
fn test_reset() {
let mut tracker = TimeoutTracker::new(Duration::from_millis(100));
tracker.start();
tracker.reset();
assert!(!tracker.check());
}
#[test]
fn test_elapsed_nothing_started() {
let tracker = TimeoutTracker::new(Duration::from_millis(100));
assert_eq!(tracker.elapsed(), Duration::ZERO);
}
#[test]
fn test_zero_timeout() {
let mut tracker = TimeoutTracker::new(Duration::ZERO);
tracker.start();
assert!(tracker.check());
}
#[test]
fn test_is_active() {
let mut tracker = TimeoutTracker::new(Duration::from_millis(100));
assert!(!tracker.is_active());
tracker.start();
assert!(tracker.is_active());
tracker.reset();
assert!(!tracker.is_active());
}
}
+414
View File
@@ -0,0 +1,414 @@
use crate::error::BindError;
use crate::key::{parse_sequence, Key};
use indexmap::IndexMap;
pub enum KeyTrie<A> {
Leaf(LeafBinding<A>),
Node(KeyTrieNode<A>),
}
pub struct LeafBinding<A> {
pub action: A,
pub description: Option<String>,
}
pub struct KeyTrieNode<A> {
pub name: String,
pub map: IndexMap<Key, KeyTrie<A>>,
}
pub enum SearchResult<'a, A> {
Found(&'a LeafBinding<A>),
Prefix(&'a KeyTrieNode<A>),
NotFound,
}
impl<A> KeyTrie<A> {
pub fn new(name: &str) -> Self {
KeyTrie::Node(KeyTrieNode {
name: name.to_string(),
map: IndexMap::new(),
})
}
pub fn bind(&mut self, keys: &str, action: A) -> Result<(), BindError> {
self.bind_internal(keys, action, None)
}
pub fn bind_desc(&mut self, keys: &str, action: A, desc: &str) -> Result<(), BindError> {
self.bind_internal(keys, action, Some(desc.to_string()))
}
fn bind_internal(
&mut self,
keys: &str,
action: A,
description: Option<String>,
) -> Result<(), BindError> {
let sequence = parse_sequence(keys)?;
if sequence.is_empty() {
return Err(BindError::EmptySequence);
}
let node = match self {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => {
return Err(BindError::ConflictWithLeaf {
existing_keys: String::new(),
})
}
};
node.bind_sequence(&sequence, action, description, String::new())
}
pub fn group<F>(&mut self, key: &str, name: &str, f: F) -> Result<(), BindError>
where
F: FnOnce(&mut KeyTrieNode<A>) -> Result<(), BindError>,
{
let parsed_key = crate::key::parse_key(key)?;
let node = match self {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => {
return Err(BindError::ConflictWithLeaf {
existing_keys: String::new(),
})
}
};
let entry = node.map.entry(parsed_key);
let child_node = match entry {
indexmap::map::Entry::Occupied(o) => match o.into_mut() {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => {
return Err(BindError::ConflictWithLeaf {
existing_keys: key.to_string(),
})
}
},
indexmap::map::Entry::Vacant(v) => {
let new_node = KeyTrie::Node(KeyTrieNode {
name: name.to_string(),
map: IndexMap::new(),
});
match v.insert(new_node) {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => unreachable!(),
}
}
};
f(child_node)
}
pub fn search(&self, keys: &[Key]) -> SearchResult<'_, A> {
if keys.is_empty() {
return SearchResult::NotFound;
}
let node = match self {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(l) => return SearchResult::Found(l),
};
node.search(keys)
}
}
impl<A> KeyTrieNode<A> {
pub fn bind(&mut self, keys: &str, action: A) -> Result<(), BindError> {
let sequence = parse_sequence(keys)?;
if sequence.is_empty() {
return Err(BindError::EmptySequence);
}
self.bind_sequence(&sequence, action, None, String::new())
}
pub fn bind_desc(&mut self, keys: &str, action: A, desc: &str) -> Result<(), BindError> {
let sequence = parse_sequence(keys)?;
if sequence.is_empty() {
return Err(BindError::EmptySequence);
}
self.bind_sequence(&sequence, action, Some(desc.to_string()), String::new())
}
fn bind_sequence(
&mut self,
keys: &[Key],
action: A,
description: Option<String>,
path: String,
) -> Result<(), BindError> {
let (first, rest) = keys.split_first().expect("non-empty keys");
let current_path = if path.is_empty() {
first.to_string()
} else {
format!("{} {}", path, first)
};
if rest.is_empty() {
match self.map.get(first) {
Some(KeyTrie::Node(_)) => {
return Err(BindError::ConflictWithPrefix {
existing_keys: current_path,
});
}
Some(KeyTrie::Leaf(_)) | None => {
self.map.insert(
*first,
KeyTrie::Leaf(LeafBinding {
action,
description,
}),
);
return Ok(());
}
}
}
let entry = self.map.entry(*first);
match entry {
indexmap::map::Entry::Occupied(mut o) => match o.get_mut() {
KeyTrie::Leaf(_) => {
return Err(BindError::ConflictWithLeaf {
existing_keys: current_path,
});
}
KeyTrie::Node(n) => {
n.bind_sequence(rest, action, description, current_path)?;
}
},
indexmap::map::Entry::Vacant(v) => {
let mut new_node = KeyTrieNode {
name: String::new(),
map: IndexMap::new(),
};
new_node.bind_sequence(rest, action, description, current_path)?;
v.insert(KeyTrie::Node(new_node));
}
}
Ok(())
}
pub fn search(&self, keys: &[Key]) -> SearchResult<'_, A> {
if keys.is_empty() {
return SearchResult::NotFound;
}
let (first, rest) = keys.split_first().expect("non-empty keys");
match self.map.get(first) {
None => SearchResult::NotFound,
Some(KeyTrie::Leaf(l)) if rest.is_empty() => SearchResult::Found(l),
Some(KeyTrie::Leaf(_)) => SearchResult::NotFound,
Some(KeyTrie::Node(n)) if rest.is_empty() => SearchResult::Prefix(n),
Some(KeyTrie::Node(n)) => n.search(rest),
}
}
pub fn group<F>(&mut self, key: &str, name: &str, f: F) -> Result<(), BindError>
where
F: FnOnce(&mut KeyTrieNode<A>) -> Result<(), BindError>,
{
let parsed_key = crate::key::parse_key(key)?;
let entry = self.map.entry(parsed_key);
let child_node = match entry {
indexmap::map::Entry::Occupied(o) => match o.into_mut() {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => {
return Err(BindError::ConflictWithLeaf {
existing_keys: key.to_string(),
})
}
},
indexmap::map::Entry::Vacant(v) => {
let new_node = KeyTrie::Node(KeyTrieNode {
name: name.to_string(),
map: IndexMap::new(),
});
match v.insert(new_node) {
KeyTrie::Node(n) => n,
KeyTrie::Leaf(_) => unreachable!(),
}
}
};
f(child_node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key::parse_key;
#[test]
fn test_conflict_leaf_then_prefix() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g", "action").unwrap();
let result = trie.bind("g g", "action2");
assert!(matches!(result, Err(BindError::ConflictWithLeaf { .. })));
}
#[test]
fn test_conflict_prefix_then_leaf() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g g", "action").unwrap();
let result = trie.bind("g", "action2");
assert!(matches!(result, Err(BindError::ConflictWithPrefix { .. })));
}
#[test]
fn test_siblings_ok() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g g", "action1").unwrap();
trie.bind("g h", "action2").unwrap();
}
#[test]
fn test_overwrite() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g", "first").unwrap();
trie.bind("g", "second").unwrap();
let key = parse_key("g").unwrap();
if let SearchResult::Found(leaf) = trie.search(&[key]) {
assert_eq!(leaf.action, "second");
} else {
panic!("expected Found");
}
}
#[test]
fn test_empty_bind() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
let result = trie.bind("", "action");
assert!(matches!(result, Err(BindError::Parse(_))));
}
#[test]
fn test_search_empty_keys() {
let trie: KeyTrie<&str> = KeyTrie::new("root");
assert!(matches!(trie.search(&[]), SearchResult::NotFound));
}
#[test]
fn test_search_empty_trie() {
let trie: KeyTrie<&str> = KeyTrie::new("root");
let key = parse_key("g").unwrap();
assert!(matches!(trie.search(&[key]), SearchResult::NotFound));
}
#[test]
fn test_search_prefix() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g g", "action").unwrap();
let g = parse_key("g").unwrap();
assert!(matches!(trie.search(&[g]), SearchResult::Prefix(_)));
}
#[test]
fn test_search_found() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g g", "action").unwrap();
let g = parse_key("g").unwrap();
if let SearchResult::Found(leaf) = trie.search(&[g, g]) {
assert_eq!(leaf.action, "action");
} else {
panic!("expected Found");
}
}
#[test]
fn test_search_not_found_wrong_key() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g g", "action").unwrap();
let g = parse_key("g").unwrap();
let h = parse_key("h").unwrap();
assert!(matches!(trie.search(&[g, h]), SearchResult::NotFound));
}
#[test]
fn test_search_beyond_leaf() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("g", "action").unwrap();
let g = parse_key("g").unwrap();
let h = parse_key("h").unwrap();
assert!(matches!(trie.search(&[g, h]), SearchResult::NotFound));
}
#[test]
fn test_group_then_bind_at_group_key() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.group("g", "goto", |_| Ok(())).unwrap();
let result = trie.bind("g", "action");
assert!(matches!(result, Err(BindError::ConflictWithPrefix { .. })));
}
#[test]
fn test_empty_group() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.group("g", "goto", |_| Ok(())).unwrap();
let g = parse_key("g").unwrap();
if let SearchResult::Prefix(node) = trie.search(&[g]) {
assert!(node.map.is_empty());
} else {
panic!("expected Prefix");
}
}
#[test]
fn test_deep_nesting() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind("a b c d e f g h", "deep").unwrap();
let keys: Vec<Key> = "a b c d e f g h"
.split_whitespace()
.map(|s| parse_key(s).unwrap())
.collect();
assert!(matches!(trie.search(&keys), SearchResult::Found(_)));
}
#[test]
fn test_wide_single_level() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
for c in 'a'..='z' {
trie.bind(&c.to_string(), "action").unwrap();
}
for c in 'a'..='z' {
let key = parse_key(&c.to_string()).unwrap();
assert!(matches!(trie.search(&[key]), SearchResult::Found(_)));
}
}
#[test]
fn test_bind_with_description() {
let mut trie: KeyTrie<&str> = KeyTrie::new("root");
trie.bind_desc("j", "down", "Move down").unwrap();
let j = parse_key("j").unwrap();
if let SearchResult::Found(leaf) = trie.search(&[j]) {
assert_eq!(leaf.description, Some("Move down".to_string()));
} else {
panic!("expected Found");
}
}
}
+111
View File
@@ -0,0 +1,111 @@
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);
}
}
}