reedline/menu/
menu_functions.rs

1//! Collection of common functions that can be used to create menus
2use crate::Suggestion;
3
4/// Index result obtained from parsing a string with an index marker
5/// For example, the next string:
6///     "this is an example :10"
7///
8/// Contains an index marker :10. This marker indicates that the user
9/// may want to select the 10th element from a list
10#[derive(Debug, PartialEq, Eq)]
11pub struct ParseResult<'buffer> {
12    /// Text before the marker
13    pub remainder: &'buffer str,
14    /// Parsed value from the marker
15    pub index: Option<usize>,
16    /// Marker representation as string
17    pub marker: Option<&'buffer str>,
18    /// Direction of the search based on the marker
19    pub action: ParseAction,
20}
21
22/// Direction of the index found in the string
23#[derive(Debug, PartialEq, Eq)]
24pub enum ParseAction {
25    /// Forward index search
26    ForwardSearch,
27    /// Backward index search
28    BackwardSearch,
29    /// Last token
30    LastToken,
31    /// Last executed command.
32    LastCommand,
33}
34
35/// Splits a string that contains a marker character
36///
37/// ## Example usage
38/// ```
39/// use reedline::menu_functions::{parse_selection_char, ParseAction, ParseResult};
40///
41/// let parsed = parse_selection_char("this is an example!10", '!');
42///
43/// assert_eq!(
44///     parsed,
45///     ParseResult {
46///         remainder: "this is an example",
47///         index: Some(10),
48///         marker: Some("!10"),
49///         action: ParseAction::ForwardSearch
50///     }
51/// )
52///
53/// ```
54pub 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
141/// Finds index for the common string in a list of suggestions
142pub 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
177/// Finds different string between two strings
178///
179/// ## Example usage
180/// ```
181/// use reedline::menu_functions::string_difference;
182///
183/// let new_string = "this is a new string";
184/// let old_string = "this is a string";
185///
186/// let res = string_difference(new_string, old_string);
187/// assert_eq!(res, (10, "new "));
188/// ```
189pub 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    // Convert char index to byte index
237    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}