oxur_cli/repl/
completer.rs

1//! Command completion for Oxur REPL
2//!
3//! Provides tab completion for:
4//! - Special commands: (help), (quit), (stats), (info)
5//! - Help topics: basics, evaluation, keyboard, etc.
6//! - Stats views: execution, cache, resources
7
8use reedline::{Completer, Span, Suggestion};
9
10/// Oxur REPL command completer
11///
12/// Implements reedline's `Completer` trait to provide tab completion
13/// for REPL commands, help topics, and stats views.
14#[derive(Clone)]
15pub struct OxurCompleter;
16
17impl OxurCompleter {
18    /// Create a new Oxur completer
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Get list of special commands
24    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    /// Get list of help topics with descriptions
43    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    /// Get list of stats views with descriptions
57    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    /// Find completions for the given partial input with descriptions
68    fn find_completions(&self, partial: &str) -> Vec<(String, Option<String>)> {
69        let mut completions = Vec::new();
70
71        // Help topics: "(help <partial>"
72        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        // Stats views: "(stats <partial>"
83        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        // New session with name hint: "(new-session "
94        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        // Switch session hint: "(switch-session "
103        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        // Close session hint: "(close-session "
112        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        // Special commands (only if no space yet)
121        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); // All 8 help topics
199        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); // All 5 stats views
246        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}