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
18        // Group code references by translation entry key
19        for entry in &result.translation_entries {
20            let mut translation_node = Self::build_translation_node(entry);
21            let mut key_node = Self::build_key_node(entry);
22
23            // Find all code references for this translation key
24            let matching_refs: Vec<_> = result
25                .code_references
26                .iter()
27                .filter(|r| r.key_path == entry.key)
28                .collect();
29
30            // Add code reference nodes as children of the key node
31            for code_ref in matching_refs {
32                let code_node = Self::build_code_node(code_ref);
33                key_node.add_child(code_node);
34            }
35
36            // Only add the key node if it has code references
37            if key_node.has_children() {
38                translation_node.children.push(key_node);
39            }
40
41            root.add_child(translation_node);
42        }
43
44        ReferenceTree::new(root)
45    }
46
47    /// Build a translation node from a translation entry
48    fn build_translation_node(entry: &TranslationEntry) -> TreeNode {
49        let content = format!("{}: '{}'", entry.key, entry.value);
50        let location = Location::new(entry.file.clone(), entry.line);
51
52        TreeNode::with_location(NodeType::Translation, content, location)
53    }
54
55    /// Build a key path node from a translation entry
56    fn build_key_node(entry: &TranslationEntry) -> TreeNode {
57        TreeNode::new(NodeType::KeyPath, entry.key.clone())
58    }
59
60    /// Build a code reference node
61    fn build_code_node(code_ref: &CodeReference) -> TreeNode {
62        let location = Location::new(code_ref.file.clone(), code_ref.line);
63        TreeNode::with_location(NodeType::CodeRef, code_ref.context.clone(), location)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use std::path::PathBuf;
71
72    fn create_test_translation_entry() -> TranslationEntry {
73        TranslationEntry {
74            key: "invoice.labels.add_new".to_string(),
75            value: "add new".to_string(),
76            line: 4,
77            file: PathBuf::from("en.yml"),
78        }
79    }
80
81    fn create_test_code_reference() -> CodeReference {
82        CodeReference {
83            file: PathBuf::from("invoices.ts"),
84            line: 14,
85            pattern: r#"I18n\.t\(['"]([^'"]+)['"]\)"#.to_string(),
86            context: "I18n.t('invoice.labels.add_new')".to_string(),
87            key_path: "invoice.labels.add_new".to_string(),
88        }
89    }
90
91    #[test]
92    fn test_build_translation_node() {
93        let entry = create_test_translation_entry();
94        let node = ReferenceTreeBuilder::build_translation_node(&entry);
95
96        assert_eq!(node.node_type, NodeType::Translation);
97        assert_eq!(node.content, "invoice.labels.add_new: 'add new'");
98        assert!(node.location.is_some());
99        assert_eq!(node.location.unwrap().line, 4);
100    }
101
102    #[test]
103    fn test_build_key_node() {
104        let entry = create_test_translation_entry();
105        let node = ReferenceTreeBuilder::build_key_node(&entry);
106
107        assert_eq!(node.node_type, NodeType::KeyPath);
108        assert_eq!(node.content, "invoice.labels.add_new");
109        assert!(node.location.is_none());
110    }
111
112    #[test]
113    fn test_build_code_node() {
114        let code_ref = create_test_code_reference();
115        let node = ReferenceTreeBuilder::build_code_node(&code_ref);
116
117        assert_eq!(node.node_type, NodeType::CodeRef);
118        assert_eq!(node.content, "I18n.t('invoice.labels.add_new')");
119        assert!(node.location.is_some());
120        assert_eq!(node.location.as_ref().unwrap().line, 14);
121    }
122
123    #[test]
124    fn test_build_tree_single_match() {
125        let result = SearchResult {
126            query: "add new".to_string(),
127            translation_entries: vec![create_test_translation_entry()],
128            code_references: vec![create_test_code_reference()],
129        };
130
131        let tree = ReferenceTreeBuilder::build(&result);
132
133        assert_eq!(tree.root.content, "add new");
134        assert_eq!(tree.root.node_type, NodeType::Root);
135        assert_eq!(tree.root.children.len(), 1);
136
137        // Check translation node
138        let translation = &tree.root.children[0];
139        assert_eq!(translation.node_type, NodeType::Translation);
140        assert!(translation.content.contains("invoice.labels.add_new"));
141        assert_eq!(translation.children.len(), 1);
142
143        // Check key path node
144        let key_path = &translation.children[0];
145        assert_eq!(key_path.node_type, NodeType::KeyPath);
146        assert_eq!(key_path.content, "invoice.labels.add_new");
147        assert_eq!(key_path.children.len(), 1);
148
149        // Check code reference node
150        let code_ref = &key_path.children[0];
151        assert_eq!(code_ref.node_type, NodeType::CodeRef);
152        assert!(code_ref.content.contains("I18n.t"));
153    }
154
155    #[test]
156    fn test_build_tree_multiple_code_refs() {
157        let entry = create_test_translation_entry();
158        let code_ref1 = create_test_code_reference();
159        let mut code_ref2 = create_test_code_reference();
160        code_ref2.line = 20;
161        code_ref2.context = "I18n.t('invoice.labels.add_new') // another usage".to_string();
162
163        let result = SearchResult {
164            query: "add new".to_string(),
165            translation_entries: vec![entry],
166            code_references: vec![code_ref1, code_ref2],
167        };
168
169        let tree = ReferenceTreeBuilder::build(&result);
170
171        // Should have one translation node
172        assert_eq!(tree.root.children.len(), 1);
173
174        // Translation should have one key path node
175        let translation = &tree.root.children[0];
176        assert_eq!(translation.children.len(), 1);
177
178        // Key path should have two code reference nodes
179        let key_path = &translation.children[0];
180        assert_eq!(key_path.children.len(), 2);
181    }
182
183    #[test]
184    fn test_build_tree_multiple_translations() {
185        let entry1 = create_test_translation_entry();
186        let mut entry2 = create_test_translation_entry();
187        entry2.key = "invoice.labels.edit".to_string();
188        entry2.value = "edit invoice".to_string();
189
190        let code_ref1 = create_test_code_reference();
191        let mut code_ref2 = create_test_code_reference();
192        code_ref2.key_path = "invoice.labels.edit".to_string();
193        code_ref2.context = "I18n.t('invoice.labels.edit')".to_string();
194
195        let result = SearchResult {
196            query: "invoice".to_string(),
197            translation_entries: vec![entry1, entry2],
198            code_references: vec![code_ref1, code_ref2],
199        };
200
201        let tree = ReferenceTreeBuilder::build(&result);
202
203        // Should have two translation nodes
204        assert_eq!(tree.root.children.len(), 2);
205
206        // Each translation should have one key path with one code ref
207        for translation in &tree.root.children {
208            assert_eq!(translation.children.len(), 1);
209            assert_eq!(translation.children[0].children.len(), 1);
210        }
211    }
212
213    #[test]
214    fn test_build_tree_no_code_refs() {
215        let result = SearchResult {
216            query: "add new".to_string(),
217            translation_entries: vec![create_test_translation_entry()],
218            code_references: vec![],
219        };
220
221        let tree = ReferenceTreeBuilder::build(&result);
222
223        // Should have one translation node
224        assert_eq!(tree.root.children.len(), 1);
225
226        // Translation should have no children (no key path without code refs)
227        let translation = &tree.root.children[0];
228        assert_eq!(translation.children.len(), 0);
229    }
230
231    #[test]
232    fn test_build_tree_empty_result() {
233        let result = SearchResult {
234            query: "nonexistent".to_string(),
235            translation_entries: vec![],
236            code_references: vec![],
237        };
238
239        let tree = ReferenceTreeBuilder::build(&result);
240
241        assert_eq!(tree.root.content, "nonexistent");
242        assert_eq!(tree.root.children.len(), 0);
243        assert!(!tree.has_results());
244    }
245
246    #[test]
247    fn test_build_tree_structure() {
248        let result = SearchResult {
249            query: "add new".to_string(),
250            translation_entries: vec![create_test_translation_entry()],
251            code_references: vec![create_test_code_reference()],
252        };
253
254        let tree = ReferenceTreeBuilder::build(&result);
255
256        // Verify tree structure
257        assert_eq!(tree.node_count(), 4); // root + translation + key + code_ref
258        assert_eq!(tree.max_depth(), 4);
259        assert!(tree.has_results());
260    }
261
262    #[test]
263    fn test_build_tree_filters_unmatched_code_refs() {
264        let entry = create_test_translation_entry();
265        let code_ref1 = create_test_code_reference();
266        let mut code_ref2 = create_test_code_reference();
267        code_ref2.key_path = "different.key".to_string();
268
269        let result = SearchResult {
270            query: "add new".to_string(),
271            translation_entries: vec![entry],
272            code_references: vec![code_ref1, code_ref2],
273        };
274
275        let tree = ReferenceTreeBuilder::build(&result);
276
277        // Should only include the matching code ref
278        let key_path = &tree.root.children[0].children[0];
279        assert_eq!(key_path.children.len(), 1);
280        assert!(key_path.children[0].content.contains("invoice.labels.add_new"));
281    }
282}