use std::collections::BTreeMap;
use std::ops::Bound;
#[derive(Debug, Clone)]
pub struct CommandIndex<A> {
map: BTreeMap<String, A>,
}
impl<A> Default for CommandIndex<A> {
fn default() -> Self {
CommandIndex {
map: BTreeMap::new(),
}
}
}
impl<A> CommandIndex<A> {
#[must_use]
pub fn new() -> Self {
CommandIndex::default()
}
pub fn bind(&mut self, name: impl Into<String>, action: A) -> Option<A> {
self.map.insert(name.into(), action)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&A> {
self.map.get(name)
}
pub fn complete<'a>(&'a self, prefix: &str) -> impl Iterator<Item = (&'a str, &'a A)> {
let start = Bound::Included(prefix.to_owned());
let end = next_prefix(prefix).map_or(Bound::Unbounded, Bound::Excluded);
self.map.range((start, end)).map(|(k, v)| (k.as_str(), v))
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &A)> {
self.map.iter().map(|(k, v)| (k.as_str(), v))
}
#[must_use]
pub fn len(&self) -> usize {
self.map.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
fn next_prefix(prefix: &str) -> Option<String> {
if prefix.is_empty() {
return None;
}
let mut bytes = prefix.as_bytes().to_owned();
for i in (0..bytes.len()).rev() {
if bytes[i] < u8::MAX {
bytes[i] += 1;
bytes.truncate(i + 1);
return String::from_utf8(bytes).ok();
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bind_last_wins_returns_old_value() {
let mut idx: CommandIndex<&str> = CommandIndex::new();
assert_eq!(idx.bind(":w", "write"), None);
assert_eq!(idx.bind(":w", "write-bang"), Some("write"));
assert_eq!(idx.get(":w"), Some(&"write-bang"));
}
#[test]
fn get_exact_match_only() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":w", 1);
idx.bind(":wq", 2);
assert_eq!(idx.get(":w"), Some(&1));
assert_eq!(idx.get(":wq"), Some(&2));
assert_eq!(idx.get(":"), None);
assert_eq!(idx.get(":x"), None);
}
#[test]
fn get_returns_none_on_empty_index() {
let idx: CommandIndex<u8> = CommandIndex::new();
assert_eq!(idx.get(":w"), None);
}
#[test]
fn complete_empty_prefix_yields_all_in_lex_order() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":wq", 2);
idx.bind(":w", 1);
idx.bind(":q", 3);
let names: Vec<&str> = idx.complete("").map(|(n, _)| n).collect();
assert_eq!(names, vec![":q", ":w", ":wq"]);
}
#[test]
fn complete_prefix_returns_matching_subset_in_lex_order() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":w", 1);
idx.bind(":wq", 2);
idx.bind(":wqa", 3);
idx.bind(":q", 4);
let names: Vec<&str> = idx.complete(":w").map(|(n, _)| n).collect();
assert_eq!(names, vec![":w", ":wq", ":wqa"]);
}
#[test]
fn complete_exact_name_as_prefix_is_included() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":w", 1);
idx.bind(":wq", 2);
let result: Vec<(&str, &u8)> = idx.complete(":w").collect();
assert_eq!(result, vec![(":w", &1), (":wq", &2)]);
}
#[test]
fn complete_no_match_yields_empty() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":w", 1);
assert_eq!(idx.complete(":x").count(), 0);
}
#[test]
fn complete_yields_name_and_action_pairs() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":w", 1);
idx.bind(":wq", 2);
let result: Vec<(&str, &u8)> = idx.complete(":w").collect();
assert_eq!(result, [(":w", &1), (":wq", &2)]);
}
#[test]
fn complete_utf8_names_lex_order() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind("écrire", 1);
idx.bind("éditer", 2);
idx.bind("effacer", 3);
let all: Vec<&str> = idx.iter().map(|(n, _)| n).collect();
assert_eq!(all, vec!["effacer", "écrire", "éditer"]);
let completions: Vec<&str> = idx.complete("é").map(|(n, _)| n).collect();
assert_eq!(completions, vec!["écrire", "éditer"]);
}
#[test]
fn iter_lexicographic_order() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
idx.bind(":wq", 2);
idx.bind(":q", 3);
idx.bind(":w", 1);
let all: Vec<(&str, &u8)> = idx.iter().collect();
assert_eq!(all, vec![(":q", &3), (":w", &1), (":wq", &2)]);
}
#[test]
fn iter_empty_index_is_empty() {
let idx: CommandIndex<u8> = CommandIndex::new();
assert_eq!(idx.iter().count(), 0);
}
#[test]
fn len_and_is_empty() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
assert!(idx.is_empty());
assert_eq!(idx.len(), 0);
idx.bind(":w", 1);
assert!(!idx.is_empty());
assert_eq!(idx.len(), 1);
}
#[test]
fn next_prefix_increments_last_byte() {
assert_eq!(next_prefix(":w"), Some(":x".to_string()));
}
#[test]
fn next_prefix_empty_returns_none() {
assert_eq!(next_prefix(""), None);
}
#[test]
fn next_prefix_all_max_bytes_returns_none() {
let raw_ff = std::str::from_utf8(&[0x7f]).unwrap(); assert_eq!(next_prefix(raw_ff), None);
}
#[test]
fn next_prefix_non_utf8_boundary_complete_returns_superset() {
let mut idx: CommandIndex<u8> = CommandIndex::new();
let del_a = std::str::from_utf8(&[0x7f, b'a']).unwrap(); let del_b = std::str::from_utf8(&[0x7f, b'b']).unwrap(); let unrelated = "zzzz";
idx.bind(del_a, 1);
idx.bind(del_b, 2);
idx.bind(unrelated, 3);
let prefix = std::str::from_utf8(&[0x7f]).unwrap();
let results: Vec<(&str, &u8)> = idx.complete(prefix).collect();
assert!(
results.iter().any(|(n, _)| *n == del_a),
"del_a must appear in superset: {results:?}"
);
assert!(
results.iter().any(|(n, _)| *n == del_b),
"del_b must appear in superset: {results:?}"
);
}
}