1use crate::{Command, CommandBuilder, CommandCategory, ProjectContext, ProjectType, RazResult};
4use async_trait::async_trait;
5
6pub 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#[async_trait]
21pub trait CommandProvider: Send + Sync {
22 fn name(&self) -> &str;
24
25 async fn commands(&self, context: &ProjectContext) -> RazResult<Vec<Command>>;
27
28 fn priority(&self) -> u8 {
30 50
31 }
32
33 fn can_handle(&self, context: &ProjectContext) -> bool {
35 let _ = context;
36 true
37 }
38}
39
40pub 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 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 if file_context.language != crate::Language::Rust {
69 return Ok(None);
70 }
71
72 let mut test_context = crate::TestContext::new();
73
74 test_context.package_name = self.resolve_package_name(context, &file_context.path)?;
76
77 test_context.target_type = self.resolve_target_type(context, &file_context.path)?;
79
80 test_context.module_path = self.build_module_path(&file_context.path, file_context)?;
82
83 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 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 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 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 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 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 has_test_module && !module_path.iter().any(|m| m == "tests") {
134 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 fn resolve_package_name(
151 &self,
152 context: &ProjectContext,
153 file_path: &std::path::Path,
154 ) -> RazResult<Option<String>> {
155 if context.workspace_members.len() == 1 {
157 return Ok(Some(context.workspace_members[0].name.clone()));
158 }
159
160 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 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 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 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 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 if file_str.contains("/src/lib.rs") || file_str.contains("/src/mod.rs") {
207 return Ok(crate::TestTargetType::Lib);
208 }
209
210 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 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 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 }
254
255 fn can_handle(&self, context: &ProjectContext) -> bool {
256 !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 commands.extend([
266 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 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 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 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 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 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 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 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 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 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 if let Some(file_context) = &context.current_file {
507 if let Ok(Some(test_context)) = self.resolve_test_context(context).await {
509 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 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
546pub 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 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 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}