shape_runtime/type_system/
suggestions.rs1pub fn levenshtein_distance(a: &str, b: &str) -> usize {
11 let a_len = a.chars().count();
12 let b_len = b.chars().count();
13
14 if a_len == 0 {
16 return b_len;
17 }
18 if b_len == 0 {
19 return a_len;
20 }
21
22 let mut prev_row: Vec<usize> = (0..=b_len).collect();
24 let mut curr_row = vec![0; b_len + 1];
25
26 for (i, a_char) in a.chars().enumerate() {
27 curr_row[0] = i + 1;
28
29 for (j, b_char) in b.chars().enumerate() {
30 let cost = if a_char == b_char { 0 } else { 1 };
31 curr_row[j + 1] = (prev_row[j + 1] + 1) .min(curr_row[j] + 1) .min(prev_row[j] + cost); }
35
36 std::mem::swap(&mut prev_row, &mut curr_row);
37 }
38
39 prev_row[b_len]
40}
41
42pub fn find_similar<'a>(
47 candidates: impl IntoIterator<Item = &'a str>,
48 target: &str,
49 max_distance: usize,
50) -> Vec<&'a str> {
51 let mut results: Vec<(&str, usize)> = candidates
52 .into_iter()
53 .filter_map(|candidate| {
54 let distance = levenshtein_distance(candidate, target);
55 if distance <= max_distance && distance > 0 {
56 Some((candidate, distance))
57 } else {
58 None
59 }
60 })
61 .collect();
62
63 results.sort_by_key(|(_, d)| *d);
65
66 results.into_iter().map(|(s, _)| s).collect()
68}
69
70pub fn reasonable_max_distance(target: &str) -> usize {
74 let len = target.len();
75 if len <= 2 {
76 1
77 } else if len <= 5 {
78 2
79 } else {
80 3
81 }
82}
83
84pub fn format_suggestion(similar: &[&str]) -> Option<String> {
86 match similar.len() {
87 0 => None,
88 1 => Some(format!("Did you mean '{}'?", similar[0])),
89 2 => Some(format!(
90 "Did you mean '{}' or '{}'?",
91 similar[0], similar[1]
92 )),
93 _ => Some(format!(
94 "Did you mean '{}', '{}', or '{}'?",
95 similar[0], similar[1], similar[2]
96 )),
97 }
98}
99
100pub fn suggest_variable(
102 undefined_name: &str,
103 available_names: impl IntoIterator<Item = impl AsRef<str>>,
104) -> Option<String> {
105 let available: Vec<String> = available_names
106 .into_iter()
107 .map(|s| s.as_ref().to_string())
108 .collect();
109 let max_dist = reasonable_max_distance(undefined_name);
110 let similar = find_similar(
111 available.iter().map(|s| s.as_str()),
112 undefined_name,
113 max_dist,
114 );
115 format_suggestion(&similar)
116}
117
118pub fn suggest_function(
120 undefined_name: &str,
121 available_functions: impl IntoIterator<Item = impl AsRef<str>>,
122) -> Option<String> {
123 let available: Vec<String> = available_functions
124 .into_iter()
125 .map(|s| s.as_ref().to_string())
126 .collect();
127 let max_dist = reasonable_max_distance(undefined_name);
128 let similar = find_similar(
129 available.iter().map(|s| s.as_str()),
130 undefined_name,
131 max_dist,
132 );
133 format_suggestion(&similar)
134}
135
136pub fn suggest_property(
138 unknown_prop: &str,
139 available_props: impl IntoIterator<Item = impl AsRef<str>>,
140) -> Option<String> {
141 let available: Vec<String> = available_props
142 .into_iter()
143 .map(|s| s.as_ref().to_string())
144 .collect();
145 let max_dist = reasonable_max_distance(unknown_prop);
146 let similar = find_similar(available.iter().map(|s| s.as_str()), unknown_prop, max_dist);
147 format_suggestion(&similar)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_levenshtein_same_string() {
156 assert_eq!(levenshtein_distance("hello", "hello"), 0);
157 }
158
159 #[test]
160 fn test_levenshtein_empty_strings() {
161 assert_eq!(levenshtein_distance("", ""), 0);
162 assert_eq!(levenshtein_distance("hello", ""), 5);
163 assert_eq!(levenshtein_distance("", "world"), 5);
164 }
165
166 #[test]
167 fn test_levenshtein_single_edit() {
168 assert_eq!(levenshtein_distance("hello", "hallo"), 1);
170 assert_eq!(levenshtein_distance("hello", "helloo"), 1);
172 assert_eq!(levenshtein_distance("hello", "helo"), 1);
174 }
175
176 #[test]
177 fn test_levenshtein_multiple_edits() {
178 assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
179 assert_eq!(levenshtein_distance("saturday", "sunday"), 3);
180 }
181
182 #[test]
183 fn test_find_similar() {
184 let candidates = vec!["count", "counter", "amount", "account", "mount"];
185 let similar = find_similar(candidates.iter().copied(), "cont", 2);
186 assert!(similar.contains(&"count"));
187 }
188
189 #[test]
190 fn test_find_similar_no_matches() {
191 let candidates = vec!["apple", "banana", "cherry"];
192 let similar = find_similar(candidates.iter().copied(), "xyz", 2);
193 assert!(similar.is_empty());
194 }
195
196 #[test]
197 fn test_format_suggestion_single() {
198 let similar = vec!["count"];
199 assert_eq!(
200 format_suggestion(&similar),
201 Some("Did you mean 'count'?".to_string())
202 );
203 }
204
205 #[test]
206 fn test_format_suggestion_multiple() {
207 let similar = vec!["count", "counter"];
208 assert_eq!(
209 format_suggestion(&similar),
210 Some("Did you mean 'count' or 'counter'?".to_string())
211 );
212 }
213
214 #[test]
215 fn test_suggest_variable() {
216 let available = vec!["count", "counter", "total"];
217 let suggestion = suggest_variable("cont", available);
218 assert!(suggestion.is_some());
219 assert!(suggestion.unwrap().contains("count"));
220 }
221
222 #[test]
223 fn test_reasonable_max_distance() {
224 assert_eq!(reasonable_max_distance("a"), 1);
225 assert_eq!(reasonable_max_distance("ab"), 1);
226 assert_eq!(reasonable_max_distance("abc"), 2);
227 assert_eq!(reasonable_max_distance("hello"), 2);
228 assert_eq!(reasonable_max_distance("variable"), 3);
229 }
230}