nu_completion/
completer.rs

1use std::borrow::Cow;
2
3use nu_parser::NewlineMode;
4use nu_source::{Span, Tag};
5
6use crate::command::CommandCompleter;
7use crate::engine;
8use crate::flag::FlagCompleter;
9use crate::matchers;
10use crate::matchers::Matcher;
11use crate::path::{PathCompleter, PathSuggestion};
12use crate::variable::VariableCompleter;
13use crate::{Completer, CompletionContext, Suggestion};
14
15pub struct NuCompleter {}
16
17impl NuCompleter {}
18
19impl NuCompleter {
20    pub fn complete<Context: CompletionContext>(
21        &self,
22        line: &str,
23        pos: usize,
24        context: &Context,
25    ) -> (usize, Vec<Suggestion>) {
26        use engine::LocationType;
27
28        let tokens = nu_parser::lex(line, 0, NewlineMode::Normal).0;
29
30        let locations = Some(nu_parser::parse_block(tokens).0)
31            .map(|block| nu_parser::classify_block(&block, context.scope()))
32            .map(|(block, _)| engine::completion_location(line, &block, pos))
33            .unwrap_or_default();
34
35        let matcher = nu_data::config::config(Tag::unknown())
36            .ok()
37            .and_then(|cfg| cfg.get("line_editor").cloned())
38            .and_then(|le| {
39                le.row_entries()
40                    .find(|&(idx, _value)| idx == "completion_match_method")
41                    .and_then(|(_idx, value)| value.as_string().ok())
42            })
43            .unwrap_or_else(String::new);
44
45        let matcher = matcher.as_str();
46        let matcher: &dyn Matcher = match matcher {
47            "case-insensitive" => &matchers::case_insensitive::Matcher,
48            "case-sensitive" => &matchers::case_sensitive::Matcher,
49            #[cfg(target_os = "windows")]
50            _ => &matchers::case_insensitive::Matcher,
51            #[cfg(not(target_os = "windows"))]
52            _ => &matchers::case_sensitive::Matcher,
53        };
54
55        if locations.is_empty() {
56            (pos, Vec::new())
57        } else {
58            let mut pos = locations[0].span.start();
59
60            for location in &locations {
61                if location.span.start() < pos {
62                    pos = location.span.start();
63                }
64            }
65            let suggestions = locations
66                .into_iter()
67                .flat_map(|location| {
68                    let partial = location.span.slice(line).to_string();
69                    match location.item {
70                        LocationType::Command => {
71                            let command_completer = CommandCompleter;
72                            command_completer.complete(context, &partial, matcher.to_owned())
73                        }
74
75                        LocationType::Flag(cmd) => {
76                            let flag_completer = FlagCompleter { cmd };
77                            flag_completer.complete(context, &partial, matcher.to_owned())
78                        }
79
80                        LocationType::Argument(cmd, _arg_name) => {
81                            let path_completer = PathCompleter;
82                            let prepend = Span::new(pos, location.span.start()).slice(line);
83
84                            const QUOTE_CHARS: &[char] = &['\'', '"'];
85
86                            // TODO Find a better way to deal with quote chars. Can the completion
87                            //      engine relay this back to us? Maybe have two spans: inner and
88                            //      outer. The former is what we want to complete, the latter what
89                            //      we'd need to replace.
90                            let (quote_char, partial) = if partial.starts_with(QUOTE_CHARS) {
91                                let (head, tail) = partial.split_at(1);
92                                (Some(head), tail.to_string())
93                            } else {
94                                (None, partial)
95                            };
96
97                            let (mut partial, quoted) = if let Some(quote_char) = quote_char {
98                                if partial.ends_with(quote_char) {
99                                    (partial[..partial.len() - 1].to_string(), true)
100                                } else {
101                                    (partial, false)
102                                }
103                            } else {
104                                (partial, false)
105                            };
106
107                            partial = partial.split('"').collect::<Vec<_>>().join("");
108                            let completed_paths =
109                                path_completer.path_suggestions(&partial, matcher);
110                            match cmd.as_deref().unwrap_or("") {
111                                "cd" => select_directory_suggestions(completed_paths),
112                                _ => completed_paths,
113                            }
114                            .into_iter()
115                            .map(|s| Suggestion {
116                                replacement: format!(
117                                    "{}{}",
118                                    prepend,
119                                    requote(s.suggestion.replacement, quoted)
120                                ),
121                                display: s.suggestion.display,
122                            })
123                            .collect()
124                        }
125
126                        LocationType::Variable => {
127                            let variable_completer = VariableCompleter;
128                            variable_completer.complete(context, &partial, matcher.to_owned())
129                        }
130                    }
131                })
132                .collect();
133
134            (pos, suggestions)
135        }
136    }
137}
138
139fn select_directory_suggestions(completed_paths: Vec<PathSuggestion>) -> Vec<PathSuggestion> {
140    completed_paths
141        .into_iter()
142        .filter(|suggestion| {
143            suggestion
144                .path
145                .metadata()
146                .map(|md| md.is_dir())
147                .unwrap_or(false)
148        })
149        .collect()
150}
151
152fn requote(orig_value: String, previously_quoted: bool) -> String {
153    let value: Cow<str> = {
154        #[cfg(feature = "rustyline-support")]
155        {
156            rustyline::completion::unescape(&orig_value, Some('\\'))
157        }
158        #[cfg(not(feature = "rustyline-support"))]
159        {
160            orig_value.into()
161        }
162    };
163
164    let mut quotes = vec!['"', '\''];
165    let mut should_quote = false;
166    for c in value.chars() {
167        if c.is_whitespace() || c == '#' {
168            should_quote = true;
169        } else if let Some(index) = quotes.iter().position(|q| *q == c) {
170            should_quote = true;
171            quotes.swap_remove(index);
172        }
173    }
174
175    if should_quote {
176        if quotes.is_empty() {
177            // TODO we don't really have an escape character, so there isn't a great option right
178            //      now. One possibility is `{{(char backtick)}}`
179            value.to_string()
180        } else {
181            let quote = quotes[0];
182            if previously_quoted {
183                format!("{}{}", quote, value)
184            } else {
185                format!("{}{}{}", quote, value, quote)
186            }
187        }
188    } else {
189        value.to_string()
190    }
191}