Skip to main content

pray_core/
cli_suggest.rs

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}