rustyline/
completion.rs

1//! Completion API
2use std::borrow::Cow::{self, Borrowed, Owned};
3use std::fs;
4use std::path::{self, Path};
5
6use super::Result;
7use line_buffer::LineBuffer;
8use memchr::memchr;
9
10// TODO: let the implementers choose/find word boudaries ???
11// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion
12// ("select t.na| from tbl as t")
13// TODO: make &self &mut self ???
14
15/// A completion candidate.
16pub trait Candidate {
17    /// Text to display when listing alternatives.
18    fn display(&self) -> &str;
19    /// Text to insert in line.
20    fn replacement(&self) -> &str;
21}
22
23impl Candidate for String {
24    fn display(&self) -> &str {
25        self.as_str()
26    }
27
28    fn replacement(&self) -> &str {
29        self.as_str()
30    }
31}
32
33pub struct Pair {
34    pub display: String,
35    pub replacement: String,
36}
37
38impl Candidate for Pair {
39    fn display(&self) -> &str {
40        self.display.as_str()
41    }
42
43    fn replacement(&self) -> &str {
44        self.replacement.as_str()
45    }
46}
47
48/// To be called for tab-completion.
49pub trait Completer {
50    type Candidate: Candidate;
51
52    /// Takes the currently edited `line` with the cursor `pos`ition and
53    /// returns the start position and the completion candidates for the
54    /// partial word to be completed.
55    ///
56    /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"]))
57    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)>;
58    /// Updates the edited `line` with the `elected` candidate.
59    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
60        let end = line.pos();
61        line.replace(start..end, elected)
62    }
63}
64
65impl Completer for () {
66    type Candidate = String;
67
68    fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
69        Ok((0, Vec::with_capacity(0)))
70    }
71
72    fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
73        unreachable!()
74    }
75}
76
77impl<'c, C: ?Sized + Completer> Completer for &'c C {
78    type Candidate = C::Candidate;
79
80    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
81        (**self).complete(line, pos)
82    }
83
84    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
85        (**self).update(line, start, elected)
86    }
87}
88macro_rules! box_completer {
89    ($($id: ident)*) => {
90        $(
91            impl<C: ?Sized + Completer> Completer for $id<C> {
92                type Candidate = C::Candidate;
93
94                fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
95                    (**self).complete(line, pos)
96                }
97                fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
98                    (**self).update(line, start, elected)
99                }
100            }
101        )*
102    }
103}
104
105use std::rc::Rc;
106use std::sync::Arc;
107box_completer! { Box Rc Arc }
108
109/// A `Completer` for file and folder names.
110pub struct FilenameCompleter {
111    break_chars: &'static [u8],
112    double_quotes_special_chars: &'static [u8],
113}
114
115static DOUBLE_QUOTES_ESCAPE_CHAR: Option<char> = Some('\\');
116
117// rl_basic_word_break_characters, rl_completer_word_break_characters
118#[cfg(unix)]
119static DEFAULT_BREAK_CHARS: [u8; 18] = [
120    b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&',
121    b'{', b'(', b'\0',
122];
123#[cfg(unix)]
124static ESCAPE_CHAR: Option<char> = Some('\\');
125// Remove \ to make file completion works on windows
126#[cfg(windows)]
127static DEFAULT_BREAK_CHARS: [u8; 17] = [
128    b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{',
129    b'(', b'\0',
130];
131#[cfg(windows)]
132static ESCAPE_CHAR: Option<char> = None;
133
134// In double quotes, not all break_chars need to be escaped
135// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
136#[cfg(unix)]
137static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`'];
138#[cfg(windows)]
139static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; // TODO Validate: only '"' ?
140
141#[derive(Clone, Copy, Debug, PartialEq)]
142pub enum Quote {
143    Double,
144    Single,
145    None,
146}
147
148impl FilenameCompleter {
149    pub fn new() -> FilenameCompleter {
150        FilenameCompleter {
151            break_chars: &DEFAULT_BREAK_CHARS,
152            double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS,
153        }
154    }
155}
156
157impl Default for FilenameCompleter {
158    fn default() -> FilenameCompleter {
159        FilenameCompleter::new()
160    }
161}
162
163impl Completer for FilenameCompleter {
164    type Candidate = Pair;
165
166    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
167        let (start, path, esc_char, break_chars, quote) =
168            if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) {
169                let start = idx + 1;
170                if quote == Quote::Double {
171                    (
172                        start,
173                        unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR),
174                        DOUBLE_QUOTES_ESCAPE_CHAR,
175                        &self.double_quotes_special_chars,
176                        quote,
177                    )
178                } else {
179                    (
180                        start,
181                        Borrowed(&line[start..pos]),
182                        None,
183                        &self.break_chars,
184                        quote,
185                    )
186                }
187            } else {
188                let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
189                let path = unescape(path, ESCAPE_CHAR);
190                (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None)
191            };
192        let matches = try!(filename_complete(&path, esc_char, break_chars, quote));
193        Ok((start, matches))
194    }
195}
196
197/// Remove escape char
198pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> {
199    if esc_char.is_none() {
200        return Borrowed(input);
201    }
202    let esc_char = esc_char.unwrap();
203    if !input.chars().any(|c| c == esc_char) {
204        return Borrowed(input);
205    }
206    let mut result = String::with_capacity(input.len());
207    let mut chars = input.chars();
208    while let Some(ch) = chars.next() {
209        if ch == esc_char {
210            if let Some(ch) = chars.next() {
211                if cfg!(windows) && ch != '"' {
212                    // TODO Validate: only '"' ?
213                    result.push(esc_char);
214                }
215                result.push(ch);
216            } else if cfg!(windows) {
217                result.push(ch);
218            }
219        } else {
220            result.push(ch);
221        }
222    }
223    Owned(result)
224}
225
226/// Escape any `break_chars` in `input` string with `esc_char`.
227/// For example, '/User Information' becomes '/User\ Information'
228/// when space is a breaking char and '\\' the escape char.
229pub fn escape(
230    mut input: String,
231    esc_char: Option<char>,
232    break_chars: &[u8],
233    quote: Quote,
234) -> String {
235    if quote == Quote::Single {
236        return input; // no escape in single quotes
237    }
238    let n = input
239        .bytes()
240        .filter(|b| memchr(*b, break_chars).is_some())
241        .count();
242    if n == 0 {
243        return input; // no need to escape
244    }
245    if esc_char.is_none() {
246        if cfg!(windows) && quote == Quote::None {
247            input.insert(0, '"'); // force double quote
248            return input;
249        }
250        return input;
251    }
252    let esc_char = esc_char.unwrap();
253    let mut result = String::with_capacity(input.len() + n);
254
255    for c in input.chars() {
256        if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
257            result.push(esc_char);
258        }
259        result.push(c);
260    }
261    result
262}
263
264fn filename_complete(
265    path: &str,
266    esc_char: Option<char>,
267    break_chars: &[u8],
268    quote: Quote,
269) -> Result<Vec<Pair>> {
270    use std::env::current_dir;
271
272    let sep = path::MAIN_SEPARATOR;
273    let (dir_name, file_name) = match path.rfind(sep) {
274        Some(idx) => path.split_at(idx + sep.len_utf8()),
275        None => ("", path),
276    };
277
278    let dir_path = Path::new(dir_name);
279    #[cfg(all(unix, not(any(target_os = "fuchsia"))))]
280    let dir = {
281        if dir_path.starts_with("~") {
282            // ~[/...]
283            if let Ok(home) = std::env::var("HOME") {
284                match dir_path.strip_prefix("~") {
285                    Ok(rel_path) => path::PathBuf::from(home).join(rel_path),
286                    _ => path::PathBuf::from(home),
287                }
288            } else {
289                dir_path.to_path_buf()
290            }
291        } else if dir_path.is_relative() {
292            // TODO ~user[/...] (https://crates.io/crates/users)
293            if let Ok(cwd) = current_dir() {
294                cwd.join(dir_path)
295            } else {
296                dir_path.to_path_buf()
297            }
298        } else {
299            dir_path.to_path_buf()
300        }
301    };
302    #[cfg(target_os = "fuchsia")]
303    let dir = {
304        if dir_path.is_relative() {
305            // TODO ~user[/...] (https://crates.io/crates/users)
306            if let Ok(cwd) = current_dir() {
307                cwd.join(dir_path)
308            } else {
309                dir_path.to_path_buf()
310            }
311        } else {
312            dir_path.to_path_buf()
313        }
314    };
315
316    let mut entries: Vec<Pair> = Vec::new();
317
318    // if dir doesn't exist, then don't offer any completions
319    if !dir.exists() {
320        return Ok(entries);
321    }
322
323    // if any of the below IO operations have errors, just ignore them
324    if let Ok(read_dir) = dir.read_dir() {
325        for entry in read_dir {
326            if let Ok(entry) = entry {
327                if let Some(s) = entry.file_name().to_str() {
328                    if s.starts_with(file_name) {
329                        if let Ok(metadata) = fs::metadata(entry.path()) {
330                            let mut path = String::from(dir_name) + s;
331                            if metadata.is_dir() {
332                                path.push(sep);
333                            }
334                            entries.push(Pair {
335                                display: String::from(s),
336                                replacement: escape(path, esc_char, break_chars, quote),
337                            });
338                        } // else ignore PermissionDenied
339                    }
340                }
341            }
342        }
343    }
344    Ok(entries)
345}
346
347/// Given a `line` and a cursor `pos`ition,
348/// try to find backward the start of a word.
349/// Return (0, `line[..pos]`) if no break char has been found.
350/// Return the word and its start position (idx, `line[idx..pos]`) otherwise.
351pub fn extract_word<'l>(
352    line: &'l str,
353    pos: usize,
354    esc_char: Option<char>,
355    break_chars: &[u8],
356) -> (usize, &'l str) {
357    let line = &line[..pos];
358    if line.is_empty() {
359        return (0, line);
360    }
361    let mut start = None;
362    for (i, c) in line.char_indices().rev() {
363        if esc_char.is_some() && start.is_some() {
364            if esc_char.unwrap() == c {
365                // escaped break char
366                start = None;
367                continue;
368            } else {
369                break;
370            }
371        }
372        if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
373            start = Some(i + c.len_utf8());
374            if esc_char.is_none() {
375                break;
376            } // else maybe escaped...
377        }
378    }
379
380    match start {
381        Some(start) => (start, &line[start..]),
382        None => (0, line),
383    }
384}
385
386pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> {
387    if candidates.is_empty() {
388        return None;
389    } else if candidates.len() == 1 {
390        return Some(&candidates[0].replacement());
391    }
392    let mut longest_common_prefix = 0;
393    'o: loop {
394        for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
395            let b1 = c1.replacement().as_bytes();
396            let b2 = candidates[i + 1].replacement().as_bytes();
397            if b1.len() <= longest_common_prefix
398                || b2.len() <= longest_common_prefix
399                || b1[longest_common_prefix] != b2[longest_common_prefix]
400            {
401                break 'o;
402            }
403        }
404        longest_common_prefix += 1;
405    }
406    let candidate = candidates[0].replacement();
407    while !candidate.is_char_boundary(longest_common_prefix) {
408        longest_common_prefix -= 1;
409    }
410    if longest_common_prefix == 0 {
411        return None;
412    }
413    Some(&candidate[0..longest_common_prefix])
414}
415
416#[derive(PartialEq)]
417enum ScanMode {
418    DoubleQuote,
419    Escape,
420    EscapeInDoubleQuote,
421    Normal,
422    SingleQuote,
423}
424
425/// try to find an unclosed single/double quote in `s`.
426/// Return `None` if no unclosed quote is found.
427/// Return the unclosed quote position and if it is a double quote.
428fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> {
429    let char_indices = s.char_indices();
430    let mut mode = ScanMode::Normal;
431    let mut quote_index = 0;
432    for (index, char) in char_indices {
433        match mode {
434            ScanMode::DoubleQuote => {
435                if char == '"' {
436                    mode = ScanMode::Normal;
437                } else if char == '\\' {
438                    // both windows and unix support escape in double quote
439                    mode = ScanMode::EscapeInDoubleQuote;
440                }
441            }
442            ScanMode::Escape => {
443                mode = ScanMode::Normal;
444            }
445            ScanMode::EscapeInDoubleQuote => {
446                mode = ScanMode::DoubleQuote;
447            }
448            ScanMode::Normal => {
449                if char == '"' {
450                    mode = ScanMode::DoubleQuote;
451                    quote_index = index;
452                } else if char == '\\' && cfg!(not(windows)) {
453                    mode = ScanMode::Escape;
454                } else if char == '\'' && cfg!(not(windows)) {
455                    mode = ScanMode::SingleQuote;
456                    quote_index = index;
457                }
458            }
459            ScanMode::SingleQuote => {
460                if char == '\'' {
461                    mode = ScanMode::Normal;
462                } // no escape in single quotes
463            }
464        };
465    }
466    if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode {
467        return Some((quote_index, Quote::Double));
468    } else if ScanMode::SingleQuote == mode {
469        return Some((quote_index, Quote::Single));
470    }
471    None
472}
473
474#[cfg(test)]
475mod tests {
476    #[test]
477    pub fn extract_word() {
478        let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
479        let line = "ls '/usr/local/b";
480        assert_eq!(
481            (4, "/usr/local/b"),
482            super::extract_word(line, line.len(), Some('\\'), &break_chars)
483        );
484        let line = "ls /User\\ Information";
485        assert_eq!(
486            (3, "/User\\ Information"),
487            super::extract_word(line, line.len(), Some('\\'), &break_chars)
488        );
489    }
490
491    #[test]
492    pub fn unescape() {
493        use std::borrow::Cow::{self, Borrowed, Owned};
494        let input = "/usr/local/b";
495        assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
496        if cfg!(windows) {
497            let input = "c:\\users\\All Users\\";
498            let result: Cow<str> = Borrowed(input);
499            assert_eq!(result, super::unescape(input, Some('\\')));
500        } else {
501            let input = "/User\\ Information";
502            let result: Cow<str> = Owned(String::from("/User Information"));
503            assert_eq!(result, super::unescape(input, Some('\\')));
504        }
505    }
506
507    #[test]
508    pub fn escape() {
509        let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
510        let input = String::from("/usr/local/b");
511        assert_eq!(
512            input.clone(),
513            super::escape(input, Some('\\'), &break_chars, super::Quote::None)
514        );
515        let input = String::from("/User Information");
516        let result = String::from("/User\\ Information");
517        assert_eq!(
518            result,
519            super::escape(input, Some('\\'), &break_chars, super::Quote::None)
520        );
521    }
522
523    #[test]
524    pub fn longest_common_prefix() {
525        let mut candidates = vec![];
526        {
527            let lcp = super::longest_common_prefix(&candidates);
528            assert!(lcp.is_none());
529        }
530
531        let s = "User";
532        let c1 = String::from(s);
533        candidates.push(c1.clone());
534        {
535            let lcp = super::longest_common_prefix(&candidates);
536            assert_eq!(Some(s), lcp);
537        }
538
539        let c2 = String::from("Users");
540        candidates.push(c2.clone());
541        {
542            let lcp = super::longest_common_prefix(&candidates);
543            assert_eq!(Some(s), lcp);
544        }
545
546        let c3 = String::from("");
547        candidates.push(c3.clone());
548        {
549            let lcp = super::longest_common_prefix(&candidates);
550            assert!(lcp.is_none());
551        }
552
553        let candidates = vec![String::from("fée"), String::from("fête")];
554        let lcp = super::longest_common_prefix(&candidates);
555        assert_eq!(Some("f"), lcp);
556    }
557
558    #[test]
559    pub fn find_unclosed_quote() {
560        assert_eq!(None, super::find_unclosed_quote("ls /etc"));
561        assert_eq!(
562            Some((3, super::Quote::Double)),
563            super::find_unclosed_quote("ls \"User Information")
564        );
565        assert_eq!(
566            None,
567            super::find_unclosed_quote("ls \"/User Information\" /etc")
568        );
569        assert_eq!(
570            Some((0, super::Quote::Double)),
571            super::find_unclosed_quote("\"c:\\users\\All Users\\")
572        )
573    }
574}