1use nix::errno::Errno;
2use rustyline::{
3 completion::{Completer, Pair},
4 error::ReadlineError,
5 highlight::Highlighter,
6 hint::Hinter,
7 history::FileHistory,
8 validate::Validator,
9 Context, Editor, Helper,
10};
11
12use crate::{
13 command::Registry,
14 diag::{Error, Result},
15 progress::State,
16};
17
18struct CmdHelper {
19 options: Vec<String>,
20}
21
22impl Completer for CmdHelper {
23 type Candidate = Pair;
24
25 fn complete(
26 &self,
27 line: &str,
28 pos: usize,
29 _: &Context<'_>,
30 ) -> rustyline::Result<(usize, Vec<Pair>)> {
31 let prefix = line[..pos].split_whitespace().collect::<Vec<_>>().join(" ");
32 let input_words: Vec<&str> = prefix.split_whitespace().collect();
33
34 let matches = self
35 .options
36 .iter()
37 .filter(|cmd| {
38 let cmd_words: Vec<&str> = cmd.split_whitespace().collect();
39 input_words
40 .iter()
41 .zip(cmd_words.iter())
42 .all(|(input, cmd)| cmd.starts_with(input))
43 && input_words.len() <= cmd_words.len()
44 })
45 .map(|cmd| Pair {
46 display: cmd.as_str().to_owned(),
47 replacement: cmd.as_str().to_owned(),
48 })
49 .collect();
50
51 Ok((0, matches))
52 }
53}
54
55impl Hinter for CmdHelper {
56 type Hint = String;
57}
58impl Highlighter for CmdHelper {}
59impl Validator for CmdHelper {}
60impl Helper for CmdHelper {}
61
62pub struct Runner {
69 readline: Editor<CmdHelper, FileHistory>,
70 registry: Registry,
71}
72
73impl Runner {
74 pub fn new() -> Result<Self> {
86 let registry = Registry::default();
87 let mut readline = Editor::new()?;
88 readline.set_helper(Some(CmdHelper {
89 options: registry.completions(),
90 }));
91
92 Ok(Self { readline, registry })
93 }
94
95 pub fn run(&mut self, state: &mut State) -> Result<()> {
112 let readline = self.readline.readline("dbg> ");
113 match readline {
114 Ok(line) => {
115 let trimmed = line.trim();
116 let input = if trimmed.is_empty() {
117 self.readline
118 .history()
119 .into_iter()
120 .last()
121 .map_or("", |s| s.trim())
122 } else {
123 self.readline.add_history_entry(trimmed)?;
124 trimmed
125 };
126
127 self.registry.run(input, state)
128 }
129 Err(ReadlineError::Interrupted | ReadlineError::Eof) => {
130 self.registry.run("quit", state)
131 }
132 _ => Err(Error::from(Errno::EIO)),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_cmdhelper_complete_prefix() {
143 let helper = CmdHelper {
144 options: vec!["foo bar".into(), "foo baz".into(), "other".into()],
145 };
146
147 let line = "fo";
148 let pos = 2usize;
149 let prefix = line[..pos].split_whitespace().collect::<Vec<_>>().join(" ");
150 let input_words: Vec<&str> = prefix.split_whitespace().collect();
151
152 let matches: Vec<String> = helper
153 .options
154 .iter()
155 .filter(|cmd| {
156 let cmd_words: Vec<&str> = cmd.split_whitespace().collect();
157 input_words
158 .iter()
159 .zip(cmd_words.iter())
160 .all(|(input, cmd)| cmd.starts_with(input))
161 && input_words.len() <= cmd_words.len()
162 })
163 .cloned()
164 .collect();
165
166 assert!(matches.contains(&"foo bar".to_string()));
167 assert!(matches.contains(&"foo baz".to_string()));
168 }
169
170 #[test]
171 fn test_runner_new() {
172 let _runner = Runner::new().expect("runner new");
173 }
174}