1use std::borrow::Cow::{self, Borrowed, Owned};
3use std::fs;
4use std::path::{self, Path};
5
6use super::Result;
7use line_buffer::LineBuffer;
8use memchr::memchr;
9
10pub trait Candidate {
17 fn display(&self) -> &str;
19 fn replacement(&self) -> &str;
21}
22
23impl Candidate for String {
24 fn display(&self) -> &str {
25 self.as_str()
26 }
27
28 fn replacement(&self) -> &str {
29 self.as_str()
30 }
31}
32
33pub struct Pair {
34 pub display: String,
35 pub replacement: String,
36}
37
38impl Candidate for Pair {
39 fn display(&self) -> &str {
40 self.display.as_str()
41 }
42
43 fn replacement(&self) -> &str {
44 self.replacement.as_str()
45 }
46}
47
48pub trait Completer {
50 type Candidate: Candidate;
51
52 fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)>;
58 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
60 let end = line.pos();
61 line.replace(start..end, elected)
62 }
63}
64
65impl Completer for () {
66 type Candidate = String;
67
68 fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> {
69 Ok((0, Vec::with_capacity(0)))
70 }
71
72 fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
73 unreachable!()
74 }
75}
76
77impl<'c, C: ?Sized + Completer> Completer for &'c C {
78 type Candidate = C::Candidate;
79
80 fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
81 (**self).complete(line, pos)
82 }
83
84 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
85 (**self).update(line, start, elected)
86 }
87}
88macro_rules! box_completer {
89 ($($id: ident)*) => {
90 $(
91 impl<C: ?Sized + Completer> Completer for $id<C> {
92 type Candidate = C::Candidate;
93
94 fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> {
95 (**self).complete(line, pos)
96 }
97 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
98 (**self).update(line, start, elected)
99 }
100 }
101 )*
102 }
103}
104
105use std::rc::Rc;
106use std::sync::Arc;
107box_completer! { Box Rc Arc }
108
109pub struct FilenameCompleter {
111 break_chars: &'static [u8],
112 double_quotes_special_chars: &'static [u8],
113}
114
115static DOUBLE_QUOTES_ESCAPE_CHAR: Option<char> = Some('\\');
116
117#[cfg(unix)]
119static DEFAULT_BREAK_CHARS: [u8; 18] = [
120 b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&',
121 b'{', b'(', b'\0',
122];
123#[cfg(unix)]
124static ESCAPE_CHAR: Option<char> = Some('\\');
125#[cfg(windows)]
127static DEFAULT_BREAK_CHARS: [u8; 17] = [
128 b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{',
129 b'(', b'\0',
130];
131#[cfg(windows)]
132static ESCAPE_CHAR: Option<char> = None;
133
134#[cfg(unix)]
137static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`'];
138#[cfg(windows)]
139static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; #[derive(Clone, Copy, Debug, PartialEq)]
142pub enum Quote {
143 Double,
144 Single,
145 None,
146}
147
148impl FilenameCompleter {
149 pub fn new() -> FilenameCompleter {
150 FilenameCompleter {
151 break_chars: &DEFAULT_BREAK_CHARS,
152 double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS,
153 }
154 }
155}
156
157impl Default for FilenameCompleter {
158 fn default() -> FilenameCompleter {
159 FilenameCompleter::new()
160 }
161}
162
163impl Completer for FilenameCompleter {
164 type Candidate = Pair;
165
166 fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
167 let (start, path, esc_char, break_chars, quote) =
168 if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) {
169 let start = idx + 1;
170 if quote == Quote::Double {
171 (
172 start,
173 unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR),
174 DOUBLE_QUOTES_ESCAPE_CHAR,
175 &self.double_quotes_special_chars,
176 quote,
177 )
178 } else {
179 (
180 start,
181 Borrowed(&line[start..pos]),
182 None,
183 &self.break_chars,
184 quote,
185 )
186 }
187 } else {
188 let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
189 let path = unescape(path, ESCAPE_CHAR);
190 (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None)
191 };
192 let matches = try!(filename_complete(&path, esc_char, break_chars, quote));
193 Ok((start, matches))
194 }
195}
196
197pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> {
199 if esc_char.is_none() {
200 return Borrowed(input);
201 }
202 let esc_char = esc_char.unwrap();
203 if !input.chars().any(|c| c == esc_char) {
204 return Borrowed(input);
205 }
206 let mut result = String::with_capacity(input.len());
207 let mut chars = input.chars();
208 while let Some(ch) = chars.next() {
209 if ch == esc_char {
210 if let Some(ch) = chars.next() {
211 if cfg!(windows) && ch != '"' {
212 result.push(esc_char);
214 }
215 result.push(ch);
216 } else if cfg!(windows) {
217 result.push(ch);
218 }
219 } else {
220 result.push(ch);
221 }
222 }
223 Owned(result)
224}
225
226pub fn escape(
230 mut input: String,
231 esc_char: Option<char>,
232 break_chars: &[u8],
233 quote: Quote,
234) -> String {
235 if quote == Quote::Single {
236 return input; }
238 let n = input
239 .bytes()
240 .filter(|b| memchr(*b, break_chars).is_some())
241 .count();
242 if n == 0 {
243 return input; }
245 if esc_char.is_none() {
246 if cfg!(windows) && quote == Quote::None {
247 input.insert(0, '"'); return input;
249 }
250 return input;
251 }
252 let esc_char = esc_char.unwrap();
253 let mut result = String::with_capacity(input.len() + n);
254
255 for c in input.chars() {
256 if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
257 result.push(esc_char);
258 }
259 result.push(c);
260 }
261 result
262}
263
264fn filename_complete(
265 path: &str,
266 esc_char: Option<char>,
267 break_chars: &[u8],
268 quote: Quote,
269) -> Result<Vec<Pair>> {
270 use std::env::current_dir;
271
272 let sep = path::MAIN_SEPARATOR;
273 let (dir_name, file_name) = match path.rfind(sep) {
274 Some(idx) => path.split_at(idx + sep.len_utf8()),
275 None => ("", path),
276 };
277
278 let dir_path = Path::new(dir_name);
279 #[cfg(all(unix, not(any(target_os = "fuchsia"))))]
280 let dir = {
281 if dir_path.starts_with("~") {
282 if let Ok(home) = std::env::var("HOME") {
284 match dir_path.strip_prefix("~") {
285 Ok(rel_path) => path::PathBuf::from(home).join(rel_path),
286 _ => path::PathBuf::from(home),
287 }
288 } else {
289 dir_path.to_path_buf()
290 }
291 } else if dir_path.is_relative() {
292 if let Ok(cwd) = current_dir() {
294 cwd.join(dir_path)
295 } else {
296 dir_path.to_path_buf()
297 }
298 } else {
299 dir_path.to_path_buf()
300 }
301 };
302 #[cfg(target_os = "fuchsia")]
303 let dir = {
304 if dir_path.is_relative() {
305 if let Ok(cwd) = current_dir() {
307 cwd.join(dir_path)
308 } else {
309 dir_path.to_path_buf()
310 }
311 } else {
312 dir_path.to_path_buf()
313 }
314 };
315
316 let mut entries: Vec<Pair> = Vec::new();
317
318 if !dir.exists() {
320 return Ok(entries);
321 }
322
323 if let Ok(read_dir) = dir.read_dir() {
325 for entry in read_dir {
326 if let Ok(entry) = entry {
327 if let Some(s) = entry.file_name().to_str() {
328 if s.starts_with(file_name) {
329 if let Ok(metadata) = fs::metadata(entry.path()) {
330 let mut path = String::from(dir_name) + s;
331 if metadata.is_dir() {
332 path.push(sep);
333 }
334 entries.push(Pair {
335 display: String::from(s),
336 replacement: escape(path, esc_char, break_chars, quote),
337 });
338 } }
340 }
341 }
342 }
343 }
344 Ok(entries)
345}
346
347pub fn extract_word<'l>(
352 line: &'l str,
353 pos: usize,
354 esc_char: Option<char>,
355 break_chars: &[u8],
356) -> (usize, &'l str) {
357 let line = &line[..pos];
358 if line.is_empty() {
359 return (0, line);
360 }
361 let mut start = None;
362 for (i, c) in line.char_indices().rev() {
363 if esc_char.is_some() && start.is_some() {
364 if esc_char.unwrap() == c {
365 start = None;
367 continue;
368 } else {
369 break;
370 }
371 }
372 if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
373 start = Some(i + c.len_utf8());
374 if esc_char.is_none() {
375 break;
376 } }
378 }
379
380 match start {
381 Some(start) => (start, &line[start..]),
382 None => (0, line),
383 }
384}
385
386pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> {
387 if candidates.is_empty() {
388 return None;
389 } else if candidates.len() == 1 {
390 return Some(&candidates[0].replacement());
391 }
392 let mut longest_common_prefix = 0;
393 'o: loop {
394 for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
395 let b1 = c1.replacement().as_bytes();
396 let b2 = candidates[i + 1].replacement().as_bytes();
397 if b1.len() <= longest_common_prefix
398 || b2.len() <= longest_common_prefix
399 || b1[longest_common_prefix] != b2[longest_common_prefix]
400 {
401 break 'o;
402 }
403 }
404 longest_common_prefix += 1;
405 }
406 let candidate = candidates[0].replacement();
407 while !candidate.is_char_boundary(longest_common_prefix) {
408 longest_common_prefix -= 1;
409 }
410 if longest_common_prefix == 0 {
411 return None;
412 }
413 Some(&candidate[0..longest_common_prefix])
414}
415
416#[derive(PartialEq)]
417enum ScanMode {
418 DoubleQuote,
419 Escape,
420 EscapeInDoubleQuote,
421 Normal,
422 SingleQuote,
423}
424
425fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> {
429 let char_indices = s.char_indices();
430 let mut mode = ScanMode::Normal;
431 let mut quote_index = 0;
432 for (index, char) in char_indices {
433 match mode {
434 ScanMode::DoubleQuote => {
435 if char == '"' {
436 mode = ScanMode::Normal;
437 } else if char == '\\' {
438 mode = ScanMode::EscapeInDoubleQuote;
440 }
441 }
442 ScanMode::Escape => {
443 mode = ScanMode::Normal;
444 }
445 ScanMode::EscapeInDoubleQuote => {
446 mode = ScanMode::DoubleQuote;
447 }
448 ScanMode::Normal => {
449 if char == '"' {
450 mode = ScanMode::DoubleQuote;
451 quote_index = index;
452 } else if char == '\\' && cfg!(not(windows)) {
453 mode = ScanMode::Escape;
454 } else if char == '\'' && cfg!(not(windows)) {
455 mode = ScanMode::SingleQuote;
456 quote_index = index;
457 }
458 }
459 ScanMode::SingleQuote => {
460 if char == '\'' {
461 mode = ScanMode::Normal;
462 } }
464 };
465 }
466 if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode {
467 return Some((quote_index, Quote::Double));
468 } else if ScanMode::SingleQuote == mode {
469 return Some((quote_index, Quote::Single));
470 }
471 None
472}
473
474#[cfg(test)]
475mod tests {
476 #[test]
477 pub fn extract_word() {
478 let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
479 let line = "ls '/usr/local/b";
480 assert_eq!(
481 (4, "/usr/local/b"),
482 super::extract_word(line, line.len(), Some('\\'), &break_chars)
483 );
484 let line = "ls /User\\ Information";
485 assert_eq!(
486 (3, "/User\\ Information"),
487 super::extract_word(line, line.len(), Some('\\'), &break_chars)
488 );
489 }
490
491 #[test]
492 pub fn unescape() {
493 use std::borrow::Cow::{self, Borrowed, Owned};
494 let input = "/usr/local/b";
495 assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
496 if cfg!(windows) {
497 let input = "c:\\users\\All Users\\";
498 let result: Cow<str> = Borrowed(input);
499 assert_eq!(result, super::unescape(input, Some('\\')));
500 } else {
501 let input = "/User\\ Information";
502 let result: Cow<str> = Owned(String::from("/User Information"));
503 assert_eq!(result, super::unescape(input, Some('\\')));
504 }
505 }
506
507 #[test]
508 pub fn escape() {
509 let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
510 let input = String::from("/usr/local/b");
511 assert_eq!(
512 input.clone(),
513 super::escape(input, Some('\\'), &break_chars, super::Quote::None)
514 );
515 let input = String::from("/User Information");
516 let result = String::from("/User\\ Information");
517 assert_eq!(
518 result,
519 super::escape(input, Some('\\'), &break_chars, super::Quote::None)
520 );
521 }
522
523 #[test]
524 pub fn longest_common_prefix() {
525 let mut candidates = vec![];
526 {
527 let lcp = super::longest_common_prefix(&candidates);
528 assert!(lcp.is_none());
529 }
530
531 let s = "User";
532 let c1 = String::from(s);
533 candidates.push(c1.clone());
534 {
535 let lcp = super::longest_common_prefix(&candidates);
536 assert_eq!(Some(s), lcp);
537 }
538
539 let c2 = String::from("Users");
540 candidates.push(c2.clone());
541 {
542 let lcp = super::longest_common_prefix(&candidates);
543 assert_eq!(Some(s), lcp);
544 }
545
546 let c3 = String::from("");
547 candidates.push(c3.clone());
548 {
549 let lcp = super::longest_common_prefix(&candidates);
550 assert!(lcp.is_none());
551 }
552
553 let candidates = vec![String::from("fée"), String::from("fête")];
554 let lcp = super::longest_common_prefix(&candidates);
555 assert_eq!(Some("f"), lcp);
556 }
557
558 #[test]
559 pub fn find_unclosed_quote() {
560 assert_eq!(None, super::find_unclosed_quote("ls /etc"));
561 assert_eq!(
562 Some((3, super::Quote::Double)),
563 super::find_unclosed_quote("ls \"User Information")
564 );
565 assert_eq!(
566 None,
567 super::find_unclosed_quote("ls \"/User Information\" /etc")
568 );
569 assert_eq!(
570 Some((0, super::Quote::Double)),
571 super::find_unclosed_quote("\"c:\\users\\All Users\\")
572 )
573 }
574}