1use inquire::autocompletion::{Autocomplete, Replacement};
2
3const SLASH_COMMANDS: &[(&str, &str)] = &[
5 ("/config", "Show current configuration"),
6 ("/help", "Show available commands"),
7 ("/quit", "Exit chat mode"),
8 ("/set", "Set option (style, to, model)"),
9];
10
11#[derive(Clone, Default)]
13pub struct SlashCommandCompleter;
14
15impl Autocomplete for SlashCommandCompleter {
16 fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
17 if !input.starts_with('/') {
18 return Ok(vec![]);
19 }
20
21 let suggestions: Vec<String> = SLASH_COMMANDS
22 .iter()
23 .filter(|(cmd, _)| cmd.starts_with(input))
24 .map(|(cmd, desc)| format!("{cmd} {desc}"))
25 .collect();
26
27 Ok(suggestions)
28 }
29
30 fn get_completion(
31 &mut self,
32 _input: &str,
33 highlighted_suggestion: Option<String>,
34 ) -> Result<Replacement, inquire::CustomUserError> {
35 let replacement =
36 highlighted_suggestion.map(|s| s.split_whitespace().next().unwrap_or("").to_string());
37 Ok(replacement)
38 }
39}
40
41#[derive(Debug, Clone)]
43pub enum SlashCommand {
44 Config,
45 Help,
46 Quit,
47 Set { key: String, value: Option<String> },
48 Unknown(String),
49}
50
51#[derive(Debug)]
53pub enum Input {
54 Text(String),
55 Command(SlashCommand),
56 Empty,
57}
58
59pub fn parse_input(input: &str) -> Input {
60 let input = input.trim();
61
62 if input.is_empty() {
63 return Input::Empty;
64 }
65
66 input
67 .strip_prefix('/')
68 .map_or_else(|| Input::Text(input.to_string()), parse_slash_command)
69}
70
71fn parse_slash_command(cmd: &str) -> Input {
72 let parts: Vec<&str> = cmd.split_whitespace().collect();
73
74 match parts.first().copied() {
75 Some("config") => Input::Command(SlashCommand::Config),
76 Some("help") => Input::Command(SlashCommand::Help),
77 Some("quit" | "exit" | "q") => Input::Command(SlashCommand::Quit),
78 Some("set") => {
79 let key = parts.get(1).map(|s| (*s).to_string()).unwrap_or_default();
80 let value = parts.get(2).map(|s| (*s).to_string());
81 Input::Command(SlashCommand::Set { key, value })
82 }
83 _ => Input::Command(SlashCommand::Unknown(parts.join(" "))),
84 }
85}
86
87#[cfg(test)]
88#[allow(clippy::unwrap_used)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_parse_empty_input() {
94 assert!(matches!(parse_input(""), Input::Empty));
95 assert!(matches!(parse_input(" "), Input::Empty));
96 }
97
98 #[test]
99 fn test_parse_text_input() {
100 match parse_input("Hello, world!") {
101 Input::Text(text) => assert_eq!(text, "Hello, world!"),
102 _ => panic!("Expected Input::Text"),
103 }
104 }
105
106 #[test]
107 fn test_parse_config_command() {
108 assert!(matches!(
109 parse_input("/config"),
110 Input::Command(SlashCommand::Config)
111 ));
112 }
113
114 #[test]
115 fn test_parse_help_command() {
116 assert!(matches!(
117 parse_input("/help"),
118 Input::Command(SlashCommand::Help)
119 ));
120 }
121
122 #[test]
123 fn test_parse_quit_commands() {
124 assert!(matches!(
125 parse_input("/quit"),
126 Input::Command(SlashCommand::Quit)
127 ));
128 assert!(matches!(
129 parse_input("/exit"),
130 Input::Command(SlashCommand::Quit)
131 ));
132 assert!(matches!(
133 parse_input("/q"),
134 Input::Command(SlashCommand::Quit)
135 ));
136 }
137
138 #[test]
139 fn test_parse_unknown_command() {
140 match parse_input("/unknown") {
141 Input::Command(SlashCommand::Unknown(cmd)) => assert_eq!(cmd, "unknown"),
142 _ => panic!("Expected Input::Command(SlashCommand::Unknown)"),
143 }
144 }
145
146 #[test]
149 fn test_parse_set_style_with_value() {
150 match parse_input("/set style casual") {
151 Input::Command(SlashCommand::Set { key, value }) => {
152 assert_eq!(key, "style");
153 assert_eq!(value, Some("casual".to_string()));
154 }
155 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
156 }
157 }
158
159 #[test]
160 fn test_parse_set_style_without_value() {
161 match parse_input("/set style") {
162 Input::Command(SlashCommand::Set { key, value }) => {
163 assert_eq!(key, "style");
164 assert_eq!(value, None);
165 }
166 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
167 }
168 }
169
170 #[test]
171 fn test_parse_set_to() {
172 match parse_input("/set to ja") {
173 Input::Command(SlashCommand::Set { key, value }) => {
174 assert_eq!(key, "to");
175 assert_eq!(value, Some("ja".to_string()));
176 }
177 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
178 }
179 }
180
181 #[test]
182 fn test_parse_set_model() {
183 match parse_input("/set model gpt-4o") {
184 Input::Command(SlashCommand::Set { key, value }) => {
185 assert_eq!(key, "model");
186 assert_eq!(value, Some("gpt-4o".to_string()));
187 }
188 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
189 }
190 }
191
192 #[test]
193 fn test_parse_set_without_key() {
194 match parse_input("/set") {
195 Input::Command(SlashCommand::Set { key, value }) => {
196 assert_eq!(key, "");
197 assert_eq!(value, None);
198 }
199 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
200 }
201 }
202
203 #[test]
204 fn test_parse_set_with_extra_whitespace() {
205 match parse_input("/set style casual") {
206 Input::Command(SlashCommand::Set { key, value }) => {
207 assert_eq!(key, "style");
208 assert_eq!(value, Some("casual".to_string()));
209 }
210 _ => panic!("Expected Input::Command(SlashCommand::Set)"),
211 }
212 }
213
214 #[test]
217 fn test_completer_no_suggestions_for_regular_text() {
218 let mut completer = SlashCommandCompleter;
219 let suggestions = completer.get_suggestions("hello").unwrap();
220 assert!(suggestions.is_empty());
221 }
222
223 #[test]
224 fn test_completer_suggestions_for_slash() {
225 let mut completer = SlashCommandCompleter;
226 let suggestions = completer.get_suggestions("/").unwrap();
227 assert_eq!(suggestions.len(), 4); }
229
230 #[test]
231 fn test_completer_suggestions_filter_by_prefix() {
232 let mut completer = SlashCommandCompleter;
233
234 let suggestions = completer.get_suggestions("/c").unwrap();
235 assert_eq!(suggestions.len(), 1);
236 assert!(suggestions[0].starts_with("/config"));
237
238 let suggestions = completer.get_suggestions("/q").unwrap();
239 assert_eq!(suggestions.len(), 1);
240 assert!(suggestions[0].starts_with("/quit"));
241 }
242
243 #[test]
244 fn test_completer_completion() {
245 let mut completer = SlashCommandCompleter;
246 let suggestion = "/config Show current configuration".to_string();
247 let completion = completer.get_completion("/c", Some(suggestion)).unwrap();
248 assert_eq!(completion, Some("/config".to_string()));
249 }
250
251 #[test]
252 fn test_completer_completion_none() {
253 let mut completer = SlashCommandCompleter;
254 let completion = completer.get_completion("/x", None).unwrap();
255 assert!(completion.is_none());
256 }
257}