1use crate::{
2 item::ShortLong,
3 meta_help::{HelpItem, HelpItems},
4 Meta, State,
5};
6
7#[derive(Debug, Copy, Clone)]
8pub(crate) enum Variant {
9 CommandLong(&'static str),
10 Flag(ShortLong),
11}
12
13#[derive(Debug)]
14pub(crate) enum Suggestion {
15 Variant(Variant),
16 MissingDash(&'static str),
18 ExtraDash(char),
20 Nested(String, Variant),
21}
22
23#[inline(never)]
25pub(crate) fn suggest(args: &State, meta: &Meta) -> Option<(usize, Suggestion)> {
26 let (ix, arg) = args.items_iter().next()?;
27
28 if arg.os_str().is_empty() {
30 return None;
31 }
32 if matches!(arg, crate::args::Arg::PosWord(_)) {
34 return None;
35 }
36 let actual = arg.to_string();
38
39 let mut hi = HelpItems::default();
41 hi.append_meta(meta);
42
43 let mut nested = HelpItems::default();
45
46 let mut best_match = None;
48 let mut best_dist = usize::MAX;
49 let mut improve = |dist, val| {
50 if best_dist > dist && dist > 0 && dist < 4 {
51 best_dist = dist;
52 best_match = Some(val);
53 }
54 };
55
56 let mut nest = None;
57
58 for item in &hi.items {
59 match item {
60 HelpItem::Command { name, meta, .. } => {
61 let distance = damerau_levenshtein(&actual, name);
65 improve(distance, Variant::CommandLong(name));
66
67 nested.items.clear();
69 nested.append_meta(meta);
70 for item in &nested.items {
71 match item {
72 HelpItem::Command { name: nname, .. } => {
73 if *nname == actual {
74 nest = Some((name, Variant::CommandLong(nname)));
75 }
76 }
77 HelpItem::Flag { name: nname, .. }
78 | HelpItem::Argument { name: nname, .. } => {
79 if *nname == &actual {
80 nest = Some((name, Variant::Flag(*nname)));
81 }
82 }
83 HelpItem::DecorSuffix { .. }
84 | HelpItem::GroupStart { .. }
85 | HelpItem::GroupEnd { .. }
86 | HelpItem::Positional { .. }
87 | HelpItem::AnywhereStart { .. }
88 | HelpItem::AnywhereStop { .. }
89 | HelpItem::Any { .. } => {}
90 }
91 }
92 }
93 HelpItem::Flag { name, .. } | HelpItem::Argument { name, .. } => {
94 if let Some(long) = name.as_long() {
95 let distance = damerau_levenshtein(&actual, &format!("--{}", long));
96 improve(distance, Variant::Flag(*name));
97 }
98 if let Some(short) = name.as_short() {
99 if let Some(act) = actual.strip_prefix("--") {
100 let mut tmp = [0u8; 4];
101 if act == short.encode_utf8(&mut tmp) {
102 return Some((ix, Suggestion::ExtraDash(short)));
103 }
104 }
105 }
106 }
107 HelpItem::Positional { .. }
108 | HelpItem::DecorSuffix { .. }
109 | HelpItem::GroupStart { .. }
110 | HelpItem::GroupEnd { .. }
111 | HelpItem::AnywhereStart { .. }
112 | HelpItem::AnywhereStop { .. }
113 | HelpItem::Any { .. } => {}
114 }
115 }
116
117 if let Some((&name, variant)) = nest {
118 Some((ix, Suggestion::Nested(name.to_string(), variant)))
119 } else {
120 if best_dist == usize::MAX {
122 return None;
123 }
124 let best_match = best_match?;
125
126 if let Variant::Flag(n) = best_match {
128 if let Some(long) = n.as_long() {
129 if actual.strip_prefix('-') == Some(long) {
130 return Some((ix, Suggestion::MissingDash(long)));
131 }
132 }
133 }
134 Some((ix, Suggestion::Variant(best_match)))
135 }
136}
137
138fn damerau_levenshtein(a: &str, b: &str) -> usize {
144 #![allow(clippy::many_single_char_names)]
145 let a_len = a.chars().count();
146 let b_len = b.chars().count();
147 let mut d = vec![0; (a_len + 1) * (b_len + 1)];
148
149 let ix = |ib, ia| a_len * ia + ib;
150
151 for i in 0..=a_len {
152 d[ix(i, 0)] = i;
153 }
154
155 for j in 0..=b_len {
156 d[ix(0, j)] = j;
157 }
158
159 let mut pa = '\0';
160 let mut pb = '\0';
161 for (i, ca) in a.chars().enumerate() {
162 let i = i + 1;
163 for (j, cb) in b.chars().enumerate() {
164 let j = j + 1;
165 let cost = usize::from(ca != cb);
166 d[ix(i, j)] = (d[ix(i - 1, j)] + 1)
167 .min(d[ix(i, j - 1)] + 1)
168 .min(d[ix(i - 1, j - 1)] + cost);
169 if i > 1 && j > 1 && ca == pb && cb == pa {
170 d[ix(i, j)] = d[ix(i, j)].min(d[ix(i - 2, j - 2)] + 1);
171 }
172 pb = cb;
173 }
174 pa = ca;
175 }
176
177 let diff = d[ix(a_len, b_len)];
178
179 if diff >= a_len.min(b_len) {
180 usize::MAX
181 } else {
182 diff
183 }
184}