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