1use reedline::{Completer, Span, Suggestion};
9
10#[derive(Clone)]
15pub struct OxurCompleter;
16
17impl OxurCompleter {
18 pub fn new() -> Self {
20 Self
21 }
22
23 fn special_commands() -> Vec<&'static str> {
25 vec![
26 "(help)",
27 "(quit)",
28 "(q)",
29 "(exit)",
30 "(info)",
31 "(stats)",
32 "(sessions)",
33 "(clear)",
34 "(banner)",
35 "(current-session)",
36 "(new-session)",
37 "(switch-session)",
38 "(close-session)",
39 ]
40 }
41
42 fn help_topics() -> Vec<(&'static str, &'static str)> {
44 vec![
45 ("basics", "Basic REPL usage and syntax"),
46 ("evaluation", "How expressions are evaluated"),
47 ("keyboard", "Keyboard shortcuts and navigation"),
48 ("sessions", "Session management and history"),
49 ("commands", "Special commands reference"),
50 ("modes", "REPL modes (interactive, server, connect)"),
51 ("performance", "Performance tips and optimization"),
52 ("stats", "Statistics and metrics"),
53 ]
54 }
55
56 fn stats_views() -> Vec<(&'static str, &'static str)> {
58 vec![
59 ("execution", "Execution tier statistics"),
60 ("cache", "Cache hit rates and performance"),
61 ("resources", "Memory and system resources"),
62 ("usage", "Command frequency and usage patterns"),
63 ("client", "Client-side latency and request metrics"),
64 ]
65 }
66
67 fn find_completions(&self, partial: &str) -> Vec<(String, Option<String>)> {
69 let mut completions = Vec::new();
70
71 if let Some(help_prefix) = partial.strip_prefix("(help ") {
73 let topic_partial = help_prefix.trim();
74 for (topic, description) in Self::help_topics() {
75 if topic.starts_with(topic_partial) {
76 completions.push((format!("(help {})", topic), Some(description.to_string())));
77 }
78 }
79 return completions;
80 }
81
82 if let Some(stats_prefix) = partial.strip_prefix("(stats ") {
84 let view_partial = stats_prefix.trim();
85 for (view, description) in Self::stats_views() {
86 if view.starts_with(view_partial) {
87 completions.push((format!("(stats {})", view), Some(description.to_string())));
88 }
89 }
90 return completions;
91 }
92
93 if partial == "(new-session " || partial.starts_with("(new-session \"") {
95 completions.push((
96 "(new-session \"name\")".to_string(),
97 Some("Create named session".to_string()),
98 ));
99 return completions;
100 }
101
102 if partial == "(switch-session " {
104 completions.push((
105 "(switch-session <session-id>)".to_string(),
106 Some("Switch to existing session by ID".to_string()),
107 ));
108 return completions;
109 }
110
111 if partial == "(close-session " {
113 completions.push((
114 "(close-session <session-id>)".to_string(),
115 Some("Close specific session by ID".to_string()),
116 ));
117 return completions;
118 }
119
120 if !partial.contains(' ') {
122 for cmd in Self::special_commands() {
123 if cmd.starts_with(partial) {
124 completions.push((cmd.to_string(), None));
125 }
126 }
127 }
128
129 completions
130 }
131}
132
133impl Default for OxurCompleter {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl Completer for OxurCompleter {
140 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
141 let partial = &line[..pos];
142
143 self.find_completions(partial)
144 .into_iter()
145 .map(|(value, description)| Suggestion {
146 value,
147 description,
148 style: None,
149 extra: None,
150 span: Span::new(0, pos),
151 append_whitespace: false,
152 match_indices: None,
153 })
154 .collect()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_complete_help_command() {
164 let mut completer = OxurCompleter::new();
165 let suggestions = completer.complete("(h", 2);
166 assert!(suggestions.iter().any(|s| s.value == "(help)"));
167 }
168
169 #[test]
170 fn test_complete_quit_commands() {
171 let mut completer = OxurCompleter::new();
172 let suggestions = completer.complete("(q", 2);
173 let values: Vec<_> = suggestions.iter().map(|s| s.value.as_str()).collect();
174 assert!(values.contains(&"(quit)"));
175 assert!(values.contains(&"(q)"));
176 }
177
178 #[test]
179 fn test_complete_help_topic_basics() {
180 let mut completer = OxurCompleter::new();
181 let suggestions = completer.complete("(help ba", 8);
182 assert_eq!(suggestions.len(), 1);
183 assert_eq!(suggestions[0].value, "(help basics)");
184 }
185
186 #[test]
187 fn test_complete_help_topic_partial() {
188 let mut completer = OxurCompleter::new();
189 let suggestions = completer.complete("(help ev", 8);
190 assert_eq!(suggestions.len(), 1);
191 assert_eq!(suggestions[0].value, "(help evaluation)");
192 }
193
194 #[test]
195 fn test_complete_help_all_topics() {
196 let mut completer = OxurCompleter::new();
197 let suggestions = completer.complete("(help ", 6);
198 assert_eq!(suggestions.len(), 8); let values: Vec<_> = suggestions.iter().map(|s| s.value.as_str()).collect();
200 assert!(values.contains(&"(help basics)"));
201 assert!(values.contains(&"(help evaluation)"));
202 assert!(values.contains(&"(help keyboard)"));
203 assert!(values.contains(&"(help sessions)"));
204 assert!(values.contains(&"(help commands)"));
205 assert!(values.contains(&"(help modes)"));
206 assert!(values.contains(&"(help performance)"));
207 assert!(values.contains(&"(help stats)"));
208 }
209
210 #[test]
211 fn test_complete_stats_command() {
212 let mut completer = OxurCompleter::new();
213 let suggestions = completer.complete("(sta", 4);
214 assert!(suggestions.iter().any(|s| s.value == "(stats)"));
215 }
216
217 #[test]
218 fn test_complete_stats_view_execution() {
219 let mut completer = OxurCompleter::new();
220 let suggestions = completer.complete("(stats ex", 9);
221 assert_eq!(suggestions.len(), 1);
222 assert_eq!(suggestions[0].value, "(stats execution)");
223 }
224
225 #[test]
226 fn test_complete_stats_view_cache() {
227 let mut completer = OxurCompleter::new();
228 let suggestions = completer.complete("(stats ca", 9);
229 assert_eq!(suggestions.len(), 1);
230 assert_eq!(suggestions[0].value, "(stats cache)");
231 }
232
233 #[test]
234 fn test_complete_stats_view_resources() {
235 let mut completer = OxurCompleter::new();
236 let suggestions = completer.complete("(stats re", 9);
237 assert_eq!(suggestions.len(), 1);
238 assert_eq!(suggestions[0].value, "(stats resources)");
239 }
240
241 #[test]
242 fn test_complete_stats_all_views() {
243 let mut completer = OxurCompleter::new();
244 let suggestions = completer.complete("(stats ", 7);
245 assert_eq!(suggestions.len(), 5); let values: Vec<_> = suggestions.iter().map(|s| s.value.as_str()).collect();
247 assert!(values.contains(&"(stats execution)"));
248 assert!(values.contains(&"(stats cache)"));
249 assert!(values.contains(&"(stats resources)"));
250 assert!(values.contains(&"(stats usage)"));
251 assert!(values.contains(&"(stats client)"));
252 }
253
254 #[test]
255 fn test_no_completion_for_regular_code() {
256 let mut completer = OxurCompleter::new();
257 let suggestions = completer.complete("(+ 1 2", 6);
258 assert!(suggestions.is_empty());
259 }
260
261 #[test]
262 fn test_no_completion_after_space_in_regular_code() {
263 let mut completer = OxurCompleter::new();
264 let suggestions = completer.complete("(deffn foo ", 11);
265 assert!(suggestions.is_empty());
266 }
267
268 #[test]
269 fn test_info_command_completion() {
270 let mut completer = OxurCompleter::new();
271 let suggestions = completer.complete("(inf", 4);
272 assert!(suggestions.iter().any(|s| s.value == "(info)"));
273 }
274
275 #[test]
276 fn test_exit_command_completion() {
277 let mut completer = OxurCompleter::new();
278 let suggestions = completer.complete("(exi", 4);
279 assert!(suggestions.iter().any(|s| s.value == "(exit)"));
280 }
281
282 #[test]
283 fn test_help_topic_has_description() {
284 let mut completer = OxurCompleter::new();
285 let suggestions = completer.complete("(help ba", 8);
286 assert_eq!(suggestions.len(), 1);
287 assert_eq!(suggestions[0].value, "(help basics)");
288 assert_eq!(suggestions[0].description, Some("Basic REPL usage and syntax".to_string()));
289 }
290
291 #[test]
292 fn test_stats_view_has_description() {
293 let mut completer = OxurCompleter::new();
294 let suggestions = completer.complete("(stats ex", 9);
295 assert_eq!(suggestions.len(), 1);
296 assert_eq!(suggestions[0].value, "(stats execution)");
297 assert_eq!(suggestions[0].description, Some("Execution tier statistics".to_string()));
298 }
299
300 #[test]
301 fn test_special_commands_no_description() {
302 let mut completer = OxurCompleter::new();
303 let suggestions = completer.complete("(h", 2);
304 assert!(suggestions.iter().any(|s| s.value == "(help)"));
305 let help_suggestion = suggestions.iter().find(|s| s.value == "(help)").unwrap();
306 assert_eq!(help_suggestion.description, None);
307 }
308
309 #[test]
310 fn test_clear_command_completion() {
311 let mut completer = OxurCompleter::new();
312 let suggestions = completer.complete("(cle", 4);
313 assert!(suggestions.iter().any(|s| s.value == "(clear)"));
314 }
315
316 #[test]
317 fn test_banner_command_completion() {
318 let mut completer = OxurCompleter::new();
319 let suggestions = completer.complete("(ban", 4);
320 assert!(suggestions.iter().any(|s| s.value == "(banner)"));
321 }
322
323 #[test]
324 fn test_session_commands_completion() {
325 let mut completer = OxurCompleter::new();
326 let suggestions = completer.complete("(cur", 4);
327 assert!(suggestions.iter().any(|s| s.value == "(current-session)"));
328
329 let suggestions = completer.complete("(new-", 5);
330 assert!(suggestions.iter().any(|s| s.value == "(new-session)"));
331
332 let suggestions = completer.complete("(switch", 7);
333 assert!(suggestions.iter().any(|s| s.value == "(switch-session)"));
334
335 let suggestions = completer.complete("(close-", 7);
336 assert!(suggestions.iter().any(|s| s.value == "(close-session)"));
337 }
338
339 #[test]
340 fn test_new_session_with_name_hint() {
341 let mut completer = OxurCompleter::new();
342 let suggestions = completer.complete("(new-session ", 13);
343 assert_eq!(suggestions.len(), 1);
344 assert_eq!(suggestions[0].value, "(new-session \"name\")");
345 assert_eq!(suggestions[0].description, Some("Create named session".to_string()));
346 }
347
348 #[test]
349 fn test_switch_session_hint() {
350 let mut completer = OxurCompleter::new();
351 let suggestions = completer.complete("(switch-session ", 16);
352 assert_eq!(suggestions.len(), 1);
353 assert_eq!(suggestions[0].value, "(switch-session <session-id>)");
354 assert_eq!(
355 suggestions[0].description,
356 Some("Switch to existing session by ID".to_string())
357 );
358 }
359
360 #[test]
361 fn test_close_session_hint() {
362 let mut completer = OxurCompleter::new();
363 let suggestions = completer.complete("(close-session ", 15);
364 assert_eq!(suggestions.len(), 1);
365 assert_eq!(suggestions[0].value, "(close-session <session-id>)");
366 assert_eq!(suggestions[0].description, Some("Close specific session by ID".to_string()));
367 }
368}