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) =
370 detector.detect_test_entry_points(&content, cursor)
371 {
372 let file_module_path =
374 Self::build_module_path_from_file(file_path, project_type);
375
376 for entry in &mut tree_sitter_entries {
378 if matches!(
379 entry.entry_type,
380 EntryPointType::Test | EntryPointType::TestModule
381 ) {
382 if let Some(existing_path) = &entry.full_path {
383 let mut full_path_parts = file_module_path.clone();
385 full_path_parts.push(existing_path.clone());
386 entry.full_path = Some(full_path_parts.join("::"));
387 } else if !file_module_path.is_empty() {
388 let mut full_path_parts = file_module_path.clone();
390 full_path_parts.push(entry.name.clone());
391 entry.full_path = Some(full_path_parts.join("::"));
392 }
393 }
394 }
395
396 tree_sitter_entries.extend(Self::detect_non_test_entries(&content)?);
398
399 return Ok(tree_sitter_entries);
403 }
404 }
405 }
407
408 let mut entry_points = Vec::new();
410 let lines: Vec<&str> = content.lines().collect();
411
412 let test_macro_regex = regex::Regex::new(r"#\[(\w+::)?test\]").unwrap();
414
415 let file_module_path = Self::build_module_path_from_file(file_path, project_type);
417
418 let mut module_stack: Vec<String> = Vec::new();
420 let mut depth_stack: Vec<u32> = Vec::new();
421 let mut current_depth: u32 = 0;
422
423 let mut in_test_module = false;
425 let mut test_module_depth = 0;
426
427 let mut in_doc_comment = false;
429 let mut in_doc_code_block = false;
430 let mut doc_test_start = None;
431 let mut doc_comment_start_line = None;
432
433 for (line_num, line) in lines.iter().enumerate() {
434 let trimmed = line.trim();
435
436 let open_braces = trimmed.matches('{').count() as u32;
438 let close_braces = trimmed.matches('}').count() as u32;
439
440 for _ in 0..close_braces {
442 current_depth = current_depth.saturating_sub(1);
443
444 while let Some(&stack_depth) = depth_stack.last() {
446 if current_depth < stack_depth {
447 depth_stack.pop();
448 module_stack.pop();
449 } else {
450 break;
451 }
452 }
453
454 if in_test_module && current_depth < test_module_depth {
456 in_test_module = false;
457 }
458 }
459
460 current_depth = current_depth.saturating_add(open_braces);
462
463 if trimmed == "#[cfg(test)]" {
465 in_test_module = true;
466 test_module_depth = current_depth;
467 }
468
469 if let Some(mod_start) = trimmed.find("mod ") {
471 let after_mod = &trimmed[mod_start + 4..];
472 if let Some(name_end) = after_mod.find([' ', '{', ';']) {
473 let module_name = after_mod[..name_end].trim();
474
475 let has_body = trimmed.contains('{')
477 || (line_num + 1 < lines.len()
478 && lines[line_num + 1].trim().starts_with('{'));
479
480 if has_body && !module_name.is_empty() {
481 module_stack.push(module_name.to_string());
483 depth_stack.push(current_depth);
484
485 if module_name.contains("test") && !in_test_module {
487 in_test_module = true;
488 test_module_depth = current_depth;
489 }
490 }
491 }
492 }
493
494 if trimmed.starts_with("///") || trimmed.starts_with("//!") {
496 if !in_doc_comment {
497 doc_comment_start_line = Some(line_num);
498 }
499 in_doc_comment = true;
500 if (trimmed == "/// ```"
502 || trimmed == "/// ```rust"
503 || trimmed == "//! ```"
504 || trimmed == "//! ```rust")
505 && !in_doc_code_block
506 {
507 in_doc_code_block = true;
509 doc_test_start = Some(line_num as u32 + 1);
510 } else if trimmed == "/// ```" && in_doc_code_block {
511 in_doc_code_block = false;
513 }
514 } else if in_doc_comment && !trimmed.starts_with("//") {
515 if let Some(cursor_pos) = cursor {
517 if let Some(start) = doc_test_start {
518 if cursor_pos.line + 1 >= start && cursor_pos.line < line_num as u32 + 1 {
519 let mut item_name = None;
521 if trimmed.starts_with("pub fn") || trimmed.starts_with("fn") {
522 item_name = Self::extract_function_name(trimmed);
523 } else if trimmed.starts_with("pub struct")
524 || trimmed.starts_with("struct")
525 {
526 item_name = Self::extract_struct_name(trimmed);
527 } else if trimmed.starts_with("impl") {
528 item_name = Self::extract_impl_name(trimmed);
529 } else {
530 for i in 1..5 {
532 if line_num + i < lines.len() {
533 let ahead_line = lines[line_num + i].trim();
534 if ahead_line.starts_with("pub fn")
535 || ahead_line.starts_with("fn")
536 {
537 item_name = Self::extract_function_name(ahead_line);
538 break;
539 } else if ahead_line.starts_with("pub struct")
540 || ahead_line.starts_with("struct")
541 {
542 item_name = Self::extract_struct_name(ahead_line);
543 break;
544 } else if ahead_line.starts_with("impl") {
545 item_name = Self::extract_impl_name(ahead_line);
546 break;
547 }
548 }
549 }
550 }
551
552 if let Some(name) = item_name {
553 entry_points.push(EntryPoint {
554 name: name.clone(),
555 entry_type: EntryPointType::DocTest,
556 line: start,
557 column: 0,
558 line_range: (
559 doc_comment_start_line.unwrap_or(0) as u32 + 1,
560 (line_num as u32).saturating_sub(1), ),
562 full_path: Some(name),
563 });
564 }
565 }
566 }
567 }
568
569 in_doc_comment = false;
570 in_doc_code_block = false;
571 doc_test_start = None;
572 doc_comment_start_line = None;
573 }
574
575 if trimmed.contains("fn main(") {
577 entry_points.push(EntryPoint {
578 name: "main".to_string(),
579 entry_type: EntryPointType::Main,
580 line: line_num as u32 + 1,
581 column: line.find("fn main(").unwrap_or(0) as u32,
582 line_range: (line_num as u32 + 1, line_num as u32 + 1),
583 full_path: None,
584 });
585 }
586
587 let build_module_path = |fn_name: &str| -> Option<String> {
589 let mut path_parts = file_module_path.clone();
590 path_parts.extend(module_stack.clone());
591 path_parts.push(fn_name.to_string());
592 if path_parts.is_empty() {
593 None
594 } else {
595 Some(path_parts.join("::"))
596 }
597 };
598
599 if test_macro_regex.is_match(trimmed) {
601 for i in 1..=3 {
603 if line_num + i < lines.len() {
604 let next_line = lines[line_num + i];
605 if let Some(fn_name) = Self::extract_function_name(next_line) {
606 let full_path = build_module_path(&fn_name);
607 entry_points.push(EntryPoint {
608 name: fn_name.clone(),
609 entry_type: EntryPointType::Test,
610 line: line_num as u32 + i as u32 + 1,
611 column: 0,
612 line_range: (line_num as u32 + 1, line_num as u32 + 1),
613 full_path,
614 });
615 break;
616 }
617 }
618 }
619 } else if (trimmed.starts_with("fn test_") || trimmed.contains("fn test_"))
620 && (in_test_module || trimmed.contains("#["))
621 {
622 if let Some(fn_name) = Self::extract_function_name(trimmed) {
623 let column = line.find(&format!("fn {fn_name}")).unwrap_or(0) as u32;
624 let full_path = build_module_path(&fn_name);
625 entry_points.push(EntryPoint {
626 name: fn_name.clone(),
627 entry_type: EntryPointType::Test,
628 line: line_num as u32 + 1,
629 column,
630 line_range: (line_num as u32 + 1, line_num as u32 + 1),
631 full_path,
632 });
633 }
634 }
635
636 if trimmed.starts_with("#[bench]") {
638 if let Some(next_line) = lines.get(line_num + 1) {
639 if let Some(fn_name) = Self::extract_function_name(next_line) {
640 entry_points.push(EntryPoint {
641 name: fn_name,
642 entry_type: EntryPointType::Benchmark,
643 line: line_num as u32 + 2,
644 column: 0,
645 line_range: (line_num as u32 + 1, line_num as u32 + 1),
646 full_path: None,
647 });
648 }
649 }
650 }
651
652 if trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!") {
654 entry_points.push(EntryPoint {
655 name: "criterion_bench".to_string(),
656 entry_type: EntryPointType::Benchmark,
657 line: line_num as u32 + 1,
658 column: 0,
659 line_range: (line_num as u32 + 1, line_num as u32 + 1),
660 full_path: None,
661 });
662 }
663 }
664
665 if in_doc_comment && cursor.is_some() {
668 let cursor_pos = cursor.unwrap();
669 if let Some(start) = doc_test_start {
670 if cursor_pos.line + 1 >= start {
671 entry_points.push(EntryPoint {
674 name: "doc_test".to_string(),
675 entry_type: EntryPointType::DocTest,
676 line: start,
677 column: 0,
678 line_range: (
679 doc_comment_start_line.unwrap_or(0) as u32 + 1,
680 lines.len() as u32,
681 ),
682 full_path: None,
683 });
684 }
685 }
686 }
687
688 if let Some(cursor_pos) = cursor {
690 if in_test_module && !module_stack.is_empty() {
691 let cursor_line = cursor_pos.line + 1;
693 let on_specific_test = entry_points
694 .iter()
695 .any(|ep| ep.entry_type == EntryPointType::Test && ep.line == cursor_line);
696
697 if !on_specific_test {
698 let mut path_parts = file_module_path.clone();
699 path_parts.extend(module_stack.clone());
700 let current_module_path = path_parts.join("::");
701 entry_points.push(EntryPoint {
702 name: format!(
703 "{}_module",
704 module_stack.last().unwrap_or(&"tests".to_string())
705 ),
706 entry_type: EntryPointType::TestModule,
707 line: cursor_line,
708 column: 0,
709 line_range: (cursor_line, cursor_line),
710 full_path: Some(current_module_path),
711 });
712 }
713 }
714 }
715
716 Ok(entry_points)
717 }
718
719 fn determine_capabilities(
721 project_type: &RustProjectType,
722 file_role: &FileRole,
723 entry_points: &[EntryPoint],
724 ) -> ExecutionCapabilities {
725 let has_main = entry_points
726 .iter()
727 .any(|ep| ep.entry_type == EntryPointType::Main);
728 let has_tests = entry_points
729 .iter()
730 .any(|ep| ep.entry_type == EntryPointType::Test);
731 let has_benches = entry_points
732 .iter()
733 .any(|ep| ep.entry_type == EntryPointType::Benchmark);
734
735 match file_role {
736 FileRole::MainBinary { .. } | FileRole::AdditionalBinary { .. } => {
737 ExecutionCapabilities {
738 can_run: has_main,
739 can_test: has_tests,
740 can_bench: has_benches,
741 can_doc_test: false,
742 requires_framework: None,
743 }
744 }
745 FileRole::FrontendLibrary { framework } => ExecutionCapabilities {
746 can_run: true, can_test: has_tests,
748 can_bench: has_benches,
749 can_doc_test: true,
750 requires_framework: Some(framework.clone()),
751 },
752 FileRole::LibraryRoot => ExecutionCapabilities {
753 can_run: false,
754 can_test: has_tests,
755 can_bench: has_benches,
756 can_doc_test: true,
757 requires_framework: None,
758 },
759 FileRole::IntegrationTest { .. } => ExecutionCapabilities {
760 can_run: false,
761 can_test: true,
762 can_bench: false,
763 can_doc_test: false,
764 requires_framework: None,
765 },
766 FileRole::Benchmark { .. } => ExecutionCapabilities {
767 can_run: false,
768 can_test: false,
769 can_bench: true,
770 can_doc_test: false,
771 requires_framework: None,
772 },
773 FileRole::Example { .. } => ExecutionCapabilities {
774 can_run: has_main,
775 can_test: has_tests,
776 can_bench: has_benches,
777 can_doc_test: false,
778 requires_framework: None,
779 },
780 FileRole::BuildScript => ExecutionCapabilities {
781 can_run: has_main,
782 can_test: false,
783 can_bench: false,
784 can_doc_test: false,
785 requires_framework: None,
786 },
787 FileRole::Standalone => match project_type {
788 RustProjectType::SingleFile { file_type, .. } => match file_type {
789 SingleFileType::Executable => ExecutionCapabilities {
790 can_run: has_main,
791 can_test: has_tests,
792 can_bench: has_benches,
793 can_doc_test: false,
794 requires_framework: None,
795 },
796 SingleFileType::Library => ExecutionCapabilities {
797 can_run: false,
798 can_test: has_tests,
799 can_bench: has_benches,
800 can_doc_test: true,
801 requires_framework: None,
802 },
803 SingleFileType::Test => ExecutionCapabilities {
804 can_run: false,
805 can_test: true,
806 can_bench: false,
807 can_doc_test: false,
808 requires_framework: None,
809 },
810 SingleFileType::Module => ExecutionCapabilities {
811 can_run: false,
812 can_test: has_tests,
813 can_bench: false,
814 can_doc_test: false,
815 requires_framework: None,
816 },
817 },
818 RustProjectType::CargoScript { .. } => ExecutionCapabilities {
819 can_run: has_main,
820 can_test: has_tests,
821 can_bench: has_benches,
822 can_doc_test: false,
823 requires_framework: None,
824 },
825 _ => {
826 ExecutionCapabilities {
828 can_run: has_main,
829 can_test: has_tests,
830 can_bench: has_benches,
831 can_doc_test: false,
832 requires_framework: None,
833 }
834 }
835 },
836 FileRole::Module => ExecutionCapabilities {
837 can_run: false,
838 can_test: has_tests,
839 can_bench: has_benches,
840 can_doc_test: false,
841 requires_framework: None,
842 },
843 }
844 }
845
846 fn is_cargo_script(content: &str) -> bool {
849 content.starts_with("#!/usr/bin/env -S cargo")
850 || content.starts_with("#!/usr/bin/env cargo")
851 || content.starts_with("#!/usr/bin/env cargo-eval")
852 || content.starts_with("#!/usr/bin/env run-cargo-script")
853 }
854
855 fn extract_cargo_script_manifest(content: &str) -> Option<String> {
856 if let Some(start) = content.find("//! ```cargo") {
858 let search_start = start + 13; if let Some(relative_end) = content[search_start..].find("//! ```") {
860 let end = search_start + relative_end;
861 return Some(content[search_start..end].trim().to_string());
862 }
863 }
864 None
865 }
866
867 fn parse_workspace_members(content: &str, root: &Path) -> RazResult<Vec<WorkspaceMember>> {
868 let mut members = Vec::new();
869
870 if let Some(members_section) = content.find("members = [") {
872 let section = &content[members_section..];
873 if let Some(end) = section.find(']') {
874 let members_str = §ion[11..end];
875 for member in members_str.split(',') {
876 let member_name = member.trim().trim_matches('"').trim_matches('\'');
877 if !member_name.is_empty() {
878 members.push(WorkspaceMember {
879 name: member_name.to_string(),
880 path: root.join(member_name),
881 is_current: false, });
883 }
884 }
885 }
886 }
887
888 Ok(members)
889 }
890
891 fn extract_package_name(content: &str) -> RazResult<String> {
892 if let Some(name_start) = content.find("name = \"") {
893 let name_section = &content[name_start + 8..];
894 if let Some(name_end) = name_section.find('"') {
895 return Ok(name_section[..name_end].to_string());
896 }
897 }
898 Ok("unknown".to_string())
899 }
900
901 fn extract_binary_name_from_path(file_path: &Path) -> String {
902 if let Some(parent) = file_path.parent() {
904 if let Some(cargo_dir) = parent.parent() {
905 let cargo_toml = cargo_dir.join("Cargo.toml");
906 if cargo_toml.exists() {
907 if let Ok(content) = fs::read_to_string(&cargo_toml) {
908 if let Ok(name) = Self::extract_package_name(&content) {
909 return name;
910 }
911 }
912 }
913 }
914 }
915 "main".to_string()
916 }
917
918 fn is_frontend_library(file_path: &Path) -> bool {
919 let path_str = file_path.to_string_lossy();
920 let file_name = file_path.file_name().unwrap_or_default().to_string_lossy();
921
922 file_name == "lib.rs"
923 && (path_str.contains("/frontend/")
924 || path_str.contains("/app/")
925 || path_str.contains("/client/")
926 || path_str.contains("/web/"))
927 }
928
929 fn detect_web_framework(file_path: &Path) -> RazResult<String> {
930 let mut current = file_path;
932 while let Some(parent) = current.parent() {
933 let cargo_toml = parent.join("Cargo.toml");
934 if cargo_toml.exists() {
935 if let Ok(content) = fs::read_to_string(&cargo_toml) {
936 if content.contains("leptos") {
937 return Ok("leptos".to_string());
938 } else if content.contains("dioxus") {
939 return Ok("dioxus".to_string());
940 } else if content.contains("yew") {
941 return Ok("yew".to_string());
942 }
943 }
944 }
945 current = parent;
946 }
947 Ok("unknown".to_string())
948 }
949
950 fn detect_non_test_entries(content: &str) -> RazResult<Vec<EntryPoint>> {
951 let mut entry_points = Vec::new();
952 let lines: Vec<&str> = content.lines().collect();
953 let mut in_doc_comment = false;
954
955 for (line_num, line) in lines.iter().enumerate() {
956 let trimmed = line.trim();
957
958 if trimmed.starts_with("///") || trimmed.starts_with("//!") {
960 in_doc_comment = true;
961 } else if in_doc_comment && !trimmed.starts_with("//") {
962 in_doc_comment = false;
963 }
964
965 if !in_doc_comment && trimmed.contains("fn main(") {
967 entry_points.push(EntryPoint {
968 name: "main".to_string(),
969 entry_type: EntryPointType::Main,
970 line: line_num as u32 + 1,
971 column: line.find("fn main(").unwrap_or(0) as u32,
972 line_range: (line_num as u32 + 1, line_num as u32 + 1),
973 full_path: None,
974 });
975 }
976
977 if !in_doc_comment && trimmed.starts_with("#[bench]") {
979 if let Some(next_line) = lines.get(line_num + 1) {
980 if let Some(fn_name) = Self::extract_function_name(next_line) {
981 entry_points.push(EntryPoint {
982 name: fn_name,
983 entry_type: EntryPointType::Benchmark,
984 line: line_num as u32 + 2,
985 column: 0,
986 line_range: (line_num as u32 + 1, line_num as u32 + 1),
987 full_path: None,
988 });
989 }
990 }
991 }
992
993 if !in_doc_comment
995 && (trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!"))
996 {
997 entry_points.push(EntryPoint {
998 name: "criterion_bench".to_string(),
999 entry_type: EntryPointType::Benchmark,
1000 line: line_num as u32 + 1,
1001 column: 0,
1002 line_range: (line_num as u32 + 1, line_num as u32 + 1),
1003 full_path: None,
1004 });
1005 }
1006 }
1007
1008 Ok(entry_points)
1009 }
1010
1011 fn extract_function_name(line: &str) -> Option<String> {
1012 let trimmed = line.trim();
1013 if let Some(fn_start) = trimmed.find("fn ") {
1014 let fn_part = &trimmed[fn_start + 3..];
1015 if let Some(paren_pos) = fn_part.find('(') {
1016 let fn_name = fn_part[..paren_pos].trim();
1017 if !fn_name.is_empty() {
1018 return Some(fn_name.to_string());
1019 }
1020 }
1021 }
1022 None
1023 }
1024
1025 fn extract_struct_name(line: &str) -> Option<String> {
1026 let trimmed = line.trim();
1027 let struct_start = if trimmed.starts_with("pub struct ") {
1028 11 } else if trimmed.starts_with("struct ") {
1030 7 } else {
1032 return None;
1033 };
1034
1035 let after_struct = &trimmed[struct_start..];
1036 let name_end = after_struct
1038 .find([' ', '<', '{', ';'])
1039 .unwrap_or(after_struct.len());
1040 let struct_name = after_struct[..name_end].trim();
1041
1042 if !struct_name.is_empty() {
1043 Some(struct_name.to_string())
1044 } else {
1045 None
1046 }
1047 }
1048
1049 fn extract_impl_name(line: &str) -> Option<String> {
1050 let trimmed = line.trim();
1051 if !trimmed.starts_with("impl") {
1052 return None;
1053 }
1054
1055 if let Some(for_pos) = trimmed.find(" for ") {
1057 let after_for = &trimmed[for_pos + 5..];
1059 let name_end = after_for.find([' ', '{', '<']).unwrap_or(after_for.len());
1060 let type_name = after_for[..name_end].trim();
1061 if !type_name.is_empty() {
1062 return Some(type_name.to_string());
1063 }
1064 } else {
1065 let after_impl = if trimmed.starts_with("impl<") {
1067 if let Some(gt_pos) = trimmed.find('>') {
1069 &trimmed[gt_pos + 1..].trim()
1070 } else {
1071 return None;
1072 }
1073 } else {
1074 &trimmed[4..].trim() };
1076
1077 let name_end = after_impl.find([' ', '{', '<']).unwrap_or(after_impl.len());
1078 let type_name = after_impl[..name_end].trim();
1079 if !type_name.is_empty() {
1080 return Some(type_name.to_string());
1081 }
1082 }
1083 None
1084 }
1085
1086 fn build_module_path_from_file(
1088 file_path: &Path,
1089 project_type: &RustProjectType,
1090 ) -> Vec<String> {
1091 let mut module_path = Vec::new();
1092
1093 let src_dir = match project_type {
1095 RustProjectType::CargoWorkspace { root, .. } => root.join("src"),
1096 RustProjectType::CargoPackage { root, .. } => root.join("src"),
1097 _ => return module_path,
1098 };
1099
1100 if let Ok(relative_path) = file_path.strip_prefix(&src_dir) {
1102 for component in relative_path.components() {
1104 if let std::path::Component::Normal(os_str) = component {
1105 if let Some(name) = os_str.to_str() {
1106 let module_name = if name == "lib.rs" || name == "main.rs" {
1108 continue; } else if name == "mod.rs" {
1110 if let Some(parent) = relative_path.parent() {
1112 if let Some(parent_name) = parent.file_name() {
1113 parent_name.to_str().unwrap_or("").to_string()
1114 } else {
1115 continue;
1116 }
1117 } else {
1118 continue;
1119 }
1120 } else if name.ends_with(".rs") {
1121 name.trim_end_matches(".rs").to_string()
1122 } else {
1123 name.to_string()
1125 };
1126
1127 if !module_name.is_empty() {
1128 module_path.push(module_name);
1129 }
1130 }
1131 }
1132 }
1133 }
1134
1135 module_path
1136 }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142 use tempfile::TempDir;
1143
1144 #[test]
1145 fn test_cargo_script_detection() {
1146 let content = "#!/usr/bin/env -S cargo +nightly -Zscript\n\nfn main() {}";
1147 assert!(FileDetector::is_cargo_script(content));
1148 }
1149
1150 #[test]
1151 fn test_function_name_extraction() {
1152 assert_eq!(
1153 FileDetector::extract_function_name("fn test_something() {"),
1154 Some("test_something".to_string())
1155 );
1156 assert_eq!(
1157 FileDetector::extract_function_name(" fn main() {"),
1158 Some("main".to_string())
1159 );
1160 }
1161
1162 #[test]
1163 fn test_frontend_library_detection() {
1164 let temp_dir = TempDir::new().unwrap();
1165 let frontend_lib = temp_dir.path().join("frontend").join("src").join("lib.rs");
1166 assert!(FileDetector::is_frontend_library(&frontend_lib));
1167
1168 let regular_lib = temp_dir.path().join("src").join("lib.rs");
1169 assert!(!FileDetector::is_frontend_library(®ular_lib));
1170 }
1171
1172 #[test]
1173 fn test_nested_module_path_resolution() -> RazResult<()> {
1174 let temp_dir = TempDir::new()?;
1175 let test_file = temp_dir.path().join("nested_test.rs");
1176
1177 let content = r#"
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181
1182 #[test]
1183 fn test_top_level() {
1184 assert_eq!(2 + 2, 4);
1185 }
1186
1187 mod integration {
1188 use super::*;
1189
1190 #[test]
1191 fn test_integration_basic() {
1192 assert!(true);
1193 }
1194
1195 mod database {
1196 use super::*;
1197
1198 #[test]
1199 fn test_db_connection() {
1200 assert!(true);
1201 }
1202
1203 mod transactions {
1204 use super::*;
1205
1206 #[test]
1207 fn test_transaction_rollback() {
1208 assert!(true);
1209 }
1210 }
1211 }
1212 }
1213}
1214"#;
1215
1216 fs::write(&test_file, content)?;
1217
1218 let context = FileDetector::detect_context(
1220 &test_file,
1221 Some(Position {
1222 line: 27,
1223 column: 1,
1224 }),
1225 )?;
1226
1227 let nested_test = context.entry_points.iter().find(|ep| {
1229 ep.name == "test_transaction_rollback" && ep.entry_type == EntryPointType::Test
1230 });
1231
1232 assert!(
1233 nested_test.is_some(),
1234 "Should find the deeply nested test function"
1235 );
1236
1237 let test_entry = nested_test.unwrap();
1238 assert_eq!(
1239 test_entry.full_path.as_ref().unwrap(),
1240 "tests::integration::database::transactions::test_transaction_rollback",
1241 "Should have correct full module path for deeply nested test"
1242 );
1243
1244 let context2 = FileDetector::detect_context(
1246 &test_file,
1247 Some(Position {
1248 line: 19,
1249 column: 1,
1250 }),
1251 )?;
1252
1253 let mid_level_test = context2
1254 .entry_points
1255 .iter()
1256 .find(|ep| ep.name == "test_db_connection" && ep.entry_type == EntryPointType::Test);
1257
1258 assert!(
1259 mid_level_test.is_some(),
1260 "Should find the mid-level nested test"
1261 );
1262
1263 let test_entry2 = mid_level_test.unwrap();
1264 assert_eq!(
1265 test_entry2.full_path.as_ref().unwrap(),
1266 "tests::integration::database::test_db_connection",
1267 "Should have correct full module path for mid-level nested test"
1268 );
1269
1270 let top_level_test = context
1272 .entry_points
1273 .iter()
1274 .find(|ep| ep.name == "test_top_level" && ep.entry_type == EntryPointType::Test);
1275
1276 assert!(top_level_test.is_some(), "Should find the top-level test");
1277
1278 let test_entry3 = top_level_test.unwrap();
1279 assert_eq!(
1280 test_entry3.full_path.as_ref().unwrap(),
1281 "tests::test_top_level",
1282 "Should have correct module path for top-level test"
1283 );
1284
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn test_module_stack_handling_with_complex_nesting() -> RazResult<()> {
1290 let temp_dir = TempDir::new()?;
1291 let test_file = temp_dir.path().join("complex_nested.rs");
1292
1293 let content = r#"
1294mod outer {
1295 mod middle {
1296 #[cfg(test)]
1297 mod tests {
1298 #[test]
1299 fn test_deeply_nested() {
1300 assert!(true);
1301 }
1302
1303 mod sub_tests {
1304 #[test]
1305 fn test_sub_nested() {
1306 assert!(true);
1307 }
1308 }
1309 }
1310
1311 mod other {
1312 fn regular_function() {}
1313 }
1314 }
1315}
1316
1317#[cfg(test)]
1318mod main_tests {
1319 #[test]
1320 fn test_main_level() {
1321 assert!(true);
1322 }
1323}
1324"#;
1325
1326 fs::write(&test_file, content)?;
1327
1328 let context = FileDetector::detect_context(&test_file, None)?;
1329
1330 let deeply_nested = context
1332 .entry_points
1333 .iter()
1334 .find(|ep| ep.name == "test_deeply_nested");
1335
1336 assert!(deeply_nested.is_some(), "Should find deeply nested test");
1337 assert_eq!(
1338 deeply_nested.unwrap().full_path.as_ref().unwrap(),
1339 "outer::middle::tests::test_deeply_nested",
1340 "Should correctly build path through multiple modules"
1341 );
1342
1343 let sub_nested = context
1344 .entry_points
1345 .iter()
1346 .find(|ep| ep.name == "test_sub_nested");
1347
1348 assert!(sub_nested.is_some(), "Should find sub-nested test");
1349 assert_eq!(
1350 sub_nested.unwrap().full_path.as_ref().unwrap(),
1351 "outer::middle::tests::sub_tests::test_sub_nested",
1352 "Should correctly build path through all nested modules"
1353 );
1354
1355 let main_test = context
1356 .entry_points
1357 .iter()
1358 .find(|ep| ep.name == "test_main_level");
1359
1360 assert!(main_test.is_some(), "Should find main level test");
1361 assert_eq!(
1362 main_test.unwrap().full_path.as_ref().unwrap(),
1363 "main_tests::test_main_level",
1364 "Should correctly build path for main level test"
1365 );
1366
1367 Ok(())
1368 }
1369}