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 |