embedded_cli/
autocomplete.rs

1use crate::utils;
2
3#[derive(Clone, Debug)]
4#[non_exhaustive]
5pub enum Request<'a> {
6    /// Request to autocomplete given text to command name
7    CommandName(&'a str),
8}
9
10impl<'a> Request<'a> {
11    pub fn from_input(input: &'a str) -> Option<Self> {
12        let input = utils::trim_start(input);
13
14        if input.is_empty() {
15            return None;
16        }
17
18        // if no space given, then only command name is entered so we complete it
19        if !input.contains(' ') {
20            Some(Request::CommandName(input))
21        } else {
22            None
23        }
24    }
25}
26
27#[derive(Debug)]
28pub struct Autocompletion<'a> {
29    autocompleted: Option<usize>,
30    buffer: &'a mut [u8],
31    partial: bool,
32}
33
34impl<'a> Autocompletion<'a> {
35    pub fn new(buffer: &'a mut [u8]) -> Self {
36        Self {
37            autocompleted: None,
38            buffer,
39            partial: false,
40        }
41    }
42
43    pub fn autocompleted(&self) -> Option<&str> {
44        self.autocompleted.map(|len| {
45            // SAFETY: we store only &str in this buffer, so it is a valid utf-8 sequence
46            unsafe { core::str::from_utf8_unchecked(&self.buffer[..len]) }
47        })
48    }
49
50    /// Whether autocompletion is partial
51    /// and further input is required
52    pub fn is_partial(&self) -> bool {
53        self.partial
54    }
55
56    /// Mark this autocompletion as partial
57    pub fn mark_partial(&mut self) {
58        self.partial = true;
59    }
60
61    /// Merge this autocompletion with another one
62    pub fn merge_autocompletion(&mut self, autocompletion: &str) {
63        if autocompletion.is_empty() || self.buffer.is_empty() {
64            self.partial = self.partial
65                || self.autocompleted.is_some()
66                || (self.buffer.is_empty() && !autocompletion.is_empty());
67            self.autocompleted = Some(0);
68            return;
69        }
70
71        // compare new autocompletion to existing and keep
72        // only common prefix
73        let len = match self.autocompleted() {
74            Some(current) => utils::common_prefix_len(autocompletion, current),
75            None => autocompletion.len(),
76        };
77
78        if len > self.buffer.len() {
79            // if buffer is full with this autocompletion, there is not much sense in doing it
80            // since user will not be able to type anything else
81            // so just do nothing with it
82        } else {
83            self.partial =
84                self.partial || len < autocompletion.len() || self.autocompleted.is_some();
85            self.buffer[..len].copy_from_slice(&autocompletion.as_bytes()[..len]);
86            self.autocompleted = Some(len);
87        };
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use rstest::rstest;
94
95    use crate::autocomplete::Autocompletion;
96
97    #[test]
98    fn no_merge() {
99        let mut input = [0; 64];
100
101        let autocompletion = Autocompletion::new(&mut input);
102
103        assert!(!autocompletion.is_partial());
104        assert_eq!(autocompletion.autocompleted(), None);
105    }
106
107    #[rstest]
108    #[case("abc", "abc")]
109    #[case("", "")]
110    fn merge_single(#[case] text: &str, #[case] expected: &str) {
111        let mut input = [0; 64];
112
113        let mut autocompletion = Autocompletion::new(&mut input);
114
115        autocompletion.merge_autocompletion(text);
116
117        assert!(!autocompletion.is_partial());
118        assert_eq!(autocompletion.autocompleted(), Some(expected));
119        assert_eq!(&input[..expected.len()], expected.as_bytes());
120    }
121
122    #[rstest]
123    #[case("abc1", "abc2", "abc")]
124    #[case("ab", "abc", "ab")]
125    #[case("abc", "ab", "ab")]
126    #[case("", "ab", "")]
127    #[case("ab", "", "")]
128    #[case("abc", "def", "")]
129    fn merge_multiple(#[case] text1: &str, #[case] text2: &str, #[case] expected: &str) {
130        let mut input = [0; 64];
131
132        let mut autocompletion = Autocompletion::new(&mut input);
133
134        autocompletion.merge_autocompletion(text1);
135        autocompletion.merge_autocompletion(text2);
136
137        assert!(autocompletion.is_partial());
138        assert_eq!(autocompletion.autocompleted(), Some(expected));
139        assert_eq!(&input[..expected.len()], expected.as_bytes());
140    }
141}