reedline/completion/
default.rs

1use crate::{Completer, Span, Suggestion};
2use std::{
3    collections::{BTreeMap, BTreeSet},
4    str::Chars,
5    sync::Arc,
6};
7
8/// A default completer that can detect keywords
9///
10/// # Example
11///
12/// ```rust
13/// use reedline::{DefaultCompleter, Reedline};
14///
15/// let commands = vec![
16///  "test".into(),
17///  "hello world".into(),
18///  "hello world reedline".into(),
19///  "this is the reedline crate".into(),
20/// ];
21/// let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2));
22///
23/// let mut line_editor = Reedline::create().with_completer(completer);
24/// ```
25#[derive(Debug, Clone)]
26pub struct DefaultCompleter {
27    root: CompletionNode,
28    min_word_len: usize,
29}
30
31impl Default for DefaultCompleter {
32    fn default() -> Self {
33        let inclusions = Arc::new(BTreeSet::new());
34        Self {
35            root: CompletionNode::new(inclusions),
36            min_word_len: 2,
37        }
38    }
39}
40impl Completer for DefaultCompleter {
41    /// Returns a vector of completions and the position in which they must be replaced;
42    /// based on the provided input.
43    ///
44    /// # Arguments
45    ///
46    /// * `line`    The line to complete
47    /// * `pos`   The cursor position
48    ///
49    /// # Example
50    /// ```
51    /// use reedline::{DefaultCompleter,Completer,Span,Suggestion};
52    ///
53    /// let mut completions = DefaultCompleter::default();
54    /// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
55    /// assert_eq!(
56    ///     completions.complete("bat",3),
57    ///     vec![
58    ///         Suggestion {value: "batcave".into(), description: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
59    ///         Suggestion {value: "batman".into(), description: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
60    ///         Suggestion {value: "batmobile".into(), description: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
61    ///     ]);
62    ///
63    /// assert_eq!(
64    ///     completions.complete("to the bat",10),
65    ///     vec![
66    ///         Suggestion {value: "batcave".into(), description: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false},
67    ///         Suggestion {value: "batman".into(), description: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false},
68    ///         Suggestion {value: "batmobile".into(), description: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false},
69    ///     ]);
70    /// ```
71    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
72        let mut span_line_whitespaces = 0;
73        let mut completions = vec![];
74        if !line.is_empty() {
75            let mut split = line[0..pos].split(' ').rev();
76            let mut span_line: String = String::new();
77            for _ in 0..split.clone().count() {
78                if let Some(s) = split.next() {
79                    if s.is_empty() {
80                        span_line_whitespaces += 1;
81                        continue;
82                    }
83                    if span_line.is_empty() {
84                        span_line = s.to_string();
85                    } else {
86                        span_line = format!("{s} {span_line}");
87                    }
88                    if let Some(mut extensions) = self.root.complete(span_line.chars()) {
89                        extensions.sort();
90                        completions.extend(
91                            extensions
92                                .iter()
93                                .map(|ext| {
94                                    let span = Span::new(
95                                        pos - span_line.len() - span_line_whitespaces,
96                                        pos,
97                                    );
98
99                                    Suggestion {
100                                        value: format!("{span_line}{ext}"),
101                                        description: None,
102                                        extra: None,
103                                        span,
104                                        append_whitespace: false,
105                                    }
106                                })
107                                .filter(|t| t.value.len() > (t.span.end - t.span.start))
108                                .collect::<Vec<Suggestion>>(),
109                        );
110                    }
111                }
112            }
113        }
114        completions.dedup();
115        completions
116    }
117}
118impl DefaultCompleter {
119    /// Construct the default completer with a list of commands/keywords to highlight
120    pub fn new(external_commands: Vec<String>) -> Self {
121        let mut dc = DefaultCompleter::default();
122        dc.insert(external_commands);
123        dc
124    }
125
126    /// Construct the default completer with a list of commands/keywords to highlight, given a minimum word length
127    pub fn new_with_wordlen(external_commands: Vec<String>, min_word_len: usize) -> Self {
128        let mut dc = DefaultCompleter::default().set_min_word_len(min_word_len);
129        dc.insert(external_commands);
130        dc
131    }
132
133    /// Insert `external_commands` list in the object root
134    ///
135    /// # Arguments
136    ///
137    /// * `line`    A vector of `String` containing the external commands
138    ///
139    /// # Example
140    /// ```
141    /// use reedline::{DefaultCompleter,Completer};
142    ///
143    /// let mut completions = DefaultCompleter::default();
144    ///
145    /// // Insert multiple words
146    /// completions.insert(vec!["a","line","with","many","words"].iter().map(|s| s.to_string()).collect());
147    ///
148    /// // The above line is equal to the following:
149    /// completions.insert(vec!["a","line","with"].iter().map(|s| s.to_string()).collect());
150    /// completions.insert(vec!["many","words"].iter().map(|s| s.to_string()).collect());
151    /// ```
152    pub fn insert(&mut self, words: Vec<String>) {
153        for word in words {
154            if word.len() >= self.min_word_len {
155                self.root.insert(word.chars());
156            }
157        }
158    }
159
160    /// Create a new `DefaultCompleter` with provided non alphabet characters whitelisted.
161    /// The default `DefaultCompleter` will only parse alphabet characters (a-z, A-Z). Use this to
162    /// introduce additional accepted special characters.
163    ///
164    /// # Arguments
165    ///
166    /// * `incl`    An array slice with allowed characters
167    ///
168    /// # Example
169    /// ```
170    /// use reedline::{DefaultCompleter,Completer,Span,Suggestion};
171    ///
172    /// let mut completions = DefaultCompleter::default();
173    /// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
174    /// assert_eq!(
175    ///     completions.complete("te",2),
176    ///     vec![Suggestion {value: "test".into(), description: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false}]);
177    ///
178    /// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
179    /// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
180    /// assert_eq!(
181    ///     completions.complete("te",2),
182    ///     vec![
183    ///         Suggestion {value: "test-hyphen".into(), description: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
184    ///         Suggestion {value: "test_underscore".into(), description: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
185    ///     ]);
186    /// ```
187    pub fn with_inclusions(incl: &[char]) -> Self {
188        let mut set = BTreeSet::new();
189        set.extend(incl.iter());
190        let inclusions = Arc::new(set);
191        Self {
192            root: CompletionNode::new(inclusions),
193            ..Self::default()
194        }
195    }
196
197    /// Clears all the data from the tree
198    /// # Example
199    /// ```
200    /// use reedline::{DefaultCompleter,Completer};
201    ///
202    /// let mut completions = DefaultCompleter::default();
203    /// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
204    /// assert_eq!(completions.word_count(), 5);
205    /// assert_eq!(completions.size(), 24);
206    /// completions.clear();
207    /// assert_eq!(completions.size(), 1);
208    /// assert_eq!(completions.word_count(), 0);
209    /// ```
210    pub fn clear(&mut self) {
211        self.root.clear();
212    }
213
214    /// Returns a count of how many words that exist in the tree
215    /// # Example
216    /// ```
217    /// use reedline::{DefaultCompleter,Completer};
218    ///
219    /// let mut completions = DefaultCompleter::default();
220    /// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
221    /// assert_eq!(completions.word_count(), 5);
222    /// ```
223    pub fn word_count(&self) -> u32 {
224        self.root.word_count()
225    }
226
227    /// Returns the size of the tree, the amount of nodes, not words
228    /// # Example
229    /// ```
230    /// use reedline::{DefaultCompleter,Completer};
231    ///
232    /// let mut completions = DefaultCompleter::default();
233    /// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
234    /// assert_eq!(completions.size(), 24);
235    /// ```
236    pub fn size(&self) -> u32 {
237        self.root.subnode_count()
238    }
239
240    /// Returns the minimum word length to complete. This allows you
241    /// to pass full sentences to `insert()` and not worry about
242    /// pruning out small words like "a" or "to", because they will be
243    /// ignored.
244    /// # Example
245    /// ```
246    /// use reedline::{DefaultCompleter,Completer};
247    ///
248    /// let mut completions = DefaultCompleter::default().set_min_word_len(4);
249    /// completions.insert(vec!["one","two","three","four","five"].iter().map(|s| s.to_string()).collect());
250    /// assert_eq!(completions.word_count(), 3);
251    ///
252    /// let mut completions = DefaultCompleter::default().set_min_word_len(1);
253    /// completions.insert(vec!["one","two","three","four","five"].iter().map(|s| s.to_string()).collect());
254    /// assert_eq!(completions.word_count(), 5);
255    /// ```
256    pub fn min_word_len(&self) -> usize {
257        self.min_word_len
258    }
259
260    /// Sets the minimum word length to complete on. Smaller words are
261    /// ignored. This only affects future calls to `insert()` -
262    /// changing this won't start completing on smaller words that
263    /// were added in the past, nor will it exclude larger words
264    /// already inserted into the completion tree.
265    #[must_use]
266    pub fn set_min_word_len(mut self, len: usize) -> Self {
267        self.min_word_len = len;
268        self
269    }
270}
271
272#[derive(Debug, Clone)]
273struct CompletionNode {
274    subnodes: BTreeMap<char, CompletionNode>,
275    leaf: bool,
276    inclusions: Arc<BTreeSet<char>>,
277}
278
279impl CompletionNode {
280    fn new(incl: Arc<BTreeSet<char>>) -> Self {
281        Self {
282            subnodes: BTreeMap::new(),
283            leaf: false,
284            inclusions: incl,
285        }
286    }
287
288    fn clear(&mut self) {
289        self.subnodes.clear();
290    }
291
292    fn word_count(&self) -> u32 {
293        let mut count = self.subnodes.values().map(CompletionNode::word_count).sum();
294        if self.leaf {
295            count += 1;
296        }
297        count
298    }
299
300    fn subnode_count(&self) -> u32 {
301        self.subnodes
302            .values()
303            .map(CompletionNode::subnode_count)
304            .sum::<u32>()
305            + 1
306    }
307
308    fn insert(&mut self, mut iter: Chars) {
309        if let Some(c) = iter.next() {
310            if self.inclusions.contains(&c) || c.is_alphanumeric() || c.is_whitespace() {
311                let inclusions = self.inclusions.clone();
312                let subnode = self
313                    .subnodes
314                    .entry(c)
315                    .or_insert_with(|| CompletionNode::new(inclusions));
316                subnode.insert(iter);
317            } else {
318                self.leaf = true;
319            }
320        } else {
321            self.leaf = true;
322        }
323    }
324
325    fn complete(&self, mut iter: Chars) -> Option<Vec<String>> {
326        if let Some(c) = iter.next() {
327            if let Some(subnode) = self.subnodes.get(&c) {
328                subnode.complete(iter)
329            } else {
330                None
331            }
332        } else {
333            Some(self.collect(""))
334        }
335    }
336
337    fn collect(&self, partial: &str) -> Vec<String> {
338        let mut completions = vec![];
339        if self.leaf {
340            completions.push(partial.to_string());
341        }
342
343        if !self.subnodes.is_empty() {
344            for (c, node) in &self.subnodes {
345                let mut partial = partial.to_string();
346                partial.push(*c);
347                completions.append(&mut node.collect(&partial));
348            }
349        }
350        completions
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    #[test]
357    fn default_completer_with_non_ansi() {
358        use super::*;
359
360        let mut completions = DefaultCompleter::default();
361        completions.insert(
362            ["nushell", "null", "number"]
363                .iter()
364                .map(|s| s.to_string())
365                .collect(),
366        );
367
368        assert_eq!(
369            completions.complete("n", 3),
370            [
371                Suggestion {
372                    value: "null".into(),
373                    description: None,
374                    extra: None,
375                    span: Span { start: 0, end: 3 },
376                    append_whitespace: false,
377                },
378                Suggestion {
379                    value: "number".into(),
380                    description: None,
381                    extra: None,
382                    span: Span { start: 0, end: 3 },
383                    append_whitespace: false,
384                },
385                Suggestion {
386                    value: "nushell".into(),
387                    description: None,
388                    extra: None,
389                    span: Span { start: 0, end: 3 },
390                    append_whitespace: false,
391                },
392            ]
393        );
394    }
395}