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