bmux_plugin_cli_plugin 0.0.1-alpha.1

Shipped plugin CLI plugin for bmux
pub fn suggest_top_matches<'a>(
    target: &str,
    candidates: impl IntoIterator<Item = &'a str>,
    limit: usize,
) -> Vec<String> {
    if limit == 0 {
        return Vec::new();
    }

    let candidate_list = candidates.into_iter().collect::<Vec<_>>();
    if candidate_list.is_empty() {
        return Vec::new();
    }

    let lower_target = target.to_ascii_lowercase();
    let max_distance = lower_target.chars().count().max(3) / 2 + 1;

    let mut ranked = candidate_list
        .iter()
        .map(|candidate| {
            let lower_candidate = candidate.to_ascii_lowercase();
            let distance = levenshtein(&lower_target, &lower_candidate);
            let prefix_match = lower_candidate.starts_with(&lower_target)
                || lower_target.starts_with(&lower_candidate);
            (distance, !prefix_match, *candidate)
        })
        .filter(|(distance, prefix_penalty, _)| *distance <= max_distance || !*prefix_penalty)
        .collect::<Vec<_>>();

    ranked.sort_unstable();
    ranked
        .into_iter()
        .map(|(_, _, candidate)| candidate.to_string())
        .take(limit)
        .collect()
}

fn levenshtein(left: &str, right: &str) -> usize {
    let left_chars = left.chars().collect::<Vec<_>>();
    let right_chars = right.chars().collect::<Vec<_>>();
    if left_chars.is_empty() {
        return right_chars.len();
    }
    if right_chars.is_empty() {
        return left_chars.len();
    }

    let mut prev = (0..=right_chars.len()).collect::<Vec<_>>();
    let mut curr = vec![0; right_chars.len() + 1];
    for (i, l) in left_chars.iter().enumerate() {
        curr[0] = i + 1;
        for (j, r) in right_chars.iter().enumerate() {
            let cost = usize::from(l != r);
            curr[j + 1] = (curr[j] + 1).min(prev[j + 1] + 1).min(prev[j] + cost);
        }
        std::mem::swap(&mut prev, &mut curr);
    }
    prev[right_chars.len()]
}

#[cfg(test)]
mod tests {
    use super::suggest_top_matches;

    #[test]
    fn suggest_top_matches_limits_and_filters_results() {
        let candidates = ["bmux.plugin_cli", "bmux.permissions", "bmux.windows"];
        let matches = suggest_top_matches("bmux.plugin", candidates.iter().copied(), 2);
        assert!(!matches.is_empty());
        assert_eq!(matches[0], "bmux.plugin_cli");
    }
}