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