nu_completion/
completer.rs1use 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 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 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}