1use crate::{AstVisualizer, GraphVizExporter, ParserValidator, PerformanceProfiler};
4use anyhow::Result;
5use colored::Colorize;
6use std::io::{self, Write};
7
8#[derive(Debug)]
10pub struct DevRepl {
11 language: Option<String>,
12 history: Vec<String>,
13 current_source: Option<String>,
14 visualizer: Option<AstVisualizer>,
15 validator: Option<ParserValidator>,
16 profiler: Option<PerformanceProfiler>,
17 exporter: Option<GraphVizExporter>,
18 prompt: String,
19}
20
21#[derive(Debug, Clone)]
23pub enum ReplCommand {
24 Parse {
25 source: String,
26 },
27 Load {
28 file_path: String,
29 },
30 Show {
31 what: ShowTarget,
32 },
33 Set {
34 option: String,
35 value: String,
36 },
37 Export {
38 format: ExportFormat,
39 output: Option<String>,
40 },
41 Compare {
42 old_source: String,
43 new_source: String,
44 },
45 Profile {
46 command: String,
47 },
48 Help,
49 Clear,
50 History,
51 Exit,
52 Unknown {
53 input: String,
54 },
55}
56
57#[derive(Debug, Clone)]
59pub enum ShowTarget {
60 Ast,
61 Nodes,
62 Edges,
63 Stats,
64 Tree,
65 Validation,
66 Performance,
67 Config,
68}
69
70#[derive(Debug, Clone)]
72pub enum ExportFormat {
73 GraphViz,
74 Json,
75 Csv,
76 Tree,
77}
78
79#[derive(Debug)]
81pub struct ReplResult {
82 pub success: bool,
83 pub output: String,
84 pub error: Option<String>,
85}
86
87impl DevRepl {
88 pub fn new(language: Option<&str>) -> Result<Self> {
90 Ok(Self {
91 language: language.map(|s| s.to_string()),
92 history: Vec::new(),
93 current_source: None,
94 visualizer: None,
95 validator: None,
96 profiler: None,
97 exporter: None,
98 prompt: "codeprism> ".to_string(),
99 })
100 }
101
102 pub fn set_visualizer(&mut self, visualizer: AstVisualizer) {
104 self.visualizer = Some(visualizer);
105 }
106
107 pub fn set_validator(&mut self, validator: ParserValidator) {
109 self.validator = Some(validator);
110 }
111
112 pub fn set_profiler(&mut self, profiler: PerformanceProfiler) {
114 self.profiler = Some(profiler);
115 }
116
117 pub fn set_exporter(&mut self, exporter: GraphVizExporter) {
119 self.exporter = Some(exporter);
120 }
121
122 pub async fn run(&mut self) -> Result<()> {
124 self.print_welcome();
125 self.print_help();
126
127 loop {
128 match self.read_command().await {
129 Ok(command) => {
130 if matches!(command, ReplCommand::Exit) {
131 break;
132 }
133
134 let result = self.execute_command(command).await;
135 self.print_result(&result);
136 }
137 Err(e) => {
138 eprintln!("Error reading command: {}", e);
139 break;
140 }
141 }
142 }
143
144 Ok(())
145 }
146
147 fn print_welcome(&self) {
149 println!("{}", "CodePrism Parser Development REPL".bold().blue());
150 println!("{}", "====================================".blue());
151 if let Some(ref lang) = self.language {
152 println!("Language: {}", lang.green());
153 }
154 println!("Type 'help' for available commands or 'exit' to quit.\n");
155 }
156
157 fn print_help(&self) {
159 println!("{}", "Available Commands:".bold());
160 println!(" {} <code> - Parse source code", "parse".cyan());
161 println!(
162 " {} <file> - Load source from file",
163 "load".cyan()
164 );
165 println!(
166 " {} <target> - Show AST, nodes, edges, stats, etc.",
167 "show".cyan()
168 );
169 println!(
170 " {} <opt> <val> - Set configuration option",
171 "set".cyan()
172 );
173 println!(
174 " {} <fmt> [file] - Export to GraphViz, JSON, etc.",
175 "export".cyan()
176 );
177 println!(
178 " {} <old> <new> - Compare two code snippets",
179 "compare".cyan()
180 );
181 println!(
182 " {} <cmd> - Profile parsing performance",
183 "profile".cyan()
184 );
185 println!(
186 " {} - Show command history",
187 "history".cyan()
188 );
189 println!(" {} - Clear screen", "clear".cyan());
190 println!(" {} - Show this help", "help".cyan());
191 println!(" {} - Exit REPL", "exit".cyan());
192 println!();
193 }
194
195 async fn read_command(&mut self) -> Result<ReplCommand> {
197 print!("{}", self.prompt);
198 io::stdout().flush()?;
199
200 let mut input = String::new();
201 io::stdin().read_line(&mut input)?;
202 let input = input.trim().to_string();
203
204 if !input.is_empty() {
205 self.history.push(input.clone());
206 }
207
208 Ok(self.parse_command(&input))
209 }
210
211 fn parse_command(&self, input: &str) -> ReplCommand {
213 let parts: Vec<&str> = input.split_whitespace().collect();
214
215 if parts.is_empty() {
216 return ReplCommand::Unknown {
217 input: input.to_string(),
218 };
219 }
220
221 match parts[0].to_lowercase().as_str() {
222 "parse" => {
223 if parts.len() > 1 {
224 let source = parts[1..].join(" ");
225 ReplCommand::Parse { source }
226 } else {
227 ReplCommand::Unknown {
228 input: input.to_string(),
229 }
230 }
231 }
232 "load" => {
233 if parts.len() > 1 {
234 ReplCommand::Load {
235 file_path: parts[1].to_string(),
236 }
237 } else {
238 ReplCommand::Unknown {
239 input: input.to_string(),
240 }
241 }
242 }
243 "show" => {
244 if parts.len() > 1 {
245 let target = match parts[1].to_lowercase().as_str() {
246 "ast" => ShowTarget::Ast,
247 "nodes" => ShowTarget::Nodes,
248 "edges" => ShowTarget::Edges,
249 "stats" => ShowTarget::Stats,
250 "tree" => ShowTarget::Tree,
251 "validation" => ShowTarget::Validation,
252 "performance" => ShowTarget::Performance,
253 "config" => ShowTarget::Config,
254 _ => {
255 return ReplCommand::Unknown {
256 input: input.to_string(),
257 }
258 }
259 };
260 ReplCommand::Show { what: target }
261 } else {
262 ReplCommand::Unknown {
263 input: input.to_string(),
264 }
265 }
266 }
267 "set" => {
268 if parts.len() > 2 {
269 ReplCommand::Set {
270 option: parts[1].to_string(),
271 value: parts[2..].join(" "),
272 }
273 } else {
274 ReplCommand::Unknown {
275 input: input.to_string(),
276 }
277 }
278 }
279 "export" => {
280 if parts.len() > 1 {
281 let format = match parts[1].to_lowercase().as_str() {
282 "graphviz" | "dot" => ExportFormat::GraphViz,
283 "json" => ExportFormat::Json,
284 "csv" => ExportFormat::Csv,
285 "tree" => ExportFormat::Tree,
286 _ => {
287 return ReplCommand::Unknown {
288 input: input.to_string(),
289 }
290 }
291 };
292 let output = if parts.len() > 2 {
293 Some(parts[2].to_string())
294 } else {
295 None
296 };
297 ReplCommand::Export { format, output }
298 } else {
299 ReplCommand::Unknown {
300 input: input.to_string(),
301 }
302 }
303 }
304 "compare" => {
305 if parts.len() > 2 {
306 ReplCommand::Compare {
307 old_source: parts[1].to_string(),
308 new_source: parts[2..].join(" "),
309 }
310 } else {
311 ReplCommand::Unknown {
312 input: input.to_string(),
313 }
314 }
315 }
316 "profile" => {
317 if parts.len() > 1 {
318 ReplCommand::Profile {
319 command: parts[1..].join(" "),
320 }
321 } else {
322 ReplCommand::Unknown {
323 input: input.to_string(),
324 }
325 }
326 }
327 "help" => ReplCommand::Help,
328 "clear" => ReplCommand::Clear,
329 "history" => ReplCommand::History,
330 "exit" | "quit" | "q" => ReplCommand::Exit,
331 _ => ReplCommand::Unknown {
332 input: input.to_string(),
333 },
334 }
335 }
336
337 async fn execute_command(&mut self, command: ReplCommand) -> ReplResult {
339 match command {
340 ReplCommand::Parse { source } => self.handle_parse(&source).await,
341 ReplCommand::Load { file_path } => self.handle_load(&file_path).await,
342 ReplCommand::Show { what } => self.handle_show(what).await,
343 ReplCommand::Set { option, value } => self.handle_set(&option, &value).await,
344 ReplCommand::Export { format, output } => self.handle_export(format, output).await,
345 ReplCommand::Compare {
346 old_source,
347 new_source,
348 } => self.handle_compare(&old_source, &new_source).await,
349 ReplCommand::Profile { command } => self.handle_profile(&command).await,
350 ReplCommand::Help => self.handle_help().await,
351 ReplCommand::Clear => self.handle_clear().await,
352 ReplCommand::History => self.handle_history().await,
353 ReplCommand::Exit => ReplResult {
354 success: true,
355 output: "Goodbye!".to_string(),
356 error: None,
357 },
358 ReplCommand::Unknown { input } => ReplResult {
359 success: false,
360 output: String::new(),
361 error: Some(format!(
362 "Unknown command: '{}'. Type 'help' for available commands.",
363 input
364 )),
365 },
366 }
367 }
368
369 async fn handle_parse(&mut self, source: &str) -> ReplResult {
371 self.current_source = Some(source.to_string());
374
375 ReplResult {
376 success: true,
377 output: format!("Parsed source: '{}' (mock implementation)", source),
378 error: None,
379 }
380 }
381
382 async fn handle_load(&mut self, file_path: &str) -> ReplResult {
384 match std::fs::read_to_string(file_path) {
385 Ok(content) => {
386 self.current_source = Some(content.clone());
387 ReplResult {
388 success: true,
389 output: format!("Loaded {} bytes from '{}'", content.len(), file_path),
390 error: None,
391 }
392 }
393 Err(e) => ReplResult {
394 success: false,
395 output: String::new(),
396 error: Some(format!("Failed to load file '{}': {}", file_path, e)),
397 },
398 }
399 }
400
401 async fn handle_show(&self, what: ShowTarget) -> ReplResult {
403 match what {
404 ShowTarget::Ast => {
405 if let Some(ref source) = self.current_source {
406 ReplResult {
407 success: true,
408 output: format!("AST for source (simplified):\n{}", source),
409 error: None,
410 }
411 } else {
412 ReplResult {
413 success: false,
414 output: String::new(),
415 error: Some("No source loaded. Use 'parse' or 'load' first.".to_string()),
416 }
417 }
418 }
419 ShowTarget::Config => {
420 let config_info = format!(
421 "REPL Configuration:\n- Language: {:?}\n- Prompt: {}\n- History size: {}",
422 self.language,
423 self.prompt,
424 self.history.len()
425 );
426 ReplResult {
427 success: true,
428 output: config_info,
429 error: None,
430 }
431 }
432 _ => ReplResult {
433 success: true,
434 output: format!("Show {:?} - not yet implemented", what),
435 error: None,
436 },
437 }
438 }
439
440 async fn handle_set(&mut self, option: &str, value: &str) -> ReplResult {
442 match option.to_lowercase().as_str() {
443 "prompt" => {
444 self.prompt = value.to_string();
445 ReplResult {
446 success: true,
447 output: format!("Prompt set to '{}'", value),
448 error: None,
449 }
450 }
451 "language" => {
452 self.language = Some(value.to_string());
453 ReplResult {
454 success: true,
455 output: format!("Language set to '{}'", value),
456 error: None,
457 }
458 }
459 _ => ReplResult {
460 success: false,
461 output: String::new(),
462 error: Some(format!("Unknown option: '{}'", option)),
463 },
464 }
465 }
466
467 async fn handle_export(&self, format: ExportFormat, output: Option<String>) -> ReplResult {
469 let output_desc = output.as_deref().unwrap_or("stdout");
470 ReplResult {
471 success: true,
472 output: format!(
473 "Export to {:?} format -> {} (not yet implemented)",
474 format, output_desc
475 ),
476 error: None,
477 }
478 }
479
480 async fn handle_compare(&self, old_source: &str, new_source: &str) -> ReplResult {
482 ReplResult {
483 success: true,
484 output: format!(
485 "Compare '{}' vs '{}' (not yet implemented)",
486 old_source, new_source
487 ),
488 error: None,
489 }
490 }
491
492 async fn handle_profile(&mut self, command: &str) -> ReplResult {
494 ReplResult {
495 success: true,
496 output: format!("Profile command '{}' (not yet implemented)", command),
497 error: None,
498 }
499 }
500
501 async fn handle_help(&self) -> ReplResult {
503 self.print_help();
504 ReplResult {
505 success: true,
506 output: String::new(),
507 error: None,
508 }
509 }
510
511 async fn handle_clear(&self) -> ReplResult {
513 print!("\x1B[2J\x1B[1;1H");
515 io::stdout().flush().unwrap_or(());
516
517 ReplResult {
518 success: true,
519 output: String::new(),
520 error: None,
521 }
522 }
523
524 async fn handle_history(&self) -> ReplResult {
526 let mut output = String::new();
527 output.push_str("Command History:\n");
528
529 for (i, cmd) in self.history.iter().enumerate() {
530 output.push_str(&format!(" {}: {}\n", i + 1, cmd));
531 }
532
533 if self.history.is_empty() {
534 output.push_str(" (no commands in history)\n");
535 }
536
537 ReplResult {
538 success: true,
539 output,
540 error: None,
541 }
542 }
543
544 fn print_result(&self, result: &ReplResult) {
546 if let Some(ref error) = result.error {
547 eprintln!("{}", error.red());
548 }
549
550 if !result.output.is_empty() {
551 println!("{}", result.output);
552 }
553
554 if !result.success && result.error.is_none() {
555 eprintln!("{}", "Command failed".red());
556 }
557
558 println!(); }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_repl_creation() {
568 let repl = DevRepl::new(Some("rust")).unwrap();
569 assert_eq!(repl.language, Some("rust".to_string()));
570 assert_eq!(repl.prompt, "codeprism> ");
571 assert!(repl.history.is_empty());
572 }
573
574 #[test]
575 fn test_parse_command() {
576 let repl = DevRepl::new(None).unwrap();
577
578 let cmd = repl.parse_command("parse fn main() {}");
579 match cmd {
580 ReplCommand::Parse { source } => assert_eq!(source, "fn main() {}"),
581 _ => panic!("Expected parse command"),
582 }
583 }
584
585 #[test]
586 fn test_parse_load_command() {
587 let repl = DevRepl::new(None).unwrap();
588
589 let cmd = repl.parse_command("load test.rs");
590 match cmd {
591 ReplCommand::Load { file_path } => assert_eq!(file_path, "test.rs"),
592 _ => panic!("Expected load command"),
593 }
594 }
595
596 #[test]
597 fn test_parse_show_command() {
598 let repl = DevRepl::new(None).unwrap();
599
600 let cmd = repl.parse_command("show ast");
601 match cmd {
602 ReplCommand::Show { what } => assert!(matches!(what, ShowTarget::Ast)),
603 _ => panic!("Expected show command"),
604 }
605 }
606
607 #[test]
608 fn test_parse_unknown_command() {
609 let repl = DevRepl::new(None).unwrap();
610
611 let cmd = repl.parse_command("unknown_command");
612 match cmd {
613 ReplCommand::Unknown { input } => assert_eq!(input, "unknown_command"),
614 _ => panic!("Expected unknown command"),
615 }
616 }
617
618 #[test]
619 fn test_parse_exit_command() {
620 let repl = DevRepl::new(None).unwrap();
621
622 let cmd = repl.parse_command("exit");
623 assert!(matches!(cmd, ReplCommand::Exit));
624
625 let cmd = repl.parse_command("quit");
626 assert!(matches!(cmd, ReplCommand::Exit));
627
628 let cmd = repl.parse_command("q");
629 assert!(matches!(cmd, ReplCommand::Exit));
630 }
631}