this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 330 lines 12 kB view raw
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}