1use crate::{Position, RazResult};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::{Path, PathBuf};
10
11#[cfg(feature = "tree-sitter-support")]
12use crate::tree_sitter_test_detector::TreeSitterTestDetector;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub enum RustProjectType {
17 CargoWorkspace {
19 root: PathBuf,
20 members: Vec<WorkspaceMember>,
21 },
22 CargoPackage { root: PathBuf, package_name: String },
24 CargoScript {
26 file_path: PathBuf,
27 manifest: Option<String>,
28 },
29 SingleFile {
31 file_path: PathBuf,
32 file_type: SingleFileType,
33 },
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum SingleFileType {
39 Executable,
41 Library,
43 Test,
45 Module,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct WorkspaceMember {
52 pub name: String,
53 pub path: PathBuf,
54 pub is_current: bool,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct FileExecutionContext {
60 pub project_type: RustProjectType,
61 pub file_role: FileRole,
62 pub entry_points: Vec<EntryPoint>,
63 pub capabilities: ExecutionCapabilities,
64 pub file_path: PathBuf,
65}
66
67impl FileExecutionContext {
68 pub fn get_workspace_root(&self) -> Option<&Path> {
70 match &self.project_type {
71 RustProjectType::CargoWorkspace { root, .. } => Some(root),
72 RustProjectType::CargoPackage { root, .. } => Some(root),
73 RustProjectType::CargoScript { .. } => None,
74 RustProjectType::SingleFile { .. } => None,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub enum FileRole {
82 MainBinary { binary_name: String },
84 AdditionalBinary { binary_name: String },
86 LibraryRoot,
88 FrontendLibrary { framework: String },
90 IntegrationTest { test_name: String },
92 Benchmark { bench_name: String },
94 Example { example_name: String },
96 BuildScript,
98 Module,
100 Standalone,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct EntryPoint {
107 pub name: String,
108 pub entry_type: EntryPointType,
109 pub line: u32,
110 pub column: u32,
111 pub line_range: (u32, u32),
113 pub full_path: Option<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub enum EntryPointType {
120 Main,
122 Test,
124 TestModule,
126 Benchmark,
128 Example,
130 DocTest,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct ExecutionCapabilities {
137 pub can_run: bool,
138 pub can_test: bool,
139 pub can_bench: bool,
140 pub can_doc_test: bool,
141 pub requires_framework: Option<String>,
142}
143
144pub struct FileDetector;
146
147impl FileDetector {
148 pub fn detect_context(
150 file_path: &Path,
151 cursor: Option<Position>,
152 ) -> RazResult<FileExecutionContext> {
153 Self::detect_context_with_options(file_path, cursor, false)
154 }
155
156 pub fn detect_context_with_options(
158 file_path: &Path,
159 cursor: Option<Position>,
160 force_standalone: bool,
161 ) -> RazResult<FileExecutionContext> {
162 let mut project_type = if force_standalone {
163 Self::analyze_single_file(file_path)?
165 } else {
166 Self::detect_project_type(file_path)?
167 };
168 let file_role = Self::detect_file_role(file_path, &project_type)?;
169
170 if matches!(file_role, FileRole::Standalone)
172 && matches!(
173 project_type,
174 RustProjectType::CargoPackage { .. } | RustProjectType::CargoWorkspace { .. }
175 )
176 {
177 project_type = Self::analyze_single_file(file_path)?;
178 }
179
180 let entry_points = Self::detect_entry_points(file_path, cursor, &project_type)?;
181 let capabilities = Self::determine_capabilities(&project_type, &file_role, &entry_points);
182
183 Ok(FileExecutionContext {
184 project_type,
185 file_role,
186 entry_points,
187 capabilities,
188 file_path: file_path.to_path_buf(),
189 })
190 }
191
192 fn detect_project_type(file_path: &Path) -> RazResult<RustProjectType> {
194 let mut current_dir = if file_path.is_file() {
196 file_path.parent().unwrap_or(file_path)
197 } else {
198 file_path
199 };
200
201 loop {
203 let cargo_toml = current_dir.join("Cargo.toml");
204 if cargo_toml.exists() {
205 return Self::analyze_cargo_project(&cargo_toml, file_path);
206 }
207
208 match current_dir.parent() {
209 Some(parent) => current_dir = parent,
210 None => break,
211 }
212 }
213
214 Self::analyze_single_file(file_path)
216 }
217
218 fn analyze_cargo_project(
220 cargo_toml_path: &Path,
221 file_path: &Path,
222 ) -> RazResult<RustProjectType> {
223 let content = fs::read_to_string(cargo_toml_path)?;
224 let root = cargo_toml_path.parent().unwrap().to_path_buf();
225
226 if content.contains("[workspace]") {
228 let members = Self::parse_workspace_members(&content, &root)?;
229 Ok(RustProjectType::CargoWorkspace { root, members })
230 } else if content.contains("[package]") {
231 let package_name = Self::extract_package_name(&content)?;
232 Ok(RustProjectType::CargoPackage { root, package_name })
233 } else {
234 Self::analyze_single_file(file_path)
236 }
237 }
238
239 fn analyze_single_file(file_path: &Path) -> RazResult<RustProjectType> {
241 let content = fs::read_to_string(file_path)?;
242
243 if Self::is_cargo_script(&content) {
245 let manifest = Self::extract_cargo_script_manifest(&content);
246 return Ok(RustProjectType::CargoScript {
247 file_path: file_path.to_path_buf(),
248 manifest,
249 });
250 }
251
252 let file_type = if content.contains("fn main(") {
254 SingleFileType::Executable
255 } else if content.contains("pub ") {
256 SingleFileType::Library
258 } else if content.contains("#[test]") || content.contains("#[cfg(test)]") {
259 SingleFileType::Test
260 } else {
261 SingleFileType::Module
262 };
263
264 Ok(RustProjectType::SingleFile {
265 file_path: file_path.to_path_buf(),
266 file_type,
267 })
268 }
269
270 fn detect_file_role(file_path: &Path, project_type: &RustProjectType) -> RazResult<FileRole> {
272 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
273 let path_str = file_path.to_string_lossy();
274
275 match project_type {
277 RustProjectType::CargoWorkspace { .. } | RustProjectType::CargoPackage { .. } => {
278 if file_name == "build.rs" {
279 Ok(FileRole::BuildScript)
280 } else if path_str.contains("/src/main.rs") {
281 let binary_name = Self::extract_binary_name_from_path(file_path);
282 Ok(FileRole::MainBinary { binary_name })
283 } else if path_str.contains("/src/bin/") {
284 let binary_name = file_path
285 .file_stem()
286 .and_then(|s| s.to_str())
287 .unwrap_or("unknown")
288 .to_string();
289 Ok(FileRole::AdditionalBinary { binary_name })
290 } else if file_name == "lib.rs" {
291 if Self::is_frontend_library(file_path) {
293 let framework = Self::detect_web_framework(file_path)?;
294 Ok(FileRole::FrontendLibrary { framework })
295 } else {
296 Ok(FileRole::LibraryRoot)
297 }
298 } else if path_str.contains("/tests/") {
299 let test_name = file_path
300 .file_stem()
301 .and_then(|s| s.to_str())
302 .unwrap_or("unknown")
303 .to_string();
304 Ok(FileRole::IntegrationTest { test_name })
305 } else if path_str.contains("/benches/") {
306 let bench_name = file_path
307 .file_stem()
308 .and_then(|s| s.to_str())
309 .unwrap_or("unknown")
310 .to_string();
311 Ok(FileRole::Benchmark { bench_name })
312 } else if path_str.contains("/examples/") {
313 let example_name = file_path
314 .file_stem()
315 .and_then(|s| s.to_str())
316 .unwrap_or("unknown")
317 .to_string();
318 Ok(FileRole::Example { example_name })
319 } else if path_str.contains("/src/") && file_name.ends_with(".rs") {
320 Ok(FileRole::Module)
322 } else {
323 Ok(FileRole::Standalone)
326 }
327 }
328 RustProjectType::CargoScript { .. } => {
329 let binary_name = file_path
330 .file_stem()
331 .and_then(|s| s.to_str())
332 .unwrap_or("script")
333 .to_string();
334 Ok(FileRole::MainBinary { binary_name })
335 }
336 RustProjectType::SingleFile { file_type, .. } => {
337 if file_name == "build.rs" {
339 Ok(FileRole::BuildScript)
340 } else {
341 match file_type {
342 SingleFileType::Executable => {
343 let binary_name = file_path
344 .file_stem()
345 .and_then(|s| s.to_str())
346 .unwrap_or("main")
347 .to_string();
348 Ok(FileRole::MainBinary { binary_name })
349 }
350 _ => Ok(FileRole::Standalone),
351 }
352 }
353 }
354 }
355 }
356
357 fn detect_entry_points(
359 file_path: &Path,
360 cursor: Option<Position>,
361 project_type: &RustProjectType,
362 ) -> RazResult<Vec<EntryPoint>> {
363 let content = fs::read_to_string(file_path)?;
364
365 #[cfg(feature = "tree-sitter-support")]
367 {
368 if let Ok(mut detector) = TreeSitterTestDetector::new() {
369 if let Ok(mut tree_sitter_entries) = detector.detect_entry_points(&content, cursor)
370 {
371 let file_module_path =
373 Self::build_module_path_from_file(file_path, project_type);
374
375 for entry in &mut tree_sitter_entries {
377 if matches!(
378 entry.entry_type,
379 EntryPointType::Test | EntryPointType::TestModule
380 ) {
381 if let Some(existing_path) = &entry.full_path {
382 let mut full_path_parts = file_module_path.clone();
384 full_path_parts.push(existing_path.clone());
385 entry.full_path = Some(full_path_parts.join("::"));
386 } else if !file_module_path.is_empty() {
387 let mut full_path_parts = file_module_path.clone();
389 full_path_parts.push(entry.name.clone());
390 entry.full_path = Some(full_path_parts.join("::"));
391 }
392 }
393 }
394
395 tree_sitter_entries.extend(Self::detect_non_test_entries(&content)?);
397
398 return Ok(tree_sitter_entries);
402 }
403 }
404 }
406
407 let mut entry_points = Vec::new();
409 let lines: Vec<&str> = content.lines().collect();
410
411 let test_macro_regex = regex::Regex::new(r"#\[(\w+::)?test\]").unwrap();
413
414 let file_module_path = Self::build_module_path_from_file(file_path, project_type);
416
417 let mut module_stack: Vec<String> = Vec::new();
419 let mut depth_stack: Vec<u32> = Vec::new();
420 let mut current_depth: u32 = 0;
421
422 let mut in_test_module = false;
424 let mut test_module_depth = 0;
425
426 let mut in_doc_comment = false;
428 let mut in_doc_code_block = false;
429 let mut doc_test_start = None;
430 let mut doc_comment_start_line = None;
431
432 for (line_num, line) in lines.iter().enumerate() {
433 let trimmed = line.trim();
434
435 let open_braces = trimmed.matches('{').count() as u32;
437 let close_braces = trimmed.matches('}').count() as u32;
438
439 for _ in 0..close_braces {
441 current_depth = current_depth.saturating_sub(1);
442
443 while let Some(&stack_depth) = depth_stack.last() {
445 if current_depth < stack_depth {
446 depth_stack.pop();
447 module_stack.pop();
448 } else {
449 break;
450 }
451 }
452
453 if in_test_module && current_depth < test_module_depth {
455 in_test_module = false;
456 }
457 }
458
459 current_depth = current_depth.saturating_add(open_braces);
461
462 if trimmed == "#[cfg(test)]" {
464 in_test_module = true;
465 test_module_depth = current_depth;
466 }
467
468 if let Some(mod_start) = trimmed.find("mod ") {
470 let after_mod = &trimmed[mod_start + 4..];
471 if let Some(name_end) = after_mod.find([' ', '{', ';']) {
472 let module_name = after_mod[..name_end].trim();
473
474 let has_body = trimmed.contains('{')
476 || (line_num + 1 < lines.len()
477 && lines[line_num + 1].trim().starts_with('{'));
478
479 if has_body && !module_name.is_empty() {
480 module_stack.push(module_name.to_string());
482 depth_stack.push(current_depth);
483
484 if module_name.contains("test") && !in_test_module {
486 in_test_module = true;
487 test_module_depth = current_depth;
488 }
489 }
490 }
491 }
492
493 if trimmed.starts_with("///") || trimmed.starts_with("//!") {
495 if !in_doc_comment {
496 doc_comment_start_line = Some(line_num);
497 }
498 in_doc_comment = true;
499 if (trimmed == "/// ```"
501 || trimmed == "/// ```rust"
502 || trimmed == "//! ```"
503 || trimmed == "//! ```rust")
504 && !in_doc_code_block
505 {
506 in_doc_code_block = true;
508 doc_test_start = Some(line_num as u32 + 1);
509 } else if trimmed == "/// ```" && in_doc_code_block {
510 in_doc_code_block = false;
512 }
513 } else if in_doc_comment && !trimmed.starts_with("//") {
514 if let Some(cursor_pos) = cursor {
516 if let Some(start) = doc_test_start {
517 if cursor_pos.line + 1 >= start && cursor_pos.line < line_num as u32 + 1 {
518 let mut item_name = None;
520 if trimmed.starts_with("pub fn") || trimmed.starts_with("fn") {
521 item_name = Self::extract_function_name(trimmed);
522 } else if trimmed.starts_with("pub struct")
523 || trimmed.starts_with("struct")
524 {
525 item_name = Self::extract_struct_name(trimmed);
526 } else if trimmed.starts_with("impl") {
527 item_name = Self::extract_impl_name(trimmed);
528 } else {
529 for i in 1..5 {
531 if line_num + i < lines.len() {
532 let ahead_line = lines[line_num + i].trim();
533 if ahead_line.starts_with("pub fn")
534 || ahead_line.starts_with("fn")
535 {
536 item_name = Self::extract_function_name(ahead_line);
537 break;
538 } else if ahead_line.starts_with("pub struct")
539 || ahead_line.starts_with("struct")
540 {
541 item_name = Self::extract_struct_name(ahead_line);
542 break;
543 } else if ahead_line.starts_with("impl") {
544 item_name = Self::extract_impl_name(ahead_line);
545 break;
546 }
547 }
548 }
549 }
550
551 if let Some(name) = item_name {
552 entry_points.push(EntryPoint {
553 name: name.clone(),
554 entry_type: EntryPointType::DocTest,
555 line: start,
556 column: 0,
557 line_range: (
558 doc_comment_start_line.unwrap_or(0) as u32 + 1,
559 (line_num as u32).saturating_sub(1), ),
561 full_path: Some(name),
562 });
563 }
564 }
565 }
566 }
567
568 in_doc_comment = false;
569 in_doc_code_block = false;
570 doc_test_start = None;
571 doc_comment_start_line = None;
572 }
573
574 if trimmed.contains("fn main(") {
576 entry_points.push(EntryPoint {
577 name: "main".to_string(),
578 entry_type: EntryPointType::Main,
579 line: line_num as u32 + 1,
580 column: line.find("fn main(").unwrap_or(0) as u32,
581 line_range: (line_num as u32 + 1, line_num as u32 + 1),
582 full_path: None,
583 });
584 }
585
586 let build_module_path = |fn_name: &str| -> Option<String> {
588 let mut path_parts = file_module_path.clone();
589 path_parts.extend(module_stack.clone());
590 path_parts.push(fn_name.to_string());
591 if path_parts.is_empty() {
592 None
593 } else {
594 Some(path_parts.join("::"))
595 }
596 };
597
598 if test_macro_regex.is_match(trimmed) {
600 for i in 1..=3 {
602 if line_num + i < lines.len() {
603 let next_line = lines[line_num + i];
604 if let Some(fn_name) = Self::extract_function_name(next_line) {
605 let full_path = build_module_path(&fn_name);
606 entry_points.push(EntryPoint {
607 name: fn_name.clone(),
608 entry_type: EntryPointType::Test,
609 line: line_num as u32 + i as u32 + 1,
610 column: 0,
611 line_range: (line_num as u32 + 1, line_num as u32 + 1),
612 full_path,
613 });
614 break;
615 }
616 }
617 }
618 } else if (trimmed.starts_with("fn test_") || trimmed.contains("fn test_"))
619 && (in_test_module || trimmed.contains("#["))
620 {
621 if let Some(fn_name) = Self::extract_function_name(trimmed) {
622 let column = line.find(&format!("fn {fn_name}")).unwrap_or(0) as u32;
623 let full_path = build_module_path(&fn_name);
624 entry_points.push(EntryPoint {
625 name: fn_name.clone(),
626 entry_type: EntryPointType::Test,
627 line: line_num as u32 + 1,
628 column,
629 line_range: (line_num as u32 + 1, line_num as u32 + 1),
630 full_path,
631 });
632 }
633 }
634
635 if trimmed.starts_with("#[bench]") {
637 if let Some(next_line) = lines.get(line_num + 1) {
638 if let Some(fn_name) = Self::extract_function_name(next_line) {
639 entry_points.push(EntryPoint {
640 name: fn_name,
641 entry_type: EntryPointType::Benchmark,
642 line: line_num as u32 + 2,
643 column: 0,
644 line_range: (line_num as u32 + 1, line_num as u32 + 1),
645 full_path: None,
646 });
647 }
648 }
649 }
650
651 if trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!") {
653 entry_points.push(EntryPoint {
654 name: "criterion_bench".to_string(),
655 entry_type: EntryPointType::Benchmark,
656 line: line_num as u32 + 1,
657 column: 0,
658 line_range: (line_num as u32 + 1, line_num as u32 + 1),
659 full_path: None,
660 });
661 }
662 }
663
664 if in_doc_comment && cursor.is_some() {
667 let cursor_pos = cursor.unwrap();
668 if let Some(start) = doc_test_start {
669 if cursor_pos.line + 1 >= start {
670 entry_points.push(EntryPoint {
673 name: "doc_test".to_string(),
674 entry_type: EntryPointType::DocTest,
675 line: start,
676 column: 0,
677 line_range: (
678 doc_comment_start_line.unwrap_or(0) as u32 + 1,
679 lines.len() as u32,
680 ),
681 full_path: None,
682 });
683 }
684 }
685 }
686
687 if let Some(cursor_pos) = cursor {
689 if in_test_module && !module_stack.is_empty() {
690 let cursor_line = cursor_pos.line + 1;
692 let on_specific_test = entry_points
693 .iter()
694 .any(|ep| ep.entry_type == EntryPointType::Test && ep.line == cursor_line);
695
696 if !on_specific_test {
697 let mut path_parts = file_module_path.clone();
698 path_parts.extend(module_stack.clone());
699 let current_module_path = path_parts.join("::");
700 entry_points.push(EntryPoint {
701 name: format!(
702 "{}_module",
703 module_stack.last().unwrap_or(&"tests".to_string())
704 ),
705 entry_type: EntryPointType::TestModule,
706 line: cursor_line,
707 column: 0,
708 line_range: (cursor_line, cursor_line),
709 full_path: Some(current_module_path),
710 });
711 }
712 }
713 }
714
715 Ok(entry_points)
716 }
717
718 fn determine_capabilities(
720 project_type: &RustProjectType,
721 file_role: &FileRole,
722 entry_points: &[EntryPoint],
723 ) -> ExecutionCapabilities {
724 let has_main = entry_points
725 .iter()
726 .any(|ep| ep.entry_type == EntryPointType::Main);
727 let has_tests = entry_points
728 .iter()
729 .any(|ep| ep.entry_type == EntryPointType::Test);
730 let has_benches = entry_points
731 .iter()
732 .any(|ep| ep.entry_type == EntryPointType::Benchmark);
733
734 match file_role {
735 FileRole::MainBinary { .. } | FileRole::AdditionalBinary { .. } => {
736 ExecutionCapabilities {
737 can_run: has_main,
738 can_test: has_tests,
739 can_bench: has_benches,
740 can_doc_test: false,
741 requires_framework: None,
742 }
743 }
744 FileRole::FrontendLibrary { framework } => ExecutionCapabilities {
745 can_run: true, can_test: has_tests,
747 can_bench: has_benches,
748 can_doc_test: true,
749 requires_framework: Some(framework.clone()),
750 },
751 FileRole::LibraryRoot => ExecutionCapabilities {
752 can_run: false,
753 can_test: has_tests,
754 can_bench: has_benches,
755 can_doc_test: true,
756 requires_framework: None,
757 },
758 FileRole::IntegrationTest { .. } => ExecutionCapabilities {
759 can_run: false,
760 can_test: true,
761 can_bench: false,
762 can_doc_test: false,
763 requires_framework: None,
764 },
765 FileRole::Benchmark { .. } => ExecutionCapabilities {
766 can_run: false,
767 can_test: false,
768 can_bench: true,
769 can_doc_test: false,
770 requires_framework: None,
771 },
772 FileRole::Example { .. } => ExecutionCapabilities {
773 can_run: has_main,
774 can_test: has_tests,
775 can_bench: has_benches,
776 can_doc_test: false,
777 requires_framework: None,
778 },
779 FileRole::BuildScript => ExecutionCapabilities {
780 can_run: has_main,
781 can_test: false,
782 can_bench: false,
783 can_doc_test: false,
784 requires_framework: None,
785 },
786 FileRole::Standalone => match project_type {
787 RustProjectType::SingleFile { file_type, .. } => match file_type {
788 SingleFileType::Executable => ExecutionCapabilities {
789 can_run: has_main,
790 can_test: has_tests,
791 can_bench: has_benches,
792 can_doc_test: false,
793 requires_framework: None,
794 },
795 SingleFileType::Library => ExecutionCapabilities {
796 can_run: false,
797 can_test: has_tests,
798 can_bench: has_benches,
799 can_doc_test: true,
800 requires_framework: None,
801 },
802 SingleFileType::Test => ExecutionCapabilities {
803 can_run: false,
804 can_test: true,
805 can_bench: false,
806 can_doc_test: false,
807 requires_framework: None,
808 },
809 SingleFileType::Module => ExecutionCapabilities {
810 can_run: false,
811 can_test: has_tests,
812 can_bench: false,
813 can_doc_test: false,
814 requires_framework: None,
815 },
816 },
817 RustProjectType::CargoScript { .. } => ExecutionCapabilities {
818 can_run: has_main,
819 can_test: has_tests,
820 can_bench: has_benches,
821 can_doc_test: false,
822 requires_framework: None,
823 },
824 _ => {
825 ExecutionCapabilities {
827 can_run: has_main,
828 can_test: has_tests,
829 can_bench: has_benches,
830 can_doc_test: false,
831 requires_framework: None,
832 }
833 }
834 },
835 FileRole::Module => ExecutionCapabilities {
836 can_run: false,
837 can_test: has_tests,
838 can_bench: has_benches,
839 can_doc_test: false,
840 requires_framework: None,
841 },
842 }
843 }
844
845 fn is_cargo_script(content: &str) -> bool {
848 content.starts_with("#!/usr/bin/env -S cargo")
849 || content.starts_with("#!/usr/bin/env cargo")
850 || content.starts_with("#!/usr/bin/env cargo-eval")
851 || content.starts_with("#!/usr/bin/env run-cargo-script")
852 }
853
854 fn extract_cargo_script_manifest(content: &str) -> Option<String> {
855 if let Some(start) = content.find("//! ```cargo") {
857 let search_start = start + 13; if let Some(relative_end) = content[search_start..].find("//! ```") {
859 let end = search_start + relative_end;
860 return Some(content[search_start..end].trim().to_string());
861 }
862 }
863 None
864 }
865
866 fn parse_workspace_members(content: &str, root: &Path) -> RazResult<Vec<WorkspaceMember>> {
867 let mut members = Vec::new();
868
869 if let Some(members_section) = content.find("members = [") {
871 let section = &content[members_section..];
872 if let Some(end) = section.find(']') {
873 let members_str = §ion[11..end];
874 for member in members_str.split(',') {
875 let member_name = member.trim().trim_matches('"').trim_matches('\'');
876 if !member_name.is_empty() {
877 members.push(WorkspaceMember {
878 name: member_name.to_string(),
879 path: root.join(member_name),
880 is_current: false, });
882 }
883 }
884 }
885 }
886
887 Ok(members)
888 }
889
890 fn extract_package_name(content: &str) -> RazResult<String> {
891 if let Some(name_start) = content.find("name = \"") {
892 let name_section = &content[name_start + 8..];
893 if let Some(name_end) = name_section.find('"') {
894 return Ok(name_section[..name_end].to_string());
895 }
896 }
897 Ok("unknown".to_string())
898 }
899
900 fn extract_binary_name_from_path(file_path: &Path) -> String {
901 if let Some(parent) = file_path.parent() {
903 if let Some(cargo_dir) = parent.parent() {
904 let cargo_toml = cargo_dir.join("Cargo.toml");
905 if cargo_toml.exists() {
906 if let Ok(content) = fs::read_to_string(&cargo_toml) {
907 if let Ok(name) = Self::extract_package_name(&content) {
908 return name;
909 }
910 }
911 }
912 }
913 }
914 "main".to_string()
915 }
916
917 fn is_frontend_library(file_path: &Path) -> bool {
918 let path_str = file_path.to_string_lossy();
919 let file_name = file_path.file_name().unwrap_or_default().to_string_lossy();
920
921 file_name == "lib.rs"
922 && (path_str.contains("/frontend/")
923 || path_str.contains("/app/")
924 || path_str.contains("/client/")
925 || path_str.contains("/web/"))
926 }
927
928 fn detect_web_framework(file_path: &Path) -> RazResult<String> {
929 let mut current = file_path;
931 while let Some(parent) = current.parent() {
932 let cargo_toml = parent.join("Cargo.toml");
933 if cargo_toml.exists() {
934 if let Ok(content) = fs::read_to_string(&cargo_toml) {
935 if content.contains("leptos") {
936 return Ok("leptos".to_string());
937 } else if content.contains("dioxus") {
938 return Ok("dioxus".to_string());
939 } else if content.contains("yew") {
940 return Ok("yew".to_string());
941 }
942 }
943 }
944 current = parent;
945 }
946 Ok("unknown".to_string())
947 }
948
949 fn detect_non_test_entries(content: &str) -> RazResult<Vec<EntryPoint>> {
950 let mut entry_points = Vec::new();
951 let lines: Vec<&str> = content.lines().collect();
952 let mut in_doc_comment = false;
953
954 for (line_num, line) in lines.iter().enumerate() {
955 let trimmed = line.trim();
956
957 if trimmed.starts_with("///") || trimmed.starts_with("//!") {
959 in_doc_comment = true;
960 } else if in_doc_comment && !trimmed.starts_with("//") {
961 in_doc_comment = false;
962 }
963
964 if !in_doc_comment && trimmed.contains("fn main(") {
966 entry_points.push(EntryPoint {
967 name: "main".to_string(),
968 entry_type: EntryPointType::Main,
969 line: line_num as u32 + 1,
970 column: line.find("fn main(").unwrap_or(0) as u32,
971 line_range: (line_num as u32 + 1, line_num as u32 + 1),
972 full_path: None,
973 });
974 }
975
976 if !in_doc_comment && trimmed.starts_with("#[bench]") {
978 if let Some(next_line) = lines.get(line_num + 1) {
979 if let Some(fn_name) = Self::extract_function_name(next_line) {
980 entry_points.push(EntryPoint {
981 name: fn_name,
982 entry_type: EntryPointType::Benchmark,
983 line: line_num as u32 + 2,
984 column: 0,
985 line_range: (line_num as u32 + 1, line_num as u32 + 1),
986 full_path: None,
987 });
988 }
989 }
990 }
991
992 if !in_doc_comment
994 && (trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!"))
995 {
996 entry_points.push(EntryPoint {
997 name: "criterion_bench".to_string(),
998 entry_type: EntryPointType::Benchmark,
999 line: line_num as u32 + 1,
1000 column: 0,
1001 line_range: (line_num as u32 + 1, line_num as u32 + 1),
1002 full_path: None,
1003 });
1004 }
1005 }
1006
1007 Ok(entry_points)
1008 }
1009
1010 fn extract_function_name(line: &str) -> Option<String> {
1011 let trimmed = line.trim();
1012 if let Some(fn_start) = trimmed.find("fn ") {
1013 let fn_part = &trimmed[fn_start + 3..];
1014 if let Some(paren_pos) = fn_part.find('(') {
1015 let fn_name = fn_part[..paren_pos].trim();
1016 if !fn_name.is_empty() {
1017 return Some(fn_name.to_string());
1018 }
1019 }
1020 }
1021 None
1022 }
1023
1024 fn extract_struct_name(line: &str) -> Option<String> {
1025 let trimmed = line.trim();
1026 let struct_start = if trimmed.starts_with("pub struct ") {
1027 11 } else if trimmed.starts_with("struct ") {
1029 7 } else {
1031 return None;
1032 };
1033
1034 let after_struct = &trimmed[struct_start..];
1035 let name_end = after_struct
1037 .find([' ', '<', '{', ';'])
1038 .unwrap_or(after_struct.len());
1039 let struct_name = after_struct[..name_end].trim();
1040
1041 if !struct_name.is_empty() {
1042 Some(struct_name.to_string())
1043 } else {
1044 None
1045 }
1046 }
1047
1048 fn extract_impl_name(line: &str) -> Option<String> {
1049 let trimmed = line.trim();
1050 if !trimmed.starts_with("impl") {
1051 return None;
1052 }
1053
1054 if let Some(for_pos) = trimmed.find(" for ") {
1056 let after_for = &trimmed[for_pos + 5..];
1058 let name_end = after_for.find([' ', '{', '<']).unwrap_or(after_for.len());
1059 let type_name = after_for[..name_end].trim();
1060 if !type_name.is_empty() {
1061 return Some(type_name.to_string());
1062 }
1063 } else {
1064 let after_impl = if trimmed.starts_with("impl<") {
1066 if let Some(gt_pos) = trimmed.find('>') {
1068 &trimmed[gt_pos + 1..].trim()
1069 } else {
1070 return None;
1071 }
1072 } else {
1073 &trimmed[4..].trim() };
1075
1076 let name_end = after_impl.find([' ', '{', '<']).unwrap_or(after_impl.len());
1077 let type_name = after_impl[..name_end].trim();
1078 if !type_name.is_empty() {
1079 return Some(type_name.to_string());
1080 }
1081 }
1082 None
1083 }
1084
1085 fn build_module_path_from_file(
1087 file_path: &Path,
1088 project_type: &RustProjectType,
1089 ) -> Vec<String> {
1090 let mut module_path = Vec::new();
1091
1092 let src_dir = match project_type {
1094 RustProjectType::CargoWorkspace { root, .. } => root.join("src"),
1095 RustProjectType::CargoPackage { root, .. } => root.join("src"),
1096 _ => return module_path,
1097 };
1098
1099 if let Ok(relative_path) = file_path.strip_prefix(&src_dir) {
1101 for component in relative_path.components() {
1103 if let std::path::Component::Normal(os_str) = component {
1104 if let Some(name) = os_str.to_str() {
1105 let module_name = if name == "lib.rs" || name == "main.rs" {
1107 continue; } else if name == "mod.rs" {
1109 if let Some(parent) = relative_path.parent() {
1111 if let Some(parent_name) = parent.file_name() {
1112 parent_name.to_str().unwrap_or("").to_string()
1113 } else {
1114 continue;
1115 }
1116 } else {
1117 continue;
1118 }
1119 } else if name.ends_with(".rs") {
1120 name.trim_end_matches(".rs").to_string()
1121 } else {
1122 name.to_string()
1124 };
1125
1126 if !module_name.is_empty() {
1127 module_path.push(module_name);
1128 }
1129 }
1130 }
1131 }
1132 }
1133
1134 module_path
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141 use tempfile::TempDir;
1142
1143 #[test]
1144 fn test_cargo_script_detection() {
1145 let content = "#!/usr/bin/env -S cargo +nightly -Zscript\n\nfn main() {}";
1146 assert!(FileDetector::is_cargo_script(content));
1147 }
1148
1149 #[test]
1150 fn test_function_name_extraction() {
1151 assert_eq!(
1152 FileDetector::extract_function_name("fn test_something() {"),
1153 Some("test_something".to_string())
1154 );
1155 assert_eq!(
1156 FileDetector::extract_function_name(" fn main() {"),
1157 Some("main".to_string())
1158 );
1159 }
1160
1161 #[test]
1162 fn test_frontend_library_detection() {
1163 let temp_dir = TempDir::new().unwrap();
1164 let frontend_lib = temp_dir.path().join("frontend").join("src").join("lib.rs");
1165 assert!(FileDetector::is_frontend_library(&frontend_lib));
1166
1167 let regular_lib = temp_dir.path().join("src").join("lib.rs");
1168 assert!(!FileDetector::is_frontend_library(®ular_lib));
1169 }
1170
1171 #[test]
1172 fn test_nested_module_path_resolution() -> RazResult<()> {
1173 let temp_dir = TempDir::new()?;
1174 let test_file = temp_dir.path().join("nested_test.rs");
1175
1176 let content = r#"
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180
1181 #[test]
1182 fn test_top_level() {
1183 assert_eq!(2 + 2, 4);
1184 }
1185
1186 mod integration {
1187 use super::*;
1188
1189 #[test]
1190 fn test_integration_basic() {
1191 assert!(true);
1192 }
1193
1194 mod database {
1195 use super::*;
1196
1197 #[test]
1198 fn test_db_connection() {
1199 assert!(true);
1200 }
1201
1202 mod transactions {
1203 use super::*;
1204
1205 #[test]
1206 fn test_transaction_rollback() {
1207 assert!(true);
1208 }
1209 }
1210 }
1211 }
1212}
1213"#;
1214
1215 fs::write(&test_file, content)?;
1216
1217 let context = FileDetector::detect_context(
1219 &test_file,
1220 Some(Position {
1221 line: 27,
1222 column: 1,
1223 }),
1224 )?;
1225
1226 let nested_test = context.entry_points.iter().find(|ep| {
1228 ep.name == "test_transaction_rollback" && ep.entry_type == EntryPointType::Test
1229 });
1230
1231 assert!(
1232 nested_test.is_some(),
1233 "Should find the deeply nested test function"
1234 );
1235
1236 let test_entry = nested_test.unwrap();
1237 assert_eq!(
1238 test_entry.full_path.as_ref().unwrap(),
1239 "tests::integration::database::transactions::test_transaction_rollback",
1240 "Should have correct full module path for deeply nested test"
1241 );
1242
1243 let context2 = FileDetector::detect_context(
1245 &test_file,
1246 Some(Position {
1247 line: 19,
1248 column: 1,
1249 }),
1250 )?;
1251
1252 let mid_level_test = context2
1253 .entry_points
1254 .iter()
1255 .find(|ep| ep.name == "test_db_connection" && ep.entry_type == EntryPointType::Test);
1256
1257 assert!(
1258 mid_level_test.is_some(),
1259 "Should find the mid-level nested test"
1260 );
1261
1262 let test_entry2 = mid_level_test.unwrap();
1263 assert_eq!(
1264 test_entry2.full_path.as_ref().unwrap(),
1265 "tests::integration::database::test_db_connection",
1266 "Should have correct full module path for mid-level nested test"
1267 );
1268
1269 let top_level_test = context
1271 .entry_points
1272 .iter()
1273 .find(|ep| ep.name == "test_top_level" && ep.entry_type == EntryPointType::Test);
1274
1275 assert!(top_level_test.is_some(), "Should find the top-level test");
1276
1277 let test_entry3 = top_level_test.unwrap();
1278 assert_eq!(
1279 test_entry3.full_path.as_ref().unwrap(),
1280 "tests::test_top_level",
1281 "Should have correct module path for top-level test"
1282 );
1283
1284 Ok(())
1285 }
1286
1287 #[test]
1288 fn test_module_stack_handling_with_complex_nesting() -> RazResult<()> {
1289 let temp_dir = TempDir::new()?;
1290 let test_file = temp_dir.path().join("complex_nested.rs");
1291
1292 let content = r#"
1293mod outer {
1294 mod middle {
1295 #[cfg(test)]
1296 mod tests {
1297 #[test]
1298 fn test_deeply_nested() {
1299 assert!(true);
1300 }
1301
1302 mod sub_tests {
1303 #[test]
1304 fn test_sub_nested() {
1305 assert!(true);
1306 }
1307 }
1308 }
1309
1310 mod other {
1311 fn regular_function() {}
1312 }
1313 }
1314}
1315
1316#[cfg(test)]
1317mod main_tests {
1318 #[test]
1319 fn test_main_level() {
1320 assert!(true);
1321 }
1322}
1323"#;
1324
1325 fs::write(&test_file, content)?;
1326
1327 let context = FileDetector::detect_context(&test_file, None)?;
1328
1329 let deeply_nested = context
1331 .entry_points
1332 .iter()
1333 .find(|ep| ep.name == "test_deeply_nested");
1334
1335 assert!(deeply_nested.is_some(), "Should find deeply nested test");
1336 assert_eq!(
1337 deeply_nested.unwrap().full_path.as_ref().unwrap(),
1338 "outer::middle::tests::test_deeply_nested",
1339 "Should correctly build path through multiple modules"
1340 );
1341
1342 let sub_nested = context
1343 .entry_points
1344 .iter()
1345 .find(|ep| ep.name == "test_sub_nested");
1346
1347 assert!(sub_nested.is_some(), "Should find sub-nested test");
1348 assert_eq!(
1349 sub_nested.unwrap().full_path.as_ref().unwrap(),
1350 "outer::middle::tests::sub_tests::test_sub_nested",
1351 "Should correctly build path through all nested modules"
1352 );
1353
1354 let main_test = context
1355 .entry_points
1356 .iter()
1357 .find(|ep| ep.name == "test_main_level");
1358
1359 assert!(main_test.is_some(), "Should find main level test");
1360 assert_eq!(
1361 main_test.unwrap().full_path.as_ref().unwrap(),
1362 "main_tests::test_main_level",
1363 "Should correctly build path for main level test"
1364 );
1365
1366 Ok(())
1367 }
1368}