Skip to main content

raz_core/
providers.rs

1//! Command providers for different frameworks and tools
2
3use crate::{Command, CommandBuilder, CommandCategory, ProjectContext, ProjectType, RazResult};
4use async_trait::async_trait;
5
6// Framework providers
7pub mod bevy;
8pub mod dioxus;
9pub mod leptos;
10pub mod tauri;
11pub mod yew;
12
13pub use bevy::BevyProvider;
14pub use dioxus::DioxusProvider;
15pub use leptos::LeptosProvider;
16pub use tauri::TauriProvider;
17pub use yew::YewProvider;
18
19/// Trait for implementing command providers
20#[async_trait]
21pub trait CommandProvider: Send + Sync {
22    /// Unique name for this provider
23    fn name(&self) -> &str;
24
25    /// Generate commands for the given context
26    async fn commands(&self, context: &ProjectContext) -> RazResult<Vec<Command>>;
27
28    /// Priority for this provider (higher = more important)
29    fn priority(&self) -> u8 {
30        50
31    }
32
33    /// Whether this provider can handle the given context
34    fn can_handle(&self, context: &ProjectContext) -> bool {
35        let _ = context;
36        true
37    }
38}
39
40/// Built-in cargo provider that handles standard Rust project commands
41pub struct CargoProvider {
42    name: String,
43}
44
45impl Default for CargoProvider {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl CargoProvider {
52    pub fn new() -> Self {
53        Self {
54            name: "cargo".to_string(),
55        }
56    }
57
58    /// Resolve test context for enhanced command generation
59    async fn resolve_test_context(
60        &self,
61        context: &ProjectContext,
62    ) -> RazResult<Option<crate::TestContext>> {
63        let Some(ref file_context) = context.current_file else {
64            return Ok(None);
65        };
66
67        // Only proceed if we're in a Rust file
68        if file_context.language != crate::Language::Rust {
69            return Ok(None);
70        }
71
72        let mut test_context = crate::TestContext::new();
73
74        // 1. Resolve package name from workspace structure
75        test_context.package_name = self.resolve_package_name(context, &file_context.path)?;
76
77        // 2. Resolve target type from file path
78        test_context.target_type = self.resolve_target_type(context, &file_context.path)?;
79
80        // 3. Build module path from file path and detected modules
81        test_context.module_path = self.build_module_path(&file_context.path, file_context)?;
82
83        // 4. Set test name if cursor is on a test function
84        if let Some(ref cursor_symbol) = file_context.cursor_symbol {
85            if cursor_symbol.kind == crate::SymbolKind::Test
86                || (cursor_symbol.kind == crate::SymbolKind::Function
87                    && cursor_symbol.name.starts_with("test_"))
88            {
89                test_context.test_name = Some(cursor_symbol.name.clone());
90            }
91        }
92
93        // 5. Add features and environment variables
94        test_context.features = context.active_features.clone();
95        test_context.env_vars = context
96            .env_vars
97            .iter()
98            .map(|(k, v)| (k.clone(), v.clone()))
99            .collect();
100        test_context.working_dir = Some(context.workspace_root.clone());
101
102        // Only return if we have meaningful test information
103        if test_context.is_precise() || test_context.package_name.is_some() {
104            Ok(Some(test_context))
105        } else {
106            Ok(None)
107        }
108    }
109
110    /// Build module path from file path and file context
111    fn build_module_path(
112        &self,
113        file_path: &std::path::Path,
114        file_context: &crate::FileContext,
115    ) -> RazResult<Vec<String>> {
116        let mut module_path = Vec::new();
117
118        // Start with file-based module path
119        if let Some(base_path) = crate::FileAnalyzer::extract_module_path(file_path) {
120            module_path = base_path
121                .split("::")
122                .filter(|s| !s.is_empty())
123                .map(|s| s.to_string())
124                .collect();
125        }
126
127        // Look for test modules in the symbols
128        let has_test_module = file_context.symbols.iter().any(|s| {
129            s.kind == crate::SymbolKind::Module && (s.name == "tests" || s.name.contains("test"))
130        });
131
132        // If we have test modules and we're in a test context, add "tests" to the path
133        if has_test_module && !module_path.iter().any(|m| m == "tests") {
134            // Check if we're likely in a test context based on file content or cursor symbol
135            let likely_in_test = file_context
136                .cursor_symbol
137                .as_ref()
138                .map(|s| s.kind == crate::SymbolKind::Test)
139                .unwrap_or(false);
140
141            if likely_in_test {
142                module_path.push("tests".to_string());
143            }
144        }
145
146        Ok(module_path)
147    }
148
149    /// Resolve package name from workspace structure
150    fn resolve_package_name(
151        &self,
152        context: &ProjectContext,
153        file_path: &std::path::Path,
154    ) -> RazResult<Option<String>> {
155        // For single-package projects
156        if context.workspace_members.len() == 1 {
157            return Ok(Some(context.workspace_members[0].name.clone()));
158        }
159
160        // For workspace projects, find which member contains this file
161        for member in &context.workspace_members {
162            let member_path = if member.path.is_absolute() {
163                member.path.clone()
164            } else {
165                context.workspace_root.join(&member.path)
166            };
167
168            if file_path.starts_with(&member_path) {
169                return Ok(Some(member.name.clone()));
170            }
171        }
172
173        Ok(None)
174    }
175
176    /// Resolve target type from file path
177    fn resolve_target_type(
178        &self,
179        context: &ProjectContext,
180        file_path: &std::path::Path,
181    ) -> RazResult<crate::TestTargetType> {
182        let file_str = file_path.to_string_lossy();
183
184        // Check for integration tests (tests/ directory)
185        if file_str.contains("/tests/") {
186            if let Some(test_name) = file_path.file_stem().and_then(|s| s.to_str()) {
187                return Ok(crate::TestTargetType::Test(test_name.to_string()));
188            }
189        }
190
191        // Check for examples (examples/ directory)
192        if file_str.contains("/examples/") {
193            if let Some(example_name) = file_path.file_stem().and_then(|s| s.to_str()) {
194                return Ok(crate::TestTargetType::Example(example_name.to_string()));
195            }
196        }
197
198        // Check for benchmarks (benches/ directory)
199        if file_str.contains("/benches/") {
200            if let Some(bench_name) = file_path.file_stem().and_then(|s| s.to_str()) {
201                return Ok(crate::TestTargetType::Bench(bench_name.to_string()));
202            }
203        }
204
205        // Check if it's a library file
206        if file_str.contains("/src/lib.rs") || file_str.contains("/src/mod.rs") {
207            return Ok(crate::TestTargetType::Lib);
208        }
209
210        // Check for binary files
211        if file_str.contains("/src/main.rs") {
212            return Ok(crate::TestTargetType::Bin("main".to_string()));
213        }
214
215        if file_str.contains("/src/bin/") {
216            if let Some(bin_name) = file_path.file_stem().and_then(|s| s.to_str()) {
217                return Ok(crate::TestTargetType::Bin(bin_name.to_string()));
218            }
219        }
220
221        // Check against build targets for more precise detection
222        for target in &context.build_targets {
223            if file_path.starts_with(target.path.parent().unwrap_or(&context.workspace_root)) {
224                return match target.target_type {
225                    crate::TargetType::Binary => {
226                        Ok(crate::TestTargetType::Bin(target.name.clone()))
227                    }
228                    crate::TargetType::Library => Ok(crate::TestTargetType::Lib),
229                    crate::TargetType::Test => Ok(crate::TestTargetType::Test(target.name.clone())),
230                    crate::TargetType::Bench => {
231                        Ok(crate::TestTargetType::Bench(target.name.clone()))
232                    }
233                    crate::TargetType::Example => {
234                        Ok(crate::TestTargetType::Example(target.name.clone()))
235                    }
236                };
237            }
238        }
239
240        // Default to library
241        Ok(crate::TestTargetType::Lib)
242    }
243}
244
245#[async_trait]
246impl CommandProvider for CargoProvider {
247    fn name(&self) -> &str {
248        &self.name
249    }
250
251    fn priority(&self) -> u8 {
252        100 // High priority as cargo is fundamental to Rust projects
253    }
254
255    fn can_handle(&self, context: &ProjectContext) -> bool {
256        // Cargo provider can handle any Rust project
257        !matches!(context.project_type, ProjectType::Mixed(_))
258    }
259
260    async fn commands(&self, context: &ProjectContext) -> RazResult<Vec<Command>> {
261        let mut commands = Vec::new();
262        let cwd = context.workspace_root.clone();
263
264        // Basic cargo commands
265        commands.extend([
266            // Build commands
267            CommandBuilder::new("cargo-build", "cargo")
268                .label("Build Project")
269                .description("Build the project in debug mode")
270                .arg("build")
271                .category(CommandCategory::Build)
272                .priority(80)
273                .tag("cargo")
274                .tag("build")
275                .cwd(cwd.clone())
276                .estimated_duration(30)
277                .build(),
278            CommandBuilder::new("cargo-build-release", "cargo")
279                .label("Build Release")
280                .description("Build the project in release mode with optimizations")
281                .args(vec!["build".to_string(), "--release".to_string()])
282                .category(CommandCategory::Build)
283                .priority(70)
284                .tag("cargo")
285                .tag("build")
286                .tag("release")
287                .cwd(cwd.clone())
288                .estimated_duration(120)
289                .build(),
290            // Test commands
291            CommandBuilder::new("cargo-test", "cargo")
292                .label("Run Tests")
293                .description("Run all tests in the project")
294                .arg("test")
295                .category(CommandCategory::Test)
296                .priority(85)
297                .tag("cargo")
298                .tag("test")
299                .cwd(cwd.clone())
300                .estimated_duration(60)
301                .build(),
302            CommandBuilder::new("cargo-test-release", "cargo")
303                .label("Run Tests (Release)")
304                .description("Run tests in release mode")
305                .args(vec!["test".to_string(), "--release".to_string()])
306                .category(CommandCategory::Test)
307                .priority(60)
308                .tag("cargo")
309                .tag("test")
310                .tag("release")
311                .cwd(cwd.clone())
312                .estimated_duration(90)
313                .build(),
314            // Check commands
315            CommandBuilder::new("cargo-check", "cargo")
316                .label("Check Code")
317                .description("Check for compilation errors without building")
318                .arg("check")
319                .category(CommandCategory::Lint)
320                .priority(90)
321                .tag("cargo")
322                .tag("check")
323                .cwd(cwd.clone())
324                .estimated_duration(15)
325                .build(),
326            CommandBuilder::new("cargo-clippy", "cargo")
327                .label("Run Clippy")
328                .description("Run Clippy lints to catch common mistakes")
329                .arg("clippy")
330                .category(CommandCategory::Lint)
331                .priority(75)
332                .tag("cargo")
333                .tag("clippy")
334                .tag("lint")
335                .cwd(cwd.clone())
336                .estimated_duration(30)
337                .build(),
338            // Format commands
339            CommandBuilder::new("cargo-fmt", "cargo")
340                .label("Format Code")
341                .description("Format code using rustfmt")
342                .arg("fmt")
343                .category(CommandCategory::Format)
344                .priority(70)
345                .tag("cargo")
346                .tag("format")
347                .cwd(cwd.clone())
348                .estimated_duration(5)
349                .build(),
350            CommandBuilder::new("cargo-fmt-check", "cargo")
351                .label("Check Formatting")
352                .description("Check if code is properly formatted")
353                .args(vec!["fmt".to_string(), "--check".to_string()])
354                .category(CommandCategory::Format)
355                .priority(60)
356                .tag("cargo")
357                .tag("format")
358                .tag("check")
359                .cwd(cwd.clone())
360                .estimated_duration(5)
361                .build(),
362            // Clean commands
363            CommandBuilder::new("cargo-clean", "cargo")
364                .label("Clean Build")
365                .description("Remove build artifacts")
366                .arg("clean")
367                .category(CommandCategory::Clean)
368                .priority(40)
369                .tag("cargo")
370                .tag("clean")
371                .cwd(cwd.clone())
372                .estimated_duration(5)
373                .build(),
374            // Update commands
375            CommandBuilder::new("cargo-update", "cargo")
376                .label("Update Dependencies")
377                .description("Update dependencies to latest compatible versions")
378                .arg("update")
379                .category(CommandCategory::Update)
380                .priority(30)
381                .tag("cargo")
382                .tag("update")
383                .cwd(cwd.clone())
384                .estimated_duration(30)
385                .build(),
386        ]);
387
388        // Add run commands if binary targets exist
389        let has_binary = context
390            .build_targets
391            .iter()
392            .any(|target| target.target_type == crate::TargetType::Binary);
393
394        if has_binary {
395            commands.push(
396                CommandBuilder::new("cargo-run", "cargo")
397                    .label("Run Project")
398                    .description("Run the main binary")
399                    .arg("run")
400                    .category(CommandCategory::Run)
401                    .priority(95)
402                    .tag("cargo")
403                    .tag("run")
404                    .cwd(cwd.clone())
405                    .estimated_duration(60)
406                    .build(),
407            );
408
409            commands.push(
410                CommandBuilder::new("cargo-run-release", "cargo")
411                    .label("Run Project (Release)")
412                    .description("Run the main binary in release mode")
413                    .args(vec!["run".to_string(), "--release".to_string()])
414                    .category(CommandCategory::Run)
415                    .priority(80)
416                    .tag("cargo")
417                    .tag("run")
418                    .tag("release")
419                    .cwd(cwd.clone())
420                    .estimated_duration(90)
421                    .build(),
422            );
423        }
424
425        // Add example commands if examples exist
426        let examples: Vec<_> = context
427            .build_targets
428            .iter()
429            .filter(|target| target.target_type == crate::TargetType::Example)
430            .collect();
431
432        for example in examples {
433            commands.push(
434                CommandBuilder::new(format!("cargo-example-{}", example.name), "cargo")
435                    .label(format!("Run Example: {}", example.name))
436                    .description(format!("Run the '{}' example", example.name))
437                    .args(vec![
438                        "run".to_string(),
439                        "--example".to_string(),
440                        example.name.clone(),
441                    ])
442                    .category(CommandCategory::Run)
443                    .priority(65)
444                    .tag("cargo")
445                    .tag("example")
446                    .tag(&example.name)
447                    .cwd(cwd.clone())
448                    .estimated_duration(45)
449                    .build(),
450            );
451        }
452
453        // Add bench commands if bench targets exist
454        let has_bench = context
455            .build_targets
456            .iter()
457            .any(|target| target.target_type == crate::TargetType::Bench);
458
459        if has_bench {
460            commands.push(
461                CommandBuilder::new("cargo-bench", "cargo")
462                    .label("Run Benchmarks")
463                    .description("Run all benchmarks")
464                    .arg("bench")
465                    .category(CommandCategory::Test)
466                    .priority(50)
467                    .tag("cargo")
468                    .tag("bench")
469                    .cwd(cwd.clone())
470                    .estimated_duration(180)
471                    .build(),
472            );
473        }
474
475        // Add workspace-specific commands
476        if context.workspace_members.len() > 1 {
477            commands.extend([
478                CommandBuilder::new("cargo-build-all", "cargo")
479                    .label("Build All Packages")
480                    .description("Build all packages in the workspace")
481                    .args(vec!["build".to_string(), "--workspace".to_string()])
482                    .category(CommandCategory::Build)
483                    .priority(85)
484                    .tag("cargo")
485                    .tag("workspace")
486                    .tag("build")
487                    .cwd(cwd.clone())
488                    .estimated_duration(120)
489                    .build(),
490                CommandBuilder::new("cargo-test-all", "cargo")
491                    .label("Test All Packages")
492                    .description("Run tests for all packages in the workspace")
493                    .args(vec!["test".to_string(), "--workspace".to_string()])
494                    .category(CommandCategory::Test)
495                    .priority(80)
496                    .tag("cargo")
497                    .tag("workspace")
498                    .tag("test")
499                    .cwd(cwd.clone())
500                    .estimated_duration(180)
501                    .build(),
502            ]);
503        }
504
505        // Add enhanced context-specific test commands
506        if let Some(file_context) = &context.current_file {
507            // Try to resolve test context for enhanced command generation
508            if let Ok(Some(test_context)) = self.resolve_test_context(context).await {
509                // Generate enhanced test commands using the new system
510                let enhanced_commands =
511                    crate::test_commands::TestCommandGenerator::generate_commands(
512                        context,
513                        Some(&test_context),
514                    )?;
515                commands.extend(enhanced_commands);
516            } else {
517                // Fallback to basic cursor-based test command
518                if let Some(cursor_symbol) = &file_context.cursor_symbol {
519                    if cursor_symbol.kind == crate::SymbolKind::Test {
520                        commands.push(
521                            CommandBuilder::new("cargo-test-current", "cargo")
522                                .label(format!("Run Test: {}", cursor_symbol.name))
523                                .description(format!(
524                                    "Run the '{}' test function",
525                                    cursor_symbol.name
526                                ))
527                                .args(vec!["test".to_string(), cursor_symbol.name.clone()])
528                                .category(CommandCategory::Test)
529                                .priority(100)
530                                .tag("cargo")
531                                .tag("test")
532                                .tag("current")
533                                .cwd(cwd.clone())
534                                .estimated_duration(15)
535                                .build(),
536                        );
537                    }
538                }
539            }
540        }
541
542        Ok(commands)
543    }
544}
545
546/// Provider for project documentation commands
547pub struct DocProvider;
548
549impl DocProvider {
550    pub fn new() -> Self {
551        Self
552    }
553}
554
555impl Default for DocProvider {
556    fn default() -> Self {
557        Self::new()
558    }
559}
560
561#[async_trait]
562impl CommandProvider for DocProvider {
563    fn name(&self) -> &str {
564        "doc"
565    }
566
567    fn priority(&self) -> u8 {
568        60
569    }
570
571    async fn commands(&self, context: &ProjectContext) -> RazResult<Vec<Command>> {
572        let cwd = context.workspace_root.clone();
573
574        Ok(vec![
575            CommandBuilder::new("cargo-doc", "cargo")
576                .label("Generate Documentation")
577                .description("Generate documentation for the project")
578                .arg("doc")
579                .category(CommandCategory::Generate)
580                .priority(50)
581                .tag("cargo")
582                .tag("doc")
583                .cwd(cwd.clone())
584                .estimated_duration(60)
585                .build(),
586            CommandBuilder::new("cargo-doc-open", "cargo")
587                .label("Generate & Open Documentation")
588                .description("Generate documentation and open in browser")
589                .args(vec!["doc".to_string(), "--open".to_string()])
590                .category(CommandCategory::Generate)
591                .priority(55)
592                .tag("cargo")
593                .tag("doc")
594                .cwd(cwd)
595                .estimated_duration(65)
596                .build(),
597        ])
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::{BuildTarget, TargetType, WorkspaceMember};
605    use std::path::PathBuf;
606
607    fn create_test_context() -> ProjectContext {
608        ProjectContext {
609            workspace_root: PathBuf::from("/test/project"),
610            current_file: None,
611            cursor_position: None,
612            project_type: ProjectType::Binary,
613            dependencies: Vec::new(),
614            workspace_members: vec![WorkspaceMember {
615                name: "test-project".to_string(),
616                path: PathBuf::from("/test/project"),
617                package_type: ProjectType::Binary,
618            }],
619            build_targets: vec![BuildTarget {
620                name: "main".to_string(),
621                target_type: TargetType::Binary,
622                path: PathBuf::from("/test/project/src/main.rs"),
623            }],
624            active_features: Vec::new(),
625            env_vars: std::collections::HashMap::new(),
626        }
627    }
628
629    #[tokio::test]
630    async fn test_cargo_provider_basic_commands() {
631        let provider = CargoProvider::new();
632        let context = create_test_context();
633
634        let commands = provider.commands(&context).await.unwrap();
635
636        assert!(!commands.is_empty());
637        assert!(commands.iter().any(|c| c.id == "cargo-build"));
638        assert!(commands.iter().any(|c| c.id == "cargo-test"));
639        assert!(commands.iter().any(|c| c.id == "cargo-run"));
640        assert!(commands.iter().any(|c| c.id == "cargo-check"));
641        assert!(commands.iter().any(|c| c.id == "cargo-clippy"));
642        assert!(commands.iter().any(|c| c.id == "cargo-fmt"));
643    }
644
645    #[tokio::test]
646    async fn test_cargo_provider_with_examples() {
647        let provider = CargoProvider::new();
648        let mut context = create_test_context();
649
650        // Add an example target
651        context.build_targets.push(BuildTarget {
652            name: "hello".to_string(),
653            target_type: TargetType::Example,
654            path: PathBuf::from("/test/project/examples/hello.rs"),
655        });
656
657        let commands = provider.commands(&context).await.unwrap();
658
659        assert!(commands.iter().any(|c| c.id == "cargo-example-hello"));
660    }
661
662    #[tokio::test]
663    async fn test_cargo_provider_workspace() {
664        let provider = CargoProvider::new();
665        let mut context = create_test_context();
666
667        // Add another workspace member
668        context.workspace_members.push(WorkspaceMember {
669            name: "test-lib".to_string(),
670            path: PathBuf::from("/test/project/test-lib"),
671            package_type: ProjectType::Library,
672        });
673
674        let commands = provider.commands(&context).await.unwrap();
675
676        assert!(commands.iter().any(|c| c.id == "cargo-build-all"));
677        assert!(commands.iter().any(|c| c.id == "cargo-test-all"));
678    }
679
680    #[tokio::test]
681    async fn test_doc_provider() {
682        let provider = DocProvider::new();
683        let context = create_test_context();
684
685        let commands = provider.commands(&context).await.unwrap();
686
687        assert_eq!(commands.len(), 2);
688        assert!(commands.iter().any(|c| c.id == "cargo-doc"));
689        assert!(commands.iter().any(|c| c.id == "cargo-doc-open"));
690    }
691}