1use std::borrow::Cow::{self, Borrowed, Owned};
3use std::fs;
4use std::path::{self, Path};
5
6use crate::line_buffer::LineBuffer;
7use crate::{Context, Result};
8use memchr::memchr;
9
10pub trait Candidate {
12 fn display(&self) -> &str;
14 fn replacement(&self) -> &str;
16}
17
18impl Candidate for String {
19 fn display(&self) -> &str {
20 self.as_str()
21 }
22
23 fn replacement(&self) -> &str {
24 self.as_str()
25 }
26}
27
28impl Candidate for str {
30 fn display(&self) -> &str {
31 self
32 }
33
34 fn replacement(&self) -> &str {
35 self
36 }
37}
38
39impl Candidate for &'_ str {
40 fn display(&self) -> &str {
41 self
42 }
43
44 fn replacement(&self) -> &str {
45 self
46 }
47}
48
49pub struct Pair {
51 pub display: String,
53 pub replacement: String,
55}
56
57impl Candidate for Pair {
58 fn display(&self) -> &str {
59 self.display.as_str()
60 }
61
62 fn replacement(&self) -> &str {
63 self.replacement.as_str()
64 }
65}
66
67pub trait Completer {
72 type Candidate: Candidate;
74
75 fn complete(
83 &self, line: &str,
85 pos: usize,
86 ctx: &Context<'_>,
87 ) -> Result<(usize, Vec<Self::Candidate>)> {
88 let _ = (line, pos, ctx);
89 Ok((0, Vec::with_capacity(0)))
90 }
91 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
93 let end = line.pos();
94 line.replace(start..end, elected);
95 }
96}
97
98impl Completer for () {
99 type Candidate = String;
100
101 fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {
102 unreachable!();
103 }
104}
105
106impl<'c, C: ?Sized + Completer> Completer for &'c C {
107 type Candidate = C::Candidate;
108
109 fn complete(
110 &self,
111 line: &str,
112 pos: usize,
113 ctx: &Context<'_>,
114 ) -> Result<(usize, Vec<Self::Candidate>)> {
115 (**self).complete(line, pos, ctx)
116 }
117
118 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
119 (**self).update(line, start, elected);
120 }
121}
122macro_rules! box_completer {
123 ($($id: ident)*) => {
124 $(
125 impl<C: ?Sized + Completer> Completer for $id<C> {
126 type Candidate = C::Candidate;
127
128 fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec<Self::Candidate>)> {
129 (**self).complete(line, pos, ctx)
130 }
131 fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) {
132 (**self).update(line, start, elected)
133 }
134 }
135 )*
136 }
137}
138
139use std::rc::Rc;
140use std::sync::Arc;
141box_completer! { Box Rc Arc }
142
143pub struct FilenameCompleter {
145 break_chars: &'static [u8],
146 double_quotes_special_chars: &'static [u8],
147}
148
149const DOUBLE_QUOTES_ESCAPE_CHAR: Option<char> = Some('\\');
150
151cfg_if::cfg_if! {
152 if #[cfg(unix)] {
153 const DEFAULT_BREAK_CHARS: [u8; 18] = [
155 b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&',
156 b'{', b'(', b'\0',
157 ];
158 const ESCAPE_CHAR: Option<char> = Some('\\');
159 const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`'];
162 } else if #[cfg(windows)] {
163 const DEFAULT_BREAK_CHARS: [u8; 17] = [
165 b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{',
166 b'(', b'\0',
167 ];
168 const ESCAPE_CHAR: Option<char> = None;
169 const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; } else if #[cfg(target_arch = "wasm32")] {
171 const DEFAULT_BREAK_CHARS: [u8; 0] = [];
172 const ESCAPE_CHAR: Option<char> = None;
173 const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 0] = [];
174 }
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub enum Quote {
180 Double,
182 Single,
184 None,
186}
187
188impl FilenameCompleter {
189 #[must_use]
191 pub fn new() -> Self {
192 Self {
193 break_chars: &DEFAULT_BREAK_CHARS,
194 double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS,
195 }
196 }
197
198 pub fn complete_path(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
202 let (start, path, esc_char, break_chars, quote) =
203 if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) {
204 let start = idx + 1;
205 if quote == Quote::Double {
206 (
207 start,
208 unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR),
209 DOUBLE_QUOTES_ESCAPE_CHAR,
210 &self.double_quotes_special_chars,
211 quote,
212 )
213 } else {
214 (
215 start,
216 Borrowed(&line[start..pos]),
217 None,
218 &self.break_chars,
219 quote,
220 )
221 }
222 } else {
223 let (start, path) = extract_word(line, pos, ESCAPE_CHAR, self.break_chars);
224 let path = unescape(path, ESCAPE_CHAR);
225 (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None)
226 };
227 let mut matches = filename_complete(&path, esc_char, break_chars, quote);
228 #[allow(clippy::unnecessary_sort_by)]
229 matches.sort_by(|a, b| a.display().cmp(b.display()));
230 Ok((start, matches))
231 }
232}
233
234impl Default for FilenameCompleter {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240impl Completer for FilenameCompleter {
241 type Candidate = Pair;
242
243 fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Result<(usize, Vec<Pair>)> {
244 self.complete_path(line, pos)
245 }
246}
247
248#[must_use]
250pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<'_, str> {
251 let esc_char = if let Some(c) = esc_char {
252 c
253 } else {
254 return Borrowed(input);
255 };
256 if !input.chars().any(|c| c == esc_char) {
257 return Borrowed(input);
258 }
259 let mut result = String::with_capacity(input.len());
260 let mut chars = input.chars();
261 while let Some(ch) = chars.next() {
262 if ch == esc_char {
263 if let Some(ch) = chars.next() {
264 if cfg!(windows) && ch != '"' {
265 result.push(esc_char);
267 }
268 result.push(ch);
269 } else if cfg!(windows) {
270 result.push(ch);
271 }
272 } else {
273 result.push(ch);
274 }
275 }
276 Owned(result)
277}
278
279#[must_use]
283pub fn escape(
284 mut input: String,
285 esc_char: Option<char>,
286 break_chars: &[u8],
287 quote: Quote,
288) -> String {
289 if quote == Quote::Single {
290 return input; }
292 let n = input
293 .bytes()
294 .filter(|b| memchr(*b, break_chars).is_some())
295 .count();
296 if n == 0 {
297 return input; }
299 let esc_char = if let Some(c) = esc_char {
300 c
301 } else {
302 if cfg!(windows) && quote == Quote::None {
303 input.insert(0, '"'); return input;
305 }
306 return input;
307 };
308 let mut result = String::with_capacity(input.len() + n);
309
310 for c in input.chars() {
311 if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
312 result.push(esc_char);
313 }
314 result.push(c);
315 }
316 result
317}
318
319fn filename_complete(
320 path: &str,
321 esc_char: Option<char>,
322 break_chars: &[u8],
323 quote: Quote,
324) -> Vec<Pair> {
325 #[cfg(feature = "with-dirs")]
326 use dirs_next::home_dir;
327 use std::env::current_dir;
328
329 let sep = path::MAIN_SEPARATOR;
330 let (dir_name, file_name) = match path.rfind(sep) {
331 Some(idx) => path.split_at(idx + sep.len_utf8()),
332 None => ("", path),
333 };
334
335 let dir_path = Path::new(dir_name);
336 let dir = if dir_path.starts_with("~") {
337 #[cfg(feature = "with-dirs")]
339 {
340 if let Some(home) = home_dir() {
341 match dir_path.strip_prefix("~") {
342 Ok(rel_path) => home.join(rel_path),
343 _ => home,
344 }
345 } else {
346 dir_path.to_path_buf()
347 }
348 }
349 #[cfg(not(feature = "with-dirs"))]
350 {
351 dir_path.to_path_buf()
352 }
353 } else if dir_path.is_relative() {
354 if let Ok(cwd) = current_dir() {
356 cwd.join(dir_path)
357 } else {
358 dir_path.to_path_buf()
359 }
360 } else {
361 dir_path.to_path_buf()
362 };
363
364 let mut entries: Vec<Pair> = Vec::new();
365
366 if !dir.exists() {
368 return entries;
369 }
370
371 if let Ok(read_dir) = dir.read_dir() {
373 let file_name = normalize(file_name);
374 for entry in read_dir.flatten() {
375 if let Some(s) = entry.file_name().to_str() {
376 let ns = normalize(s);
377 if ns.starts_with(file_name.as_ref()) {
378 if let Ok(metadata) = fs::metadata(entry.path()) {
379 let mut path = String::from(dir_name) + s;
380 if metadata.is_dir() {
381 path.push(sep);
382 }
383 entries.push(Pair {
384 display: String::from(s),
385 replacement: escape(path, esc_char, break_chars, quote),
386 });
387 } }
389 }
390 }
391 }
392 entries
393}
394
395#[cfg(any(windows, target_os = "macos"))]
396fn normalize(s: &str) -> Cow<str> {
397 Owned(s.to_lowercase())
399}
400
401#[cfg(not(any(windows, target_os = "macos")))]
402fn normalize(s: &str) -> Cow<str> {
403 Cow::Borrowed(s)
404}
405
406#[must_use]
411pub fn extract_word<'l>(
412 line: &'l str,
413 pos: usize,
414 esc_char: Option<char>,
415 break_chars: &[u8],
416) -> (usize, &'l str) {
417 let line = &line[..pos];
418 if line.is_empty() {
419 return (0, line);
420 }
421 let mut start = None;
422 for (i, c) in line.char_indices().rev() {
423 if let (Some(esc_char), true) = (esc_char, start.is_some()) {
424 if esc_char == c {
425 start = None;
427 continue;
428 }
429 break;
430 }
431 if c.is_ascii() && memchr(c as u8, break_chars).is_some() {
432 start = Some(i + c.len_utf8());
433 if esc_char.is_none() {
434 break;
435 } }
437 }
438
439 match start {
440 Some(start) => (start, &line[start..]),
441 None => (0, line),
442 }
443}
444
445pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> {
447 if candidates.is_empty() {
448 return None;
449 } else if candidates.len() == 1 {
450 return Some(candidates[0].replacement());
451 }
452 let mut longest_common_prefix = 0;
453 'o: loop {
454 for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
455 let b1 = c1.replacement().as_bytes();
456 let b2 = candidates[i + 1].replacement().as_bytes();
457 if b1.len() <= longest_common_prefix
458 || b2.len() <= longest_common_prefix
459 || b1[longest_common_prefix] != b2[longest_common_prefix]
460 {
461 break 'o;
462 }
463 }
464 longest_common_prefix += 1;
465 }
466 let candidate = candidates[0].replacement();
467 while !candidate.is_char_boundary(longest_common_prefix) {
468 longest_common_prefix -= 1;
469 }
470 if longest_common_prefix == 0 {
471 return None;
472 }
473 Some(&candidate[0..longest_common_prefix])
474}
475
476#[derive(Eq, PartialEq)]
477enum ScanMode {
478 DoubleQuote,
479 Escape,
480 EscapeInDoubleQuote,
481 Normal,
482 SingleQuote,
483}
484
485fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> {
489 let char_indices = s.char_indices();
490 let mut mode = ScanMode::Normal;
491 let mut quote_index = 0;
492 for (index, char) in char_indices {
493 match mode {
494 ScanMode::DoubleQuote => {
495 if char == '"' {
496 mode = ScanMode::Normal;
497 } else if char == '\\' {
498 mode = ScanMode::EscapeInDoubleQuote;
500 }
501 }
502 ScanMode::Escape => {
503 mode = ScanMode::Normal;
504 }
505 ScanMode::EscapeInDoubleQuote => {
506 mode = ScanMode::DoubleQuote;
507 }
508 ScanMode::Normal => {
509 if char == '"' {
510 mode = ScanMode::DoubleQuote;
511 quote_index = index;
512 } else if char == '\\' && cfg!(not(windows)) {
513 mode = ScanMode::Escape;
514 } else if char == '\'' && cfg!(not(windows)) {
515 mode = ScanMode::SingleQuote;
516 quote_index = index;
517 }
518 }
519 ScanMode::SingleQuote => {
520 if char == '\'' {
521 mode = ScanMode::Normal;
522 } }
524 };
525 }
526 if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode {
527 return Some((quote_index, Quote::Double));
528 } else if ScanMode::SingleQuote == mode {
529 return Some((quote_index, Quote::Single));
530 }
531 None
532}
533
534#[cfg(test)]
535mod tests {
536 #[test]
537 pub fn extract_word() {
538 let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
539 let line = "ls '/usr/local/b";
540 assert_eq!(
541 (4, "/usr/local/b"),
542 super::extract_word(line, line.len(), Some('\\'), break_chars)
543 );
544 let line = "ls /User\\ Information";
545 assert_eq!(
546 (3, "/User\\ Information"),
547 super::extract_word(line, line.len(), Some('\\'), break_chars)
548 );
549 }
550
551 #[test]
552 pub fn unescape() {
553 use std::borrow::Cow::{self, Borrowed, Owned};
554 let input = "/usr/local/b";
555 assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
556 if cfg!(windows) {
557 let input = "c:\\users\\All Users\\";
558 let result: Cow<'_, str> = Borrowed(input);
559 assert_eq!(result, super::unescape(input, Some('\\')));
560 } else {
561 let input = "/User\\ Information";
562 let result: Cow<'_, str> = Owned(String::from("/User Information"));
563 assert_eq!(result, super::unescape(input, Some('\\')));
564 }
565 }
566
567 #[test]
568 pub fn escape() {
569 let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS;
570 let input = String::from("/usr/local/b");
571 assert_eq!(
572 input.clone(),
573 super::escape(input, Some('\\'), break_chars, super::Quote::None)
574 );
575 let input = String::from("/User Information");
576 let result = String::from("/User\\ Information");
577 assert_eq!(
578 result,
579 super::escape(input, Some('\\'), break_chars, super::Quote::None)
580 );
581 }
582
583 #[test]
584 pub fn longest_common_prefix() {
585 let mut candidates = vec![];
586 {
587 let lcp = super::longest_common_prefix(&candidates);
588 assert!(lcp.is_none());
589 }
590
591 let s = "User";
592 let c1 = String::from(s);
593 candidates.push(c1);
594 {
595 let lcp = super::longest_common_prefix(&candidates);
596 assert_eq!(Some(s), lcp);
597 }
598
599 let c2 = String::from("Users");
600 candidates.push(c2);
601 {
602 let lcp = super::longest_common_prefix(&candidates);
603 assert_eq!(Some(s), lcp);
604 }
605
606 let c3 = String::from("");
607 candidates.push(c3);
608 {
609 let lcp = super::longest_common_prefix(&candidates);
610 assert!(lcp.is_none());
611 }
612
613 let candidates = vec![String::from("fée"), String::from("fête")];
614 let lcp = super::longest_common_prefix(&candidates);
615 assert_eq!(Some("f"), lcp);
616 }
617
618 #[test]
619 pub fn find_unclosed_quote() {
620 assert_eq!(None, super::find_unclosed_quote("ls /etc"));
621 assert_eq!(
622 Some((3, super::Quote::Double)),
623 super::find_unclosed_quote("ls \"User Information")
624 );
625 assert_eq!(
626 None,
627 super::find_unclosed_quote("ls \"/User Information\" /etc")
628 );
629 assert_eq!(
630 Some((0, super::Quote::Double)),
631 super::find_unclosed_quote("\"c:\\users\\All Users\\")
632 )
633 }
634
635 #[cfg(windows)]
636 #[test]
637 pub fn normalize() {
638 assert_eq!(super::normalize("Windows"), "windows")
639 }
640}