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 &self, 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 C {
117 type Candidate = C::Candidate;
118
119 fn complete(
120 &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(&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;
151use std::sync::Arc;
152box_completer! { Box Rc Arc }
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(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Result<(usize, Vec<Pair>)> {
261 self.complete_path(line, pos)
262 }
263}
264
265#[must_use]
267pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<'_, str> {
268 let esc_char = if let Some(c) = esc_char {
269 c
270 } else {
271 return Borrowed(input);
272 };
273 if !input.chars().any(|c| c == esc_char) {
274 return Borrowed(input);
275 }
276 let mut result = String::with_capacity(input.len());
277 let mut chars = input.chars();
278 while let Some(ch) = chars.next() {
279 if ch == esc_char {
280 if let Some(ch) = chars.next() {
281 if cfg!(windows) && ch != '"' {
282 result.push(esc_char);
284 }
285 result.push(ch);
286 } else if cfg!(windows) {
287 result.push(ch);
288 }
289 } else {
290 result.push(ch);
291 }
292 }
293 Owned(result)
294}
295
296#[must_use]
300pub fn escape(
301 mut input: String,
302 esc_char: Option<char>,
303 is_break_char: fn(char) -> bool,
304 quote: Quote,
305) -> String {
306 if quote == Quote::Single {
307 return input; }
309 let n = input.chars().filter(|c| is_break_char(*c)).count();
310 if n == 0 {
311 return input; }
313 let esc_char = if let Some(c) = esc_char {
314 c
315 } else {
316 if cfg!(windows) && quote == Quote::None {
317 input.insert(0, '"'); return input;
319 }
320 return input;
321 };
322 let mut result = String::with_capacity(input.len() + n);
323
324 for c in input.chars() {
325 if is_break_char(c) {
326 result.push(esc_char);
327 }
328 result.push(c);
329 }
330 result
331}
332
333fn filename_complete(
334 path: &str,
335 esc_char: Option<char>,
336 is_break_char: fn(char) -> bool,
337 quote: Quote,
338) -> Vec<Pair> {
339 #[cfg(feature = "with-dirs")]
340 use home::home_dir;
341 use std::env::current_dir;
342
343 let sep = path::MAIN_SEPARATOR;
344 let (dir_name, file_name) = match path.rfind(sep) {
345 Some(idx) => path.split_at(idx + sep.len_utf8()),
346 None => ("", path),
347 };
348
349 let dir_path = Path::new(dir_name);
350 let dir = if dir_path.starts_with("~") {
351 #[cfg(feature = "with-dirs")]
353 {
354 if let Some(home) = home_dir() {
355 match dir_path.strip_prefix("~") {
356 Ok(rel_path) => home.join(rel_path),
357 _ => home,
358 }
359 } else {
360 dir_path.to_path_buf()
361 }
362 }
363 #[cfg(not(feature = "with-dirs"))]
364 {
365 dir_path.to_path_buf()
366 }
367 } else if dir_path.is_relative() {
368 if let Ok(cwd) = current_dir() {
370 cwd.join(dir_path)
371 } else {
372 dir_path.to_path_buf()
373 }
374 } else {
375 dir_path.to_path_buf()
376 };
377
378 let mut entries: Vec<Pair> = Vec::new();
379
380 if !dir.exists() {
382 return entries;
383 }
384
385 if let Ok(read_dir) = dir.read_dir() {
387 let file_name = normalize(file_name);
388 for entry in read_dir.flatten() {
389 if let Some(s) = entry.file_name().to_str() {
390 let ns = normalize(s);
391 if ns.starts_with(file_name.as_ref()) {
392 if let Ok(metadata) = fs::metadata(entry.path()) {
393 let mut path = String::from(dir_name) + s;
394 if metadata.is_dir() {
395 path.push(sep);
396 }
397 entries.push(Pair {
398 display: String::from(s),
399 replacement: escape(path, esc_char, is_break_char, quote),
400 });
401 } }
403 }
404 }
405 }
406 entries
407}
408
409#[cfg(any(windows, target_os = "macos"))]
410fn normalize(s: &str) -> Cow<str> {
411 Owned(s.to_lowercase())
413}
414
415#[cfg(not(any(windows, target_os = "macos")))]
416fn normalize(s: &str) -> Cow<str> {
417 Cow::Borrowed(s)
418}
419
420#[must_use]
425pub fn extract_word(
426 line: &str,
427 pos: usize,
428 esc_char: Option<char>,
429 is_break_char: fn(char) -> bool,
430) -> (usize, &str) {
431 let line = &line[..pos];
432 if line.is_empty() {
433 return (0, line);
434 }
435 let mut start = None;
436 for (i, c) in line.char_indices().rev() {
437 if let (Some(esc_char), true) = (esc_char, start.is_some()) {
438 if esc_char == c {
439 start = None;
441 continue;
442 }
443 break;
444 }
445 if is_break_char(c) {
446 start = Some(i + c.len_utf8());
447 if esc_char.is_none() {
448 break;
449 } }
451 }
452
453 match start {
454 Some(start) => (start, &line[start..]),
455 None => (0, line),
456 }
457}
458
459pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> {
461 if candidates.is_empty() {
462 return None;
463 } else if candidates.len() == 1 {
464 return Some(candidates[0].replacement());
465 }
466 let mut longest_common_prefix = 0;
467 'o: loop {
468 for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) {
469 let b1 = c1.replacement().as_bytes();
470 let b2 = candidates[i + 1].replacement().as_bytes();
471 if b1.len() <= longest_common_prefix
472 || b2.len() <= longest_common_prefix
473 || b1[longest_common_prefix] != b2[longest_common_prefix]
474 {
475 break 'o;
476 }
477 }
478 longest_common_prefix += 1;
479 }
480 let candidate = candidates[0].replacement();
481 while !candidate.is_char_boundary(longest_common_prefix) {
482 longest_common_prefix -= 1;
483 }
484 if longest_common_prefix == 0 {
485 return None;
486 }
487 Some(&candidate[0..longest_common_prefix])
488}
489
490#[derive(Eq, PartialEq)]
491enum ScanMode {
492 DoubleQuote,
493 Escape,
494 EscapeInDoubleQuote,
495 Normal,
496 SingleQuote,
497}
498
499fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> {
503 let char_indices = s.char_indices();
504 let mut mode = ScanMode::Normal;
505 let mut quote_index = 0;
506 for (index, char) in char_indices {
507 match mode {
508 ScanMode::DoubleQuote => {
509 if char == '"' {
510 mode = ScanMode::Normal;
511 } else if char == '\\' {
512 mode = ScanMode::EscapeInDoubleQuote;
514 }
515 }
516 ScanMode::Escape => {
517 mode = ScanMode::Normal;
518 }
519 ScanMode::EscapeInDoubleQuote => {
520 mode = ScanMode::DoubleQuote;
521 }
522 ScanMode::Normal => {
523 if char == '"' {
524 mode = ScanMode::DoubleQuote;
525 quote_index = index;
526 } else if char == '\\' && cfg!(not(windows)) {
527 mode = ScanMode::Escape;
528 } else if char == '\'' && cfg!(not(windows)) {
529 mode = ScanMode::SingleQuote;
530 quote_index = index;
531 }
532 }
533 ScanMode::SingleQuote => {
534 if char == '\'' {
535 mode = ScanMode::Normal;
536 } }
538 };
539 }
540 if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode {
541 return Some((quote_index, Quote::Double));
542 } else if ScanMode::SingleQuote == mode {
543 return Some((quote_index, Quote::Single));
544 }
545 None
546}
547
548#[cfg(test)]
549mod tests {
550 #[test]
551 pub fn extract_word() {
552 let break_chars = super::default_break_chars;
553 let line = "ls '/usr/local/b";
554 assert_eq!(
555 (4, "/usr/local/b"),
556 super::extract_word(line, line.len(), Some('\\'), break_chars)
557 );
558 let line = "ls /User\\ Information";
559 assert_eq!(
560 (3, "/User\\ Information"),
561 super::extract_word(line, line.len(), Some('\\'), break_chars)
562 );
563 }
564
565 #[test]
566 pub fn unescape() {
567 use std::borrow::Cow::{self, Borrowed, Owned};
568 let input = "/usr/local/b";
569 assert_eq!(Borrowed(input), super::unescape(input, Some('\\')));
570 if cfg!(windows) {
571 let input = "c:\\users\\All Users\\";
572 let result: Cow<'_, str> = Borrowed(input);
573 assert_eq!(result, super::unescape(input, Some('\\')));
574 } else {
575 let input = "/User\\ Information";
576 let result: Cow<'_, str> = Owned(String::from("/User Information"));
577 assert_eq!(result, super::unescape(input, Some('\\')));
578 }
579 }
580
581 #[test]
582 pub fn escape() {
583 let break_chars = super::default_break_chars;
584 let input = String::from("/usr/local/b");
585 assert_eq!(
586 input.clone(),
587 super::escape(input, Some('\\'), break_chars, super::Quote::None)
588 );
589 let input = String::from("/User Information");
590 let result = String::from("/User\\ Information");
591 assert_eq!(
592 result,
593 super::escape(input, Some('\\'), break_chars, super::Quote::None)
594 );
595 }
596
597 #[test]
598 pub fn longest_common_prefix() {
599 let mut candidates = vec![];
600 {
601 let lcp = super::longest_common_prefix(&candidates);
602 assert!(lcp.is_none());
603 }
604
605 let s = "User";
606 let c1 = String::from(s);
607 candidates.push(c1);
608 {
609 let lcp = super::longest_common_prefix(&candidates);
610 assert_eq!(Some(s), lcp);
611 }
612
613 let c2 = String::from("Users");
614 candidates.push(c2);
615 {
616 let lcp = super::longest_common_prefix(&candidates);
617 assert_eq!(Some(s), lcp);
618 }
619
620 let c3 = String::from("");
621 candidates.push(c3);
622 {
623 let lcp = super::longest_common_prefix(&candidates);
624 assert!(lcp.is_none());
625 }
626
627 let candidates = vec![String::from("fée"), String::from("fête")];
628 let lcp = super::longest_common_prefix(&candidates);
629 assert_eq!(Some("f"), lcp);
630 }
631
632 #[test]
633 pub fn find_unclosed_quote() {
634 assert_eq!(None, super::find_unclosed_quote("ls /etc"));
635 assert_eq!(
636 Some((3, super::Quote::Double)),
637 super::find_unclosed_quote("ls \"User Information")
638 );
639 assert_eq!(
640 None,
641 super::find_unclosed_quote("ls \"/User Information\" /etc")
642 );
643 assert_eq!(
644 Some((0, super::Quote::Double)),
645 super::find_unclosed_quote("\"c:\\users\\All Users\\")
646 )
647 }
648
649 #[cfg(windows)]
650 #[test]
651 pub fn normalize() {
652 assert_eq!(super::normalize("Windows"), "windows")
653 }
654}