1use std::borrow::Cow::{self, Borrowed, Owned};
2use std::cell::RefCell;
3use std::path::Path;
4use std::rc::Rc;
5
6use colored::*;
7
8use rustyline::completion::{Completer, Pair};
9use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
10use rustyline::hint::{Hinter, HistoryHinter};
11use rustyline::validate::{self, MatchingBracketValidator, Validator};
12use rustyline::{Config, Context, Editor};
13use rustyline_derive::Helper;
14
15use crate::command::Completion;
16use crate::command_set::CommandSet;
17use crate::parser::Parser;
18use crate::shell::Shell;
19use crate::Result;
20
21pub struct Readline<'a, S> {
23 rl: Editor<ExecHelper<'a, S>>,
24}
25
26impl<'a, S> Readline<'a, S> {
27 pub fn new(
29 parser: Parser,
30 cmds: Rc<RefCell<CommandSet<'a, S>>>,
31 builtins: Rc<CommandSet<'a, Shell<'a, S>>>,
32 ) -> Readline<'a, S> {
33 let config = Config::builder()
34 .completion_type(rustyline::CompletionType::List)
35 .build();
36 let mut rl = Editor::with_config(config);
37 rl.set_helper(Some(ExecHelper::new(parser, cmds, builtins)));
38 Readline { rl }
39 }
40
41 pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
46 self.rl.load_history(path)?;
47 Ok(())
48 }
49
50 pub fn save_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
55 self.rl.save_history(path)?;
56 Ok(())
57 }
58
59 pub fn add_history_entry<E: AsRef<str> + Into<String>>(&mut self, line: E) -> bool {
62 self.rl.add_history_entry(line)
63 }
64
65 pub fn readline(&mut self, prompt: &str) -> rustyline::Result<String> {
70 let mut input = self.rl.readline(prompt)?;
71 input = input.replace("\\\n", "");
81
82 Ok(input)
83 }
84
85 pub fn history(&self) -> &rustyline::history::History {
96 self.rl.history()
97 }
98}
99
100#[derive(Helper)]
101pub struct ExecHelper<'a, S> {
103 completer: ExecCompleter<'a, S>,
104 highlighter: MatchingBracketHighlighter,
105 validator: ExecValidator,
106 hinter: HistoryHinter,
107 colored_prompt: String,
108}
109
110impl<'a, S> ExecHelper<'a, S> {
111 fn new(
113 parser: Parser,
114 cmds: Rc<RefCell<CommandSet<'a, S>>>,
115 builtins: Rc<CommandSet<'a, Shell<'a, S>>>,
116 ) -> ExecHelper<'a, S> {
117 ExecHelper {
118 completer: ExecCompleter::new(parser, cmds, builtins),
119 highlighter: MatchingBracketHighlighter::new(),
120 validator: ExecValidator::new(),
121 hinter: HistoryHinter {},
122 colored_prompt: "| ".to_string(),
123 }
124 }
125}
126
127impl<'a, S> Completer for ExecHelper<'a, S> {
128 type Candidate = Pair;
129
130 fn complete(
131 &self,
132 line: &str,
133 pos: usize,
134 _: &Context<'_>,
135 ) -> rustyline::Result<(usize, Vec<Pair>)> {
136 Ok(self.completer.complete(line, pos))
137 }
138}
139
140impl<'a, S> Hinter for ExecHelper<'a, S> {
141 type Hint = String;
142
143 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
144 self.hinter.hint(line, pos, ctx)
145 }
146}
147
148impl<'a, S> Highlighter for ExecHelper<'a, S> {
149 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
150 &'s self,
151 prompt: &'p str,
152 default: bool,
153 ) -> Cow<'b, str> {
154 if default {
155 Borrowed(&self.colored_prompt)
156 } else {
157 Borrowed(prompt)
158 }
159 }
160
161 fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
162 self.highlighter.highlight(line, pos)
163 }
164
165 fn highlight_char(&self, line: &str, pos: usize) -> bool {
166 self.highlighter.highlight_char(line, pos)
167 }
168
169 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
170 Owned(hint.black().bold().to_string())
171 }
172}
173
174impl<'a, S> Validator for ExecHelper<'a, S> {
175 fn validate(
176 &self,
177 ctx: &mut validate::ValidationContext,
178 ) -> rustyline::Result<validate::ValidationResult> {
179 self.validator.validate(ctx)
180 }
181
182 fn validate_while_typing(&self) -> bool {
183 self.validator.validate_while_typing()
184 }
185}
186
187struct ExecValidator {
189 brackets: MatchingBracketValidator,
190}
191
192impl ExecValidator {
193 fn new() -> ExecValidator {
194 ExecValidator {
195 brackets: MatchingBracketValidator::new(),
196 }
197 }
198
199 fn is_currently_in_quote(&self, input: &str) -> bool {
200 let input_iter = input.chars();
201
202 let mut escaped = false;
203 let mut currently_in_quote = false;
204 let mut current_quote = ' ';
205
206 for ch in input_iter {
232 if ch == '\\' {
233 escaped = !escaped;
234 continue;
235 }
236
237 let is_quote = ch == '\"' || ch == '\'';
238 if is_quote && !escaped {
239 if currently_in_quote && ch == current_quote {
240 currently_in_quote = false;
243 current_quote = ' ';
245 } else if currently_in_quote && ch != current_quote {
246 continue;
249 } else {
250 currently_in_quote = true;
253 current_quote = ch;
254 }
255 }
256
257 escaped = false;
260 }
261
262 currently_in_quote
263 }
264
265 #[allow(clippy::unnecessary_wraps)]
266 fn validate_quotes(&self, cur_input: &str) -> rustyline::Result<validate::ValidationResult> {
267 if self.is_currently_in_quote(cur_input) {
268 return Ok(validate::ValidationResult::Incomplete);
269 }
270
271 Ok(validate::ValidationResult::Valid(None))
272 }
273
274 #[allow(clippy::unnecessary_wraps)]
288 fn validate_multiline(&self, cur_input: &str) -> rustyline::Result<validate::ValidationResult> {
289 if let Some('\\') = cur_input.chars().last() {
290 return Ok(validate::ValidationResult::Incomplete);
291 }
292
293 Ok(validate::ValidationResult::Valid(None))
294 }
295
296 fn merge_validation_results(
297 &self,
298 reses: Vec<validate::ValidationResult>,
299 ) -> validate::ValidationResult {
300 for res in reses.into_iter() {
301 match res {
302 validate::ValidationResult::Valid(_) => continue,
303 _ => return res,
304 };
305 }
306
307 validate::ValidationResult::Valid(None)
308 }
309}
310
311impl Validator for ExecValidator {
312 fn validate(
313 &self,
314 ctx: &mut validate::ValidationContext,
315 ) -> rustyline::Result<validate::ValidationResult> {
316 Ok(self.merge_validation_results(vec![
317 self.brackets.validate(ctx)?,
318 self.validate_quotes(ctx.input())?,
319 self.validate_multiline(ctx.input())?,
320 ]))
321 }
322
323 fn validate_while_typing(&self) -> bool {
324 self.brackets.validate_while_typing()
325 }
326}
327
328struct ExecCompleter<'a, S> {
330 parser: Parser,
331 cmds: Rc<RefCell<CommandSet<'a, S>>>,
332 builtins: Rc<CommandSet<'a, Shell<'a, S>>>,
333}
334
335impl<'a, S> ExecCompleter<'a, S> {
336 fn new(
343 parser: Parser,
344 cmds: Rc<RefCell<CommandSet<'a, S>>>,
345 builtins: Rc<CommandSet<'a, Shell<'a, S>>>,
346 ) -> ExecCompleter<'a, S> {
347 ExecCompleter {
348 parser,
349 cmds,
350 builtins,
351 }
352 }
353
354 fn complete(&self, line: &str, pos: usize) -> (usize, Vec<Pair>) {
375 let partial = match line.get(..pos) {
378 Some(p) => p,
379 None => {
380 return (0, Vec::new());
383 }
384 };
385
386 let outcome = self
388 .parser
389 .parse(partial, &self.cmds.borrow(), &self.builtins);
390
391 if outcome.complete {
394 match outcome.leaf_completion {
395 None => {
396 return (pos, vec![]);
397 }
398 Some(completion) => match completion {
399 Completion::Nothing => {
400 return (pos, vec![]);
401 }
402 Completion::PartialArgCompletion(arg_suffixes) => {
403 return (
404 pos,
405 arg_suffixes
406 .iter()
407 .map(|suffix_poss| Pair {
408 display: suffix_poss.clone(),
409 replacement: suffix_poss.clone(),
410 })
411 .collect(),
412 );
413 }
414 Completion::Possibilities(possibilities) => {
415 if !partial.ends_with(' ') {
422 return (
423 pos,
424 vec![Pair {
425 display: String::from(" "),
426 replacement: String::from(" "),
427 }],
428 );
429 }
430
431 return (
432 pos,
433 possibilities
434 .iter()
435 .map(|poss| Pair {
436 display: poss.clone(),
437 replacement: poss.clone(),
438 })
439 .collect(),
440 );
441 }
442 },
443 }
444 }
445
446 let prefix = if let Some(first_token) = outcome.remaining.first() {
450 first_token
451 } else {
452 if partial.is_empty() {
461 ""
462 } else if !partial.ends_with(' ') {
463 return (
466 pos,
467 vec![Pair {
468 display: String::from(" "),
469 replacement: String::from(" "),
470 }],
471 );
472 } else {
473 ""
476 }
477 };
478
479 let candidates = outcome.possibilities.into_iter().filter_map(|poss| {
482 if poss.starts_with(prefix) {
483 poss.get(prefix.len()..).map(|s| s.to_string())
486 } else {
487 None
488 }
489 });
490
491 let pairs: Vec<Pair> = candidates
493 .map(|candidate| Pair {
494 display: candidate.to_string(),
495 replacement: candidate,
498 })
499 .collect();
500
501 (pos, pairs)
502 }
503}
504
505#[cfg(test)]
506mod test {
507 use super::*;
508
509 mod completions {
510 use super::*;
511 use crate::parser::test::make_parser_cmds;
512 use crate::parser::Parser;
513
514 use pretty_assertions::assert_eq;
515
516 fn make_completer<'a>() -> ExecCompleter<'a, ()> {
517 let (cmds, builtins) = make_parser_cmds();
518
519 let cmds = Rc::new(RefCell::new(cmds));
521 let builtins = Rc::new(builtins);
522
523 ExecCompleter::new(Parser::new(), cmds, builtins)
524 }
525
526 fn test_completion(
527 completer: ExecCompleter<'_, ()>,
528 line: &str,
529 pos: usize,
530 expected_pairs: Vec<Pair>,
531 ) {
532 let cmpl_res = completer.complete(line, pos);
533 let (cmpl_pos, pairs) = cmpl_res;
534 assert_eq!(cmpl_pos, pos, "mismatched positions");
536
537 assert_eq!(
538 pairs.len(),
539 expected_pairs.len(),
540 "mismatched number of completions"
541 );
542
543 for (p1, p2) in pairs.iter().zip(expected_pairs.iter()) {
544 assert_eq!(p1.display, p2.display, "non-matching display strings");
545 assert_eq!(
546 p1.replacement, p2.replacement,
547 "non-matching replacement strings"
548 );
549 }
550 }
551
552 #[test]
553 fn simple() {
554 let completer = make_completer();
555
556 let line = "grau";
557
558 test_completion(
559 completer,
560 line,
561 line.len(),
562 vec![Pair {
563 display: "lt-c".to_string(),
564 replacement: "lt-c".to_string(),
565 }],
566 )
567 }
568
569 #[test]
570 fn no_matches() {
571 let completer = make_completer();
572
573 let line = "idontexistlol";
574
575 test_completion(completer, line, line.len(), vec![])
576 }
577
578 #[test]
579 fn multiple_matches() {
580 let completer = make_completer();
581
582 let line = "conflict-";
583
584 test_completion(
585 completer,
586 line,
587 line.len(),
588 vec![
589 Pair {
590 display: "tie".to_string(),
591 replacement: "tie".to_string(),
592 },
593 Pair {
594 display: "builtin-longer-match-but-still-loses".to_string(),
595 replacement: "builtin-longer-match-but-still-loses".to_string(),
596 },
597 Pair {
598 display: "custom-wins".to_string(),
599 replacement: "custom-wins".to_string(),
600 },
601 ],
602 )
603 }
604
605 #[test]
606 fn nested() {
607 let completer = make_completer();
608
609 let line = "foo-c qu";
610
611 test_completion(
612 completer,
613 line,
614 line.len(),
615 vec![Pair {
616 display: "x-c".to_string(),
617 replacement: "x-c".to_string(),
618 }],
619 )
620 }
621
622 #[test]
623 fn already_completed() {
624 let completer = make_completer();
625
626 let line = "foo-c qux-c quux-c";
627
628 test_completion(completer, line, line.len(), vec![])
629 }
630
631 #[test]
632 fn completely_blank_for_last_command() {
633 let completer = make_completer();
634
635 let line = "foo-c qux-c ";
636
637 test_completion(
638 completer,
639 line,
640 line.len(),
641 vec![
642 Pair {
643 display: "quux-c".to_string(),
644 replacement: "quux-c".to_string(),
645 },
646 Pair {
647 display: "corge-c".to_string(),
648 replacement: "corge-c".to_string(),
649 },
650 ],
651 )
652 }
653
654 #[test]
655 fn completion_includes_a_space() {
656 let completer = make_completer();
657
658 let line = "foo-c qux-c";
659
660 test_completion(
661 completer,
662 line,
663 line.len(),
664 vec![Pair {
665 display: " ".to_string(),
666 replacement: " ".to_string(),
667 }],
668 )
669 }
670
671 #[test]
672 fn nothing_typed() {
673 let completer = make_completer();
674
675 let line = "";
676
677 test_completion(
678 completer,
679 line,
680 line.len(),
681 vec![
682 Pair {
683 display: "foo-c".to_string(),
684 replacement: "foo-c".to_string(),
685 },
686 Pair {
687 display: "grault-c".to_string(),
688 replacement: "grault-c".to_string(),
689 },
690 Pair {
691 display: "conflict-tie".to_string(),
692 replacement: "conflict-tie".to_string(),
693 },
694 Pair {
695 display: "conflict-builtin-longer-match-but-still-loses".to_string(),
696 replacement: "conflict-builtin-longer-match-but-still-loses".to_string(),
697 },
698 Pair {
699 display: "conflict-custom-wins".to_string(),
700 replacement: "conflict-custom-wins".to_string(),
701 },
702 ],
703 )
704 }
705
706 #[test]
707 fn non_end_pos() {
708 let completer = make_completer();
709
710 let line = "grault-c";
711
712 test_completion(
713 completer,
714 line, 3, vec![Pair {
717 display: "ult-c".to_string(),
718 replacement: "ult-c".to_string(),
719 }],
720 )
721 }
722
723 #[test]
724 fn nested_non_end_pos() {
725 let completer = make_completer();
726
727 let line = "foo-c qux-c quux-c";
728
729 test_completion(
730 completer,
731 line, 8, vec![Pair {
734 display: "x-c".to_string(),
735 replacement: "x-c".to_string(),
736 }],
737 )
738 }
739 }
740
741 mod validator {
742 use super::*;
743
744 #[derive(Debug, PartialEq)]
745 enum TestValidationResult {
746 Valid,
747 Invalid,
748 Incomplete,
749 }
750
751 impl From<validate::ValidationResult> for TestValidationResult {
752 fn from(res: validate::ValidationResult) -> Self {
753 match res {
754 validate::ValidationResult::Valid(_) => TestValidationResult::Valid,
755 validate::ValidationResult::Invalid(_) => TestValidationResult::Invalid,
756 validate::ValidationResult::Incomplete => TestValidationResult::Incomplete,
757 _ => panic!("unexpected ValidationResult kind"),
761 }
762 }
763 }
764
765 fn validation_res_eq(a: validate::ValidationResult, b: validate::ValidationResult) {
766 assert_eq!(TestValidationResult::from(a), TestValidationResult::from(b));
767 }
768
769 fn check_validation_res(
770 res: rustyline::Result<validate::ValidationResult>,
771 expected: validate::ValidationResult,
772 ) {
773 match res {
774 Ok(res) => {
775 validation_res_eq(res, expected);
776 }
777 Err(err) => {
778 panic!("did not expect an error during validation: {}", err);
779 }
780 }
781 }
782
783 fn test_validation_quotes(input: &str, expected_validity: validate::ValidationResult) {
784 let validator = ExecValidator::new();
785
786 let validation_res = validator.validate_quotes(input);
789
790 check_validation_res(validation_res, expected_validity);
791 }
792
793 fn test_validation_multiline(input: &str, expected_validity: validate::ValidationResult) {
794 let validator = ExecValidator::new();
795
796 let validation_res = validator.validate_multiline(input);
797
798 check_validation_res(validation_res, expected_validity);
799 }
800
801 #[test]
802 fn one_single_quote() {
803 test_validation_quotes("\'", validate::ValidationResult::Incomplete);
804 }
805
806 #[test]
807 fn one_double_quote() {
808 test_validation_quotes("\"", validate::ValidationResult::Incomplete);
809 }
810
811 #[test]
812 fn balanced_single() {
813 test_validation_quotes("\'\'", validate::ValidationResult::Valid(None));
814 }
815
816 #[test]
817 fn balanced_double() {
818 test_validation_quotes("\"\"", validate::ValidationResult::Valid(None));
819 }
820
821 #[test]
822 fn unbalanced_but_escaped_is_ok() {
823 test_validation_quotes("\\'", validate::ValidationResult::Valid(None));
824 }
825
826 #[test]
827 fn balanced_but_mismatched_quote_types_is_incomplete() {
828 test_validation_quotes("\'\"", validate::ValidationResult::Incomplete);
829 }
830
831 #[test]
832 fn nested_quotes_unbalanced_still_incomplete() {
833 test_validation_quotes("\'\"\'\"\'", validate::ValidationResult::Incomplete);
834 }
835
836 #[test]
837 fn overlapping_but_balanced_quotes_is_incomplete() {
838 test_validation_quotes("\' \" \' \"", validate::ValidationResult::Incomplete);
839 }
840
841 #[test]
842 fn multiple_escapes_valid() {
843 test_validation_quotes("\\\\\\'", validate::ValidationResult::Valid(None));
846 }
847
848 #[test]
849 fn multiple_escapes_incomplete() {
850 test_validation_quotes("\\\\\\\\'", validate::ValidationResult::Incomplete);
853 }
854
855 #[test]
856 fn closed_quote_block_with_unmatched_quote_inside_is_valid() {
857 test_validation_quotes("\"'\"", validate::ValidationResult::Valid(None));
858 }
859
860 #[test]
861 fn many_quoted_blocks() {
862 test_validation_quotes(
863 "\'hey how are you?\' \"im doing ok\" \'please thank me for asking\'",
864 validate::ValidationResult::Valid(None),
865 );
866
867 test_validation_quotes(
868 "'hey how are you?' \"im doing ok\" \\\\'please thank me for asking'",
869 validate::ValidationResult::Valid(None),
870 );
871 }
872
873 #[test]
874 fn slash_at_end_is_incomplete() {
875 test_validation_multiline("hello world\\", validate::ValidationResult::Incomplete);
876 }
877
878 #[test]
879 fn slash_with_trailing_character_is_complete() {
880 test_validation_multiline("hello world\\g", validate::ValidationResult::Valid(None));
881 }
882
883 #[test]
884 fn slash_with_trailing_space_is_complete() {
885 test_validation_multiline("hello world\\ ", validate::ValidationResult::Valid(None));
886 }
887
888 #[test]
889 fn no_issues_is_complete() {
890 test_validation_multiline("hello world", validate::ValidationResult::Valid(None));
891 }
892 }
893}