Skip to main content

keymap_core/
cmd.rs

1//! Command name → action index with front-prefix completion.
2//!
3//! Ex-command input (`:wq`, `:w file`) decomposes into three stages, none of
4//! which require a new library type for *execution*: `:` is an ordinary
5//! [`Keymap::get`](crate::Keymap::get) hit, text accumulates in a caller-owned
6//! line buffer, and Enter dispatches through a `FnMut(&str) -> Option<A>` — see
7//! `examples/ex_command.rs`.
8//!
9//! What a caller-owned name table *cannot* give cheaply is the **discovery
10//! layer**: `:w<Tab>` completions (front-prefix enumeration) and a full command
11//! listing for a palette. [`CommandIndex`] is that one type — a name-keyed
12//! `BTreeMap` whose [`complete`](CommandIndex::complete) method is the primary
13//! differentiator.
14//!
15//! The design deliberately avoids a closed result enum: `:w` can be both an
16//! exact command *and* the prefix of `:wq` and `:wqa`, so [`get`] and
17//! [`complete`] are two orthogonal pure functions rather than a merged
18//! discriminant. Normalisation (trim, case-fold) and dispatch are the caller's
19//! responsibility; the index stores names byte-for-byte as bound.
20//!
21//! [`get`]: CommandIndex::get
22//! [`complete`]: CommandIndex::complete
23
24use std::collections::BTreeMap;
25use std::ops::Bound;
26
27/// A name-to-action index with front-prefix completion.
28///
29/// Stores an arbitrary number of named commands, each mapped to a
30/// caller-defined action `A`. The primary use case is a vim-style command
31/// palette: bind names with [`bind`](Self::bind), look up an exact name with
32/// [`get`](Self::get), and enumerate candidates for a partial name with
33/// [`complete`](Self::complete).
34///
35/// **Case sensitivity and whitespace** are the caller's concern: the index
36/// stores names byte-for-byte as given. Trim and case-fold before calling
37/// `bind` / `get` / `complete` if uniformity is desired.
38///
39/// **Lexicographic order is a specification**, not just an implementation
40/// detail. [`complete`](Self::complete) and [`iter`](Self::iter) both return
41/// results in ascending byte order (the order of the underlying `BTreeMap`),
42/// so callers may rely on this for stable, deterministic output.
43#[derive(Debug, Clone)]
44pub struct CommandIndex<A> {
45    map: BTreeMap<String, A>,
46}
47
48impl<A> Default for CommandIndex<A> {
49    fn default() -> Self {
50        CommandIndex {
51            map: BTreeMap::new(),
52        }
53    }
54}
55
56impl<A> CommandIndex<A> {
57    /// Creates an empty index.
58    #[must_use]
59    pub fn new() -> Self {
60        CommandIndex::default()
61    }
62
63    /// Binds `name` to `action`, returning the action previously bound to that
64    /// name, if any (last-wins — same semantic as [`Keymap::bind`](crate::Keymap::bind)).
65    ///
66    /// The name is stored byte-for-byte; no trimming or case-folding is applied.
67    ///
68    /// ```
69    /// use keymap_core::cmd::CommandIndex;
70    ///
71    /// let mut idx: CommandIndex<&str> = CommandIndex::new();
72    /// assert_eq!(idx.bind(":w", "write"), None);
73    /// // Rebinding returns the old action.
74    /// assert_eq!(idx.bind(":w", "write-force"), Some("write"));
75    /// assert_eq!(idx.get(":w"), Some(&"write-force"));
76    /// ```
77    pub fn bind(&mut self, name: impl Into<String>, action: A) -> Option<A> {
78        self.map.insert(name.into(), action)
79    }
80
81    /// Returns the action bound to `name`, or [`None`] if no exact match exists.
82    ///
83    /// This is a **complete-match** lookup. For prefix enumeration (e.g.
84    /// `:w<Tab>`), use [`complete`](Self::complete).
85    ///
86    /// ```
87    /// use keymap_core::cmd::CommandIndex;
88    ///
89    /// let mut idx: CommandIndex<u8> = CommandIndex::new();
90    /// idx.bind(":w", 1);
91    /// idx.bind(":wq", 2);
92    ///
93    /// assert_eq!(idx.get(":w"), Some(&1));
94    /// assert_eq!(idx.get(":wq"), Some(&2));
95    /// // Prefix is not an exact match.
96    /// assert_eq!(idx.get(":"), None);
97    /// ```
98    #[must_use]
99    pub fn get(&self, name: &str) -> Option<&A> {
100        self.map.get(name)
101    }
102
103    /// Enumerates every command whose name starts with `prefix`, in
104    /// **lexicographic (byte) order**.
105    ///
106    /// An empty `prefix` is equivalent to [`iter`](Self::iter) — it yields
107    /// every bound command. Lexicographic order is a **specification**: callers
108    /// may rely on it for stable, deterministic output.
109    ///
110    /// Note that a name can be both an exact match for one call *and* a prefix
111    /// for another: `:w` completes under the prefix `:` and is itself exact
112    /// under `get(":w")`. There is no closed result enum merging these two
113    /// cases.
114    ///
115    /// ```
116    /// use keymap_core::cmd::CommandIndex;
117    ///
118    /// let mut idx: CommandIndex<u8> = CommandIndex::new();
119    /// idx.bind(":w", 1);
120    /// idx.bind(":wq", 2);
121    /// idx.bind(":wqa", 3);
122    /// idx.bind(":q", 4);
123    ///
124    /// // Prefix ":w" matches ":w", ":wq", ":wqa" in lexicographic order.
125    /// let completions: Vec<(&str, &u8)> = idx.complete(":w").collect();
126    /// assert_eq!(completions, vec![(":w", &1), (":wq", &2), (":wqa", &3)]);
127    ///
128    /// // Empty prefix enumerates everything.
129    /// assert_eq!(idx.complete("").count(), 4);
130    /// ```
131    pub fn complete<'a>(&'a self, prefix: &str) -> impl Iterator<Item = (&'a str, &'a A)> {
132        // BTreeMap::range on a string map: the lower bound is the prefix itself;
133        // the upper bound is the first string that is lexicographically greater
134        // than every string with the given prefix — obtained by incrementing the
135        // last byte. If the prefix is empty or incrementing would overflow, there
136        // is no upper bound.
137        let start = Bound::Included(prefix.to_owned());
138        let end = next_prefix(prefix).map_or(Bound::Unbounded, Bound::Excluded);
139
140        self.map.range((start, end)).map(|(k, v)| (k.as_str(), v))
141    }
142
143    /// Iterates over every `(name, action)` pair in **lexicographic (byte)
144    /// order**.
145    ///
146    /// This is the discovery dual of [`complete`](Self::complete) (with an
147    /// empty prefix) and mirrors [`Keymap::iter`](crate::Keymap::iter) in
148    /// purpose.
149    ///
150    /// ```
151    /// use keymap_core::cmd::CommandIndex;
152    ///
153    /// let mut idx: CommandIndex<u8> = CommandIndex::new();
154    /// idx.bind(":q", 2);
155    /// idx.bind(":w", 1);
156    ///
157    /// let all: Vec<(&str, &u8)> = idx.iter().collect();
158    /// // Lexicographic order: ":q" before ":w".
159    /// assert_eq!(all, vec![(":q", &2), (":w", &1)]);
160    /// ```
161    pub fn iter(&self) -> impl Iterator<Item = (&str, &A)> {
162        self.map.iter().map(|(k, v)| (k.as_str(), v))
163    }
164
165    /// Returns the number of commands in the index.
166    #[must_use]
167    pub fn len(&self) -> usize {
168        self.map.len()
169    }
170
171    /// Returns `true` if the index contains no commands.
172    #[must_use]
173    pub fn is_empty(&self) -> bool {
174        self.map.is_empty()
175    }
176}
177
178/// Returns the lexicographically smallest string that is strictly greater than
179/// every string with the given prefix, or `None` if no such string exists
180/// (empty prefix, or the prefix ends with `u8::MAX` bytes).
181///
182/// Used as the exclusive upper bound for the `BTreeMap::range` call in
183/// [`CommandIndex::complete`].
184fn next_prefix(prefix: &str) -> Option<String> {
185    if prefix.is_empty() {
186        return None;
187    }
188    let mut bytes = prefix.as_bytes().to_owned();
189    // Walk backwards to find the last byte that can be incremented.
190    for i in (0..bytes.len()).rev() {
191        if bytes[i] < u8::MAX {
192            bytes[i] += 1;
193            bytes.truncate(i + 1);
194            // The resulting bytes may not be valid UTF-8 (e.g. if we incremented
195            // into a multi-byte sequence boundary), but BTreeMap<String, _> uses
196            // the byte comparison of Ord<String>, and Rust's String Ord delegates
197            // to the byte comparison of str, so constructing via from_utf8_lossy
198            // would change the sort key. Instead we use from_utf8 and, if it
199            // fails, fall back to Unbounded — which is safe (returns a superset).
200            return String::from_utf8(bytes).ok();
201        }
202    }
203    None
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    // ---- bind / get ----------------------------------------------------
211
212    #[test]
213    fn bind_last_wins_returns_old_value() {
214        let mut idx: CommandIndex<&str> = CommandIndex::new();
215        assert_eq!(idx.bind(":w", "write"), None);
216        assert_eq!(idx.bind(":w", "write-bang"), Some("write"));
217        assert_eq!(idx.get(":w"), Some(&"write-bang"));
218    }
219
220    #[test]
221    fn get_exact_match_only() {
222        let mut idx: CommandIndex<u8> = CommandIndex::new();
223        idx.bind(":w", 1);
224        idx.bind(":wq", 2);
225        assert_eq!(idx.get(":w"), Some(&1));
226        assert_eq!(idx.get(":wq"), Some(&2));
227        // Prefix is not an exact match.
228        assert_eq!(idx.get(":"), None);
229        // Unknown name.
230        assert_eq!(idx.get(":x"), None);
231    }
232
233    #[test]
234    fn get_returns_none_on_empty_index() {
235        let idx: CommandIndex<u8> = CommandIndex::new();
236        assert_eq!(idx.get(":w"), None);
237    }
238
239    // ---- complete ------------------------------------------------------
240
241    #[test]
242    fn complete_empty_prefix_yields_all_in_lex_order() {
243        let mut idx: CommandIndex<u8> = CommandIndex::new();
244        idx.bind(":wq", 2);
245        idx.bind(":w", 1);
246        idx.bind(":q", 3);
247        let names: Vec<&str> = idx.complete("").map(|(n, _)| n).collect();
248        assert_eq!(names, vec![":q", ":w", ":wq"]);
249    }
250
251    #[test]
252    fn complete_prefix_returns_matching_subset_in_lex_order() {
253        let mut idx: CommandIndex<u8> = CommandIndex::new();
254        idx.bind(":w", 1);
255        idx.bind(":wq", 2);
256        idx.bind(":wqa", 3);
257        idx.bind(":q", 4);
258
259        let names: Vec<&str> = idx.complete(":w").map(|(n, _)| n).collect();
260        assert_eq!(names, vec![":w", ":wq", ":wqa"]);
261    }
262
263    #[test]
264    fn complete_exact_name_as_prefix_is_included() {
265        // ":w" is both an exact command and a prefix for ":wq"; both come back.
266        let mut idx: CommandIndex<u8> = CommandIndex::new();
267        idx.bind(":w", 1);
268        idx.bind(":wq", 2);
269
270        let result: Vec<(&str, &u8)> = idx.complete(":w").collect();
271        assert_eq!(result, vec![(":w", &1), (":wq", &2)]);
272    }
273
274    #[test]
275    fn complete_no_match_yields_empty() {
276        let mut idx: CommandIndex<u8> = CommandIndex::new();
277        idx.bind(":w", 1);
278        assert_eq!(idx.complete(":x").count(), 0);
279    }
280
281    #[test]
282    fn complete_yields_name_and_action_pairs() {
283        let mut idx: CommandIndex<u8> = CommandIndex::new();
284        idx.bind(":w", 1);
285        idx.bind(":wq", 2);
286
287        let result: Vec<(&str, &u8)> = idx.complete(":w").collect();
288        assert_eq!(result, [(":w", &1), (":wq", &2)]);
289    }
290
291    #[test]
292    fn complete_utf8_names_lex_order() {
293        // Non-ASCII names: byte order determines lexicographic sort.
294        let mut idx: CommandIndex<u8> = CommandIndex::new();
295        idx.bind("écrire", 1);
296        idx.bind("éditer", 2);
297        idx.bind("effacer", 3);
298
299        // "éc" < "éd" < "ef" in byte order (all start with 0xc3 0xa9 = 'é', then diverge)
300        // actually "écrire" starts 0xc3,0xa9,0x63 and "éditer" starts 0xc3,0xa9,0x64 and
301        // "effacer" starts 0x65,0x66 — "effacer" is ASCII 'e' which is 0x65 < 0xc3.
302        // So lex order: "effacer" < "écrire" < "éditer".
303        let all: Vec<&str> = idx.iter().map(|(n, _)| n).collect();
304        assert_eq!(all, vec!["effacer", "écrire", "éditer"]);
305
306        // Complete on "é" (bytes 0xc3 0xa9) matches "écrire" and "éditer".
307        let completions: Vec<&str> = idx.complete("é").map(|(n, _)| n).collect();
308        assert_eq!(completions, vec!["écrire", "éditer"]);
309    }
310
311    // ---- iter ----------------------------------------------------------
312
313    #[test]
314    fn iter_lexicographic_order() {
315        let mut idx: CommandIndex<u8> = CommandIndex::new();
316        idx.bind(":wq", 2);
317        idx.bind(":q", 3);
318        idx.bind(":w", 1);
319        let all: Vec<(&str, &u8)> = idx.iter().collect();
320        assert_eq!(all, vec![(":q", &3), (":w", &1), (":wq", &2)]);
321    }
322
323    #[test]
324    fn iter_empty_index_is_empty() {
325        let idx: CommandIndex<u8> = CommandIndex::new();
326        assert_eq!(idx.iter().count(), 0);
327    }
328
329    // ---- len / is_empty ------------------------------------------------
330
331    #[test]
332    fn len_and_is_empty() {
333        let mut idx: CommandIndex<u8> = CommandIndex::new();
334        assert!(idx.is_empty());
335        assert_eq!(idx.len(), 0);
336        idx.bind(":w", 1);
337        assert!(!idx.is_empty());
338        assert_eq!(idx.len(), 1);
339    }
340
341    // ---- next_prefix helper --------------------------------------------
342
343    #[test]
344    fn next_prefix_increments_last_byte() {
345        assert_eq!(next_prefix(":w"), Some(":x".to_string()));
346    }
347
348    #[test]
349    fn next_prefix_empty_returns_none() {
350        assert_eq!(next_prefix(""), None);
351    }
352
353    #[test]
354    fn next_prefix_all_max_bytes_returns_none() {
355        // All bytes are u8::MAX (0xFF): no byte can be incremented, so the
356        // function returns None.
357        // U+FFFF encodes as [0xEF, 0xBF, 0xBF] in UTF-8, all of which are < u8::MAX,
358        // so that would not reach the None path. Use a single DEL byte (0x7f) instead:
359        // 0x7f + 1 = 0x80, which is not a valid UTF-8 leading byte.
360        let raw_ff = std::str::from_utf8(&[0x7f]).unwrap(); // 0x7f is valid ASCII
361        // 0x7f + 1 = 0x80, which is not a valid UTF-8 leading byte → from_utf8 fails
362        // → next_prefix returns None (safe fallback to Unbounded range).
363        assert_eq!(next_prefix(raw_ff), None);
364    }
365
366    /// When `next_prefix` returns `None` (non-UTF-8 boundary), `complete` falls
367    /// back to an unbounded upper range and returns a superset — no panic, no
368    /// missing results.
369    #[test]
370    fn next_prefix_non_utf8_boundary_complete_returns_superset() {
371        let mut idx: CommandIndex<u8> = CommandIndex::new();
372        // Bind names that start with DEL (0x7f), which is a valid single-byte
373        // ASCII character (the highest ASCII byte). Its next_prefix increments to
374        // 0x80, which is invalid UTF-8 → fallback to Unbounded.
375        let del_a = std::str::from_utf8(&[0x7f, b'a']).unwrap(); // "\x7fa"
376        let del_b = std::str::from_utf8(&[0x7f, b'b']).unwrap(); // "\x7fb"
377        let unrelated = "zzzz";
378        idx.bind(del_a, 1);
379        idx.bind(del_b, 2);
380        idx.bind(unrelated, 3);
381
382        // Prefix is a single DEL byte: next_prefix("\x7f") → None (0x7f+1 = 0x80,
383        // not valid UTF-8) → Unbounded upper → complete returns all entries ≥ "\x7f",
384        // which includes both "\x7fa" and "\x7fb" but also "zzzz" (superset is ok).
385        let prefix = std::str::from_utf8(&[0x7f]).unwrap();
386        let results: Vec<(&str, &u8)> = idx.complete(prefix).collect();
387
388        // Must include both del_a and del_b (no results dropped).
389        assert!(
390            results.iter().any(|(n, _)| *n == del_a),
391            "del_a must appear in superset: {results:?}"
392        );
393        assert!(
394            results.iter().any(|(n, _)| *n == del_b),
395            "del_b must appear in superset: {results:?}"
396        );
397        // Must not panic: the function completed without error.
398    }
399}