use std::collections::HashMap;
use keymap_core::KeyInput;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SequenceKeymap<A> {
root: Node<A>,
}
#[derive(Debug, Clone)]
struct Node<A> {
action: Option<A>,
children: HashMap<KeyInput, Node<A>>,
}
impl<A> Node<A> {
fn new() -> Self {
Node {
action: None,
children: HashMap::new(),
}
}
fn is_terminal(&self) -> bool {
self.action.is_some()
}
}
fn extend_to_terminal<A>(node: &Node<A>, seq: &mut Vec<KeyInput>) {
let mut cur = node;
while !cur.is_terminal() {
let Some((key, child)) = cur.children.iter().next() else {
break;
};
seq.push(*key);
cur = child;
}
}
fn collect_bindings<'a, A>(
node: &'a Node<A>,
path: &mut Vec<KeyInput>,
out: &mut Vec<(Vec<KeyInput>, &'a A)>,
) {
if let Some(action) = &node.action {
out.push((path.clone(), action));
}
for (key, child) in &node.children {
path.push(*key);
collect_bindings(child, path, out);
path.pop();
}
}
impl<A> Default for SequenceKeymap<A> {
fn default() -> Self {
SequenceKeymap { root: Node::new() }
}
}
impl<A> SequenceKeymap<A> {
#[must_use]
pub fn new() -> Self {
SequenceKeymap::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.root.children.is_empty()
}
pub fn bind(
&mut self,
sequence: impl IntoIterator<Item = KeyInput>,
action: A,
) -> Result<Option<A>, SeqBindError> {
let keys: Vec<KeyInput> = sequence.into_iter().collect();
if keys.is_empty() {
return Err(SeqBindError::Empty);
}
let mut node = &mut self.root;
for (depth, key) in keys.iter().enumerate() {
if node.is_terminal() {
return Err(SeqBindError::PrefixShadow {
sequence: keys.clone(),
conflict: keys[..depth].to_vec(),
});
}
node = node.children.entry(*key).or_insert_with(Node::new);
}
if !node.children.is_empty() {
let mut conflict = keys.clone();
extend_to_terminal(node, &mut conflict);
return Err(SeqBindError::PrefixShadow {
sequence: keys,
conflict,
});
}
Ok(node.action.replace(action))
}
pub fn lookup(&self, keys: &[KeyInput]) -> Match<'_, A> {
let mut node = &self.root;
for key in keys {
match node.children.get(key) {
Some(child) => node = child,
None => return Match::NoMatch,
}
}
match &node.action {
Some(action) => Match::Exact(action),
None if node.children.is_empty() => Match::NoMatch,
None => Match::Prefix,
}
}
pub fn bindings(&self) -> impl Iterator<Item = (Vec<KeyInput>, &A)> {
let mut out = Vec::new();
let mut path = Vec::new();
collect_bindings(&self.root, &mut path, &mut out);
out.into_iter()
}
pub fn continuations(
&self,
prefix: &[KeyInput],
) -> impl Iterator<Item = (KeyInput, Continuation<'_, A>)> {
let mut node = &self.root;
for key in prefix {
match node.children.get(key) {
Some(child) => node = child,
None => return None.into_iter().flatten(),
}
}
Some(node.children.iter().map(|(key, child)| {
let continuation = child
.action
.as_ref()
.map_or(Continuation::Prefix, Continuation::Action);
(*key, continuation)
}))
.into_iter()
.flatten()
}
}
#[derive(Debug, PartialEq, Eq)]
#[must_use]
pub enum Match<'a, A> {
Exact(&'a A),
Prefix,
NoMatch,
}
#[derive(Debug, PartialEq, Eq)]
#[must_use]
pub enum Continuation<'a, A> {
Action(&'a A),
Prefix,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SeqBindError {
Empty,
PrefixShadow {
sequence: Vec<KeyInput>,
conflict: Vec<KeyInput>,
},
}
impl core::fmt::Display for SeqBindError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SeqBindError::Empty => f.write_str("empty key sequence"),
SeqBindError::PrefixShadow { .. } => {
f.write_str("key sequence shares a prefix with an existing binding")
}
}
}
}
impl std::error::Error for SeqBindError {}
#[cfg(test)]
mod tests {
use super::*;
use keymap_core::{Key, Modifiers};
#[derive(Debug, Clone, PartialEq)]
enum Action {
Save,
Quit,
Top,
Solo,
}
fn ctrl(c: char) -> KeyInput {
KeyInput::new(Key::Char(c), Modifiers::CTRL)
}
fn plain(c: char) -> KeyInput {
KeyInput::new(Key::Char(c), Modifiers::NONE)
}
fn sample() -> SequenceKeymap<Action> {
let mut map = SequenceKeymap::new();
map.bind([ctrl('x'), ctrl('s')], Action::Save).unwrap();
map.bind([ctrl('x'), ctrl('c')], Action::Quit).unwrap();
map.bind([plain('g'), plain('g')], Action::Top).unwrap();
map.bind([plain('q')], Action::Solo).unwrap();
map
}
#[test]
fn single_key_sequence_resolves() {
let map = sample();
assert_eq!(map.lookup(&[plain('q')]), Match::Exact(&Action::Solo));
}
#[test]
fn prefix_then_leaf() {
let map = sample();
assert_eq!(map.lookup(&[ctrl('x')]), Match::Prefix);
assert_eq!(
map.lookup(&[ctrl('x'), ctrl('s')]),
Match::Exact(&Action::Save)
);
}
#[test]
fn branching_prefix_reaches_both_leaves() {
let map = sample();
assert_eq!(
map.lookup(&[ctrl('x'), ctrl('c')]),
Match::Exact(&Action::Quit)
);
assert_eq!(
map.lookup(&[ctrl('x'), ctrl('s')]),
Match::Exact(&Action::Save)
);
}
#[test]
fn miss_on_first_key() {
let map = sample();
assert_eq!(map.lookup(&[plain('z')]), Match::NoMatch);
}
#[test]
fn miss_off_a_prefix_path() {
let map = sample();
assert_eq!(map.lookup(&[ctrl('x'), plain('z')]), Match::NoMatch);
}
#[test]
fn empty_buffer_on_nonempty_map_is_prefix() {
let map = sample();
assert_eq!(map.lookup(&[]), Match::Prefix);
}
#[test]
fn empty_buffer_on_empty_map_is_no_match() {
let map: SequenceKeymap<Action> = SequenceKeymap::new();
assert!(map.is_empty());
assert_eq!(map.lookup(&[]), Match::NoMatch);
assert_eq!(map.lookup(&[ctrl('x')]), Match::NoMatch);
}
#[test]
fn rebinding_exact_sequence_overwrites_and_returns_previous() {
let mut map = SequenceKeymap::new();
assert_eq!(map.bind([ctrl('x'), ctrl('s')], Action::Save), Ok(None));
assert_eq!(
map.bind([ctrl('x'), ctrl('s')], Action::Quit),
Ok(Some(Action::Save))
);
assert_eq!(
map.lookup(&[ctrl('x'), ctrl('s')]),
Match::Exact(&Action::Quit)
);
}
#[test]
fn empty_sequence_is_rejected() {
let mut map: SequenceKeymap<Action> = SequenceKeymap::new();
assert_eq!(map.bind([], Action::Save), Err(SeqBindError::Empty));
}
#[test]
fn existing_short_binding_shadows_new_longer_one() {
let mut map = SequenceKeymap::new();
map.bind([plain('g')], Action::Solo).unwrap();
assert_eq!(
map.bind([plain('g'), plain('g')], Action::Top),
Err(SeqBindError::PrefixShadow {
sequence: vec![plain('g'), plain('g')],
conflict: vec![plain('g')],
})
);
assert_eq!(map.lookup(&[plain('g')]), Match::Exact(&Action::Solo));
}
#[test]
fn new_short_binding_shadows_existing_longer_one() {
let mut map = SequenceKeymap::new();
map.bind([plain('g'), plain('g')], Action::Top).unwrap();
assert_eq!(
map.bind([plain('g')], Action::Solo),
Err(SeqBindError::PrefixShadow {
sequence: vec![plain('g')],
conflict: vec![plain('g'), plain('g')],
})
);
assert_eq!(map.lookup(&[plain('g')]), Match::Prefix);
assert_eq!(
map.lookup(&[plain('g'), plain('g')]),
Match::Exact(&Action::Top)
);
}
#[test]
fn deep_sequence_is_prefix_at_every_step_then_exact() {
let mut map = SequenceKeymap::new();
map.bind([plain('a'), plain('b'), plain('c')], Action::Top)
.unwrap();
assert_eq!(map.lookup(&[plain('a')]), Match::Prefix);
assert_eq!(map.lookup(&[plain('a'), plain('b')]), Match::Prefix);
assert_eq!(
map.lookup(&[plain('a'), plain('b'), plain('c')]),
Match::Exact(&Action::Top)
);
assert_eq!(
map.lookup(&[plain('a'), plain('b'), plain('z')]),
Match::NoMatch
);
}
#[test]
fn independent_single_and_sequence_coexist() {
let map = sample();
assert_eq!(map.lookup(&[plain('q')]), Match::Exact(&Action::Solo));
assert_eq!(map.lookup(&[ctrl('x')]), Match::Prefix);
}
fn sorted_continuations<'a>(
map: &'a SequenceKeymap<Action>,
prefix: &[KeyInput],
) -> Vec<(KeyInput, Continuation<'a, Action>)> {
let mut out: Vec<_> = map.continuations(prefix).collect();
out.sort_by_key(|(key, _)| key.to_string());
out
}
#[test]
fn continuations_at_root_list_the_leader_keys() {
assert_eq!(
sorted_continuations(&sample(), &[]),
vec![
(ctrl('x'), Continuation::Prefix),
(plain('g'), Continuation::Prefix),
(plain('q'), Continuation::Action(&Action::Solo)),
]
);
}
#[test]
fn continuations_after_a_prefix_classify_each_child() {
assert_eq!(
sorted_continuations(&sample(), &[ctrl('x')]),
vec![
(ctrl('c'), Continuation::Action(&Action::Quit)),
(ctrl('s'), Continuation::Action(&Action::Save)),
]
);
}
#[test]
fn continuations_mix_action_and_prefix() {
let mut map = SequenceKeymap::new();
map.bind([plain('a'), plain('b')], Action::Save).unwrap();
map.bind([plain('a'), plain('c'), plain('d')], Action::Quit)
.unwrap();
assert_eq!(
sorted_continuations(&map, &[plain('a')]),
vec![
(plain('b'), Continuation::Action(&Action::Save)),
(plain('c'), Continuation::Prefix),
]
);
}
#[test]
fn a_completed_binding_has_no_continuations() {
assert_eq!(sample().continuations(&[plain('q')]).count(), 0);
assert_eq!(sample().continuations(&[ctrl('x'), ctrl('s')]).count(), 0);
}
#[test]
fn off_tree_prefix_has_no_continuations() {
let map = sample();
assert_eq!(map.continuations(&[plain('z')]).count(), 0);
assert_eq!(map.continuations(&[ctrl('x'), plain('z')]).count(), 0);
}
#[test]
fn empty_map_has_no_continuations() {
let map: SequenceKeymap<Action> = SequenceKeymap::new();
assert_eq!(map.continuations(&[]).count(), 0);
assert_eq!(map.continuations(&[plain('a')]).count(), 0);
}
#[test]
fn continuations_descend_to_deeper_internal_nodes() {
let mut map = SequenceKeymap::new();
map.bind([plain('a'), plain('b')], Action::Save).unwrap();
map.bind(
[plain('a'), plain('c'), plain('d'), plain('e')],
Action::Quit,
)
.unwrap();
assert_eq!(
sorted_continuations(&map, &[plain('a'), plain('c')]),
vec![(plain('d'), Continuation::Prefix)]
);
assert_eq!(
sorted_continuations(&map, &[plain('a'), plain('c'), plain('d')]),
vec![(plain('e'), Continuation::Action(&Action::Quit))]
);
}
fn sorted_bindings(map: &SequenceKeymap<Action>) -> Vec<(Vec<KeyInput>, &Action)> {
let mut out: Vec<_> = map.bindings().collect();
out.sort_by_key(|(path, _)| {
path.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
});
out
}
#[test]
fn bindings_enumerate_every_leaf_path() {
assert_eq!(
sorted_bindings(&sample()),
vec![
(vec![ctrl('x'), ctrl('c')], &Action::Quit),
(vec![ctrl('x'), ctrl('s')], &Action::Save),
(vec![plain('g'), plain('g')], &Action::Top),
(vec![plain('q')], &Action::Solo),
]
);
}
#[test]
fn bindings_of_empty_map_is_empty() {
let map: SequenceKeymap<Action> = SequenceKeymap::new();
assert_eq!(map.bindings().count(), 0);
}
#[test]
fn bindings_never_yield_internal_prefixes() {
let map = sample();
for (path, action) in map.bindings() {
assert_eq!(map.lookup(&path), Match::Exact(action));
}
}
#[test]
fn continuations_nonempty_iff_lookup_is_prefix() {
let map = sample();
let cases: &[&[KeyInput]] = &[
&[],
&[plain('q')],
&[ctrl('x')],
&[ctrl('x'), ctrl('s')],
&[plain('z')],
&[ctrl('x'), plain('z')],
];
for keys in cases {
let has_next = map.continuations(keys).count() > 0;
match map.lookup(keys) {
Match::Prefix => assert!(has_next, "Prefix must have continuations: {keys:?}"),
Match::Exact(_) | Match::NoMatch => {
assert!(!has_next, "non-Prefix must have no continuations: {keys:?}");
}
}
}
}
}