datafusion_rustyline/
completion.rs

1//! Completion API
2use std::borrow::Cow::{self, Borrowed, Owned};
3use std::collections::BTreeSet;
4use std::fs;
5use std::path::{self, Path};
6
7use super::Result;
8use line_buffer::LineBuffer;
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/// To be called for tab-completion.
16pub trait Completer {
17    /// Takes the currently edited `line` with the cursor `pos`ition and
18    /// returns the start position and the completion candidates for the
19    /// partial word to be completed.
20    ///
21    /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"]))
22    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)>;
23    /// Updates the edited `line` with the `elected` candidate.
24    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
25        let end = line.pos();
26        line.replace(start..end, elected)
27    }
28}
29
30impl Completer for () {
31    fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
32        Ok((0, Vec::with_capacity(0)))
33    }
34    fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
35        unreachable!()
36    }
37}
38
39impl<'c, C: ?Sized + Completer> Completer for &'c C {
40    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
41        (**self).complete(line, pos)
42    }
43    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
44        (**self).update(line, start, elected)
45    }
46}
47macro_rules! box_completer {
48    ($($id: ident)*) => {
49        $(
50            impl<C: ?Sized + Completer> Completer for $id<C> {
51                fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
52                    (**self).complete(line, pos)
53                }
54                fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
55                    (**self).update(line, start, elected)
56                }
57            }
58        )*
59    }
60}
61
62use std::rc::Rc;
63use std::sync::Arc;
64box_completer! { Box Rc Arc }
65
66/// A `Completer` for file and folder names.
67pub struct FilenameCompleter {
68    break_chars: BTreeSet<char>,
69    double_quotes_special_chars: BTreeSet<char>,
70}
71
72// rl_basic_word_break_characters, rl_completer_word_break_characters
73#[cfg(unix)]
74static DEFAULT_BREAK_CHARS: [char; 18] = [
75    ' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
76];
77#[cfg(unix)]
78static ESCAPE_CHAR: Option<char> = Some('\\');
79// Remove \ to make file completion works on windows
80#[cfg(windows)]
81static DEFAULT_BREAK_CHARS: [char; 17] = [
82    ' ', '\t', '\n', '"', '\'', '`', '@', '$', '>', '<', '=', ';', '|', '&', '{', '(', '\0',
83];
84#[cfg(windows)]
85static ESCAPE_CHAR: Option<char> = None;
86
87// In double quotes, not all break_chars need to be escaped
88// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
89static DOUBLE_QUOTES_SPECIAL_CHARS: [char; 4] = ['"', '$', '\\', '`'];
90
91impl FilenameCompleter {
92    pub fn new() -> FilenameCompleter {
93        FilenameCompleter {
94            break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect(),
95            double_quotes_special_chars: DOUBLE_QUOTES_SPECIAL_CHARS.iter().cloned().collect(),
96        }
97    }
98}
99
100impl Default for FilenameCompleter {
101    fn default() -> FilenameCompleter {
102        FilenameCompleter::new()
103    }
104}
105
106impl Completer for FilenameCompleter {
107    fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
108        let (start, path, esc_char, break_chars) =
109            if let Some((idx, double_quote)) = find_unclosed_quote(&line[..pos]) {
110                let start = idx + 1;
111                if double_quote {
112                    (
113                        start,
114                        unescape(&line[start..pos], ESCAPE_CHAR),
115                        ESCAPE_CHAR,
116                        &self.double_quotes_special_chars,
117                    )
118                } else {
119                    (start, Borrowed(&line[start..pos]), None, &self.break_chars)
120                }
121            } else {
122                let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
123                let path = unescape(path, ESCAPE_CHAR);
124                (start, path, ESCAPE_CHAR, &self.break_chars)
125            };
126        let matches = try!(filename_complete(&path, esc_char, break_chars));
127        Ok((start, matches))
128    }
129}
130
131/// Remove escape char
132pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> {
133    if esc_char.is_none() {
134        return Borrowed(input);
135    }
136    let esc_char = esc_char.unwrap();
137    let n = input.chars().filter(|&c| c == esc_char).count();
138    if n == 0 {
139        return Borrowed(input);
140    }
141    let mut result = String::with_capacity(input.len() - n);
142    let mut chars = input.chars();
143    while let Some(ch) = chars.next() {
144        if ch == esc_char {
145            if let Some(ch) = chars.next() {
146                result.push(ch);
147            }
148        } else {
149            result.push(ch);
150        }
151    }
152    Owned(result)
153}
154
155/// Escape any `break_chars` in `input` string with `esc_char`.
156/// For example, '/User Information' becomes '/User\ Information'
157/// when space is a breaking char and '\\' the escape char.
158pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char>) -> String {
159    if esc_char.is_none() {
160        return input;
161    }
162    let esc_char = esc_char.unwrap();
163    let n = input.chars().filter(|c| break_chars.contains(c)).count();
164    if n == 0 {
165        return input;
166    }
167    let mut result = String::with_capacity(input.len() + n);
168
169    for c in input.chars() {
170        if break_chars.contains(&c) {
171            result.push(esc_char);
172        }
173        result.push(c);
174    }
175    result
176}
177
178fn filename_complete(
179    path: &str,
180    esc_char: Option<char>,
181    break_chars: &BTreeSet<char>,
182) -> Result<Vec<String>> {
183    use std::env::{current_dir, home_dir};
184
185    let sep = path::MAIN_SEPARATOR;
186    let (dir_name, file_name) = match path.rfind(sep) {
187        Some(idx) => path.split_at(idx + sep.len_utf8()),
188        None => ("", path),
189    };
190
191    let dir_path = Path::new(dir_name);
192    let dir = if dir_path.starts_with("~") {
193        // ~[/...]
194        if let Some(home) = home_dir() {
195            match dir_path.strip_prefix("~") {
196                Ok(rel_path) => home.join(rel_path),
197                _ => home,
198            }
199        } else {
200            dir_path.to_path_buf()
201        }
202    } else if dir_path.is_relative() {
203        // TODO ~user[/...] (https://crates.io/crates/users)
204        if let Ok(cwd) = current_dir() {
205            cwd.join(dir_path)
206        } else {
207            dir_path.to_path_buf()
208        }
209    } else {
210        dir_path.to_path_buf()
211    };
212
213    let mut entries: Vec<String> = Vec::new();
214    for entry in try!(dir.read_dir()) {
215        let entry = try!(entry);
216        if let Some(s) = entry.file_name().to_str() {
217            if s.starts_with(file_name) {
218                let mut path = String::from(dir_name) + s;
219                if try!(fs::metadata(entry.path())).is_dir() {
220                    path.push(sep);
221                }
222                entries.push(escape(path, esc_char, break_chars));
223            }
224        }
225    }
226    Ok(entries)
227}
228
229/// Given a `line` and a cursor `pos`ition,
230/// try to find backward the start of a word.
231/// Return (0, `line[..pos]`) if no break char has been found.
232/// Return the word and its start position (idx, `line[idx..pos]`) otherwise.
233pub fn extract_word<'l>(
234    line: &'l str,
235    pos: usize,
236    esc_char: Option<char>,
237    break_chars: &BTreeSet<char>,
238) -> (usize, &'l str) {
239    let line = &line[..pos];
240    if line.is_empty() {
241        return (0, line);
242    }
243    let mut start = None;
244    for (i, c) in line.char_indices().rev() {
245        if esc_char.is_some() && start.is_some() {
246            if esc_char.unwrap() == c {
247                // escaped break char
248                start = None;
249                continue;
250            } else {
251                break;
252            }
253        }
254        if break_chars.contains(&c) {
255            start = Some(i + c.len_utf8());
256            if esc_char.is_none() {
257                break;
258            } // else maybe escaped...
259        }
260    }
261
262    match start {
263        Some(start) => (start, &line[start..]),
264        None => (0, line),
265    }
266}
267
268pub fn longest_common_prefix(candidates: &[String]) -> Option<&str> {
269    if candidates.is_empty() {
270        return None;
271    } else if candidates.len() == 1 {
272        return Some(&candidates[0]);
273    }
274    let mut longest_common_prefix = 0;
275    'o: loop {
276        for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
277            let b1 = c1.as_bytes();
278            let b2 = candidates[i + 1].as_bytes();
279            if b1.len() <= longest_common_prefix
280                || b2.len() <= longest_common_prefix
281                || b1[longest_common_prefix] != b2[longest_common_prefix]
282            {
283                break 'o;
284            }
285        }
286        longest_common_prefix += 1;
287    }
288    while !candidates[0].is_char_boundary(longest_common_prefix) {
289        longest_common_prefix -= 1;
290    }
291    if longest_common_prefix == 0 {
292        return None;
293    }
294    Some(&candidates[0][0..longest_common_prefix])
295}
296
297#[derive(PartialEq)]
298enum ScanMode {
299    DoubleQuote,
300    Escape,
301    EscapeInDoubleQuote,
302    Normal,
303    SingleQuote,
304}
305
306/// try to find an unclosed single/double quote in `s`.
307/// Return `None` if no unclosed quote is found.
308/// Return the unclosed quote position and if it is a double quote.
309fn find_unclosed_quote(s: &str) -> Option<(usize, bool)> {
310    let char_indices = s.char_indices();
311    let mut mode = ScanMode::Normal;
312    let mut quote_index = 0;
313    for (index, char) in char_indices {
314        match mode {
315            ScanMode::DoubleQuote => {
316                if char == '"' {
317                    mode = ScanMode::Normal;
318                } else if char == '\\' {
319                    mode = ScanMode::EscapeInDoubleQuote;
320                }
321            }
322            ScanMode::Escape => {
323                mode = ScanMode::Normal;
324            }
325            ScanMode::EscapeInDoubleQuote => {
326                mode = ScanMode::DoubleQuote;
327            }
328            ScanMode::Normal => {
329                if char == '"' {
330                    mode = ScanMode::DoubleQuote;
331                    quote_index = index;
332                } else if char == '\\' && cfg!(not(windows)) {
333                    mode = ScanMode::Escape;
334                } else if char == '\'' && cfg!(not(windows)) {
335                    mode = ScanMode::SingleQuote;
336                    quote_index = index;
337                }
338            }
339            ScanMode::SingleQuote => {
340                if char == '\'' {
341                    mode = ScanMode::Normal;
342                } // no escape in single quotes
343            }
344        };
345    }
346    if ScanMode::DoubleQuote == mode || ScanMode::SingleQuote == mode {
347        return Some((quote_index, ScanMode::DoubleQuote == mode));
348    }
349    None
350}
351
352#[cfg(test)]
353mod tests {
354    use std::collections::BTreeSet;
355
356    #[test]
357    pub fn extract_word() {
358        let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
359        let line = "ls '/usr/local/b";
360        assert_eq!(
361            (4, "/usr/local/b"),
362            super::extract_word(line, line.len(), Some('\\'), &break_chars)
363        );
364        let line = "ls /User\\ Information";
365        assert_eq!(
366            (3, "/User\\ Information"),
367            super::extract_word(line, line.len(), Some('\\'), &break_chars)
368        );
369    }
370
371    #[test]
372    pub fn unescape() {
373        use std::borrow::Cow::{self, Borrowed, Owned};
374        let input = "/usr/local/b";
375        assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
376        let input = "/User\\ Information";
377        let result: Cow<str> = Owned(String::from("/User Information"));
378        assert_eq!(result, super::unescape(input, Some('\\')));
379    }
380
381    #[test]
382    pub fn escape() {
383        let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect();
384        let input = String::from("/usr/local/b");
385        assert_eq!(
386            input.clone(),
387            super::escape(input, Some('\\'), &break_chars)
388        );
389        let input = String::from("/User Information");
390        let result = String::from("/User\\ Information");
391        assert_eq!(result, super::escape(input, Some('\\'), &break_chars));
392    }
393
394    #[test]
395    pub fn longest_common_prefix() {
396        let mut candidates = vec![];
397        {
398            let lcp = super::longest_common_prefix(&candidates);
399            assert!(lcp.is_none());
400        }
401
402        let s = "User";
403        let c1 = String::from(s);
404        candidates.push(c1.clone());
405        {
406            let lcp = super::longest_common_prefix(&candidates);
407            assert_eq!(Some(s), lcp);
408        }
409
410        let c2 = String::from("Users");
411        candidates.push(c2.clone());
412        {
413            let lcp = super::longest_common_prefix(&candidates);
414            assert_eq!(Some(s), lcp);
415        }
416
417        let c3 = String::from("");
418        candidates.push(c3.clone());
419        {
420            let lcp = super::longest_common_prefix(&candidates);
421            assert!(lcp.is_none());
422        }
423
424        let candidates = vec![String::from("fée"), String::from("fête")];
425        let lcp = super::longest_common_prefix(&candidates);
426        assert_eq!(Some("f"), lcp);
427    }
428
429    #[test]
430    pub fn find_unclosed_quote() {
431        assert_eq!(None, super::find_unclosed_quote("ls /etc"));
432        assert_eq!(
433            Some((3, true)),
434            super::find_unclosed_quote("ls \"User Information")
435        );
436        assert_eq!(
437            None,
438            super::find_unclosed_quote("ls \"/User Information\" /etc")
439        );
440    }
441}