mirror of
https://github.com/TECHNOFAB11/pay-respects.git
synced 2025-12-12 06:20:09 +01:00
feat: all typo candidates for executables
This commit is contained in:
parent
0a799db5e5
commit
bf16675d4e
9 changed files with 173 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
||||
if suggest.contains("{{exes") {
|
||||
let (placeholder, args) = eval_placeholder(suggest, "{{exes", "}}");
|
||||
|
||||
let index = if suggest.contains('[') {
|
||||
let split = suggest[args.to_owned()]
|
||||
.split(&['[', ']'])
|
||||
.collect::<Vec<&str>>();
|
||||
let command_index = split[1];
|
||||
if !command_index.contains(':') {
|
||||
let command_index = command_index.parse::<i32>().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", "}}");
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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<String> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,50 @@ pub fn typo(suggest: &mut String, replace_list: &mut Vec<TokenStream2>) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exes(suggest: &mut String, exes_list: &mut Vec<TokenStream2>) {
|
||||
if suggest.contains("{{exes") {
|
||||
let (placeholder, args) = eval_placeholder(suggest, "{{exes", "}}");
|
||||
|
||||
let index = if suggest.contains('[') {
|
||||
let split = suggest[args.to_owned()]
|
||||
.split(&['[', ']'])
|
||||
.collect::<Vec<&str>>();
|
||||
let command_index = split[1];
|
||||
if !command_index.contains(':') {
|
||||
let command_index = command_index.parse::<i32>().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<String>) {
|
||||
while suggest.contains("{{shell") {
|
||||
let (placeholder, args) = eval_placeholder(suggest, "{{shell", "}}");
|
||||
|
|
|
|||
1
rules.md
1
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[<index>]}}`: 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::<Regular Expression>}}`: 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::<Regular Expression>}}`: Get the matching captures from the last command. Unlike `{{opt}}`, this won't remove the string after matching
|
||||
- `{{err::<Regular Expression}}`: Get the matching captures from the error message.
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ pattern = [
|
|||
]
|
||||
suggest = [
|
||||
'''
|
||||
{{typo[0](path)}} {{command[1:]}} '''
|
||||
{{exes[0]}} {{command[1:]}} '''
|
||||
]
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ pub fn best_match_path(typo: &str, executables: &[String]) -> Option<String> {
|
|||
find_similar(typo, executables, Some(3))
|
||||
}
|
||||
|
||||
pub fn best_matches_path(typo: &str, executables: &[String]) -> Option<Vec<String>> {
|
||||
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<usize>)
|
|||
None
|
||||
}
|
||||
|
||||
pub fn find_similars(typo: &str, candidates: &[String], threshold: Option<usize>) -> Option<Vec<String>> {
|
||||
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];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue