1 use auto_enums::auto_enum;
2 use glob::{glob_with, MatchOptions};
3 use ion_shell::{
4     expansion::{unescape, Expander},
5     Shell,
6 };
7 use liner::{Completer, CursorPosition, Event, EventKind};
8 use std::{env, iter, path::PathBuf, str};
9 
10 pub struct IonCompleter<'a, 'b> {
11     shell:      &'b Shell<'a>,
12     completion: CompletionType,
13 }
14 
15 /// Escapes filenames from the completer so that special characters will be properly escaped.
16 ///
17 /// NOTE: Perhaps we should submit a PR to Liner to add a &'static [u8] field to
18 /// `FilenameCompleter` so that we don't have to perform the escaping ourselves?
escape(input: &str) -> String19 fn escape(input: &str) -> String {
20     let mut output = Vec::with_capacity(input.len());
21     for character in input.bytes() {
22         match character {
23             b'(' | b')' | b'[' | b']' | b'&' | b'$' | b'@' | b'{' | b'}' | b'<' | b'>' | b';'
24             | b'"' | b'\'' | b'#' | b'^' | b'*' | b' ' => output.push(b'\\'),
25             _ => (),
26         }
27         output.push(character);
28     }
29     unsafe { String::from_utf8_unchecked(output) }
30 }
31 
32 enum CompletionType {
33     Nothing,
34     Command,
35     VariableAndFiles,
36 }
37 
38 impl<'a, 'b> IonCompleter<'a, 'b> {
new(shell: &'b Shell<'a>) -> Self39     pub fn new(shell: &'b Shell<'a>) -> Self {
40         IonCompleter { shell, completion: CompletionType::Nothing }
41     }
42 }
43 
44 impl<'a, 'b> Completer for IonCompleter<'a, 'b> {
completions(&mut self, start: &str) -> Vec<String>45     fn completions(&mut self, start: &str) -> Vec<String> {
46         let mut completions = IonFileCompleter::new(None, &self.shell).completions(start);
47         let vars = self.shell.variables();
48 
49         match self.completion {
50             CompletionType::VariableAndFiles => {
51                 // Initialize a new completer from the definitions collected.
52                 // Creates a list of definitions from the shell environment that
53                 // will be used
54                 // in the creation of a custom completer.
55                 if start.is_empty() {
56                     completions.extend(vars.string_vars().map(|(s, _)| format!("${}", s)));
57                     completions.extend(vars.arrays().map(|(s, _)| format!("@{}", s)));
58                 } else if start.starts_with('$') {
59                     completions.extend(
60                         // Add the list of available variables to the completer's
61                         // definitions. TODO: We should make
62                         // it free to do String->SmallString
63                         //       and mostly free to go back (free if allocated)
64                         vars.string_vars()
65                             .filter(|(s, _)| s.starts_with(&start[1..]))
66                             .map(|(s, _)| format!("${}", &s)),
67                     );
68                 } else if start.starts_with('@') {
69                     completions.extend(
70                         vars.arrays()
71                             .filter(|(s, _)| s.starts_with(&start[1..]))
72                             .map(|(s, _)| format!("@{}", &s)),
73                     );
74                 }
75             }
76             CompletionType::Command => {
77                 // Initialize a new completer from the definitions collected.
78                 // Creates a list of definitions from the shell environment that
79                 // will be used
80                 // in the creation of a custom completer.
81                 completions.extend(
82                     self.shell
83                         .builtins()
84                         .keys()
85                         // Add built-in commands to the completer's definitions.
86                         .map(ToString::to_string)
87                         // Add the aliases to the completer's definitions.
88                         .chain(vars.aliases().map(|(key, _)| key.to_string()))
89                         // Add the list of available functions to the completer's
90                         // definitions.
91                         .chain(vars.functions().map(|(key, _)| key.to_string()))
92                         .filter(|s| s.starts_with(start)),
93                 );
94                 // Creates completers containing definitions from all directories
95                 // listed
96                 // in the environment's **$PATH** variable.
97                 let file_completers: Vec<_> = if let Some(paths) = env::var_os("PATH") {
98                     env::split_paths(&paths)
99                         .map(|s| {
100                             let s = if !s.to_string_lossy().ends_with('/') {
101                                 let mut oss = s.into_os_string();
102                                 oss.push("/");
103                                 oss.into()
104                             } else {
105                                 s
106                             };
107                             IonFileCompleter::new(Some(s), &self.shell)
108                         })
109                         .collect()
110                 } else {
111                     vec![IonFileCompleter::new(Some("/bin/".into()), &self.shell)]
112                 };
113                 // Merge the collected definitions with the file path definitions.
114                 completions.extend(MultiCompleter::new(file_completers).completions(start));
115             }
116             CompletionType::Nothing => (),
117         }
118 
119         completions
120     }
121 
on_event<W: std::io::Write>(&mut self, event: Event<'_, '_, W>)122     fn on_event<W: std::io::Write>(&mut self, event: Event<'_, '_, W>) {
123         if let EventKind::BeforeComplete = event.kind {
124             let (words, pos) = event.editor.get_words_and_cursor_position();
125             self.completion = match pos {
126                 _ if words.is_empty() => CompletionType::Nothing,
127                 CursorPosition::InWord(0) => CompletionType::Command,
128                 CursorPosition::OnWordRightEdge(index) => {
129                     if index == 0 {
130                         CompletionType::Command
131                     } else {
132                         let is_pipe = words
133                             .into_iter()
134                             .nth(index - 1)
135                             .map(|(start, end)| event.editor.current_buffer().range(start, end))
136                             .filter(|filename| {
137                                 filename.ends_with('|')
138                                     || filename.ends_with('&')
139                                     || filename.ends_with(';')
140                             })
141                             .is_some();
142                         if is_pipe {
143                             CompletionType::Command
144                         } else {
145                             CompletionType::VariableAndFiles
146                         }
147                     }
148                 }
149                 _ => CompletionType::VariableAndFiles,
150             };
151         }
152     }
153 }
154 
155 /// Performs escaping to an inner `FilenameCompleter` to enable a handful of special cases
156 /// needed by the shell, such as expanding '~' to a home directory, or adding a backslash
157 /// when a special character is contained within an expanded filename.
158 pub struct IonFileCompleter<'a, 'b> {
159     shell: &'b Shell<'a>,
160     /// The directory the expansion takes place in
161     path: PathBuf,
162     for_command: bool,
163 }
164 
165 impl<'a, 'b> IonFileCompleter<'a, 'b> {
new(path: Option<PathBuf>, shell: &'b Shell<'a>) -> Self166     pub fn new(path: Option<PathBuf>, shell: &'b Shell<'a>) -> Self {
167         // The only time a path is Some is when looking for a command not a directory
168         // so save this fact to strip the paths when completing commands.
169         let for_command = path.is_some();
170         let path = path.unwrap_or_default();
171         IonFileCompleter { shell, path, for_command }
172     }
173 }
174 
175 impl<'a, 'b> Completer for IonFileCompleter<'a, 'b> {
176     /// When the tab key is pressed, **Liner** will use this method to perform completions of
177     /// filenames. As our `IonFileCompleter` is a wrapper around **Liner**'s
178     /// `FilenameCompleter`,
179     /// the purpose of our custom `Completer` is to expand possible `~` characters in the
180     /// `start`
181     /// value that we receive from the prompt, grab completions from the inner
182     /// `FilenameCompleter`,
183     /// and then escape the resulting filenames, as well as remove the expanded form of the `~`
184     /// character and re-add the `~` character in it's place.
completions(&mut self, start: &str) -> Vec<String>185     fn completions(&mut self, start: &str) -> Vec<String> {
186         // Dereferencing the raw pointers here should be entirely safe, theoretically,
187         // because no changes will occur to either of the underlying references in the
188         // duration between creation of the completers and execution of their
189         // completions.
190         let expanded = match self.shell.tilde(start) {
191             Ok(expanded) => expanded,
192             Err(why) => {
193                 eprintln!("ion: {}", why);
194                 return vec![start.into()];
195             }
196         };
197         // Now we obtain completions for the `expanded` form of the `start` value.
198         let completions = filename_completion(&expanded, &self.path);
199         if expanded == start {
200             return if self.for_command {
201                 completions
202                     .map(|s| s.rsplit('/').next().map(|s| s.to_string()).unwrap_or(s))
203                     .collect()
204             } else {
205                 completions.collect()
206             };
207         }
208         // We can do that by obtaining the index position where the tilde character
209         // ends. We don't search with `~` because we also want to
210         // handle other tilde variants.
211         let t_index = start.find('/').unwrap_or(1);
212         // `tilde` is the tilde pattern, and `search` is the pattern that follows.
213         let (tilde, search) = start.split_at(t_index);
214 
215         if search.len() < 2 {
216             // If the length of the search pattern is less than 2, the search pattern is
217             // empty, and thus the completions actually contain files and directories in
218             // the home directory.
219 
220             // The tilde pattern will actually be our `start` command in itself,
221             // and the completed form will be all of the characters beyond the length of
222             // the expanded form of the tilde pattern.
223             completions.map(|completion| [start, &completion[expanded.len()..]].concat()).collect()
224         // To save processing time, we should get obtain the index position where our
225         // search pattern begins, and re-use that index to slice the completions so
226         // that we may re-add the tilde character with the completion that follows.
227         } else if let Some(e_index) = expanded.rfind(search) {
228             // And then we will need to take those completions and remove the expanded form
229             // of the tilde pattern and replace it with that pattern yet again.
230             completions
231                 .map(|completion| escape(&[tilde, &completion[e_index..]].concat()))
232                 .collect()
233         } else {
234             Vec::new()
235         }
236     }
237 }
238 
239 #[auto_enum]
filename_completion<'a>(start: &'a str, path: &'a PathBuf) -> impl Iterator<Item = String> + 'a240 fn filename_completion<'a>(start: &'a str, path: &'a PathBuf) -> impl Iterator<Item = String> + 'a {
241     let unescaped_start = unescape(start);
242 
243     let mut split_start = unescaped_start.split('/');
244     let mut string = String::with_capacity(128);
245 
246     // When 'start' is an absolute path, "/..." gets split to ["", "..."]
247     // So we skip the first element and add "/" to the start of the string
248     if unescaped_start.starts_with('/') {
249         split_start.next();
250         string.push('/');
251     } else {
252         string.push_str(&path.to_string_lossy());
253     }
254 
255     for element in split_start {
256         string.push_str(element);
257         if element != "." && element != ".." {
258             string.push('*');
259         }
260         string.push('/');
261     }
262 
263     string.pop(); // pop out the last '/' character
264     if string.ends_with('.') {
265         string.push('*')
266     }
267     let globs = glob_with(
268         &string,
269         MatchOptions {
270             case_sensitive:              true,
271             require_literal_separator:   true,
272             require_literal_leading_dot: false,
273         },
274     )
275     .ok()
276     .map(|completions| {
277         completions.filter_map(Result::ok).filter_map(move |file| {
278             let out = file.to_str()?;
279             let mut joined = String::with_capacity(out.len() + 3); // worst case senario
280             if unescaped_start.starts_with("./") {
281                 joined.push_str("./");
282             }
283             joined.push_str(out);
284             if file.is_dir() {
285                 joined.push('/');
286             }
287             Some(escape(&joined))
288         })
289     });
290 
291     #[auto_enum(Iterator)]
292     match globs {
293         Some(iter) => iter,
294         None => iter::once(escape(start)),
295     }
296 }
297 
298 /// A completer that combines suggestions from multiple completers.
299 #[derive(Clone, Debug, Eq, PartialEq)]
300 pub struct MultiCompleter<A>(Vec<A>);
301 
302 impl<A> MultiCompleter<A> {
new(completions: Vec<A>) -> Self303     pub fn new(completions: Vec<A>) -> Self { MultiCompleter(completions) }
304 }
305 
306 impl<A> Completer for MultiCompleter<A>
307 where
308     A: Completer,
309 {
completions(&mut self, start: &str) -> Vec<String>310     fn completions(&mut self, start: &str) -> Vec<String> {
311         self.0.iter_mut().flat_map(|comp| comp.completions(start)).collect()
312     }
313 }
314 
315 #[cfg(test)]
316 mod tests {
317     use super::*;
318 
319     #[test]
filename_completion()320     fn filename_completion() {
321         let shell = Shell::default();
322         let mut completer = IonFileCompleter::new(None, &shell);
323         assert_eq!(completer.completions("testing"), vec!["testing/"]);
324         assert_eq!(completer.completions("testing/file"), vec!["testing/file_with_text"]);
325         if cfg!(not(target_os = "redox")) {
326             assert_eq!(completer.completions("~"), vec!["~/"]);
327         }
328         assert_eq!(completer.completions("tes/fil"), vec!["testing/file_with_text"]);
329     }
330 }
331