From bf16675d4e5e8443d5474b018c16c71b032521c3 Mon Sep 17 00:00:00 2001 From: iff Date: Mon, 6 Jan 2025 14:11:42 +0100 Subject: [PATCH] feat: all typo candidates for executables --- CHANGELOG.md | 4 +++ core/src/modes.rs | 26 +++++++++------- module-runtime-rules/src/replaces.rs | 43 +++++++++++++++++++++++++++ module-runtime-rules/src/rules.rs | 26 +++++++++++----- parser/src/lib.rs | 19 +++++++++++- parser/src/replaces.rs | 44 ++++++++++++++++++++++++++++ rules.md | 1 + rules/pr_general.toml | 2 +- utils/src/evals.rs | 27 +++++++++++++++++ 9 files changed, 173 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c81ad..0c3e19f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Include all candidates with the same distances for executable typos + ### Changed - Running standard modules in a separated thread diff --git a/core/src/modes.rs b/core/src/modes.rs index 82e740a..e957319 100644 --- a/core/src/modes.rs +++ b/core/src/modes.rs @@ -4,7 +4,7 @@ use crate::system; use crate::{shell, suggestions}; use colored::Colorize; use inquire::*; -use pay_respects_utils::evals::best_match_path; +use pay_respects_utils::evals::best_matches_path; use pay_respects_utils::files::best_match_file; use std::path::Path; @@ -66,23 +66,29 @@ pub fn cnf(data: &mut Data) { executable ); - let best_match = { + let best_matches = { if executable.contains(std::path::MAIN_SEPARATOR) { - best_match_file(executable) + let file = best_match_file(executable); + if file.is_some() { + Some(vec![file.unwrap()]) + } else { + None + } } else { - best_match_path(executable, &data.executables) + best_matches_path(executable, &data.executables) } }; - if best_match.is_some() { - let best_match = best_match.unwrap(); - split_command[0] = best_match; - let suggest = split_command.join(" "); - - data.candidates.push(suggest.clone()); + if let Some(best_matches) = best_matches { + for best_match in best_matches { + split_command[0] = best_match; + let suggest = split_command.join(" "); + data.candidates.push(suggest); + } suggestions::select_candidate(data); let status = suggestions::confirm_suggestion(data); if status.is_err() { + let suggest = data.suggest.clone().unwrap(); data.update_command(&suggest); let msg = Some( status diff --git a/module-runtime-rules/src/replaces.rs b/module-runtime-rules/src/replaces.rs index 90952ec..e1e0f79 100644 --- a/module-runtime-rules/src/replaces.rs +++ b/module-runtime-rules/src/replaces.rs @@ -173,6 +173,49 @@ pub fn typo(suggest: &mut String, split_command: &[String], executables: &[Strin } } +pub fn exes(suggest: &mut String, split_command: &[String], executables: &[String], exes_list: &mut Vec) { + if suggest.contains("{{exes") { + let (placeholder, args) = eval_placeholder(suggest, "{{exes", "}}"); + + let index = if suggest.contains('[') { + let split = suggest[args.to_owned()] + .split(&['[', ']']) + .collect::>(); + let command_index = split[1]; + if !command_index.contains(':') { + let command_index = command_index.parse::().unwrap(); + + let index = if command_index < 0 { + split_command.len() as i32 + command_index + } else { + command_index + }; + index as usize + } else { + unreachable!("Exes suggestion does not support range"); + } + } else { + unreachable!("Exes suggestion must have a command index"); + }; + + let matches = { + let res = best_matches_path(&split_command[index], executables); + if res.is_none() { + vec![split_command[index].clone()] + } else { + res.unwrap() + } + }; + for match_ in matches { + exes_list.push(match_); + } + + let tag = "{{exes}}"; + let placeholder = suggest[placeholder.clone()].to_owned(); + *suggest = suggest.replace(&placeholder, &tag); + } +} + pub fn shell(suggest: &mut String, shell: &str) { while suggest.contains("{{shell") { let (placeholder, args) = eval_placeholder(suggest, "{{shell", "}}"); diff --git a/module-runtime-rules/src/rules.rs b/module-runtime-rules/src/rules.rs index d85feda..94a6b5f 100644 --- a/module-runtime-rules/src/rules.rs +++ b/module-runtime-rules/src/rules.rs @@ -83,11 +83,11 @@ pub fn runtime_match( if pure_suggest.contains("{{command}}") { pure_suggest = pure_suggest.replace("{{command}}", last_command); } - print!( - "{}", - eval_suggest(&pure_suggest, last_command, error_msg, executables, shell,) - ); - print!("<_PR_BR>"); + let suggests = eval_suggest(&pure_suggest, last_command, error_msg, executables, shell); + for suggest in suggests { + print!("{}", suggest); + print!("<_PR_BR>"); + } } } } @@ -121,7 +121,7 @@ fn eval_suggest( error_msg: &str, executables: &[String], shell: &str, -) -> String { +) -> Vec { let mut suggest = suggest.to_owned(); if suggest.contains("{{command}}") { suggest = suggest.replace("{{command}}", "{last_command}"); @@ -139,11 +139,23 @@ fn eval_suggest( replaces::shell(&mut suggest, shell); replaces::typo(&mut suggest, &split_command, executables, shell); + let mut exes_list = Vec::new(); + replaces::exes(&mut suggest, &split_command, executables, &mut exes_list); + for (tag, value) in opt_list { suggest = suggest.replace(&tag, &value); } - suggest + let mut suggests = vec![]; + if exes_list.is_empty() { + suggests.push(suggest); + } else { + for exe in exes_list { + let eval_suggest = suggest.clone().replace("{{exes}}", &exe); + suggests.push(eval_suggest); + } + } + suggests } fn get_rule(executable: &str) -> Option { diff --git a/parser/src/lib.rs b/parser/src/lib.rs index a38cced..148c06d 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -188,6 +188,7 @@ fn eval_suggest(suggest: &str) -> TokenStream2 { } let mut replace_list = Vec::new(); + let mut exes_list = Vec::new(); let mut opt_list = Vec::new(); let mut cmd_list = Vec::new(); @@ -197,10 +198,26 @@ fn eval_suggest(suggest: &str) -> TokenStream2 { replaces::command(&mut suggest, &mut replace_list); replaces::shell(&mut suggest, &mut cmd_list); replaces::typo(&mut suggest, &mut replace_list); + replaces::exes(&mut suggest, &mut exes_list); replaces::shell_tag(&mut suggest, &mut replace_list, &cmd_list); + let suggests = if exes_list.is_empty() { + quote! { + candidates.push(format!{#suggest, #(#replace_list),*}); + } + } else { + quote! { + #(#exes_list)* + let suggest = format!{#suggest, #(#replace_list),*}; + for match_ in exes_matches { + let suggest = suggest.replace("{{exes}}", &match_); + candidates.push(suggest); + } + } + }; + quote! { #(#opt_list)* - candidates.push(format!{#suggest, #(#replace_list),*}); + #suggests } } diff --git a/parser/src/replaces.rs b/parser/src/replaces.rs index e77de91..a4750c6 100644 --- a/parser/src/replaces.rs +++ b/parser/src/replaces.rs @@ -230,6 +230,50 @@ pub fn typo(suggest: &mut String, replace_list: &mut Vec) { } } +pub fn exes(suggest: &mut String, exes_list: &mut Vec) { + if suggest.contains("{{exes") { + let (placeholder, args) = eval_placeholder(suggest, "{{exes", "}}"); + + let index = if suggest.contains('[') { + let split = suggest[args.to_owned()] + .split(&['[', ']']) + .collect::>(); + let command_index = split[1]; + if !command_index.contains(':') { + let command_index = command_index.parse::().unwrap(); + + if command_index < 0 { + quote! {split.len() as usize + #command_index} + } else { + quote! {#command_index as usize} + } + } else { + unreachable!("Exes suggestion does not support range"); + + } + } else { + unreachable!("Exes suggestion must have a command index"); + }; + + let command = quote! { + let exes_matches = { + let split = split_command(&last_command); + let res = best_matches_path(&split[#index], executables); + if res.is_none() { + vec![split[#index].clone()] + } else { + res.unwrap() + } + }; + }; + exes_list.push(command); + + let tag = "{{{{exes}}}}"; + let placeholder = suggest[placeholder.clone()].to_owned(); + *suggest = suggest.replace(&placeholder, &tag); + } +} + pub fn shell(suggest: &mut String, cmd_list: &mut Vec) { while suggest.contains("{{shell") { let (placeholder, args) = eval_placeholder(suggest, "{{shell", "}}"); diff --git a/rules.md b/rules.md index 0a4940f..8f71a12 100644 --- a/rules.md +++ b/rules.md @@ -57,6 +57,7 @@ The placeholder is evaluated as following: - `{{command[1]}}`: The first argument of the command (the command itself has index of 0). Negative values will count from reverse. - `{{command[2:5]}}`: The second to fifth arguments. If any of the side is not specified, then it defaults to the start (if it is left) or the end (if it is right). - `{{typo[2](fix1, fix2)}}`: This will try to change the second argument to candidates in the parenthesis. The argument in parentheses must have at least 2 values. Single arguments are reserved for specific matches, for instance, `path` to search all commands found in the `$PATH` environment, or the `{{shell}}` placeholder, among others. + - `{{exes[]}}`: Special case for executables, will create multiple suggestions for each match with the same linguistic distance. Currently, only can appear once to avoid recursions. - `{{opt::}}`: Optional patterns captured in the command with RegEx ([see regex crate for syntax](https://docs.rs/regex-lite/latest/regex_lite/#syntax)). Note that all patterns matching this placeholder will be removed from indexing. - `{{cmd::}}`: Get the matching captures from the last command. Unlike `{{opt}}`, this won't remove the string after matching - `{{err:: Option { find_similar(typo, executables, Some(3)) } +pub fn best_matches_path(typo: &str, executables: &[String]) -> Option> { + find_similars(typo, executables, Some(3)) +} + // higher the threshold, the stricter the comparison // 1: anything // 2: 50% @@ -143,6 +147,29 @@ pub fn find_similar(typo: &str, candidates: &[String], threshold: Option) None } +pub fn find_similars(typo: &str, candidates: &[String], threshold: Option) -> Option> { + let threshold = threshold.unwrap_or(2); + let mut min_distance = typo.chars().count() / threshold + 1; + let mut min_distance_index = vec![]; + for (i, candidate) in candidates.iter().enumerate() { + if candidate.is_empty() { + continue; + } + let distance = compare_string(typo, candidate); + if distance == min_distance { + min_distance_index.push(i); + } else if distance < min_distance { + min_distance = distance; + min_distance_index.clear(); + min_distance_index.push(i); + } + } + if !min_distance_index.is_empty() { + return Some(min_distance_index.iter().map(|&i| candidates[i].to_string()).collect()); + } + None +} + #[allow(clippy::needless_range_loop)] pub fn compare_string(a: &str, b: &str) -> usize { let mut matrix = vec![vec![0; b.chars().count() + 1]; a.chars().count() + 1];