1pub const TOP_LEVEL_COMMANDS: &[&str] = &[
2 "add", "apply", "clean", "confess", "drift", "explain", "fmt", "format", "help", "init",
3 "install", "list", "login", "manifest", "outdated", "package", "plan", "prayer", "publish",
4 "remove", "render", "repo", "serve", "sync", "tree", "trust", "unlock", "update", "upgrade",
5 "vendor", "verify", "version",
6];
7
8pub fn unknown_command_message(command: &str) -> String {
9 let mut message = format!("unknown command: {command}");
10 if let Some(suggestion) = suggest_command(command, TOP_LEVEL_COMMANDS) {
11 message.push_str(&format!("\nDid you mean `{suggestion}`?"));
12 }
13 message
14}
15
16pub fn suggest_command<'a>(input: &str, candidates: &'a [&str]) -> Option<&'a str> {
17 let maximum_distance = if input.chars().count() <= 3 { 1 } else { 2 };
18 candidates
19 .iter()
20 .copied()
21 .filter(|candidate| levenshtein_distance(input, candidate) <= maximum_distance)
22 .min_by_key(|candidate| levenshtein_distance(input, candidate))
23}
24
25fn levenshtein_distance(left: &str, right: &str) -> usize {
26 let left_chars: Vec<char> = left.chars().collect();
27 let right_chars: Vec<char> = right.chars().collect();
28 let left_length = left_chars.len();
29 let right_length = right_chars.len();
30
31 if left_length == 0 {
32 return right_length;
33 }
34 if right_length == 0 {
35 return left_length;
36 }
37
38 let mut previous_row: Vec<usize> = (0..=right_length).collect();
39 let mut current_row = vec![0; right_length + 1];
40
41 for (left_index, left_character) in left_chars.iter().enumerate() {
42 current_row[0] = left_index + 1;
43 for (right_index, right_character) in right_chars.iter().enumerate() {
44 let substitution_cost = if left_character == right_character {
45 0
46 } else {
47 1
48 };
49 current_row[right_index + 1] = (previous_row[right_index + 1] + 1)
50 .min(current_row[right_index] + 1)
51 .min(previous_row[right_index] + substitution_cost);
52 }
53 std::mem::swap(&mut previous_row, &mut current_row);
54 }
55
56 previous_row[right_length]
57}
58
59#[cfg(test)]
60mod tests {
61 use super::{suggest_command, unknown_command_message, TOP_LEVEL_COMMANDS};
62
63 #[test]
64 fn suggests_install_for_common_typo() {
65 assert_eq!(
66 suggest_command("instal", TOP_LEVEL_COMMANDS),
67 Some("install")
68 );
69 }
70
71 #[test]
72 fn unknown_command_includes_suggestion() {
73 let message = unknown_command_message("instal");
74 assert!(message.contains("unknown command: instal"));
75 assert!(message.contains("Did you mean `install`?"));
76 }
77}