1use crate::Suggestion;
3
4#[derive(Debug, PartialEq, Eq)]
11pub struct ParseResult<'buffer> {
12 pub remainder: &'buffer str,
14 pub index: Option<usize>,
16 pub marker: Option<&'buffer str>,
18 pub action: ParseAction,
20}
21
22#[derive(Debug, PartialEq, Eq)]
24pub enum ParseAction {
25 ForwardSearch,
27 BackwardSearch,
29 LastToken,
31 LastCommand,
33}
34
35pub fn parse_selection_char(buffer: &str, marker: char) -> ParseResult {
55 if buffer.is_empty() {
56 return ParseResult {
57 remainder: buffer,
58 index: None,
59 marker: None,
60 action: ParseAction::ForwardSearch,
61 };
62 }
63
64 let mut input = buffer.chars().peekable();
65
66 let mut index = 0;
67 let mut action = ParseAction::ForwardSearch;
68 while let Some(char) = input.next() {
69 if char == marker {
70 match input.peek() {
71 #[cfg(feature = "bashisms")]
72 Some(&x) if x == marker => {
73 return ParseResult {
74 remainder: &buffer[0..index],
75 index: Some(0),
76 marker: Some(&buffer[index..index + 2 * marker.len_utf8()]),
77 action: ParseAction::LastCommand,
78 }
79 }
80 #[cfg(feature = "bashisms")]
81 Some(&x) if x == '$' => {
82 return ParseResult {
83 remainder: &buffer[0..index],
84 index: Some(0),
85 marker: Some(&buffer[index..index + 2]),
86 action: ParseAction::LastToken,
87 }
88 }
89 Some(&x) if x.is_ascii_digit() || x == '-' => {
90 let mut count: usize = 0;
91 let mut size: usize = marker.len_utf8();
92 while let Some(&c) = input.peek() {
93 if c == '-' {
94 let _ = input.next();
95 size += 1;
96 action = ParseAction::BackwardSearch;
97 } else if c.is_ascii_digit() {
98 let c = c.to_digit(10).expect("already checked if is a digit");
99 let _ = input.next();
100 count *= 10;
101 count += c as usize;
102 size += 1;
103 } else {
104 return ParseResult {
105 remainder: &buffer[0..index],
106 index: Some(count),
107 marker: Some(&buffer[index..index + size]),
108 action,
109 };
110 }
111 }
112 return ParseResult {
113 remainder: &buffer[0..index],
114 index: Some(count),
115 marker: Some(&buffer[index..index + size]),
116 action,
117 };
118 }
119 None => {
120 return ParseResult {
121 remainder: &buffer[0..index],
122 index: Some(0),
123 marker: Some(&buffer[index..buffer.len()]),
124 action,
125 }
126 }
127 _ => {}
128 }
129 }
130 index += char.len_utf8();
131 }
132
133 ParseResult {
134 remainder: buffer,
135 index: None,
136 marker: None,
137 action,
138 }
139}
140
141pub fn find_common_string(values: &[Suggestion]) -> (Option<&Suggestion>, Option<usize>) {
143 let first = values.iter().next();
144
145 let index = first.and_then(|first| {
146 values.iter().skip(1).fold(None, |index, suggestion| {
147 if suggestion.value.starts_with(&first.value) {
148 Some(first.value.len())
149 } else {
150 first
151 .value
152 .char_indices()
153 .zip(suggestion.value.char_indices())
154 .find(|((_, mut lhs), (_, mut rhs))| {
155 lhs.make_ascii_lowercase();
156 rhs.make_ascii_lowercase();
157
158 lhs != rhs
159 })
160 .map(|((new_index, _), _)| match index {
161 Some(index) => {
162 if index <= new_index {
163 index
164 } else {
165 new_index
166 }
167 }
168 None => new_index,
169 })
170 }
171 })
172 });
173
174 (first, index)
175}
176
177pub fn string_difference<'a>(new_string: &'a str, old_string: &str) -> (usize, &'a str) {
190 if old_string.is_empty() {
191 return (0, new_string);
192 }
193
194 let old_chars = old_string.char_indices().collect::<Vec<(usize, char)>>();
195 let new_chars = new_string.char_indices().collect::<Vec<(usize, char)>>();
196
197 let (_, start, end) = new_chars.iter().enumerate().fold(
198 (0, None, None),
199 |(old_char_index, start, end), (new_char_index, (_, c))| {
200 let equal = if start.is_some() {
201 if (old_chars.len() - old_char_index) == (new_chars.len() - new_char_index) {
202 let new_iter = new_chars.iter().skip(new_char_index);
203 let old_iter = old_chars.iter().skip(old_char_index);
204
205 new_iter
206 .zip(old_iter)
207 .all(|((_, new), (_, old))| new == old)
208 } else {
209 false
210 }
211 } else {
212 *c == old_chars[old_char_index].1
213 };
214
215 if equal {
216 let old_char_index = (old_char_index + 1).min(old_chars.len() - 1);
217
218 let end = match (start, end) {
219 (Some(_), Some(_)) => end,
220 (Some(_), None) => Some(new_char_index),
221 _ => None,
222 };
223
224 (old_char_index, start, end)
225 } else {
226 let start = match start {
227 Some(_) => start,
228 None => Some(new_char_index),
229 };
230
231 (old_char_index, start, end)
232 }
233 },
234 );
235
236 let start = start.map(|i| new_chars[i].0);
238 let end = end.map(|i| new_chars[i].0);
239
240 match (start, end) {
241 (Some(start), Some(end)) => (start, &new_string[start..end]),
242 (Some(start), None) => (start, &new_string[start..]),
243 (None, None) => (new_string.len(), ""),
244 (None, Some(_)) => unreachable!(),
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn parse_row_test() {
254 let input = "search:6";
255 let res = parse_selection_char(input, ':');
256
257 assert_eq!(res.remainder, "search");
258 assert_eq!(res.index, Some(6));
259 assert_eq!(res.marker, Some(":6"));
260 }
261
262 #[cfg(feature = "bashisms")]
263 #[test]
264 fn handles_multi_byte_char_as_marker_and_number() {
265 let buffer = "searchは6";
266 let parse_result = parse_selection_char(buffer, 'は');
267
268 assert_eq!(parse_result.remainder, "search");
269 assert_eq!(parse_result.index, Some(6));
270 assert_eq!(parse_result.marker, Some("は6"));
271 }
272
273 #[cfg(feature = "bashisms")]
274 #[test]
275 fn handles_multi_byte_char_as_double_marker() {
276 let buffer = "Testはは";
277 let parse_result = parse_selection_char(buffer, 'は');
278
279 assert_eq!(parse_result.remainder, "Test");
280 assert_eq!(parse_result.index, Some(0));
281 assert_eq!(parse_result.marker, Some("はは"));
282 assert!(matches!(parse_result.action, ParseAction::LastCommand));
283 }
284
285 #[cfg(feature = "bashisms")]
286 #[test]
287 fn handles_multi_byte_char_as_remainder() {
288 let buffer = "Testは!!";
289 let parse_result = parse_selection_char(buffer, '!');
290
291 assert_eq!(parse_result.remainder, "Testは");
292 assert_eq!(parse_result.index, Some(0));
293 assert_eq!(parse_result.marker, Some("!!"));
294 assert!(matches!(parse_result.action, ParseAction::LastCommand));
295 }
296
297 #[cfg(feature = "bashisms")]
298 #[test]
299 fn parse_double_char() {
300 let input = "search!!";
301 let res = parse_selection_char(input, '!');
302
303 assert_eq!(res.remainder, "search");
304 assert_eq!(res.index, Some(0));
305 assert_eq!(res.marker, Some("!!"));
306 assert!(matches!(res.action, ParseAction::LastCommand));
307 }
308
309 #[cfg(feature = "bashisms")]
310 #[test]
311 fn parse_last_token() {
312 let input = "!$";
313 let res = parse_selection_char(input, '!');
314
315 assert_eq!(res.remainder, "");
316 assert_eq!(res.index, Some(0));
317 assert_eq!(res.marker, Some("!$"));
318 assert!(matches!(res.action, ParseAction::LastToken));
319 }
320
321 #[test]
322 fn parse_row_other_marker_test() {
323 let input = "search?9";
324 let res = parse_selection_char(input, '?');
325
326 assert_eq!(res.remainder, "search");
327 assert_eq!(res.index, Some(9));
328 assert_eq!(res.marker, Some("?9"));
329 }
330
331 #[test]
332 fn parse_row_double_test() {
333 let input = "ls | where:16";
334 let res = parse_selection_char(input, ':');
335
336 assert_eq!(res.remainder, "ls | where");
337 assert_eq!(res.index, Some(16));
338 assert_eq!(res.marker, Some(":16"));
339 }
340
341 #[test]
342 fn parse_row_empty_test() {
343 let input = ":10";
344 let res = parse_selection_char(input, ':');
345
346 assert_eq!(res.remainder, "");
347 assert_eq!(res.index, Some(10));
348 assert_eq!(res.marker, Some(":10"));
349 }
350
351 #[test]
352 fn parse_row_fake_indicator_test() {
353 let input = "let a: another :10";
354 let res = parse_selection_char(input, ':');
355
356 assert_eq!(res.remainder, "let a: another ");
357 assert_eq!(res.index, Some(10));
358 assert_eq!(res.marker, Some(":10"));
359 }
360
361 #[test]
362 fn parse_row_no_number_test() {
363 let input = "let a: another:";
364 let res = parse_selection_char(input, ':');
365
366 assert_eq!(res.remainder, "let a: another");
367 assert_eq!(res.index, Some(0));
368 assert_eq!(res.marker, Some(":"));
369 }
370
371 #[test]
372 fn parse_empty_buffer_test() {
373 let input = "";
374 let res = parse_selection_char(input, ':');
375
376 assert_eq!(res.remainder, "");
377 assert_eq!(res.index, None);
378 assert_eq!(res.marker, None);
379 }
380
381 #[test]
382 fn parse_negative_direction() {
383 let input = "!-2";
384 let res = parse_selection_char(input, '!');
385
386 assert_eq!(res.remainder, "");
387 assert_eq!(res.index, Some(2));
388 assert_eq!(res.marker, Some("!-2"));
389 assert!(matches!(res.action, ParseAction::BackwardSearch));
390 }
391
392 #[test]
393 fn string_difference_test() {
394 let new_string = "this is a new string";
395 let old_string = "this is a string";
396
397 let res = string_difference(new_string, old_string);
398 assert_eq!(res, (10, "new "));
399 }
400
401 #[test]
402 fn string_difference_new_larger() {
403 let new_string = "this is a new string";
404 let old_string = "this is";
405
406 let res = string_difference(new_string, old_string);
407 assert_eq!(res, (7, " a new string"));
408 }
409
410 #[test]
411 fn string_difference_new_shorter() {
412 let new_string = "this is the";
413 let old_string = "this is the original";
414
415 let res = string_difference(new_string, old_string);
416 assert_eq!(res, (11, ""));
417 }
418
419 #[test]
420 fn string_difference_inserting() {
421 let new_string = "let a = (insert) | ";
422 let old_string = "let a = () | ";
423
424 let res = string_difference(new_string, old_string);
425 assert_eq!(res, (9, "insert"));
426 }
427
428 #[test]
429 fn string_difference_longer_string() {
430 let new_string = "this is a new another";
431 let old_string = "this is a string";
432
433 let res = string_difference(new_string, old_string);
434 assert_eq!(res, (10, "new another"));
435 }
436
437 #[test]
438 fn string_difference_start_same() {
439 let new_string = "this is a new something string";
440 let old_string = "this is a string";
441
442 let res = string_difference(new_string, old_string);
443 assert_eq!(res, (10, "new something "));
444 }
445
446 #[test]
447 fn string_difference_empty_old() {
448 let new_string = "this new another";
449 let old_string = "";
450
451 let res = string_difference(new_string, old_string);
452 assert_eq!(res, (0, "this new another"));
453 }
454
455 #[test]
456 fn string_difference_very_difference() {
457 let new_string = "this new another";
458 let old_string = "complete different string";
459
460 let res = string_difference(new_string, old_string);
461 assert_eq!(res, (0, "this new another"));
462 }
463
464 #[test]
465 fn string_difference_both_equal() {
466 let new_string = "this new another";
467 let old_string = "this new another";
468
469 let res = string_difference(new_string, old_string);
470 assert_eq!(res, (16, ""));
471 }
472
473 #[test]
474 fn string_difference_with_non_ansi() {
475 let new_string = "nushell";
476 let old_string = "null";
477
478 let res = string_difference(new_string, old_string);
479 assert_eq!(res, (6, "she"));
480 }
481
482 #[test]
483 fn find_common_string_with_ansi() {
484 use crate::Span;
485
486 let input: Vec<_> = ["nushell", "null"]
487 .into_iter()
488 .map(|s| Suggestion {
489 value: s.into(),
490 description: None,
491 extra: None,
492 span: Span::new(0, s.len()),
493 append_whitespace: false,
494 })
495 .collect();
496 let res = find_common_string(&input);
497
498 assert!(matches!(res, (Some(elem), Some(2)) if elem == &input[0]));
499 }
500
501 #[test]
502 fn find_common_string_with_non_ansi() {
503 use crate::Span;
504
505 let input: Vec<_> = ["nushell", "null"]
506 .into_iter()
507 .map(|s| Suggestion {
508 value: s.into(),
509 description: None,
510 extra: None,
511 span: Span::new(0, s.len()),
512 append_whitespace: false,
513 })
514 .collect();
515 let res = find_common_string(&input);
516
517 assert!(matches!(res, (Some(elem), Some(6)) if elem == &input[0]));
518 }
519}