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: '{}'. Type 'help' for available commands.",
363                    input
364                )),
365            },
366        }
367    }
368
369    /// Handle parse command
370    async fn handle_parse(&mut self, source: &str) -> ReplResult {
371        // This is a simplified implementation
372        // In a real REPL, this would use the actual parser
373        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    /// Handle load command
383    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    /// Handle show command
402    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    /// Handle set command
441    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    /// Handle export command
468    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    /// Handle compare command
481    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    /// Handle profile command
493    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    /// Handle help command
502    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    /// Handle clear command
512    async fn handle_clear(&self) -> ReplResult {
513        // Clear screen
514        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    /// Handle history command
525    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    /// Print command result
545    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!(); // Add spacing
559    }
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}