project_map_cli_rust/core/
utils.rs1use std::path::Path;
2
3pub fn path_to_fqn(root: &Path, path: &Path) -> String {
4 let rel = path.strip_prefix(root).unwrap_or(path);
5 let mut parts = Vec::new();
6
7 for component in rel.components() {
8 let part = component.as_os_str().to_string_lossy();
9 if part == "__init__.py" || part == "mod.rs" || part == "lib.rs" || part == "index.ts" || part == "index.tsx" {
10 continue;
11 }
12 let clean_part = part.trim_end_matches(".py")
13 .trim_end_matches(".rs")
14 .trim_end_matches(".tsx")
15 .trim_end_matches(".ts")
16 .trim_end_matches(".kt")
17 .trim_end_matches(".sql")
18 .trim_end_matches(".vue")
19 .trim_end_matches(".md");
20
21 if !clean_part.is_empty() {
22 parts.push(clean_part.to_string());
23 }
24 }
25
26 parts.join(".")
27}
28
29pub fn resolve_import_path(current_file: &str, import_specifier: &str) -> String {
30 if !import_specifier.starts_with('.') {
31 return import_specifier.to_string();
32 }
33
34 let current_path = Path::new(current_file);
35 let current_dir = current_path.parent().unwrap_or_else(|| Path::new(""));
36 let mut resolved = current_dir.to_path_buf();
37
38 for part in import_specifier.split('/') {
39 if part == "." {
40 continue;
41 } else if part == ".." {
42 resolved.pop();
43 } else {
44 resolved.push(part);
45 }
46 }
47
48 resolved.to_string_lossy().to_string()
49}
50
51pub fn render_tree(paths: &[String], max_depth: usize) -> String {
52 use std::collections::BTreeMap;
53
54 #[derive(Default)]
55 struct TreeNode {
56 children: BTreeMap<String, TreeNode>,
57 }
58
59 let mut root = TreeNode::default();
60 for path_str in paths {
61 let path = Path::new(path_str);
62 let mut current = &mut root;
63 for component in path.components() {
64 let name = component.as_os_str().to_string_lossy().into_owned();
65 current = current.children.entry(name).or_default();
66 }
67 }
68
69 fn render_node(node: &TreeNode, name: &str, prefix: &str, is_last: bool, depth: usize, max_depth: usize) -> String {
70 if depth > max_depth {
71 return "".to_string();
72 }
73
74 let mut output = String::new();
75 if !name.is_empty() {
76 let marker = if is_last { "└── " } else { "├── " };
77 output.push_str(&format!("{}{}{}\n", prefix, marker, name));
78 }
79
80 let new_prefix = if name.is_empty() {
81 "".to_string()
82 } else {
83 format!("{}{}", prefix, if is_last { " " } else { "│ " })
84 };
85
86 let child_count = node.children.len();
87 for (i, (child_name, child_node)) in node.children.iter().enumerate() {
88 let is_child_last = i == child_count - 1;
89 output.push_str(&render_node(child_node, child_name, &new_prefix, is_child_last, depth + 1, max_depth));
90 }
91
92 output
93 }
94
95 render_node(&root, "", "", true, 0, max_depth)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_path_to_fqn() {
104 let root = Path::new("/project");
105 let path = Path::new("/project/src/core/utils.py");
106 assert_eq!(path_to_fqn(root, path), "src.core.utils");
107
108 let path2 = Path::new("/project/src/main.rs");
109 assert_eq!(path_to_fqn(root, path2), "src.main");
110
111 let path3 = Path::new("/project/src/core/__init__.py");
112 assert_eq!(path_to_fqn(root, path3), "src.core");
113
114 let path4 = Path::new("/project/tests/integration/test_main.py");
115 assert_eq!(path_to_fqn(root, path4), "tests.integration.test_main");
116
117 let path5 = Path::new("/project/src/components/Button.tsx");
118 assert_eq!(path_to_fqn(root, path5), "src.components.Button");
119
120 let path6 = Path::new("/project/src/components/index.ts");
121 assert_eq!(path_to_fqn(root, path6), "src.components");
122 }
123
124 #[test]
125 fn test_resolve_import_path() {
126 assert_eq!(resolve_import_path("src/main.ts", "./utils"), "src/utils");
127 assert_eq!(resolve_import_path("src/core/parser.ts", "../utils"), "src/utils");
128 assert_eq!(resolve_import_path("src/index.ts", "lodash"), "lodash");
129 }
130}