1use crate::interface::Completion;
2use rustyline::completion::{Completer, FilenameCompleter};
3use rustyline::highlight::Highlighter;
4use rustyline::hint::Hinter;
5use rustyline::history::DefaultHistory;
6use rustyline::line_buffer::LineBuffer;
7use rustyline::validate::Validator;
8use rustyline::{Changeset, Context, Editor, Helper};
9use std::collections::HashMap;
10
11pub type CommandEditor = Editor<CommandHelper, DefaultHistory>;
12
13pub struct CommandHelper {
14 commands: Vec<String>,
15 completions: HashMap<String, Completion>,
16 completer: FilenameCompleter,
17}
18
19impl CommandHelper {
21 pub fn new() -> Self {
22 let commands = Vec::new();
23 let completions = HashMap::new();
24 let completer = FilenameCompleter::new();
25 Self { commands, completions, completer }
26 }
27
28 pub fn set_commands(&mut self, commands: Vec<String>) {
29 self.commands = commands;
30 }
31
32 pub fn set_completions(&mut self, completions: HashMap<String, Completion>) {
33 self.completions = completions;
34 }
35
36 fn requires_filename(&self, line: &str) -> bool {
37 let line = line.trim_start();
38 if let Some((command, remainder)) = line.split_once(' ') {
39 if let Some(Completion::Filename) = self.completions.get(command) {
40 let remainder = remainder.trim_start();
41 return !remainder.contains(' ');
42 }
43 }
44 false
45 }
46
47 fn complete_line(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<String>)> {
48 let line = &line[..pos];
49 if self.requires_filename(line) {
50 let (start, candidates) = self.completer.complete_path(line, pos)?;
51 let candidates = candidates.into_iter().map(|x| x.replacement).collect();
52 Ok((start, candidates))
53 } else {
54 if let Some((start, initial)) = line.rsplit_once(' ') {
55 let start = start.len() + 1;
56 let candidates = self.complete_command(initial);
57 Ok((start, candidates))
58 } else {
59 let candidates = self.complete_command(line);
60 Ok((0, candidates))
61 }
62 }
63 }
64
65 fn complete_command(&self, initial: &str) -> Vec<String> {
66 self.commands.iter()
67 .filter(|x| x.starts_with(initial))
68 .map(|x| x.to_string())
69 .collect()
70 }
71
72 fn adjust_candidates(candidates: Vec<String>) -> Vec<String> {
73 if candidates.len() == 1 {
74 candidates.into_iter().map(Self::adjust_candidate).collect()
75 } else {
76 candidates
77 }
78 }
79
80 fn adjust_candidate(candidate: String) -> String {
81 if candidate.ends_with(&['\\', '/']) {
82 candidate
83 } else {
84 candidate + " "
85 }
86 }
87}
88
89impl Helper for CommandHelper {
90}
91
92impl Completer for CommandHelper {
94 type Candidate = String;
95
96 fn complete(
97 &self,
98 line: &str,
99 pos: usize,
100 _context: &Context<'_>,
101 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
102 let (start, candidates) = self.complete_line(line, pos)?;
103 let candidates = Self::adjust_candidates(candidates);
104 Ok((start, candidates))
105 }
106
107 fn update(
108 &self,
109 line: &mut LineBuffer,
110 start: usize,
111 elected: &str,
112 changes: &mut Changeset,
113 ) {
114 let end = line.pos();
115 line.replace(start..end, &elected, changes);
116 }
117}
118
119impl Highlighter for CommandHelper {
120}
121
122impl Hinter for CommandHelper {
123 type Hint = String;
124}
125
126impl Validator for CommandHelper {
127}
128
129#[cfg(test)]
130pub mod tests {
131 use crate::helper::CommandHelper;
132 use crate::interface::Completion;
133 use std::collections::HashMap;
134
135 #[test]
136 fn test_requires_filename_for_import_directive() {
137 let helper = create_helper();
138 assert_eq!(false, helper.requires_filename(""));
139 assert_eq!(false, helper.requires_filename("import"));
140 assert_eq!(true, helper.requires_filename("import "));
141 assert_eq!(true, helper.requires_filename("import file"));
142 assert_eq!(false, helper.requires_filename("import file "));
143 assert_eq!(false, helper.requires_filename("import file 999"));
144 assert_eq!(false, helper.requires_filename(" "));
145 assert_eq!(false, helper.requires_filename(" import"));
146 assert_eq!(true, helper.requires_filename(" import "));
147 assert_eq!(true, helper.requires_filename(" import file"));
148 assert_eq!(false, helper.requires_filename(" import file "));
149 assert_eq!(false, helper.requires_filename(" import file 999"));
150 }
151
152 #[test]
153 fn test_requires_filename_for_define_directive() {
154 let helper = create_helper();
155 assert_eq!(false, helper.requires_filename(""));
156 assert_eq!(false, helper.requires_filename("define"));
157 assert_eq!(false, helper.requires_filename("define "));
158 assert_eq!(false, helper.requires_filename("define file"));
159 assert_eq!(false, helper.requires_filename("define file "));
160 assert_eq!(false, helper.requires_filename("define file 999"));
161 assert_eq!(false, helper.requires_filename(" "));
162 assert_eq!(false, helper.requires_filename(" define"));
163 assert_eq!(false, helper.requires_filename(" define "));
164 assert_eq!(false, helper.requires_filename(" define file"));
165 assert_eq!(false, helper.requires_filename(" define file "));
166 assert_eq!(false, helper.requires_filename(" define file 999"));
167 }
168
169 #[test]
170 fn test_requires_filename_for_unknown_directive() {
171 let helper = create_helper();
172 assert_eq!(false, helper.requires_filename(""));
173 assert_eq!(false, helper.requires_filename("unknown"));
174 assert_eq!(false, helper.requires_filename("unknown "));
175 assert_eq!(false, helper.requires_filename("unknown file"));
176 assert_eq!(false, helper.requires_filename("unknown file "));
177 assert_eq!(false, helper.requires_filename("unknown file 999"));
178 assert_eq!(false, helper.requires_filename(" "));
179 assert_eq!(false, helper.requires_filename(" unknown"));
180 assert_eq!(false, helper.requires_filename(" unknown "));
181 assert_eq!(false, helper.requires_filename(" unknown file"));
182 assert_eq!(false, helper.requires_filename(" unknown file "));
183 assert_eq!(false, helper.requires_filename(" unknown file 999"));
184 }
185
186 #[test]
187 fn test_only_token_is_completed_with_no_matches() {
188 let helper = create_helper();
189 let (start, candidates) = helper.complete_line("xxx 999", 3).unwrap();
190 assert_eq!(start, 0);
191 assert!(candidates.is_empty())
192 }
193
194 #[test]
195 fn test_final_token_is_completed_with_no_matches() {
196 let helper = create_helper();
197 let (start, candidates) = helper.complete_line("1 2 xxx 999", 7).unwrap();
198 assert_eq!(start, 4);
199 assert!(candidates.is_empty())
200 }
201
202 #[test]
203 fn test_only_token_is_completed_with_one_match() {
204 let helper = create_helper();
205 let (start, candidates) = helper.complete_line("aaa 999", 3).unwrap();
206 assert_eq!(start, 0);
207 assert_eq!(candidates, vec![String::from("aaaaa123")]);
208 }
209
210 #[test]
211 fn test_final_token_is_completed_with_one_match() {
212 let helper = create_helper();
213 let (start, candidates) = helper.complete_line("1 2 aaa 999", 7).unwrap();
214 assert_eq!(start, 4);
215 assert_eq!(candidates, vec![String::from("aaaaa123")]);
216 }
217
218 #[test]
219 fn test_only_token_is_completed_with_multiple_matches() {
220 let helper = create_helper();
221 let (start, candidates) = helper.complete_line("bbb 999", 3).unwrap();
222 assert_eq!(start, 0);
223 assert_eq!(candidates, vec![String::from("bbbbb456"), String::from("bbbbb789")]);
224 }
225
226 #[test]
227 fn test_final_token_is_completed_with_multiple_matches() {
228 let helper = create_helper();
229 let (start, candidates) = helper.complete_line("1 2 bbb 999", 7).unwrap();
230 assert_eq!(start, 4);
231 assert_eq!(candidates, vec![String::from("bbbbb456"), String::from("bbbbb789")]);
232 }
233
234 fn create_helper() -> CommandHelper {
235 let mut helper = CommandHelper::new();
236 helper.set_commands(vec![
237 String::from("aaaaa123"),
238 String::from("bbbbb456"),
239 String::from("bbbbb789"),
240 ]);
241 helper.set_completions(HashMap::from([
242 (String::from("import"), Completion::Filename),
243 (String::from("export"), Completion::Filename),
244 (String::from("define"), Completion::Keyword),
245 ]));
246 helper
247 }
248
249 #[test]
250 fn test_space_is_added_to_single_candidate() {
251 assert_eq!(Vec::<String>::new(), adjust_candidates(vec![]));
252 assert_eq!(vec!["foo "], adjust_candidates(vec!["foo"]));
253 assert_eq!(vec!["foo", "bar"], adjust_candidates(vec!["foo", "bar"]));
254 assert_eq!(vec!["subdir\\"], adjust_candidates(vec!["subdir\\"]));
255 assert_eq!(vec!["subdir/"], adjust_candidates(vec!["subdir/"]));
256 }
257
258 fn adjust_candidates(candidates: Vec<&str>) -> Vec<String> {
259 let candidates = candidates.into_iter().map(String::from).collect();
260 CommandHelper::adjust_candidates(candidates)
261 }
262}