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 content = format!("{}: '{}'", entry.key, entry.value);
75 let location = Location::new(entry.file.clone(), entry.line);
76
77 TreeNode::with_location(NodeType::Translation, content, location)
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: 'add new'");
127 assert!(node.location.is_some());
128 assert_eq!(node.location.unwrap().line, 4);
129 }
130
131 #[test]
132 fn test_build_key_node() {
133 let entry = create_test_translation_entry();
134 let node = ReferenceTreeBuilder::build_key_node(&entry);
135
136 assert_eq!(node.node_type, NodeType::KeyPath);
137 assert_eq!(node.content, "invoice.labels.add_new");
138 assert!(node.location.is_none());
139 }
140
141 #[test]
142 fn test_build_code_node() {
143 let code_ref = create_test_code_reference();
144 let node = ReferenceTreeBuilder::build_code_node(&code_ref);
145
146 assert_eq!(node.node_type, NodeType::CodeRef);
147 assert_eq!(node.content, "I18n.t('invoice.labels.add_new')");
148 assert!(node.location.is_some());
149 assert_eq!(node.location.as_ref().unwrap().line, 14);
150 }
151
152 #[test]
153 fn test_build_tree_single_match() {
154 let result = SearchResult {
155 query: "add new".to_string(),
156 translation_entries: vec![create_test_translation_entry()],
157 code_references: vec![create_test_code_reference()],
158 };
159
160 let tree = ReferenceTreeBuilder::build(&result);
161
162 assert_eq!(tree.root.content, "add new");
163 assert_eq!(tree.root.node_type, NodeType::Root);
164 assert_eq!(tree.root.children.len(), 1);
165
166 let translation = &tree.root.children[0];
168 assert_eq!(translation.node_type, NodeType::Translation);
169 assert!(translation.content.contains("invoice.labels.add_new"));
170 assert_eq!(translation.children.len(), 1);
171
172 let key_path = &translation.children[0];
174 assert_eq!(key_path.node_type, NodeType::KeyPath);
175 assert_eq!(key_path.content, "invoice.labels.add_new");
176 assert_eq!(key_path.children.len(), 1);
177
178 let code_ref = &key_path.children[0];
180 assert_eq!(code_ref.node_type, NodeType::CodeRef);
181 assert!(code_ref.content.contains("I18n.t"));
182 }
183
184 #[test]
185 fn test_build_tree_multiple_code_refs() {
186 let entry = create_test_translation_entry();
187 let code_ref1 = create_test_code_reference();
188 let mut code_ref2 = create_test_code_reference();
189 code_ref2.line = 20;
190 code_ref2.context = "I18n.t('invoice.labels.add_new') // another usage".to_string();
191
192 let result = SearchResult {
193 query: "add new".to_string(),
194 translation_entries: vec![entry],
195 code_references: vec![code_ref1, code_ref2],
196 };
197
198 let tree = ReferenceTreeBuilder::build(&result);
199
200 assert_eq!(tree.root.children.len(), 1);
202
203 let translation = &tree.root.children[0];
205 assert_eq!(translation.children.len(), 1);
206
207 let key_path = &translation.children[0];
209 assert_eq!(key_path.children.len(), 2);
210 }
211
212 #[test]
213 fn test_build_tree_multiple_translations() {
214 let entry1 = create_test_translation_entry();
215 let mut entry2 = create_test_translation_entry();
216 entry2.key = "invoice.labels.edit".to_string();
217 entry2.value = "edit invoice".to_string();
218
219 let code_ref1 = create_test_code_reference();
220 let mut code_ref2 = create_test_code_reference();
221 code_ref2.key_path = "invoice.labels.edit".to_string();
222 code_ref2.context = "I18n.t('invoice.labels.edit')".to_string();
223
224 let result = SearchResult {
225 query: "invoice".to_string(),
226 translation_entries: vec![entry1, entry2],
227 code_references: vec![code_ref1, code_ref2],
228 };
229
230 let tree = ReferenceTreeBuilder::build(&result);
231
232 assert_eq!(tree.root.children.len(), 2);
234
235 for translation in &tree.root.children {
237 assert_eq!(translation.children.len(), 1);
238 assert_eq!(translation.children[0].children.len(), 1);
239 }
240 }
241
242 #[test]
243 fn test_build_tree_no_code_refs() {
244 let result = SearchResult {
245 query: "add new".to_string(),
246 translation_entries: vec![create_test_translation_entry()],
247 code_references: vec![],
248 };
249
250 let tree = ReferenceTreeBuilder::build(&result);
251
252 assert_eq!(tree.root.children.len(), 1);
254
255 let translation = &tree.root.children[0];
257 assert_eq!(translation.children.len(), 0);
258 }
259
260 #[test]
261 fn test_build_tree_empty_result() {
262 let result = SearchResult {
263 query: "nonexistent".to_string(),
264 translation_entries: vec![],
265 code_references: vec![],
266 };
267
268 let tree = ReferenceTreeBuilder::build(&result);
269
270 assert_eq!(tree.root.content, "nonexistent");
271 assert_eq!(tree.root.children.len(), 0);
272 assert!(!tree.has_results());
273 }
274
275 #[test]
276 fn test_build_tree_structure() {
277 let result = SearchResult {
278 query: "add new".to_string(),
279 translation_entries: vec![create_test_translation_entry()],
280 code_references: vec![create_test_code_reference()],
281 };
282
283 let tree = ReferenceTreeBuilder::build(&result);
284
285 assert_eq!(tree.node_count(), 4); assert_eq!(tree.max_depth(), 4);
288 assert!(tree.has_results());
289 }
290
291 #[test]
292 fn test_build_tree_filters_unmatched_code_refs() {
293 let entry = create_test_translation_entry();
294 let code_ref1 = create_test_code_reference();
295 let mut code_ref2 = create_test_code_reference();
296 code_ref2.key_path = "different.key".to_string();
297
298 let result = SearchResult {
299 query: "add new".to_string(),
300 translation_entries: vec![entry],
301 code_references: vec![code_ref1, code_ref2],
302 };
303
304 let tree = ReferenceTreeBuilder::build(&result);
305
306 let key_path = &tree.root.children[0].children[0];
308 assert_eq!(key_path.children.len(), 1);
309 assert!(key_path.children[0]
310 .content
311 .contains("invoice.labels.add_new"));
312 }
313}