1 | // Internal |
2 | use crate::builder::Command; |
3 | |
4 | /// Find strings from an iterable of `possible_values` similar to a given value `v` |
5 | /// Returns a Vec of all possible values that exceed a similarity threshold |
6 | /// sorted by ascending similarity, most similar comes last |
7 | #[cfg (feature = "suggestions" )] |
8 | pub(crate) fn did_you_mean<T, I>(v: &str, possible_values: I) -> Vec<String> |
9 | where |
10 | T: AsRef<str>, |
11 | I: IntoIterator<Item = T>, |
12 | { |
13 | use std::cmp::Ordering; |
14 | |
15 | let mut candidates: Vec<(f64, String)> = Vec::new(); |
16 | for pv in possible_values { |
17 | // GH #4660: using `jaro` because `jaro_winkler` implementation in `strsim-rs` is wrong |
18 | // causing strings with common prefix >=10 to be considered perfectly similar |
19 | let confidence = strsim::jaro(v, pv.as_ref()); |
20 | |
21 | if confidence > 0.7 { |
22 | let new_elem = (confidence, pv.as_ref().to_owned()); |
23 | let pos = candidates |
24 | .binary_search_by(|probe| { |
25 | if probe.0 > confidence { |
26 | Ordering::Greater |
27 | } else { |
28 | Ordering::Less |
29 | } |
30 | }) |
31 | .unwrap_or_else(|e| e); |
32 | candidates.insert(pos, new_elem); |
33 | } |
34 | } |
35 | |
36 | candidates.into_iter().map(|(_, pv)| pv).collect() |
37 | } |
38 | |
39 | #[cfg (not(feature = "suggestions" ))] |
40 | pub(crate) fn did_you_mean<T, I>(_: &str, _: I) -> Vec<String> |
41 | where |
42 | T: AsRef<str>, |
43 | I: IntoIterator<Item = T>, |
44 | { |
45 | Vec::new() |
46 | } |
47 | |
48 | /// Returns a suffix that can be empty, or is the standard 'did you mean' phrase |
49 | pub(crate) fn did_you_mean_flag<'a, 'help, I, T>( |
50 | arg: &str, |
51 | remaining_args: &[&std::ffi::OsStr], |
52 | longs: I, |
53 | subcommands: impl IntoIterator<Item = &'a mut Command>, |
54 | ) -> Option<(String, Option<String>)> |
55 | where |
56 | 'help: 'a, |
57 | T: AsRef<str>, |
58 | I: IntoIterator<Item = T>, |
59 | { |
60 | use crate::mkeymap::KeyType; |
61 | |
62 | match did_you_mean(arg, longs).pop() { |
63 | Some(candidate) => Some((candidate, None)), |
64 | None => subcommands |
65 | .into_iter() |
66 | .filter_map(|subcommand| { |
67 | subcommand._build_self(false); |
68 | |
69 | let longs = subcommand.get_keymap().keys().filter_map(|a| { |
70 | if let KeyType::Long(v) = a { |
71 | Some(v.to_string_lossy().into_owned()) |
72 | } else { |
73 | None |
74 | } |
75 | }); |
76 | |
77 | let subcommand_name = subcommand.get_name(); |
78 | |
79 | let candidate = some!(did_you_mean(arg, longs).pop()); |
80 | let score = some!(remaining_args.iter().position(|x| subcommand_name == *x)); |
81 | Some((score, (candidate, Some(subcommand_name.to_string())))) |
82 | }) |
83 | .min_by_key(|(x, _)| *x) |
84 | .map(|(_, suggestion)| suggestion), |
85 | } |
86 | } |
87 | |
88 | #[cfg (all(test, feature = "suggestions" ))] |
89 | mod test { |
90 | use super::*; |
91 | |
92 | #[test ] |
93 | fn missing_letter() { |
94 | let p_vals = ["test" , "possible" , "values" ]; |
95 | assert_eq!(did_you_mean("tst" , p_vals.iter()), vec!["test" ]); |
96 | } |
97 | |
98 | #[test ] |
99 | fn ambiguous() { |
100 | let p_vals = ["test" , "temp" , "possible" , "values" ]; |
101 | assert_eq!(did_you_mean("te" , p_vals.iter()), vec!["test" , "temp" ]); |
102 | } |
103 | |
104 | #[test ] |
105 | fn unrelated() { |
106 | let p_vals = ["test" , "possible" , "values" ]; |
107 | assert_eq!( |
108 | did_you_mean("hahaahahah" , p_vals.iter()), |
109 | Vec::<String>::new() |
110 | ); |
111 | } |
112 | |
113 | #[test ] |
114 | fn best_fit() { |
115 | let p_vals = [ |
116 | "test" , |
117 | "possible" , |
118 | "values" , |
119 | "alignmentStart" , |
120 | "alignmentScore" , |
121 | ]; |
122 | assert_eq!( |
123 | did_you_mean("alignmentScorr" , p_vals.iter()), |
124 | vec!["alignmentStart" , "alignmentScore" ] |
125 | ); |
126 | } |
127 | |
128 | #[test ] |
129 | fn best_fit_long_common_prefix_issue_4660() { |
130 | let p_vals = ["alignmentScore" , "alignmentStart" ]; |
131 | assert_eq!( |
132 | did_you_mean("alignmentScorr" , p_vals.iter()), |
133 | vec!["alignmentStart" , "alignmentScore" ] |
134 | ); |
135 | } |
136 | |
137 | #[test ] |
138 | fn flag_missing_letter() { |
139 | let p_vals = ["test" , "possible" , "values" ]; |
140 | assert_eq!( |
141 | did_you_mean_flag("tst" , &[], p_vals.iter(), []), |
142 | Some(("test" .to_owned(), None)) |
143 | ); |
144 | } |
145 | |
146 | #[test ] |
147 | fn flag_ambiguous() { |
148 | let p_vals = ["test" , "temp" , "possible" , "values" ]; |
149 | assert_eq!( |
150 | did_you_mean_flag("te" , &[], p_vals.iter(), []), |
151 | Some(("temp" .to_owned(), None)) |
152 | ); |
153 | } |
154 | |
155 | #[test ] |
156 | fn flag_unrelated() { |
157 | let p_vals = ["test" , "possible" , "values" ]; |
158 | assert_eq!( |
159 | did_you_mean_flag("hahaahahah" , &[], p_vals.iter(), []), |
160 | None |
161 | ); |
162 | } |
163 | |
164 | #[test ] |
165 | fn flag_best_fit() { |
166 | let p_vals = [ |
167 | "test" , |
168 | "possible" , |
169 | "values" , |
170 | "alignmentStart" , |
171 | "alignmentScore" , |
172 | ]; |
173 | assert_eq!( |
174 | did_you_mean_flag("alignmentScorr" , &[], p_vals.iter(), []), |
175 | Some(("alignmentScore" .to_owned(), None)) |
176 | ); |
177 | } |
178 | } |
179 | |