Skip to main content

brush_rustyline_fork/
completion.rs

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