this repo has no description
1use std::io::{self, Write};
2use std::collections::VecDeque;
3use crate::cli::{CommandResult, GigaBrainCli};
4
5/// Enhanced REPL with multiline support, command editing, and keyboard shortcuts
6pub struct EnhancedRepl {
7 input_buffer: String,
8 multiline_mode: bool,
9 prompt_prefix: String,
10 continuation_prompt: String,
11}
12
13impl EnhancedRepl {
14 pub fn new() -> Self {
15 Self {
16 input_buffer: String::new(),
17 multiline_mode: false,
18 prompt_prefix: "gigabrain> ".to_string(),
19 continuation_prompt: " -> ".to_string(),
20 }
21 }
22
23 /// Read a complete command, handling multiline input
24 pub async fn read_command(&mut self) -> Result<String, io::Error> {
25 self.input_buffer.clear();
26 self.multiline_mode = false;
27
28 loop {
29 // Show appropriate prompt
30 let prompt = if self.multiline_mode {
31 &self.continuation_prompt
32 } else {
33 &self.prompt_prefix
34 };
35
36 print!("{}", prompt);
37 io::stdout().flush()?;
38
39 let mut line = String::new();
40 io::stdin().read_line(&mut line)?;
41 let line = line.trim();
42
43 // Handle special cases
44 if line.is_empty() && !self.multiline_mode {
45 continue;
46 }
47
48 if line.is_empty() && self.multiline_mode {
49 // Empty line in multiline mode - finish the command
50 break;
51 }
52
53 // Check for multiline indicators
54 if line.ends_with('\\') {
55 // Line continuation
56 self.input_buffer.push_str(&line[..line.len()-1]);
57 self.input_buffer.push(' ');
58 self.multiline_mode = true;
59 continue;
60 }
61
62 if self.is_multiline_starter(line) {
63 // Start of multiline command
64 self.input_buffer.push_str(line);
65 self.input_buffer.push('\n');
66 self.multiline_mode = true;
67 continue;
68 }
69
70 if self.multiline_mode && self.is_multiline_ender(line) {
71 // End of multiline command
72 self.input_buffer.push_str(line);
73 break;
74 }
75
76 if self.multiline_mode {
77 // Continue multiline command
78 self.input_buffer.push_str(line);
79 self.input_buffer.push('\n');
80 continue;
81 }
82
83 // Single line command
84 self.input_buffer = line.to_string();
85 break;
86 }
87
88 Ok(self.input_buffer.clone())
89 }
90
91 /// Check if line starts a multiline command
92 fn is_multiline_starter(&self, line: &str) -> bool {
93 let line_lower = line.to_lowercase();
94
95 // Cypher queries that typically span multiple lines
96 (line_lower.starts_with("match") ||
97 line_lower.starts_with("create") ||
98 line_lower.starts_with("merge") ||
99 line_lower.starts_with("with")) &&
100 !line_lower.contains("return") &&
101 !line.trim().ends_with(';')
102 }
103
104 /// Check if line ends a multiline command
105 fn is_multiline_ender(&self, line: &str) -> bool {
106 let line_lower = line.to_lowercase();
107
108 line_lower.contains("return") ||
109 line.trim().ends_with(';') ||
110 line_lower.starts_with("delete") ||
111 line_lower.starts_with("set") ||
112 line_lower.starts_with("remove")
113 }
114}
115
116/// Interactive command suggestions and auto-completion
117#[derive(Debug)]
118pub struct ReplSuggestions {
119 cypher_keywords: Vec<String>,
120 meta_commands: Vec<String>,
121 recent_patterns: VecDeque<String>,
122}
123
124impl ReplSuggestions {
125 pub fn new() -> Self {
126 let cypher_keywords = vec![
127 "MATCH".to_string(), "CREATE".to_string(), "MERGE".to_string(),
128 "DELETE".to_string(), "RETURN".to_string(), "WITH".to_string(),
129 "WHERE".to_string(), "SET".to_string(), "REMOVE".to_string(),
130 "UNWIND".to_string(), "CALL".to_string(), "YIELD".to_string(),
131 "ORDER BY".to_string(), "LIMIT".to_string(), "SKIP".to_string(),
132 "UNION".to_string(), "DISTINCT".to_string(), "AS".to_string(),
133 "AND".to_string(), "OR".to_string(), "NOT".to_string(),
134 "IN".to_string(), "CONTAINS".to_string(), "STARTS WITH".to_string(),
135 "ENDS WITH".to_string(), "IS NULL".to_string(), "IS NOT NULL".to_string(),
136 ];
137
138 let meta_commands = vec![
139 ":help".to_string(), ":exit".to_string(), ":quit".to_string(),
140 ":stats".to_string(), ":show nodes".to_string(), ":show relationships".to_string(),
141 ":show schema".to_string(), ":format table".to_string(), ":format json".to_string(),
142 ":format csv".to_string(), ":format plain".to_string(), ":timing".to_string(),
143 ":history".to_string(), ":clear".to_string(), ":export".to_string(),
144 ":import".to_string(),
145 ];
146
147 Self {
148 cypher_keywords,
149 meta_commands,
150 recent_patterns: VecDeque::with_capacity(50),
151 }
152 }
153
154 /// Get command suggestions based on partial input
155 pub fn get_suggestions(&self, partial: &str) -> Vec<String> {
156 let mut suggestions = Vec::new();
157 let partial_lower = partial.to_lowercase();
158
159 // Meta command suggestions
160 if partial.starts_with(':') {
161 for cmd in &self.meta_commands {
162 if cmd.to_lowercase().starts_with(&partial_lower) {
163 suggestions.push(cmd.clone());
164 }
165 }
166 return suggestions;
167 }
168
169 // Cypher keyword suggestions
170 for keyword in &self.cypher_keywords {
171 if keyword.to_lowercase().starts_with(&partial_lower) {
172 suggestions.push(keyword.clone());
173 }
174 }
175
176 // Recent pattern suggestions
177 for pattern in &self.recent_patterns {
178 if pattern.to_lowercase().contains(&partial_lower) && !suggestions.contains(pattern) {
179 suggestions.push(pattern.clone());
180 }
181 }
182
183 suggestions.sort();
184 suggestions.truncate(10); // Limit to top 10 suggestions
185 suggestions
186 }
187
188 /// Add a pattern to recent patterns
189 pub fn add_pattern(&mut self, pattern: String) {
190 self.recent_patterns.push_back(pattern);
191 if self.recent_patterns.len() > 50 {
192 self.recent_patterns.pop_front();
193 }
194 }
195
196 /// Get common Cypher patterns
197 pub fn get_common_patterns(&self) -> Vec<(&str, &str)> {
198 vec![
199 ("Find all nodes", "MATCH (n) RETURN n"),
200 ("Find nodes by label", "MATCH (n:Label) RETURN n"),
201 ("Find relationships", "MATCH (a)-[r]->(b) RETURN a, r, b"),
202 ("Create node", "CREATE (n:Label {property: 'value'})"),
203 ("Create relationship", "MATCH (a), (b) WHERE ... CREATE (a)-[:RELATION]->(b)"),
204 ("Delete nodes", "MATCH (n) DELETE n"),
205 ("Update properties", "MATCH (n) WHERE ... SET n.property = 'new_value'"),
206 ("Count nodes", "MATCH (n) RETURN count(n)"),
207 ("Find paths", "MATCH path = (a)-[*..5]->(b) RETURN path"),
208 ("Group by property", "MATCH (n) RETURN n.property, count(*)"),
209 ]
210 }
211}
212
213/// REPL command execution context
214#[derive(Debug)]
215pub struct ReplContext {
216 pub current_query: Option<String>,
217 pub last_result_count: usize,
218 pub session_queries: usize,
219 pub session_start: std::time::Instant,
220 pub variables: std::collections::HashMap<String, String>,
221}
222
223impl ReplContext {
224 pub fn new() -> Self {
225 Self {
226 current_query: None,
227 last_result_count: 0,
228 session_queries: 0,
229 session_start: std::time::Instant::now(),
230 variables: std::collections::HashMap::new(),
231 }
232 }
233
234 /// Update context after command execution
235 pub fn update_after_command(&mut self, command: &str, result: &CommandResult) {
236 self.current_query = Some(command.to_string());
237 self.session_queries += 1;
238
239 match result {
240 CommandResult::Success(_) => {
241 // Update success metrics
242 }
243 CommandResult::Error(_) => {
244 // Update error metrics
245 }
246 _ => {}
247 }
248 }
249
250 /// Get session statistics
251 pub fn get_session_stats(&self) -> String {
252 let duration = self.session_start.elapsed();
253 format!(
254 "Session Stats:\n Duration: {:?}\n Queries executed: {}\n Variables: {}",
255 duration,
256 self.session_queries,
257 self.variables.len()
258 )
259 }
260}
261
262/// REPL keyboard shortcuts and commands
263pub struct ReplShortcuts;
264
265impl ReplShortcuts {
266 /// Get help text for keyboard shortcuts
267 pub fn get_shortcuts_help() -> String {
268 let mut help = String::new();
269 help.push_str("Keyboard Shortcuts:\n\n");
270 help.push_str("Input Editing:\n");
271 help.push_str(" Ctrl+C Cancel current input\n");
272 help.push_str(" Ctrl+D Exit (EOF)\n");
273 help.push_str(" Tab Auto-complete command\n");
274 help.push_str(" Up/Down Arrow Navigate command history\n");
275 help.push_str(" \\ Line continuation\n\n");
276 help.push_str("Multiline Input:\n");
277 help.push_str(" Empty line Complete multiline command\n");
278 help.push_str(" Ctrl+Enter Force execute current input\n\n");
279 help.push_str("Display:\n");
280 help.push_str(" Ctrl+L Clear screen\n");
281 help.push_str(" :clear Clear screen (command)\n\n");
282 help.push_str("Quick Commands:\n");
283 help.push_str(" :h Help\n");
284 help.push_str(" :q Quit\n");
285 help.push_str(" :s Stats\n");
286
287 help
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_multiline_detection() {
297 let repl = EnhancedRepl::new();
298
299 assert!(repl.is_multiline_starter("MATCH (n:Person)"));
300 assert!(repl.is_multiline_starter("CREATE (a:Node)"));
301 assert!(!repl.is_multiline_starter("MATCH (n) RETURN n"));
302 assert!(!repl.is_multiline_starter("CREATE (n) RETURN n"));
303
304 assert!(repl.is_multiline_ender("RETURN n"));
305 assert!(repl.is_multiline_ender("DELETE n;"));
306 assert!(!repl.is_multiline_ender("WHERE n.age > 25"));
307 }
308
309 #[test]
310 fn test_suggestions() {
311 let suggestions = ReplSuggestions::new();
312
313 let matches = suggestions.get_suggestions("MA");
314 assert!(matches.contains(&"MATCH".to_string()));
315
316 let meta_matches = suggestions.get_suggestions(":h");
317 assert!(meta_matches.contains(&":help".to_string()));
318 }
319
320 #[test]
321 fn test_repl_context() {
322 let mut ctx = ReplContext::new();
323
324 assert_eq!(ctx.session_queries, 0);
325
326 ctx.update_after_command("MATCH (n) RETURN n", &CommandResult::Success(None));
327 assert_eq!(ctx.session_queries, 1);
328 assert!(ctx.current_query.is_some());
329 }
330}