hjkl_keymap/trie.rs
1//! Trie (prefix tree) data structure backing [`Keymap`].
2//!
3//! Each node can optionally carry a [`Binding`] (terminal action) and has
4//! zero or more children keyed by the next [`KeyEvent`] in the chord.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::key::KeyEvent;
10
11/// A predicate closure that gates a binding.
12///
13/// When the predicate returns `false` the binding is treated as if it does
14/// not exist: `lookup` skips it and `has_prefix` still reports deeper
15/// children so the chord can extend. The closure is stored in an `Arc` so
16/// that `Binding<A>: Clone` even when the closure is not `Clone`.
17pub type Predicate = Arc<dyn Fn() -> bool + Send + Sync>;
18
19/// The action and metadata stored at a terminal trie node.
20#[derive(Clone)]
21pub struct Binding<A> {
22 /// The user-defined action associated with this chord.
23 pub action: A,
24 /// Human-readable description (shown in which-key).
25 pub desc: String,
26 /// `true` = recursive (`:map`), `false` = non-recursive (`:noremap`).
27 /// Ignored in v1 dispatch — reserved for future expansion.
28 pub recursive: bool,
29 /// Optional runtime gate: when `Some(pred)` and `pred()` returns `false`,
30 /// this binding is treated as `Unbound` for resolution purposes.
31 /// The trie node stays present so longer chords extending this prefix
32 /// remain reachable.
33 pub condition: Option<Predicate>,
34}
35
36impl<A: std::fmt::Debug> std::fmt::Debug for Binding<A> {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("Binding")
39 .field("action", &self.action)
40 .field("desc", &self.desc)
41 .field("recursive", &self.recursive)
42 .field("condition", &self.condition.as_ref().map(|_| "<predicate>"))
43 .finish()
44 }
45}
46
47impl<A> Binding<A> {
48 /// Evaluate the condition. Returns `true` when the binding should fire
49 /// (no predicate, or predicate returns `true`).
50 pub fn is_active(&self) -> bool {
51 self.condition.as_ref().is_none_or(|pred| pred())
52 }
53}
54
55/// A single node in the trie.
56pub(crate) struct TrieNode<A> {
57 /// Action bound at this node (if this is a terminal chord).
58 pub(crate) action: Option<Binding<A>>,
59 /// Children keyed by next key event.
60 pub(crate) children: HashMap<KeyEvent, TrieNode<A>>,
61}
62
63impl<A> Default for TrieNode<A> {
64 fn default() -> Self {
65 Self {
66 action: None,
67 children: HashMap::new(),
68 }
69 }
70}
71
72impl<A: Clone> TrieNode<A> {
73 /// Insert a binding at the given chord path (relative to this node).
74 pub(crate) fn insert(&mut self, events: &[KeyEvent], binding: Binding<A>) {
75 if events.is_empty() {
76 self.action = Some(binding);
77 return;
78 }
79 let child = self.children.entry(events[0]).or_default();
80 child.insert(&events[1..], binding);
81 }
82
83 /// Remove the binding for the given chord path.
84 /// Returns `true` if something was removed.
85 pub(crate) fn remove(&mut self, events: &[KeyEvent]) -> bool {
86 if events.is_empty() {
87 let had = self.action.is_some();
88 self.action = None;
89 return had;
90 }
91 if let Some(child) = self.children.get_mut(&events[0]) {
92 let removed = child.remove(&events[1..]);
93 // Prune empty leaf nodes.
94 if child.action.is_none() && child.children.is_empty() {
95 self.children.remove(&events[0]);
96 }
97 removed
98 } else {
99 false
100 }
101 }
102
103 /// Exact-match lookup: returns the binding if the chord terminates here
104 /// **and** the binding's condition (if any) evaluates to `true`.
105 ///
106 /// A binding with a false predicate is invisible to callers: the trie
107 /// node remains present so that longer chords extending this prefix are
108 /// still reachable, but the binding itself is treated as absent.
109 pub(crate) fn lookup(&self, events: &[KeyEvent]) -> Option<&Binding<A>> {
110 if events.is_empty() {
111 return self.action.as_ref().filter(|b| b.is_active());
112 }
113 self.children.get(&events[0])?.lookup(&events[1..])
114 }
115
116 /// Returns `true` if `events` is a proper prefix of at least one chord in this trie.
117 pub(crate) fn has_prefix(&self, events: &[KeyEvent]) -> bool {
118 if events.is_empty() {
119 // We are at the node reached by the prefix — it has a prefix if
120 // it has any children (deeper chords exist).
121 return !self.children.is_empty();
122 }
123 match self.children.get(&events[0]) {
124 Some(child) => child.has_prefix(&events[1..]),
125 None => false,
126 }
127 }
128
129 /// Iterate over direct-child *terminal* bindings reachable from `prefix`.
130 /// Each item is `(key_event, &Binding)` for each child node that has an action.
131 pub(crate) fn children_of<'a>(
132 &'a self,
133 prefix: &[KeyEvent],
134 ) -> Vec<(&'a KeyEvent, &'a Binding<A>)> {
135 if prefix.is_empty() {
136 // Return direct children that are terminals.
137 return self
138 .children
139 .iter()
140 .filter_map(|(k, node)| node.action.as_ref().map(|b| (k, b)))
141 .collect();
142 }
143 match self.children.get(&prefix[0]) {
144 Some(child) => child.children_of(&prefix[1..]),
145 None => vec![],
146 }
147 }
148
149 /// Iterate over **all** direct children reachable from `prefix` —
150 /// both terminal nodes (with a bound action) and pure-prefix nodes
151 /// (no action but with deeper children).
152 ///
153 /// Returns `(key_event, Option<&Binding>)` where `None` signals a
154 /// prefix-only entry (a submenu with no action of its own).
155 pub(crate) fn all_children_of<'a>(
156 &'a self,
157 prefix: &[KeyEvent],
158 ) -> Vec<(&'a KeyEvent, Option<&'a Binding<A>>)> {
159 if prefix.is_empty() {
160 return self
161 .children
162 .iter()
163 .filter(|(_, node)| node.action.is_some() || !node.children.is_empty())
164 .map(|(k, node)| (k, node.action.as_ref()))
165 .collect();
166 }
167 match self.children.get(&prefix[0]) {
168 Some(child) => child.all_children_of(&prefix[1..]),
169 None => vec![],
170 }
171 }
172}