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