pray-core 1.9.1

Core library for Prayfile, the package manager for the language placed before inference
Documentation
pub const TOP_LEVEL_COMMANDS: &[&str] = &[
    "add",
    "apply",
    "clean",
    "completion",
    "confess",
    "drift",
    "explain",
    "fmt",
    "format",
    "help",
    "init",
    "install",
    "list",
    "login",
    "manifest",
    "outdated",
    "package",
    "plan",
    "prayer",
    "publish",
    "remove",
    "render",
    "repo",
    "search",
    "serve",
    "sync",
    "token",
    "tree",
    "trust",
    "unlock",
    "update",
    "upgrade",
    "vendor",
    "verify",
    "version",
    "yank",
];

pub fn unknown_command_message(command: &str) -> String {
    let mut message = format!("unknown command: {command}");
    if let Some(suggestion) = suggest_command(command, TOP_LEVEL_COMMANDS) {
        message.push_str(&format!("\nDid you mean `{suggestion}`?"));
    }
    message.push_str("\nSee 'pray --help'.");
    message
}

pub fn suggest_command<'a>(input: &str, candidates: &'a [&str]) -> Option<&'a str> {
    let maximum_distance = if input.chars().count() <= 3 { 1 } else { 2 };
    candidates
        .iter()
        .copied()
        .filter(|candidate| levenshtein_distance(input, candidate) <= maximum_distance)
        .min_by_key(|candidate| levenshtein_distance(input, candidate))
}

fn levenshtein_distance(left: &str, right: &str) -> usize {
    let left_chars: Vec<char> = left.chars().collect();
    let right_chars: Vec<char> = right.chars().collect();
    let left_length = left_chars.len();
    let right_length = right_chars.len();

    if left_length == 0 {
        return right_length;
    }
    if right_length == 0 {
        return left_length;
    }

    let mut previous_row: Vec<usize> = (0..=right_length).collect();
    let mut current_row = vec![0; right_length + 1];

    for (left_index, left_character) in left_chars.iter().enumerate() {
        current_row[0] = left_index + 1;
        for (right_index, right_character) in right_chars.iter().enumerate() {
            let substitution_cost = if left_character == right_character {
                0
            } else {
                1
            };
            current_row[right_index + 1] = (previous_row[right_index + 1] + 1)
                .min(current_row[right_index] + 1)
                .min(previous_row[right_index] + substitution_cost);
        }
        std::mem::swap(&mut previous_row, &mut current_row);
    }

    previous_row[right_length]
}

#[cfg(test)]
mod tests {
    use super::{suggest_command, unknown_command_message, TOP_LEVEL_COMMANDS};

    #[test]
    fn suggests_install_for_common_typo() {
        assert_eq!(
            suggest_command("instal", TOP_LEVEL_COMMANDS),
            Some("install")
        );
    }

    #[test]
    fn unknown_command_includes_suggestion() {
        let message = unknown_command_message("instal");
        assert!(message.contains("unknown command: instal"));
        assert!(message.contains("Did you mean `install`?"));
        assert!(message.contains("See 'pray --help'."));
    }
}