pub(crate) fn nearest_builtin<'a, S: AsRef<str>>(
token: &str,
builtin_names: &'a [S],
) -> Option<&'a str> {
if token.is_empty() {
return None;
}
let token_chars: Vec<char> = token.chars().collect();
let length_scaled = token_chars.len().checked_div(3).unwrap_or(0);
let threshold = std::cmp::max(2, length_scaled);
let mut name_chars: Vec<char> = Vec::new();
let mut best: Option<(usize, &'a str)> = None;
for name in builtin_names {
let name: &'a str = name.as_ref();
name_chars.clear();
name_chars.extend(name.chars());
let dist = levenshtein_chars(&token_chars, &name_chars);
if dist == 0 || dist > threshold {
continue;
}
match best {
Some((best_dist, best_name))
if dist > best_dist || (dist == best_dist && name >= best_name) => {},
_ => best = Some((dist, name)),
}
}
best.map(|(_, name)| name)
}
fn levenshtein_chars(a: &[char], b: &[char]) -> usize {
if a.is_empty() {
return b.len();
}
if b.is_empty() {
return a.len();
}
let (outer, inner) = if a.len() >= b.len() { (a, b) } else { (b, a) };
let width = inner.len().saturating_add(1);
let mut prev: Vec<usize> = (0..width).collect();
let mut curr: Vec<usize> = vec![0; width];
for (i, &co) in outer.iter().enumerate() {
curr[0] = i.saturating_add(1);
for (j, &ci) in inner.iter().enumerate() {
let cost = usize::from(co != ci);
let deletion = curr[j].saturating_add(1);
let insertion = prev[j.saturating_add(1)].saturating_add(1);
let substitution = prev[j].saturating_add(cost);
curr[j.saturating_add(1)] = deletion.min(insertion).min(substitution);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[inner.len()]
}
#[cfg(test)]
mod tests {
use super::*;
const BUILTINS: &[&str] = &[
"chat",
"run",
"agent",
"group",
"caps",
"quota",
"invite",
"keypair",
"pair-device",
"secret",
"voucher",
"trust",
"audit",
"budget",
"session",
"capsule",
"mcp",
"distro",
"build",
"init",
"config",
"wit",
"gc",
"start",
"status",
"stop",
"restart",
"logs",
"ps",
"top",
"who",
"doctor",
"setup",
"version",
"completions",
"update",
"self-update",
];
#[test]
fn nearest_builtin_flags_obvious_builtin_typo() {
assert_eq!(nearest_builtin("statuss", BUILTINS), Some("status"));
assert_eq!(nearest_builtin("agnet", BUILTINS), Some("agent"));
}
#[test]
fn nearest_builtin_passes_through_real_capsule_verb() {
assert_eq!(nearest_builtin("identity-export", BUILTINS), None);
assert_eq!(nearest_builtin("models", BUILTINS), None);
}
#[test]
fn nearest_builtin_exact_builtin_is_not_a_suggestion() {
assert_eq!(nearest_builtin("status", BUILTINS), None);
}
#[test]
fn nearest_builtin_empty_token_is_not_a_suggestion() {
assert_eq!(nearest_builtin("", BUILTINS), None);
}
#[test]
fn levenshtein_is_symmetric() {
let pairs: &[(&str, &str)] = &[("status", "statuss"), ("agnet", "agent"), ("", "run")];
for (a, b) in pairs {
let ca: Vec<char> = a.chars().collect();
let cb: Vec<char> = b.chars().collect();
assert_eq!(
levenshtein_chars(&ca, &cb),
levenshtein_chars(&cb, &ca),
"levenshtein({a:?}, {b:?}) must equal levenshtein({b:?}, {a:?})"
);
}
}
#[test]
fn nearest_builtin_is_deterministic_on_ties() {
let tie: &[&str] = &["bb", "ac"];
assert_eq!(nearest_builtin("ab", tie), Some("ac"));
let tie_rev: &[&str] = &["ac", "bb"];
assert_eq!(nearest_builtin("ab", tie_rev), Some("ac"));
}
#[test]
fn builtins_slice_matches_clap_subcommands() {
use clap::CommandFactory;
use std::collections::BTreeSet;
let actual: BTreeSet<String> = crate::cli::Cli::command()
.get_subcommands()
.flat_map(|s| {
std::iter::once(s.get_name().to_string())
.chain(s.get_all_aliases().map(std::string::ToString::to_string))
})
.filter(|name| !name.is_empty())
.collect();
let slice: BTreeSet<String> = BUILTINS.iter().map(|s| (*s).to_string()).collect();
let missing: Vec<&String> = actual.difference(&slice).collect();
let extra: Vec<&String> = slice.difference(&actual).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"BUILTINS test slice drifted from clap's root subcommands.\n \
missing (in clap, absent from slice): {missing:?}\n \
extra (in slice, absent from clap): {extra:?}"
);
}
}