ricecoder_tui/
input.rs

1//! Input handling for the TUI
2
3/// Intent types for natural language input
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Intent {
6    /// Generate code
7    Generate,
8    /// Explain something
9    Explain,
10    /// Fix code
11    Fix,
12    /// Refactor code
13    Refactor,
14    /// Test code
15    Test,
16    /// Document code
17    Document,
18    /// Execute command
19    Execute,
20    /// Help request
21    Help,
22    /// General chat
23    Chat,
24}
25
26/// Input analyzer for intent detection
27pub struct InputAnalyzer;
28
29impl InputAnalyzer {
30    /// Detect intent from user input
31    pub fn detect_intent(input: &str) -> Intent {
32        let lower = input.to_lowercase();
33
34        if lower.contains("generate") || lower.contains("create") || lower.contains("write") {
35            Intent::Generate
36        } else if lower.contains("explain")
37            || lower.contains("what is")
38            || lower.contains("how does")
39        {
40            Intent::Explain
41        } else if lower.contains("fix") || lower.contains("bug") || lower.contains("error") {
42            Intent::Fix
43        } else if lower.contains("refactor")
44            || lower.contains("improve")
45            || lower.contains("optimize")
46        {
47            Intent::Refactor
48        } else if lower.contains("test") || lower.contains("unit test") {
49            Intent::Test
50        } else if lower.contains("document") || lower.contains("comment") {
51            Intent::Document
52        } else if lower.contains("execute") || lower.contains("run") || lower.contains("command") {
53            Intent::Execute
54        } else if lower.contains("help") || lower.contains("?") {
55            Intent::Help
56        } else {
57            Intent::Chat
58        }
59    }
60
61    /// Get suggested commands based on intent
62    pub fn suggest_commands(intent: Intent) -> Vec<&'static str> {
63        match intent {
64            Intent::Generate => vec!["generate", "create", "scaffold"],
65            Intent::Explain => vec!["explain", "describe", "clarify"],
66            Intent::Fix => vec!["fix", "debug", "resolve"],
67            Intent::Refactor => vec!["refactor", "improve", "optimize"],
68            Intent::Test => vec!["test", "unit-test", "validate"],
69            Intent::Document => vec!["document", "comment", "annotate"],
70            Intent::Execute => vec!["execute", "run", "apply"],
71            Intent::Help => vec!["help", "guide", "tutorial"],
72            Intent::Chat => vec!["chat", "discuss", "ask"],
73        }
74    }
75
76    /// Validate input
77    pub fn validate_input(input: &str) -> Result<(), String> {
78        if input.trim().is_empty() {
79            return Err("Input cannot be empty".to_string());
80        }
81
82        if input.len() > 10000 {
83            return Err("Input is too long (max 10000 characters)".to_string());
84        }
85
86        Ok(())
87    }
88}
89
90/// Chat input widget
91pub struct ChatInputWidget {
92    /// Current input text
93    pub text: String,
94    /// Cursor position
95    pub cursor: usize,
96    /// Input history
97    pub history: Vec<String>,
98    /// History index
99    pub history_index: Option<usize>,
100    /// Detected intent
101    pub intent: Intent,
102}
103
104impl ChatInputWidget {
105    /// Create a new chat input widget
106    pub fn new() -> Self {
107        Self {
108            text: String::new(),
109            cursor: 0,
110            history: Vec::new(),
111            history_index: None,
112            intent: Intent::Chat,
113        }
114    }
115
116    /// Insert character at cursor
117    pub fn insert_char(&mut self, ch: char) {
118        self.text.insert(self.cursor, ch);
119        self.cursor += 1;
120        self.update_intent();
121    }
122
123    /// Delete character before cursor
124    pub fn backspace(&mut self) {
125        if self.cursor > 0 {
126            self.text.remove(self.cursor - 1);
127            self.cursor -= 1;
128            self.update_intent();
129        }
130    }
131
132    /// Delete character at cursor
133    pub fn delete(&mut self) {
134        if self.cursor < self.text.len() {
135            self.text.remove(self.cursor);
136            self.update_intent();
137        }
138    }
139
140    /// Move cursor left
141    pub fn move_left(&mut self) {
142        if self.cursor > 0 {
143            self.cursor -= 1;
144        }
145    }
146
147    /// Move cursor right
148    pub fn move_right(&mut self) {
149        if self.cursor < self.text.len() {
150            self.cursor += 1;
151        }
152    }
153
154    /// Move cursor to start
155    pub fn move_start(&mut self) {
156        self.cursor = 0;
157    }
158
159    /// Move cursor to end
160    pub fn move_end(&mut self) {
161        self.cursor = self.text.len();
162    }
163
164    /// Submit input
165    pub fn submit(&mut self) -> String {
166        let input = self.text.clone();
167        self.history.push(input.clone());
168        self.text.clear();
169        self.cursor = 0;
170        self.history_index = None;
171        self.intent = Intent::Chat;
172        input
173    }
174
175    /// Navigate history up
176    pub fn history_up(&mut self) {
177        if self.history.is_empty() {
178            return;
179        }
180
181        match self.history_index {
182            None => {
183                self.history_index = Some(self.history.len() - 1);
184                self.text = self.history[self.history.len() - 1].clone();
185            }
186            Some(idx) if idx > 0 => {
187                self.history_index = Some(idx - 1);
188                self.text = self.history[idx - 1].clone();
189            }
190            _ => {}
191        }
192
193        self.cursor = self.text.len();
194    }
195
196    /// Navigate history down
197    pub fn history_down(&mut self) {
198        match self.history_index {
199            Some(idx) if idx < self.history.len() - 1 => {
200                self.history_index = Some(idx + 1);
201                self.text = self.history[idx + 1].clone();
202                self.cursor = self.text.len();
203            }
204            Some(_) => {
205                self.history_index = None;
206                self.text.clear();
207                self.cursor = 0;
208            }
209            None => {}
210        }
211    }
212
213    /// Update detected intent
214    pub fn update_intent(&mut self) {
215        self.intent = InputAnalyzer::detect_intent(&self.text);
216    }
217
218    /// Get suggested commands
219    pub fn suggestions(&self) -> Vec<&'static str> {
220        InputAnalyzer::suggest_commands(self.intent)
221    }
222}
223
224impl Default for ChatInputWidget {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_intent_detection() {
236        assert_eq!(
237            InputAnalyzer::detect_intent("generate code"),
238            Intent::Generate
239        );
240        assert_eq!(
241            InputAnalyzer::detect_intent("explain this"),
242            Intent::Explain
243        );
244        assert_eq!(InputAnalyzer::detect_intent("fix the bug"), Intent::Fix);
245        assert_eq!(
246            InputAnalyzer::detect_intent("refactor this"),
247            Intent::Refactor
248        );
249        assert_eq!(InputAnalyzer::detect_intent("unit test"), Intent::Test);
250        assert_eq!(
251            InputAnalyzer::detect_intent("document this"),
252            Intent::Document
253        );
254        assert_eq!(
255            InputAnalyzer::detect_intent("execute command"),
256            Intent::Execute
257        );
258        assert_eq!(InputAnalyzer::detect_intent("help me"), Intent::Help);
259        assert_eq!(InputAnalyzer::detect_intent("hello"), Intent::Chat);
260    }
261
262    #[test]
263    fn test_input_validation() {
264        assert!(InputAnalyzer::validate_input("hello").is_ok());
265        assert!(InputAnalyzer::validate_input("").is_err());
266        assert!(InputAnalyzer::validate_input("   ").is_err());
267        assert!(InputAnalyzer::validate_input(&"x".repeat(10001)).is_err());
268    }
269
270    #[test]
271    fn test_chat_input_widget() {
272        let widget = ChatInputWidget::new();
273        assert!(widget.text.is_empty());
274        assert_eq!(widget.cursor, 0);
275    }
276
277    #[test]
278    fn test_insert_char() {
279        let mut widget = ChatInputWidget::new();
280        widget.insert_char('h');
281        widget.insert_char('i');
282        assert_eq!(widget.text, "hi");
283        assert_eq!(widget.cursor, 2);
284    }
285
286    #[test]
287    fn test_backspace() {
288        let mut widget = ChatInputWidget::new();
289        widget.insert_char('h');
290        widget.insert_char('i');
291        widget.backspace();
292        assert_eq!(widget.text, "h");
293        assert_eq!(widget.cursor, 1);
294    }
295
296    #[test]
297    fn test_cursor_movement() {
298        let mut widget = ChatInputWidget::new();
299        widget.text = "hello".to_string();
300        widget.cursor = 5;
301
302        widget.move_left();
303        assert_eq!(widget.cursor, 4);
304
305        widget.move_right();
306        assert_eq!(widget.cursor, 5);
307
308        widget.move_start();
309        assert_eq!(widget.cursor, 0);
310
311        widget.move_end();
312        assert_eq!(widget.cursor, 5);
313    }
314
315    #[test]
316    fn test_submit() {
317        let mut widget = ChatInputWidget::new();
318        widget.text = "hello".to_string();
319        widget.cursor = 5;
320
321        let submitted = widget.submit();
322        assert_eq!(submitted, "hello");
323        assert!(widget.text.is_empty());
324        assert_eq!(widget.cursor, 0);
325        assert_eq!(widget.history.len(), 1);
326    }
327
328    #[test]
329    fn test_history_navigation() {
330        let mut widget = ChatInputWidget::new();
331        widget.submit_text("first");
332        widget.submit_text("second");
333        widget.submit_text("third");
334
335        widget.history_up();
336        assert_eq!(widget.text, "third");
337
338        widget.history_up();
339        assert_eq!(widget.text, "second");
340
341        widget.history_down();
342        assert_eq!(widget.text, "third");
343
344        widget.history_down();
345        assert!(widget.text.is_empty());
346    }
347}
348
349impl ChatInputWidget {
350    /// Helper for tests
351    #[cfg(test)]
352    fn submit_text(&mut self, text: &str) {
353        self.text = text.to_string();
354        self.submit();
355    }
356}