codeprism_dev_tools/
dev_repl.rs

1//! Interactive development REPL for parser development
2
3use crate::{AstVisualizer, GraphVizExporter, ParserValidator, PerformanceProfiler};
4use anyhow::Result;
5use colored::Colorize;
6use std::io::{self, Write};
7
8/// Interactive development REPL
9#[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/// REPL command types
22#[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/// What to show in the REPL
58#[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/// Export format options
71#[derive(Debug, Clone)]
72pub enum ExportFormat {
73    GraphViz,
74    Json,
75    Csv,
76    Tree,
77}
78
79/// Result of executing a REPL command
80#[derive(Debug)]
81pub struct ReplResult {
82    pub success: bool,
83    pub output: String,
84    pub error: Option<String>,
85}
86
87impl DevRepl {
88    /// Create a new development REPL
89    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    /// Set the AST visualizer
103    pub fn set_visualizer(&mut self, visualizer: AstVisualizer) {
104        self.visualizer = Some(visualizer);
105    }
106
107    /// Set the parser validator
108    pub fn set_validator(&mut self, validator: ParserValidator) {
109        self.validator = Some(validator);
110    }
111
112    /// Set the performance profiler
113    pub fn set_profiler(&mut self, profiler: PerformanceProfiler) {
114        self.profiler = Some(profiler);
115    }
116
117    /// Set the GraphViz exporter
118    pub fn set_exporter(&mut self, exporter: GraphVizExporter) {
119        self.exporter = Some(exporter);
120    }
121
122    /// Run the interactive REPL
123    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    /// Print welcome message
148    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    /// Print help information
158    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    /// Read a command from the user
196    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    /// Parse a command string
212    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    /// Execute a REPL command
338    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    /// Handle parse command
369    async fn handle_parse(&mut self, source: &str) -> ReplResult {
370        // This is a simplified implementation
371        // In a real REPL, this would use the actual parser
372        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    /// Handle load command
382    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    /// Handle show command
401    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    /// Handle set command
440    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    /// Handle export command
467    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    /// Handle compare command
477    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    /// Handle profile command
486    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    /// Handle help command
495    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    /// Handle clear command
505    async fn handle_clear(&self) -> ReplResult {
506        // Clear screen
507        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    /// Handle history command
518    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    /// Print command result
538    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!(); // Add spacing
552    }
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}