1pub mod config;
2pub mod error;
3pub mod output;
4pub mod parse;
5pub mod search;
6pub mod trace;
7pub mod tree;
8
9use std::path::PathBuf;
10
11pub use config::default_patterns;
13pub use error::{Result, SearchError};
14pub use output::TreeFormatter;
15pub use parse::{KeyExtractor, TranslationEntry, YamlParser};
16pub use search::{CodeReference, Match, PatternMatcher, TextSearcher};
17pub use trace::{
18 CallExtractor, CallGraphBuilder, CallNode, CallTree, FunctionDef, FunctionFinder,
19 TraceDirection,
20};
21pub use tree::{Location, NodeType, ReferenceTree, ReferenceTreeBuilder, TreeNode};
22
23#[derive(Debug, Clone)]
25pub struct TraceQuery {
26 pub function_name: String,
27 pub direction: TraceDirection,
28 pub max_depth: usize,
29 pub base_dir: Option<PathBuf>,
30 pub exclude_patterns: Vec<String>,
31}
32
33impl TraceQuery {
34 pub fn new(function_name: String, direction: TraceDirection, max_depth: usize) -> Self {
35 Self {
36 function_name,
37 direction,
38 max_depth,
39 base_dir: None,
40 exclude_patterns: Vec::new(),
41 }
42 }
43
44 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
45 self.base_dir = Some(base_dir);
46 self
47 }
48
49 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
50 self.exclude_patterns = exclusions;
51 self
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct SearchQuery {
58 pub text: String,
59 pub case_sensitive: bool,
60 pub word_match: bool,
61 pub is_regex: bool,
62 pub base_dir: Option<PathBuf>,
63 pub exclude_patterns: Vec<String>,
64 pub include_patterns: Vec<String>,
65}
66
67impl SearchQuery {
68 pub fn new(text: String) -> Self {
69 Self {
70 text,
71 case_sensitive: false,
72 word_match: false,
73 is_regex: false,
74 base_dir: None,
75 exclude_patterns: Vec::new(),
76 include_patterns: Vec::new(),
77 }
78 }
79
80 pub fn with_word_match(mut self, word_match: bool) -> Self {
81 self.word_match = word_match;
82 self
83 }
84
85 pub fn with_regex(mut self, is_regex: bool) -> Self {
86 self.is_regex = is_regex;
87 self
88 }
89
90 pub fn with_includes(mut self, includes: Vec<String>) -> Self {
91 self.include_patterns = includes;
92 self
93 }
94
95 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
96 self.case_sensitive = case_sensitive;
97 self
98 }
99
100 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
101 self.base_dir = Some(base_dir);
102 self
103 }
104
105 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
106 self.exclude_patterns = exclusions;
107 self
108 }
109}
110
111#[derive(Debug)]
113pub struct SearchResult {
114 pub query: String,
115 pub translation_entries: Vec<TranslationEntry>,
116 pub code_references: Vec<CodeReference>,
117}
118
119pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
127 let base_dir = query
129 .base_dir
130 .clone()
131 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
132
133 let project_type = config::detect_project_type(&base_dir);
135 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
136 .iter()
137 .map(|&s| s.to_string())
138 .collect();
139 exclusions.extend(query.exclude_patterns.clone());
140
141 let mut extractor = KeyExtractor::new();
143 extractor.set_exclusions(exclusions.clone());
144 let translation_entries = extractor.extract(&base_dir, &query.text)?;
145
146 let mut matcher = PatternMatcher::new(base_dir.clone());
149 matcher.set_exclusions(exclusions.clone());
150 let mut all_code_refs = Vec::new();
151
152 for entry in &translation_entries {
153 let key_variations = generate_partial_keys(&entry.key);
155
156 for key in &key_variations {
158 let code_refs = matcher.find_usages(key)?;
159 all_code_refs.extend(code_refs);
160 }
161 }
162
163 let text_searcher = TextSearcher::new(base_dir.clone())
166 .case_sensitive(query.case_sensitive)
167 .word_match(query.word_match)
168 .is_regex(query.is_regex)
169 .add_globs(query.include_patterns.clone())
170 .respect_gitignore(true); if let Ok(direct_matches) = text_searcher.search(&query.text) {
173 for m in direct_matches {
174 let path_str = m.file.to_string_lossy();
176 if path_str.ends_with(".yml") || path_str.ends_with(".yaml") {
177 continue;
178 }
179
180 if exclusions.iter().any(|ex| path_str.contains(ex)) {
182 continue;
183 }
184
185 all_code_refs.push(CodeReference {
187 file: m.file,
188 line: m.line,
189 pattern: "Direct Match".to_string(),
190 context: m.content,
191 key_path: query.text.clone(), });
193 }
194 }
195
196 all_code_refs.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
198 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
199
200 Ok(SearchResult {
201 query: query.text,
202 translation_entries,
203 code_references: all_code_refs,
204 })
205}
206
207pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
220 let base_dir = query
221 .base_dir
222 .clone()
223 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
224
225 let finder = FunctionFinder::new(base_dir.clone());
226 if let Some(start_fn) = finder.find_function(&query.function_name) {
227 let extractor = CallExtractor::new(base_dir);
228 let builder = CallGraphBuilder::new(query.direction, query.max_depth, &finder, &extractor);
229 builder.build_trace(&start_fn)
230 } else {
231 Ok(None)
232 }
233}
234
235pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
237 matches
238 .iter()
239 .filter(|m| {
240 let path = m.file.to_string_lossy();
241 path.ends_with(".yml") || path.ends_with(".yaml")
242 })
243 .map(|m| m.file.clone())
244 .collect()
245}
246
247pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
254 let mut keys = Vec::new();
255
256 keys.push(full_key.to_string());
258
259 let segments: Vec<&str> = full_key.split('.').collect();
260
261 if segments.len() >= 2 {
263 if segments.len() > 1 {
266 let without_first = segments[1..].join(".");
267 keys.push(without_first);
268 }
269
270 if segments.len() > 1 {
273 let without_last = segments[..segments.len() - 1].join(".");
274 keys.push(without_last);
275 }
276 }
277
278 keys
279}