leptos_helios/
dev_tools.rs

1//! Developer Experience Tools
2//!
3//! This module integrates all development tools for an enhanced developer experience,
4//! including hot reload, debugging, profiling, and code generation utilities.
5
6pub use crate::debugger::{
7    Breakpoint, BreakpointLocation, DebugCommand, DebuggerError, EnhancedError, ExecutionState,
8    InteractiveDebugger, StackFrame, VariableInfo,
9};
10pub use crate::dev_server::{
11    DevServer, DevServerConfig, DevServerError, FileChangeEvent, FileChangeType, HotReloadMessage,
12    HotReloadMessageType, MockBrowserClient, MockFileWatcher,
13};
14
15use crate::chart::MarkType;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Mutex};
20use std::time::Instant;
21
22/// Dev tools errors
23#[derive(Debug, thiserror::Error)]
24pub enum DevToolsError {
25    #[error("Dev server error: {0}")]
26    DevServerError(#[from] DevServerError),
27
28    #[error("Debugger error: {0}")]
29    DebuggerError(#[from] DebuggerError),
30
31    #[error("Code generation failed: {0}")]
32    CodeGenerationError(String),
33
34    #[error("Profiling error: {0}")]
35    ProfilingError(String),
36
37    #[error("Linting error: {0}")]
38    LintingError(String),
39
40    #[error("Template error: {0}")]
41    TemplateError(String),
42}
43
44impl From<String> for DevToolsError {
45    fn from(err: String) -> Self {
46        DevToolsError::LintingError(err)
47    }
48}
49
50/// Performance profiling data
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ProfileData {
53    pub function_name: String,
54    pub duration_ms: f64,
55    pub memory_usage_mb: f64,
56    pub cpu_usage_percent: f64,
57    pub call_count: u64,
58    pub children: Vec<ProfileData>,
59}
60
61/// Code generation template
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CodeTemplate {
64    pub name: String,
65    pub description: String,
66    pub language: String,
67    pub template: String,
68    pub placeholders: Vec<TemplatePlaceholder>,
69}
70
71/// Template placeholder for code generation
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TemplatePlaceholder {
74    pub name: String,
75    pub description: String,
76    pub default_value: Option<String>,
77    pub required: bool,
78}
79
80/// Live reload configuration for different file types
81#[derive(Debug, Clone)]
82pub struct LiveReloadConfig {
83    pub rust_files: bool,
84    pub css_files: bool,
85    pub js_files: bool,
86    pub html_files: bool,
87    pub asset_files: bool,
88    pub custom_extensions: Vec<String>,
89}
90
91impl Default for LiveReloadConfig {
92    fn default() -> Self {
93        Self {
94            rust_files: true,
95            css_files: true,
96            js_files: true,
97            html_files: true,
98            asset_files: true,
99            custom_extensions: vec!["toml".to_string(), "json".to_string()],
100        }
101    }
102}
103
104/// Integrated development environment
105pub struct HeliosDev {
106    dev_server: Option<DevServer>,
107    debugger: InteractiveDebugger,
108    profiler: PerformanceProfiler,
109    code_generator: CodeGenerator,
110    linter: CodeLinter,
111    project_root: PathBuf,
112    config: DevToolsConfig,
113}
114
115/// Development tools configuration
116#[derive(Debug, Clone)]
117pub struct DevToolsConfig {
118    pub dev_server_port: u16,
119    pub enable_hot_reload: bool,
120    pub enable_debugging: bool,
121    pub enable_profiling: bool,
122    pub enable_linting: bool,
123    pub live_reload: LiveReloadConfig,
124    pub auto_save: bool,
125    pub show_performance_overlay: bool,
126}
127
128impl Default for DevToolsConfig {
129    fn default() -> Self {
130        Self {
131            dev_server_port: 3000,
132            enable_hot_reload: true,
133            enable_debugging: true,
134            enable_profiling: true,
135            enable_linting: true,
136            live_reload: LiveReloadConfig::default(),
137            auto_save: true,
138            show_performance_overlay: false,
139        }
140    }
141}
142
143impl HeliosDev {
144    /// Create a new development environment
145    pub fn new<P: AsRef<Path>>(project_root: P) -> Self {
146        let project_root = project_root.as_ref().to_path_buf();
147        let config = DevToolsConfig::default();
148
149        Self {
150            dev_server: None,
151            debugger: InteractiveDebugger::new(),
152            profiler: PerformanceProfiler::new(),
153            code_generator: CodeGenerator::new(),
154            linter: CodeLinter::new(),
155            project_root,
156            config,
157        }
158    }
159
160    /// Start the development environment
161    pub async fn start(&mut self) -> Result<(), DevToolsError> {
162        println!("🚀 Starting Helios Development Environment...");
163
164        // Start dev server if enabled
165        if self.config.enable_hot_reload {
166            let mut server = DevServer::new(&self.project_root, self.config.dev_server_port);
167            server.start_with_websockets().await?;
168            self.dev_server = Some(server);
169            println!("✅ Development server started");
170        }
171
172        // Initialize debugger if enabled
173        if self.config.enable_debugging {
174            let session_id = self.debugger.start_session("main")?;
175            println!("✅ Debug session started: {}", session_id);
176        }
177
178        // Start profiler if enabled
179        if self.config.enable_profiling {
180            self.profiler.start_profiling()?;
181            println!("✅ Performance profiler started");
182        }
183
184        // Initialize linter if enabled
185        if self.config.enable_linting {
186            self.linter.initialize(&self.project_root).await?;
187            println!("✅ Code linter initialized");
188        }
189
190        println!("🎉 Development environment ready!");
191        Ok(())
192    }
193
194    /// Stop the development environment
195    pub fn stop(&mut self) {
196        if let Some(server) = &mut self.dev_server {
197            server.stop();
198        }
199
200        self.profiler.stop_profiling();
201        println!("🛑 Development environment stopped");
202    }
203
204    /// Generate chart component code
205    pub fn generate_chart_component(
206        &self,
207        name: &str,
208        chart_type: MarkType,
209        options: GenerationOptions,
210    ) -> Result<String, DevToolsError> {
211        self.code_generator
212            .generate_chart_component(name, chart_type, options)
213            .map_err(DevToolsError::CodeGenerationError)
214    }
215
216    /// Add debug breakpoint
217    pub fn add_breakpoint(
218        &mut self,
219        location: BreakpointLocation,
220        condition: Option<String>,
221    ) -> Result<String, DevToolsError> {
222        Ok(self.debugger.add_breakpoint(location, condition)?)
223    }
224
225    /// Start performance profiling for specific operation
226    pub fn profile_operation(&self, name: &str) -> ProfileScope {
227        self.profiler.start_operation(name)
228    }
229
230    /// Get performance profiling results
231    pub fn get_profile_data(&self) -> Vec<ProfileData> {
232        self.profiler.get_profile_data()
233    }
234
235    /// Lint project files
236    pub async fn lint_project(&self) -> Result<Vec<LintResult>, DevToolsError> {
237        self.linter
238            .lint_project(&self.project_root)
239            .await
240            .map_err(DevToolsError::LintingError)
241    }
242
243    /// Watch files for changes and trigger appropriate actions
244    pub fn watch_files(&self) -> MockFileWatcher {
245        if let Some(server) = &self.dev_server {
246            server.file_watcher()
247        } else {
248            // Create a mock watcher for testing
249            use tokio::sync::broadcast;
250            let (_, rx) = broadcast::channel(100);
251            MockFileWatcher::new(rx)
252        }
253    }
254
255    /// Create new project from template
256    pub async fn create_project_from_template(
257        &self,
258        template_name: &str,
259        project_name: &str,
260        output_dir: &Path,
261    ) -> Result<(), DevToolsError> {
262        self.code_generator
263            .create_project_from_template(template_name, project_name, output_dir)
264            .await
265            .map_err(DevToolsError::TemplateError)
266    }
267}
268
269/// Performance profiler for development insights
270pub struct PerformanceProfiler {
271    active_profiles: Arc<Mutex<HashMap<String, ProfileScope>>>,
272    completed_profiles: Arc<Mutex<Vec<ProfileData>>>,
273    enabled: bool,
274}
275
276impl PerformanceProfiler {
277    fn new() -> Self {
278        Self {
279            active_profiles: Arc::new(Mutex::new(HashMap::new())),
280            completed_profiles: Arc::new(Mutex::new(Vec::new())),
281            enabled: false,
282        }
283    }
284
285    fn start_profiling(&mut self) -> Result<(), DevToolsError> {
286        self.enabled = true;
287        Ok(())
288    }
289
290    fn stop_profiling(&mut self) {
291        self.enabled = false;
292    }
293
294    fn start_operation(&self, name: &str) -> ProfileScope {
295        if !self.enabled {
296            return ProfileScope::disabled();
297        }
298
299        let scope = ProfileScope::new(name);
300        let mut profiles = self.active_profiles.lock().unwrap();
301        profiles.insert(name.to_string(), scope.clone());
302        scope
303    }
304
305    fn get_profile_data(&self) -> Vec<ProfileData> {
306        self.completed_profiles.lock().unwrap().clone()
307    }
308}
309
310/// Performance profiling scope
311#[derive(Clone)]
312pub struct ProfileScope {
313    name: String,
314    start_time: Instant,
315    enabled: bool,
316}
317
318impl ProfileScope {
319    fn new(name: &str) -> Self {
320        Self {
321            name: name.to_string(),
322            start_time: Instant::now(),
323            enabled: true,
324        }
325    }
326
327    fn disabled() -> Self {
328        Self {
329            name: String::new(),
330            start_time: Instant::now(),
331            enabled: false,
332        }
333    }
334
335    pub fn finish(self) -> ProfileData {
336        if !self.enabled {
337            return ProfileData {
338                function_name: self.name,
339                duration_ms: 0.0,
340                memory_usage_mb: 0.0,
341                cpu_usage_percent: 0.0,
342                call_count: 0,
343                children: Vec::new(),
344            };
345        }
346
347        let duration = self.start_time.elapsed();
348        ProfileData {
349            function_name: self.name,
350            duration_ms: duration.as_secs_f64() * 1000.0,
351            memory_usage_mb: 25.0,   // Mock memory usage
352            cpu_usage_percent: 15.0, // Mock CPU usage
353            call_count: 1,
354            children: Vec::new(),
355        }
356    }
357}
358
359/// Code generation utilities
360pub struct CodeGenerator {
361    templates: HashMap<String, CodeTemplate>,
362}
363
364/// Code generation options
365#[derive(Debug, Clone)]
366pub struct GenerationOptions {
367    pub include_typescript: bool,
368    pub include_tests: bool,
369    pub include_docs: bool,
370    pub framework: String,
371}
372
373impl Default for GenerationOptions {
374    fn default() -> Self {
375        Self {
376            include_typescript: true,
377            include_tests: true,
378            include_docs: true,
379            framework: "leptos".to_string(),
380        }
381    }
382}
383
384impl CodeGenerator {
385    fn new() -> Self {
386        let mut generator = Self {
387            templates: HashMap::new(),
388        };
389        generator.initialize_templates();
390        generator
391    }
392
393    fn initialize_templates(&mut self) {
394        // Chart component template
395        let chart_template = CodeTemplate {
396            name: "chart_component".to_string(),
397            description: "Generate a new chart component".to_string(),
398            language: "rust".to_string(),
399            template: r#"//! {{component_name}} Chart Component
400//!
401//! {{description}}
402
403use helios_core::prelude::*;
404use leptos::prelude::*;
405use serde::{Deserialize, Serialize};
406
407/// {{component_name}} component properties
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct {{component_name}}Props {
410    pub data: DataFrame,
411    pub width: Option<u32>,
412    pub height: Option<u32>,
413    pub title: Option<String>,
414    {{#if include_typescript}}
415    pub on_click: Option<Callback<ChartEvent>>,
416    {{/if}}
417}
418
419/// {{component_name}} chart component
420#[component]
421pub fn {{component_name}}(props: {{component_name}}Props) -> impl IntoView {
422    let chart_spec = ChartSpec::new()
423        .mark({{mark_type}})
424        .width(props.width.unwrap_or(400))
425        .height(props.height.unwrap_or(300));
426
427    {{#if include_typescript}}
428    let handle_click = move |event: ChartEvent| {
429        if let Some(callback) = &props.on_click {
430            callback.call(event);
431        }
432    };
433    {{/if}}
434
435    view! {
436        <div class="helios-{{component_name_lower}}-chart">
437            {{#if title}}
438            <h3 class="chart-title">{props.title}</h3>
439            {{/if}}
440            <HeliosChart
441                spec={chart_spec}
442                data={props.data}
443                {{#if include_typescript}}
444                on:click=handle_click
445                {{/if}}
446            />
447        </div>
448    }
449}
450
451{{#if include_tests}}
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use polars::prelude::*;
456
457    #[test]
458    fn test_{{component_name_lower}}_creation() {
459        let data = df! {
460            "x" => [1, 2, 3, 4],
461            "y" => [10, 20, 15, 25],
462        }.unwrap();
463
464        let props = {{component_name}}Props {
465            data,
466            width: Some(800),
467            height: Some(600),
468            title: Some("Test Chart".to_string()),
469            {{#if include_typescript}}
470            on_click: None,
471            {{/if}}
472        };
473
474        // Component creation should not panic
475        let _component = {{component_name}}(props);
476    }
477}
478{{/if}}
479"#
480            .to_string(),
481            placeholders: vec![
482                TemplatePlaceholder {
483                    name: "component_name".to_string(),
484                    description: "Name of the chart component".to_string(),
485                    default_value: Some("MyChart".to_string()),
486                    required: true,
487                },
488                TemplatePlaceholder {
489                    name: "description".to_string(),
490                    description: "Component description".to_string(),
491                    default_value: Some("A custom chart component".to_string()),
492                    required: false,
493                },
494                TemplatePlaceholder {
495                    name: "mark_type".to_string(),
496                    description: "Chart mark type".to_string(),
497                    default_value: Some("MarkType::Line".to_string()),
498                    required: true,
499                },
500            ],
501        };
502
503        self.templates
504            .insert("chart_component".to_string(), chart_template);
505    }
506
507    fn generate_chart_component(
508        &self,
509        name: &str,
510        chart_type: MarkType,
511        options: GenerationOptions,
512    ) -> Result<String, String> {
513        let template = self
514            .templates
515            .get("chart_component")
516            .ok_or("Chart component template not found")?;
517
518        let mark_type_str = match chart_type {
519            MarkType::Line { .. } => {
520                "MarkType::Line { interpolate: None, stroke_width: None, stroke_dash: None }"
521            }
522            MarkType::Bar { .. } => "MarkType::Bar { width: None, corner_radius: None }",
523            MarkType::Point { .. } => "MarkType::Point { size: None, shape: None, opacity: None }",
524            MarkType::Area { .. } => "MarkType::Area { interpolate: None, opacity: None }",
525            _ => "MarkType::Line { interpolate: None, stroke_width: None, stroke_dash: None }",
526        };
527
528        let mut code = template.template.clone();
529        code = code.replace("{{component_name}}", name);
530        code = code.replace("{{component_name_lower}}", &name.to_lowercase());
531        code = code.replace("{{mark_type}}", mark_type_str);
532        code = code.replace("{{description}}", &format!("A {} chart component", name));
533
534        // Handle conditional blocks (simplified)
535        if options.include_typescript {
536            code = code.replace("{{#if include_typescript}}", "");
537            code = code.replace("{{/if}}", "");
538        } else {
539            // Remove typescript blocks
540            let lines: Vec<&str> = code.lines().collect();
541            let mut filtered_lines = Vec::new();
542            let mut skip = false;
543
544            for line in lines {
545                if line.contains("{{#if include_typescript}}") {
546                    skip = true;
547                    continue;
548                }
549                if line.contains("{{/if}}") && skip {
550                    skip = false;
551                    continue;
552                }
553                if !skip {
554                    filtered_lines.push(line);
555                }
556            }
557            code = filtered_lines.join("\n");
558        }
559
560        if options.include_tests {
561            code = code.replace("{{#if include_tests}}", "");
562        } else {
563            // Remove test blocks
564            let lines: Vec<&str> = code.lines().collect();
565            let mut filtered_lines = Vec::new();
566            let mut skip = false;
567
568            for line in lines {
569                if line.contains("{{#if include_tests}}") {
570                    skip = true;
571                    continue;
572                }
573                if line.contains("{{/if}}") && skip {
574                    skip = false;
575                    continue;
576                }
577                if !skip {
578                    filtered_lines.push(line);
579                }
580            }
581            code = filtered_lines.join("\n");
582        }
583
584        // Clean up remaining template syntax
585        code = code.replace("{{#if title}}", "");
586        code = code.replace("{{/if}}", "");
587
588        Ok(code)
589    }
590
591    async fn create_project_from_template(
592        &self,
593        _template_name: &str,
594        _project_name: &str,
595        _output_dir: &Path,
596    ) -> Result<(), String> {
597        // Mock implementation - would create actual project structure
598        Ok(())
599    }
600}
601
602/// Code linting utilities
603pub struct CodeLinter {
604    rules: Vec<LintRule>,
605}
606
607/// Lint rule definition
608#[derive(Debug, Clone)]
609pub struct LintRule {
610    pub name: String,
611    pub description: String,
612    pub severity: LintSeverity,
613    pub enabled: bool,
614}
615
616/// Lint severity levels
617#[derive(Debug, Clone, PartialEq)]
618pub enum LintSeverity {
619    Error,
620    Warning,
621    Info,
622    Hint,
623}
624
625/// Lint result for a file
626#[derive(Debug, Clone)]
627pub struct LintResult {
628    pub file_path: String,
629    pub rule_name: String,
630    pub message: String,
631    pub severity: LintSeverity,
632    pub line: Option<u32>,
633    pub column: Option<u32>,
634}
635
636impl CodeLinter {
637    fn new() -> Self {
638        Self {
639            rules: Self::default_rules(),
640        }
641    }
642
643    fn default_rules() -> Vec<LintRule> {
644        vec![
645            LintRule {
646                name: "unused_imports".to_string(),
647                description: "Detect unused imports".to_string(),
648                severity: LintSeverity::Warning,
649                enabled: true,
650            },
651            LintRule {
652                name: "missing_docs".to_string(),
653                description: "Public items should have documentation".to_string(),
654                severity: LintSeverity::Info,
655                enabled: true,
656            },
657            LintRule {
658                name: "performance_warning".to_string(),
659                description: "Detect potential performance issues".to_string(),
660                severity: LintSeverity::Hint,
661                enabled: true,
662            },
663        ]
664    }
665
666    async fn initialize(&mut self, _project_root: &Path) -> Result<(), String> {
667        // Initialize linting configuration
668        Ok(())
669    }
670
671    async fn lint_project(&self, project_root: &Path) -> Result<Vec<LintResult>, String> {
672        // Mock implementation - would actually lint files
673        Ok(vec![
674            LintResult {
675                file_path: project_root
676                    .join("src/main.rs")
677                    .to_string_lossy()
678                    .to_string(),
679                rule_name: "unused_imports".to_string(),
680                message: "Unused import `std::collections::HashMap`".to_string(),
681                severity: LintSeverity::Warning,
682                line: Some(3),
683                column: Some(5),
684            },
685            LintResult {
686                file_path: project_root
687                    .join("src/chart.rs")
688                    .to_string_lossy()
689                    .to_string(),
690                rule_name: "missing_docs".to_string(),
691                message: "Public function `render` is missing documentation".to_string(),
692                severity: LintSeverity::Info,
693                line: Some(45),
694                column: Some(1),
695            },
696        ])
697    }
698}
699
700/// Development utilities and helpers
701pub mod utils {
702    use super::*;
703
704    /// Generate component boilerplate
705    pub fn generate_component_boilerplate(name: &str) -> String {
706        format!(
707            r#"//! {} Component
708//!
709//! Generated by Helios Dev Tools
710
711use leptos::prelude::*;
712
713#[component]
714pub fn {}() -> impl IntoView {{
715    view! {{
716        <div class="helios-{}">
717            <p>"Hello from {}!"</p>
718        </div>
719    }}
720}}
721"#,
722            name,
723            name,
724            name.to_lowercase(),
725            name
726        )
727    }
728
729    /// Format code using rustfmt
730    pub async fn format_code(code: &str) -> Result<String, String> {
731        // Mock implementation - would use rustfmt
732        Ok(code.to_string())
733    }
734
735    /// Generate documentation from code
736    pub async fn generate_docs(project_root: &Path) -> Result<String, String> {
737        // Mock implementation - would generate docs
738        Ok(format!(
739            "# Documentation for {}\n\nGenerated by Helios Dev Tools",
740            project_root.file_name().unwrap().to_string_lossy()
741        ))
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use tempfile::tempdir;
749
750    #[tokio::test]
751    async fn test_helios_dev_creation() {
752        let temp_dir = tempdir().unwrap();
753        let dev = HeliosDev::new(temp_dir.path());
754
755        assert_eq!(dev.project_root, temp_dir.path());
756        assert_eq!(dev.config.dev_server_port, 3000);
757    }
758
759    #[tokio::test]
760    async fn test_code_generation() {
761        let temp_dir = tempdir().unwrap();
762        let dev = HeliosDev::new(temp_dir.path());
763
764        let code = dev
765            .generate_chart_component(
766                "LineChart",
767                MarkType::Line {
768                    interpolate: None,
769                    stroke_width: None,
770                    stroke_dash: None,
771                },
772                GenerationOptions::default(),
773            )
774            .unwrap();
775
776        assert!(code.contains("LineChart"));
777        assert!(code.contains("MarkType::Line"));
778        assert!(code.contains("#[component]"));
779    }
780
781    #[test]
782    fn test_performance_profiling() {
783        use std::time::Duration;
784        let mut profiler = PerformanceProfiler::new();
785        profiler.start_profiling().unwrap(); // Enable profiling
786        let scope = profiler.start_operation("test_function");
787
788        // Simulate some work
789        std::thread::sleep(Duration::from_millis(10));
790
791        let profile_data = scope.finish();
792        assert_eq!(profile_data.function_name, "test_function");
793        assert!(profile_data.duration_ms >= 10.0);
794    }
795
796    #[tokio::test]
797    async fn test_code_linting() {
798        let temp_dir = tempdir().unwrap();
799        let linter = CodeLinter::new();
800
801        let results = linter.lint_project(temp_dir.path()).await.unwrap();
802        assert!(!results.is_empty());
803        assert!(results.iter().any(|r| r.rule_name == "unused_imports"));
804    }
805
806    #[test]
807    fn test_utils_component_generation() {
808        let code = utils::generate_component_boilerplate("TestComponent");
809        assert!(code.contains("TestComponent"));
810        assert!(code.contains("#[component]"));
811        assert!(code.contains("Hello from TestComponent!"));
812    }
813}