1use crate::tree::{Location, NodeType, ReferenceTree, TreeNode};
2use crate::{CodeReference, SearchResult, TranslationEntry};
3
4pub struct ReferenceTreeBuilder;
6
7impl ReferenceTreeBuilder {
8 pub fn build(result: &SearchResult) -> ReferenceTree {
16 let mut root = TreeNode::new(NodeType::Root, result.query.clone());
17 let mut used_code_refs = std::collections::HashSet::new();
18
19 for entry in &result.translation_entries {
21 let mut translation_node = Self::build_translation_node(entry);
22 let mut key_node = Self::build_key_node(entry);
23
24 let matching_refs: Vec<_> = result
26 .code_references
27 .iter()
28 .enumerate()
29 .filter(|(_, r)| r.key_path == entry.key)
30 .collect();
31
32 for (idx, code_ref) in matching_refs {
34 let code_node = Self::build_code_node(code_ref);
35 key_node.add_child(code_node);
36 used_code_refs.insert(idx);
37 }
38
39 if key_node.has_children() {
41 translation_node.children.push(key_node);
42 }
43
44 root.add_child(translation_node);
45 }
46
47 let direct_matches: Vec<_> = result
49 .code_references
50 .iter()
51 .enumerate()
52 .filter(|(idx, _)| !used_code_refs.contains(idx))
53 .map(|(_, r)| r)
54 .collect();
55
56 if !direct_matches.is_empty() {
57 let mut direct_matches_node =
59 TreeNode::new(NodeType::KeyPath, "Direct Matches".to_string());
60
61 for code_ref in direct_matches {
62 let code_node = Self::build_code_node(code_ref);
63 direct_matches_node.add_child(code_node);
64 }
65
66 root.add_child(direct_matches_node);
67 }
68
69 ReferenceTree::new(root)
70 }
71
72 fn build_translation_node(entry: &TranslationEntry) -> TreeNode {
74 let location = Location::new(entry.file.clone(), entry.line);
75 let mut node = TreeNode::with_location(NodeType::Translation, entry.key.clone(), location);
76 node.metadata = Some(entry.value.clone());
77 node
78 }
79
80 fn build_key_node(entry: &TranslationEntry) -> TreeNode {
82 TreeNode::new(NodeType::KeyPath, entry.key.clone())
83 }
84
85 fn build_code_node(code_ref: &CodeReference) -> TreeNode {
87 let location = Location::new(code_ref.file.clone(), code_ref.line);
88 let mut node =
89 TreeNode::with_location(NodeType::CodeRef, code_ref.context.clone(), location);
90 node.metadata = Some(code_ref.key_path.clone());
92 node
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::path::PathBuf;
100
101 fn create_test_translation_entry() -> TranslationEntry {
102 TranslationEntry {
103 key: "invoice.labels.add_new".to_string(),
104 value: "add new".to_string(),
105 line: 4,
106 file: PathBuf::from("en.yml"),
107 }
108 }
109
110 fn create_test_code_reference() -> CodeReference {
111 CodeReference {
112 file: PathBuf::from("invoices.ts"),
113 line: 14,
114 pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
115 context: "I18n.t('invoice.labels.add_new')".to_string(),
116 key_path: "invoice.labels.add_new".to_string(),
117 }
118 }
119
120 #[test]
121 fn test_build_translation_node() {
122 let entry = create_test_translation_entry();
123 let node = ReferenceTreeBuilder::build_translation_node(&entry);
124
125 assert_eq!(node.node_type, NodeType::Translation);
126 assert_eq!(node.content, "invoice.labels.add_new");
127 assert_eq!(node.metadata.as_deref(), Some("add new"));
128 assert!(node.location.is_some());
129 assert_eq!(node.location.unwrap().line, 4);
130 }
131
132 #[test]
133 fn test_build_key_node() {
134 let entry = create_test_translation_entry();
135 let node = ReferenceTreeBuilder::build_key_node(&entry);
136
137 assert_eq!(node.node_type, NodeType::KeyPath);
138 assert_eq!(node.content, "invoice.labels.add_new");
139 assert!(node.location.is_none());
140 }
141
142 #[test]
143 fn test_build_code_node() {
144 let code_ref = create_test_code_reference();
145 let node = ReferenceTreeBuilder::build_code_node(&code_ref);
146
147 assert_eq!(node.node_type, NodeType::CodeRef);
148 assert_eq!(node.content, "I18n.t('invoice.labels.add_new')");
149 assert!(node.location.is_some());
150 assert_eq!(node.location.as_ref().unwrap().line, 14);
151 }
152
153 #[test]
154 fn test_build_tree_single_match() {
155 let result = SearchResult {
156 query: "add new".to_string(),
157 translation_entries: vec![create_test_translation_entry()],
158 code_references: vec![create_test_code_reference()],
159 };
160
161 let tree = ReferenceTreeBuilder::build(&result);
162
163 assert_eq!(tree.root.content, "add new");
164 assert_eq!(tree.root.node_type, NodeType::Root);
165 assert_eq!(tree.root.children.len(), 1);
166
167 let translation = &tree.root.children[0];
169 assert_eq!(translation.node_type, NodeType::Translation);
170 assert_eq!(translation.content, "invoice.labels.add_new");
171 assert_eq!(translation.metadata.as_deref(), Some("add new"));
172 assert_eq!(translation.children.len(), 1);
173
174 let key_path = &translation.children[0];
176 assert_eq!(key_path.node_type, NodeType::KeyPath);
177 assert_eq!(key_path.content, "invoice.labels.add_new");
178 assert_eq!(key_path.children.len(), 1);
179
180 let code_ref = &key_path.children[0];
182 assert_eq!(code_ref.node_type, NodeType::CodeRef);
183 assert!(code_ref.content.contains("I18n.t"));
184 }
185
186 #[test]
187 fn test_build_tree_multiple_code_refs() {
188 let entry = create_test_translation_entry();
189 let code_ref1 = create_test_code_reference();
190 let mut code_ref2 = create_test_code_reference();
191 code_ref2.line = 20;
192 code_ref2.context = "I18n.t('invoice.labels.add_new') // another usage".to_string();
193
194 let result = SearchResult {
195 query: "add new".to_string(),
196 translation_entries: vec![entry],
197 code_references: vec![code_ref1, code_ref2],
198 };
199
200 let tree = ReferenceTreeBuilder::build(&result);
201
202 assert_eq!(tree.root.children.len(), 1);
204
205 let translation = &tree.root.children[0];
207 assert_eq!(translation.children.len(), 1);
208
209 let key_path = &translation.children[0];
211 assert_eq!(key_path.children.len(), 2);
212 }
213
214 #[test]
215 fn test_build_tree_multiple_translations() {
216 let entry1 = create_test_translation_entry();
217 let mut entry2 = create_test_translation_entry();
218 entry2.key = "invoice.labels.edit".to_string();
219 entry2.value = "edit invoice".to_string();
220
221 let code_ref1 = create_test_code_reference();
222 let mut code_ref2 = create_test_code_reference();
223 code_ref2.key_path = "invoice.labels.edit".to_string();
224 code_ref2.context = "I18n.t('invoice.labels.edit')".to_string();
225
226 let result = SearchResult {
227 query: "invoice".to_string(),
228 translation_entries: vec![entry1, entry2],
229 code_references: vec![code_ref1, code_ref2],
230 };
231
232 let tree = ReferenceTreeBuilder::build(&result);
233
234 assert_eq!(tree.root.children.len(), 2);
236
237 for translation in &tree.root.children {
239 assert_eq!(translation.children.len(), 1);
240 assert_eq!(translation.children[0].children.len(), 1);
241 }
242 }
243
244 #[test]
245 fn test_build_tree_no_code_refs() {
246 let result = SearchResult {
247 query: "add new".to_string(),
248 translation_entries: vec![create_test_translation_entry()],
249 code_references: vec![],
250 };
251
252 let tree = ReferenceTreeBuilder::build(&result);
253
254 assert_eq!(tree.root.children.len(), 1);
256
257 let translation = &tree.root.children[0];
259 assert_eq!(translation.children.len(), 0);
260 }
261
262 #[test]
263 fn test_build_tree_empty_result() {
264 let result = SearchResult {
265 query: "nonexistent".to_string(),
266 translation_entries: vec![],
267 code_references: vec![],
268 };
269
270 let tree = ReferenceTreeBuilder::build(&result);
271
272 assert_eq!(tree.root.content, "nonexistent");
273 assert_eq!(tree.root.children.len(), 0);
274 assert!(!tree.has_results());
275 }
276
277 #[test]
278 fn test_build_tree_structure() {
279 let result = SearchResult {
280 query: "add new".to_string(),
281 translation_entries: vec![create_test_translation_entry()],
282 code_references: vec![create_test_code_reference()],
283 };
284
285 let tree = ReferenceTreeBuilder::build(&result);
286
287 assert_eq!(tree.node_count(), 4); assert_eq!(tree.max_depth(), 4);
290 assert!(tree.has_results());
291 }
292
293 #[test]
294 fn test_build_tree_filters_unmatched_code_refs() {
295 let entry = create_test_translation_entry();
296 let code_ref1 = create_test_code_reference();
297 let mut code_ref2 = create_test_code_reference();
298 code_ref2.key_path = "different.key".to_string();
299
300 let result = SearchResult {
301 query: "add new".to_string(),
302 translation_entries: vec![entry],
303 code_references: vec![code_ref1, code_ref2],
304 };
305
306 let tree = ReferenceTreeBuilder::build(&result);
307
308 let key_path = &tree.root.children[0].children[0];
310 assert_eq!(key_path.children.len(), 1);
311 assert!(key_path.children[0]
312 .content
313 .contains("invoice.labels.add_new"));
314 }
315}