lineread/
complete.rs

1//! Provides utilities for implementing word completion
2
3use std::borrow::Cow::{self, Borrowed, Owned};
4use std::fs::read_dir;
5use std::path::{is_separator, MAIN_SEPARATOR};
6
7use crate::prompter::Prompter;
8use crate::terminal::Terminal;
9
10/// Represents a single possible completion
11#[derive(Clone, Debug)]
12pub struct Completion {
13    /// Whole completion text
14    pub completion: String,
15    /// Listing display string; `None` if matches completion
16    pub display: Option<String>,
17    /// Completion suffix; replaces append character
18    pub suffix: Suffix,
19}
20
21/// Specifies an optional suffix to override the default value
22#[derive(Copy, Clone, Debug, Eq, PartialEq)]
23pub enum Suffix {
24    /// Use the default suffix
25    Default,
26    /// Use no suffix
27    None,
28    /// Use the given suffix
29    Some(char),
30}
31
32impl Completion {
33    /// Returns a simple `Completion` value, with display string matching
34    /// completion and using the default completion suffix.
35    pub fn simple(s: String) -> Completion {
36        Completion{
37            completion: s,
38            display: None,
39            suffix: Suffix::default(),
40        }
41    }
42
43    /// Returns the full completion string, including suffix, using the given
44    /// default suffix if one is not assigned to this completion.
45    pub fn completion(&self, def_suffix: Option<char>) -> Cow<str> {
46        let mut s = Borrowed(&self.completion[..]);
47
48        if let Some(suffix) = self.suffix.with_default(def_suffix) {
49            s.to_mut().push(suffix);
50        }
51
52        s
53    }
54
55    /// Returns the display string, including suffix
56    pub fn display(&self) -> Cow<str> {
57        let mut s = Borrowed(self.display_str());
58
59        if let Suffix::Some(suffix) = self.suffix {
60            s.to_mut().push(suffix);
61        }
62
63        s
64    }
65
66    /// Returns the number of characters displayed
67    pub fn display_chars(&self) -> usize {
68        let n = self.display_str().chars().count();
69        n + if self.suffix.is_some() { 1 } else { 0 }
70    }
71
72    fn display_str(&self) -> &str {
73        match self.display {
74            Some(ref dis) => dis,
75            None => &self.completion
76        }
77    }
78}
79
80impl Suffix {
81    /// Returns whether the `Suffix` value is the `Default` variant.
82    pub fn is_default(&self) -> bool {
83        match *self {
84            Suffix::Default => true,
85            _ => false
86        }
87    }
88
89    /// Returns whether the `Suffix` value is the `Some(_)` variant.
90    pub fn is_some(&self) -> bool {
91        match *self {
92            Suffix::Some(_) => true,
93            _ => false
94        }
95    }
96
97    /// Returns whether the `Suffix` value is the `None` variant.
98    pub fn is_none(&self) -> bool {
99        match *self {
100            Suffix::None => true,
101            _ => false
102        }
103    }
104
105    /// Returns an `Option<char>`, using the given value in place of `Default`.
106    pub fn with_default(self, default: Option<char>) -> Option<char> {
107        match self {
108            Suffix::None => None,
109            Suffix::Some(ch) => Some(ch),
110            Suffix::Default => default
111        }
112    }
113}
114
115impl Default for Suffix {
116    fn default() -> Suffix {
117        Suffix::Default
118    }
119}
120
121/// Performs completion for `Prompter` when triggered by a user input sequence
122pub trait Completer<Term: Terminal>: Send + Sync {
123    /// Returns the set of possible completions for the prefix `word`.
124    fn complete(&self, word: &str, prompter: &Prompter<Term>,
125        start: usize, end: usize) -> Option<Vec<Completion>>;
126
127    /// Returns the starting position of the word under the cursor.
128    ///
129    /// The default implementation uses `Prompter::word_break_chars()` to
130    /// detect the start of a word.
131    fn word_start(&self, line: &str, end: usize, prompter: &Prompter<Term>) -> usize {
132        word_break_start(&line[..end], prompter.word_break_chars())
133    }
134
135    /// Quotes a possible completion for insertion into input.
136    ///
137    /// The default implementation returns the word, as is.
138    fn quote<'a>(&self, word: &'a str) -> Cow<'a, str> { Borrowed(word) }
139
140    /// Unquotes a piece of user input before searching for completions.
141    ///
142    /// The default implementation returns the word, as is.
143    fn unquote<'a>(&self, word: &'a str) -> Cow<'a, str> { Borrowed(word) }
144}
145
146/// `Completer` type that performs no completion
147///
148/// This is the default `Completer` for a new `Prompter` instance.
149pub struct DummyCompleter;
150
151impl<Term: Terminal> Completer<Term> for DummyCompleter {
152    fn complete(&self, _word: &str, _reader: &Prompter<Term>,
153            _start: usize, _end: usize) -> Option<Vec<Completion>> { None }
154}
155
156/// Performs completion by searching for filenames matching the word prefix.
157pub struct PathCompleter;
158
159impl<Term: Terminal> Completer<Term> for PathCompleter {
160    fn complete(&self, word: &str, _reader: &Prompter<Term>, _start: usize, _end: usize)
161            -> Option<Vec<Completion>> {
162        Some(complete_path(word))
163    }
164
165    fn word_start(&self, line: &str, end: usize, _reader: &Prompter<Term>) -> usize {
166        escaped_word_start(&line[..end])
167    }
168
169    fn quote<'a>(&self, word: &'a str) -> Cow<'a, str> {
170        escape(word)
171    }
172
173    fn unquote<'a>(&self, word: &'a str) -> Cow<'a, str> {
174        unescape(word)
175    }
176}
177
178/// Returns a sorted list of paths whose prefix matches the given path.
179pub fn complete_path(path: &str) -> Vec<Completion> {
180    let (base_dir, fname) = split_path(path);
181    let mut res = Vec::new();
182
183    let lookup_dir = base_dir.unwrap_or(".");
184
185    if let Ok(list) = read_dir(lookup_dir) {
186        for ent in list {
187            if let Ok(ent) = ent {
188                let ent_name = ent.file_name();
189
190                // TODO: Deal with non-UTF8 paths in some way
191                if let Ok(path) = ent_name.into_string() {
192                    if path.starts_with(fname) {
193                        let (name, display) = if let Some(dir) = base_dir {
194                            (format!("{}{}{}", dir, MAIN_SEPARATOR, path),
195                                Some(path))
196                        } else {
197                            (path, None)
198                        };
199
200                        let is_dir = ent.metadata().ok()
201                            .map_or(false, |m| m.is_dir());
202
203                        let suffix = if is_dir {
204                            Suffix::Some(MAIN_SEPARATOR)
205                        } else {
206                            Suffix::Default
207                        };
208
209                        res.push(Completion{
210                            completion: name,
211                            display: display,
212                            suffix: suffix,
213                        });
214                    }
215                }
216            }
217        }
218    }
219
220    res.sort_by(|a, b| a.display_str().cmp(b.display_str()));
221    res
222}
223
224/// Returns the start position of the word that ends at the end of the string.
225pub fn word_break_start(s: &str, word_break: &str) -> usize {
226    let mut start = s.len();
227
228    for (idx, ch) in s.char_indices().rev() {
229        if word_break.contains(ch) {
230            break;
231        }
232        start = idx;
233    }
234
235    start
236}
237
238/// Returns the start position of a word with non-word characters escaped by
239/// backslash (`\\`).
240pub fn escaped_word_start(s: &str) -> usize {
241    let mut chars = s.char_indices().rev();
242    let mut start = s.len();
243
244    while let Some((idx, ch)) = chars.next() {
245        if needs_escape(ch) {
246            let n = {
247                let mut n = 0;
248
249                loop {
250                    let mut clone = chars.clone();
251
252                    let ch = match clone.next() {
253                        Some((_, ch)) => ch,
254                        None => break
255                    };
256
257                    if ch == '\\' {
258                        chars = clone;
259                        n += 1;
260                    } else {
261                        break;
262                    }
263                }
264
265                n
266            };
267
268            if n % 2 == 0 {
269                break;
270            }
271        }
272
273        start = idx;
274    }
275
276    start
277}
278
279/// Escapes a word by prefixing a backslash (`\\`) to non-word characters.
280pub fn escape(s: &str) -> Cow<str> {
281    let n = s.chars().filter(|&ch| needs_escape(ch)).count();
282
283    if n == 0 {
284        Borrowed(s)
285    } else {
286        let mut res = String::with_capacity(s.len() + n);
287
288        for ch in s.chars() {
289            if needs_escape(ch) {
290                res.push('\\');
291            }
292            res.push(ch);
293        }
294
295        Owned(res)
296    }
297}
298
299/// Unescapes a word by removing the backslash (`\\`) from escaped characters.
300pub fn unescape(s: &str) -> Cow<str> {
301    if s.contains('\\') {
302        let mut res = String::with_capacity(s.len());
303        let mut chars = s.chars();
304
305        while let Some(ch) = chars.next() {
306            if ch == '\\' {
307                if let Some(ch) = chars.next() {
308                    res.push(ch);
309                }
310            } else {
311                res.push(ch);
312            }
313        }
314
315        Owned(res)
316    } else {
317        Borrowed(s)
318    }
319}
320
321fn needs_escape(ch: char) -> bool {
322    match ch {
323        ' ' | '\t' | '\n' | '\\' => true,
324        _ => false
325    }
326}
327
328fn split_path(path: &str) -> (Option<&str>, &str) {
329    match path.rfind(is_separator) {
330        Some(pos) => (Some(&path[..pos]), &path[pos + 1..]),
331        None => (None, path)
332    }
333}