cs/tree/
builder.rs

1use crate::tree::{Location, NodeType, ReferenceTree, TreeNode};
2use crate::{CodeReference, SearchResult, TranslationEntry};
3
4/// Builder for constructing reference trees from search results
5pub struct ReferenceTreeBuilder;
6
7impl ReferenceTreeBuilder {
8    /// Build a reference tree from search results
9    ///
10    /// Creates a hierarchical tree structure:
11    /// - Root: search query text
12    ///   - Translation: translation file entry
13    ///     - KeyPath: full translation key
14    ///       - CodeRef: code reference using the key
15    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        // Group code references by translation entry key
20        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            // Find all code references for this translation key
25            let matching_refs: Vec<_> = result
26                .code_references
27                .iter()
28                .enumerate()
29                .filter(|(_, r)| r.key_path == entry.key)
30                .collect();
31
32            // Add code reference nodes as children of the key node
33            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            // Only add the key node if it has code references
40            if key_node.has_children() {
41                translation_node.children.push(key_node);
42            }
43
44            root.add_child(translation_node);
45        }
46
47        // Handle direct matches (code references not associated with any translation entry)
48        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            // Create a virtual node for direct matches
58            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    /// Build a translation node from a translation entry
73    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    /// Build a key path node from a translation entry
81    fn build_key_node(entry: &TranslationEntry) -> TreeNode {
82        TreeNode::new(NodeType::KeyPath, entry.key.clone())
83    }
84
85    /// Build a code reference node
86    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        // Store the key path (or search pattern) in metadata for highlighting
91        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        // Check translation node
167        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        // Check key path node
173        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        // Check code reference node
179        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        // Should have one translation node
201        assert_eq!(tree.root.children.len(), 1);
202
203        // Translation should have one key path node
204        let translation = &tree.root.children[0];
205        assert_eq!(translation.children.len(), 1);
206
207        // Key path should have two code reference nodes
208        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        // Should have two translation nodes
233        assert_eq!(tree.root.children.len(), 2);
234
235        // Each translation should have one key path with one code ref
236        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        // Should have one translation node
253        assert_eq!(tree.root.children.len(), 1);
254
255        // Translation should have no children (no key path without code refs)
256        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        // Verify tree structure
286        assert_eq!(tree.node_count(), 4); // root + translation + key + code_ref
287        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        // Should only include the matching code ref
307        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}