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 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    /// 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");
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        // Check translation node
168        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        // Check key path node
175        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        // Check code reference node
181        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        // Should have one translation node
203        assert_eq!(tree.root.children.len(), 1);
204
205        // Translation should have one key path node
206        let translation = &tree.root.children[0];
207        assert_eq!(translation.children.len(), 1);
208
209        // Key path should have two code reference nodes
210        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        // Should have two translation nodes
235        assert_eq!(tree.root.children.len(), 2);
236
237        // Each translation should have one key path with one code ref
238        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        // Should have one translation node
255        assert_eq!(tree.root.children.len(), 1);
256
257        // Translation should have no children (no key path without code refs)
258        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        // Verify tree structure
288        assert_eq!(tree.node_count(), 4); // root + translation + key + code_ref
289        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        // Should only include the matching code ref
309        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}