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, FileMatch, FileSearcher, 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 pub verbose: bool,
66}
67
68impl SearchQuery {
69 pub fn new(text: String) -> Self {
70 Self {
71 text,
72 case_sensitive: false,
73 word_match: false,
74 is_regex: false,
75 base_dir: None,
76 exclude_patterns: Vec::new(),
77 include_patterns: Vec::new(),
78 verbose: false,
79 }
80 }
81
82 pub fn with_word_match(mut self, word_match: bool) -> Self {
83 self.word_match = word_match;
84 self
85 }
86
87 pub fn with_regex(mut self, is_regex: bool) -> Self {
88 self.is_regex = is_regex;
89 self
90 }
91
92 pub fn with_includes(mut self, includes: Vec<String>) -> Self {
93 self.include_patterns = includes;
94 self
95 }
96
97 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
98 self.case_sensitive = case_sensitive;
99 self
100 }
101
102 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
103 self.base_dir = Some(base_dir);
104 self
105 }
106
107 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
108 self.exclude_patterns = exclusions;
109 self
110 }
111
112 pub fn with_verbose(mut self, verbose: bool) -> Self {
113 self.verbose = verbose;
114 self
115 }
116}
117
118#[derive(Debug)]
120pub struct SearchResult {
121 pub query: String,
122 pub translation_entries: Vec<TranslationEntry>,
123 pub code_references: Vec<CodeReference>,
124}
125
126pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
134 let base_dir = query
136 .base_dir
137 .clone()
138 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
139
140 let project_type = config::detect_project_type(&base_dir);
142 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
143 .iter()
144 .map(|&s| s.to_string())
145 .collect();
146 exclusions.extend(query.exclude_patterns.clone());
147
148 let mut extractor = KeyExtractor::new();
150 extractor.set_exclusions(exclusions.clone());
151 extractor.set_verbose(query.verbose);
152 let translation_entries = extractor.extract(&base_dir, &query.text)?;
153
154 let mut matcher = PatternMatcher::new(base_dir.clone());
157 matcher.set_exclusions(exclusions.clone());
158 let mut all_code_refs = Vec::new();
159
160 for entry in &translation_entries {
161 let key_variations = generate_partial_keys(&entry.key);
163
164 for key in &key_variations {
166 let code_refs = matcher.find_usages(key)?;
167 all_code_refs.extend(code_refs);
168 }
169 }
170
171 let text_searcher = TextSearcher::new(base_dir.clone())
174 .case_sensitive(query.case_sensitive)
175 .word_match(query.word_match)
176 .is_regex(query.is_regex)
177 .add_globs(query.include_patterns.clone())
178 .add_exclusions(exclusions.clone())
179 .respect_gitignore(true); if let Ok(direct_matches) = text_searcher.search(&query.text) {
182 for m in direct_matches {
183 let path_str = m.file.to_string_lossy();
185 if path_str.ends_with(".yml")
186 || path_str.ends_with(".yaml")
187 || path_str.ends_with(".json")
188 {
189 continue;
190 }
191
192 if exclusions.iter().any(|ex| path_str.contains(ex)) {
194 continue;
195 }
196
197 all_code_refs.push(CodeReference {
199 file: m.file,
200 line: m.line,
201 pattern: "Direct Match".to_string(),
202 context: m.content,
203 key_path: query.text.clone(), });
205 }
206 }
207
208 all_code_refs.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
210 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
211
212 Ok(SearchResult {
213 query: query.text,
214 translation_entries,
215 code_references: all_code_refs,
216 })
217}
218
219pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
232 let base_dir = query
233 .base_dir
234 .clone()
235 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
236
237 let finder = FunctionFinder::new(base_dir.clone());
238 if let Some(start_fn) = finder.find_function(&query.function_name) {
239 let extractor = CallExtractor::new(base_dir);
240 let builder = CallGraphBuilder::new(query.direction, query.max_depth, &finder, &extractor);
241 builder.build_trace(&start_fn)
242 } else {
243 Ok(None)
244 }
245}
246
247pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
249 matches
250 .iter()
251 .filter(|m| {
252 let path = m.file.to_string_lossy();
253 path.ends_with(".yml") || path.ends_with(".yaml") || path.ends_with(".json")
254 })
255 .map(|m| m.file.clone())
256 .collect()
257}
258
259pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
266 let mut keys = Vec::new();
267
268 keys.push(full_key.to_string());
270
271 let segments: Vec<&str> = full_key.split('.').collect();
272
273 if segments.len() >= 2 {
275 if segments.len() > 1 {
278 let without_first = segments[1..].join(".");
279 keys.push(without_first);
280 }
281
282 if segments.len() > 1 {
285 let without_last = segments[..segments.len() - 1].join(".");
286 keys.push(without_last);
287 }
288 }
289
290 keys
291}