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: '{input}'. Type 'help' for available commands."
363 )),
364 },
365 }
366 }
367
368 async fn handle_parse(&mut self, source: &str) -> ReplResult {
370 self.current_source = Some(source.to_string());
373
374 ReplResult {
375 success: true,
376 output: format!("Parsed source: '{source}' (mock implementation)"),
377 error: None,
378 }
379 }
380
381 async fn handle_load(&mut self, file_path: &str) -> ReplResult {
383 match std::fs::read_to_string(file_path) {
384 Ok(content) => {
385 self.current_source = Some(content.clone());
386 ReplResult {
387 success: true,
388 output: format!("Loaded {} bytes from '{}'", content.len(), file_path),
389 error: None,
390 }
391 }
392 Err(e) => ReplResult {
393 success: false,
394 output: String::new(),
395 error: Some(format!("Failed to load file '{file_path}': {e}")),
396 },
397 }
398 }
399
400 async fn handle_show(&self, what: ShowTarget) -> ReplResult {
402 match what {
403 ShowTarget::Ast => {
404 if let Some(ref source) = self.current_source {
405 ReplResult {
406 success: true,
407 output: format!("AST for source (simplified):\n{source}"),
408 error: None,
409 }
410 } else {
411 ReplResult {
412 success: false,
413 output: String::new(),
414 error: Some("No source loaded. Use 'parse' or 'load' first.".to_string()),
415 }
416 }
417 }
418 ShowTarget::Config => {
419 let config_info = format!(
420 "REPL Configuration:\n- Language: {:?}\n- Prompt: {}\n- History size: {}",
421 self.language,
422 self.prompt,
423 self.history.len()
424 );
425 ReplResult {
426 success: true,
427 output: config_info,
428 error: None,
429 }
430 }
431 _ => ReplResult {
432 success: true,
433 output: format!("Show {what:?} - not yet implemented"),
434 error: None,
435 },
436 }
437 }
438
439 async fn handle_set(&mut self, option: &str, value: &str) -> ReplResult {
441 match option.to_lowercase().as_str() {
442 "prompt" => {
443 self.prompt = value.to_string();
444 ReplResult {
445 success: true,
446 output: format!("Prompt set to '{value}'"),
447 error: None,
448 }
449 }
450 "language" => {
451 self.language = Some(value.to_string());
452 ReplResult {
453 success: true,
454 output: format!("Language set to '{value}'"),
455 error: None,
456 }
457 }
458 _ => ReplResult {
459 success: false,
460 output: String::new(),
461 error: Some(format!("Unknown option: '{option}'")),
462 },
463 }
464 }
465
466 async fn handle_export(&self, format: ExportFormat, output: Option<String>) -> ReplResult {
468 let output_desc = output.as_deref().unwrap_or("stdout");
469 ReplResult {
470 success: true,
471 output: format!("Export to {format:?} format -> {output_desc} (not yet implemented)"),
472 error: None,
473 }
474 }
475
476 async fn handle_compare(&self, old_source: &str, new_source: &str) -> ReplResult {
478 ReplResult {
479 success: true,
480 output: format!("Compare '{old_source}' vs '{new_source}' (not yet implemented)"),
481 error: None,
482 }
483 }
484
485 async fn handle_profile(&mut self, command: &str) -> ReplResult {
487 ReplResult {
488 success: true,
489 output: format!("Profile command '{command}' (not yet implemented)"),
490 error: None,
491 }
492 }
493
494 async fn handle_help(&self) -> ReplResult {
496 self.print_help();
497 ReplResult {
498 success: true,
499 output: String::new(),
500 error: None,
501 }
502 }
503
504 async fn handle_clear(&self) -> ReplResult {
506 print!("\x1B[2J\x1B[1;1H");
508 io::stdout().flush().unwrap_or(());
509
510 ReplResult {
511 success: true,
512 output: String::new(),
513 error: None,
514 }
515 }
516
517 async fn handle_history(&self) -> ReplResult {
519 let mut output = String::new();
520 output.push_str("Command History:\n");
521
522 for (i, cmd) in self.history.iter().enumerate() {
523 output.push_str(&format!(" {}: {}\n", i + 1, cmd));
524 }
525
526 if self.history.is_empty() {
527 output.push_str(" (no commands in history)\n");
528 }
529
530 ReplResult {
531 success: true,
532 output,
533 error: None,
534 }
535 }
536
537 fn print_result(&self, result: &ReplResult) {
539 if let Some(ref error) = result.error {
540 eprintln!("{}", error.red());
541 }
542
543 if !result.output.is_empty() {
544 println!("{}", result.output);
545 }
546
547 if !result.success && result.error.is_none() {
548 eprintln!("{}", "Command failed".red());
549 }
550
551 println!(); }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn test_repl_creation() {
561 let repl = DevRepl::new(Some("rust")).unwrap();
562 assert_eq!(repl.language, Some("rust".to_string()));
563 assert_eq!(repl.prompt, "codeprism> ");
564 assert!(repl.history.is_empty(), "Should be empty initially");
565 }
566
567 #[test]
568 fn test_parse_command() {
569 let repl = DevRepl::new(None).unwrap();
570
571 let cmd = repl.parse_command("parse fn main() {}");
572 match cmd {
573 ReplCommand::Parse { source } => assert_eq!(source, "fn main() {}"),
574 _ => panic!("Expected parse command"),
575 }
576 }
577
578 #[test]
579 fn test_parse_load_command() {
580 let repl = DevRepl::new(None).unwrap();
581
582 let cmd = repl.parse_command("load test.rs");
583 match cmd {
584 ReplCommand::Load { file_path } => assert_eq!(file_path, "test.rs"),
585 _ => panic!("Expected load command"),
586 }
587 }
588
589 #[test]
590 fn test_parse_show_command() {
591 let repl = DevRepl::new(None).unwrap();
592
593 let cmd = repl.parse_command("show ast");
594 match cmd {
595 ReplCommand::Show { what } => assert!(matches!(what, ShowTarget::Ast)),
596 _ => panic!("Expected show command"),
597 }
598 }
599
600 #[test]
601 fn test_parse_unknown_command() {
602 let repl = DevRepl::new(None).unwrap();
603
604 let cmd = repl.parse_command("unknown_command");
605 match cmd {
606 ReplCommand::Unknown { input } => assert_eq!(input, "unknown_command"),
607 _ => panic!("Expected unknown command"),
608 }
609 }
610
611 #[test]
612 fn test_parse_exit_command() {
613 let repl = DevRepl::new(None).unwrap();
614
615 let cmd = repl.parse_command("exit");
616 assert!(matches!(cmd, ReplCommand::Exit));
617
618 let cmd = repl.parse_command("quit");
619 assert!(matches!(cmd, ReplCommand::Exit));
620
621 let cmd = repl.parse_command("q");
622 assert!(matches!(cmd, ReplCommand::Exit));
623 }
624}