Skip to main content

neumann_shell/input/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Input handling for the shell (completion, highlighting, validation).
3
4mod completer;
5mod highlighter;
6mod validator;
7
8pub use completer::NeumannCompleter;
9pub use highlighter::NeumannHighlighter;
10pub use validator::NeumannValidator;
11
12use crate::style::Theme;
13use rustyline::completion::{Completer, Pair};
14use rustyline::highlight::{CmdKind, Highlighter};
15use rustyline::hint::Hinter;
16use rustyline::validate::{ValidationContext, ValidationResult, Validator};
17use rustyline::Helper;
18
19/// Combined helper providing completion, highlighting, hints, and validation.
20pub struct NeumannHelper {
21    completer: NeumannCompleter,
22    highlighter: NeumannHighlighter,
23    validator: NeumannValidator,
24}
25
26impl NeumannHelper {
27    /// Creates a new helper with the given theme.
28    #[must_use]
29    pub fn new(theme: Theme) -> Self {
30        Self {
31            completer: NeumannCompleter::new(),
32            highlighter: NeumannHighlighter::new(theme),
33            validator: NeumannValidator::new(),
34        }
35    }
36
37    /// Updates the list of available tables for completion.
38    pub fn set_tables(&mut self, tables: Vec<String>) {
39        self.completer.set_tables(tables);
40    }
41}
42
43impl Default for NeumannHelper {
44    fn default() -> Self {
45        Self::new(Theme::auto())
46    }
47}
48
49impl Helper for NeumannHelper {}
50
51impl Completer for NeumannHelper {
52    type Candidate = Pair;
53
54    fn complete(
55        &self,
56        line: &str,
57        pos: usize,
58        ctx: &rustyline::Context<'_>,
59    ) -> rustyline::Result<(usize, Vec<Pair>)> {
60        self.completer.complete(line, pos, ctx)
61    }
62}
63
64impl Highlighter for NeumannHelper {
65    fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
66        self.highlighter.highlight(line, pos)
67    }
68
69    fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool {
70        self.highlighter.highlight_char(line, pos, kind)
71    }
72
73    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
74        &'s self,
75        prompt: &'p str,
76        default: bool,
77    ) -> std::borrow::Cow<'b, str> {
78        self.highlighter.highlight_prompt(prompt, default)
79    }
80
81    fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
82        self.highlighter.highlight_hint(hint)
83    }
84
85    fn highlight_candidate<'c>(
86        &self,
87        candidate: &'c str,
88        completion: rustyline::CompletionType,
89    ) -> std::borrow::Cow<'c, str> {
90        self.highlighter.highlight_candidate(candidate, completion)
91    }
92}
93
94impl Hinter for NeumannHelper {
95    type Hint = String;
96
97    fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
98        None
99    }
100}
101
102impl Validator for NeumannHelper {
103    fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
104        self.validator.validate(ctx)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_helper_creation() {
114        let _helper = NeumannHelper::new(Theme::plain());
115    }
116
117    #[test]
118    fn test_helper_default() {
119        let _helper = NeumannHelper::default();
120    }
121
122    #[test]
123    fn test_helper_set_tables() {
124        let mut helper = NeumannHelper::new(Theme::plain());
125        helper.set_tables(vec!["users".to_string(), "orders".to_string()]);
126    }
127
128    #[test]
129    fn test_helper_completer_trait() {
130        let helper = NeumannHelper::new(Theme::plain());
131        let history = rustyline::history::DefaultHistory::new();
132        let ctx = rustyline::Context::new(&history);
133        let result = helper.complete("SELECT ", 7, &ctx);
134        assert!(result.is_ok());
135        let (start, completions) = result.unwrap();
136        assert_eq!(start, 7);
137        assert!(!completions.is_empty());
138    }
139
140    #[test]
141    fn test_helper_highlighter_trait() {
142        let helper = NeumannHelper::new(Theme::plain());
143
144        // Test highlight
145        let highlighted = helper.highlight("SELECT * FROM users", 0);
146        assert!(!highlighted.is_empty());
147
148        // Test highlight_char
149        let needs_highlight = helper.highlight_char("SELECT", 0, CmdKind::MoveCursor);
150        // Result depends on implementation
151        let _ = needs_highlight;
152
153        // Test highlight_prompt
154        let prompt = helper.highlight_prompt("neumann> ", false);
155        assert!(!prompt.is_empty());
156
157        // Test highlight_hint
158        let hint = helper.highlight_hint("users");
159        assert!(!hint.is_empty());
160
161        // Test highlight_candidate
162        let candidate = helper.highlight_candidate("SELECT", rustyline::CompletionType::List);
163        assert!(!candidate.is_empty());
164    }
165
166    #[test]
167    fn test_helper_hinter_trait() {
168        let helper = NeumannHelper::new(Theme::plain());
169        let history = rustyline::history::DefaultHistory::new();
170        let ctx = rustyline::Context::new(&history);
171        let hint = helper.hint("SELECT", 6, &ctx);
172        assert!(hint.is_none()); // Hints are disabled
173    }
174
175    #[test]
176    fn test_helper_with_different_themes() {
177        let themes = [Theme::plain(), Theme::dark(), Theme::light(), Theme::auto()];
178        for theme in themes {
179            let helper = NeumannHelper::new(theme);
180            let highlighted = helper.highlight("SELECT", 0);
181            assert!(!highlighted.is_empty());
182        }
183    }
184
185    #[test]
186    fn test_helper_complete_partial() {
187        let helper = NeumannHelper::new(Theme::plain());
188        let history = rustyline::history::DefaultHistory::new();
189        let ctx = rustyline::Context::new(&history);
190
191        // Complete partial command
192        let result = helper.complete("SEL", 3, &ctx);
193        assert!(result.is_ok());
194        let (start, completions) = result.unwrap();
195        assert_eq!(start, 0);
196        assert!(completions.iter().any(|p| p.display == "SELECT"));
197    }
198
199    #[test]
200    fn test_helper_complete_with_tables() {
201        let mut helper = NeumannHelper::new(Theme::plain());
202        helper.set_tables(vec!["users".to_string(), "orders".to_string()]);
203
204        let history = rustyline::history::DefaultHistory::new();
205        let ctx = rustyline::Context::new(&history);
206        let result = helper.complete("SELECT * FROM ", 14, &ctx);
207        assert!(result.is_ok());
208        let (_, completions) = result.unwrap();
209        assert!(completions.iter().any(|p| p.display == "users"));
210        assert!(completions.iter().any(|p| p.display == "orders"));
211    }
212
213    #[test]
214    fn test_helper_highlight_keywords() {
215        let helper = NeumannHelper::new(Theme::plain());
216
217        // Various SQL keywords
218        let _h1 = helper.highlight("INSERT INTO users VALUES (1)", 0);
219        let _h2 = helper.highlight("UPDATE users SET name = 'x'", 0);
220        let _h3 = helper.highlight("DELETE FROM users WHERE id = 1", 0);
221        let _h4 = helper.highlight("CREATE TABLE test (id INT)", 0);
222        let _h5 = helper.highlight("DROP TABLE test", 0);
223    }
224}