1pub mod cache;
2pub mod config;
3pub mod error;
4pub mod output;
5pub mod parse;
6pub mod search;
7pub mod trace;
8pub mod tree;
9
10use std::path::PathBuf;
11
12pub use cache::SearchResultCache;
14pub use config::default_patterns;
15pub use error::{Result, SearchError};
16pub use output::TreeFormatter;
17pub use parse::{KeyExtractor, TranslationEntry, YamlParser};
18pub use search::{CodeReference, FileMatch, FileSearcher, Match, PatternMatcher, TextSearcher};
19pub use trace::{
20 CallExtractor, CallGraphBuilder, CallNode, CallTree, FunctionDef, FunctionFinder,
21 TraceDirection,
22};
23pub use tree::{Location, NodeType, ReferenceTree, ReferenceTreeBuilder, TreeNode};
24
25#[derive(Debug, Clone)]
27pub struct TraceQuery {
28 pub function_name: String,
29 pub direction: TraceDirection,
30 pub max_depth: usize,
31 pub base_dir: Option<PathBuf>,
32 pub exclude_patterns: Vec<String>,
33}
34
35impl TraceQuery {
36 pub fn new(function_name: String, direction: TraceDirection, max_depth: usize) -> Self {
37 Self {
38 function_name,
39 direction,
40 max_depth,
41 base_dir: None,
42 exclude_patterns: Vec::new(),
43 }
44 }
45
46 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
47 self.base_dir = Some(base_dir);
48 self
49 }
50
51 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
52 self.exclude_patterns = exclusions;
53 self
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct SearchQuery {
60 pub text: String,
61 pub case_sensitive: bool,
62 pub word_match: bool,
63 pub is_regex: bool,
64 pub base_dir: Option<PathBuf>,
65 pub exclude_patterns: Vec<String>,
66 pub include_patterns: Vec<String>,
67 pub verbose: bool,
68 pub quiet: bool, }
70
71impl SearchQuery {
72 pub fn new(text: String) -> Self {
73 Self {
74 text,
75 case_sensitive: true,
76 word_match: false,
77 is_regex: false,
78 base_dir: None,
79 exclude_patterns: Vec::new(),
80 include_patterns: Vec::new(),
81 verbose: false,
82 quiet: false,
83 }
84 }
85
86 pub fn with_word_match(mut self, word_match: bool) -> Self {
87 self.word_match = word_match;
88 self
89 }
90
91 pub fn with_regex(mut self, is_regex: bool) -> Self {
92 self.is_regex = is_regex;
93 self
94 }
95
96 pub fn with_includes(mut self, includes: Vec<String>) -> Self {
97 self.include_patterns = includes;
98 self
99 }
100
101 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
102 self.case_sensitive = case_sensitive;
103 self
104 }
105
106 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
107 self.base_dir = Some(base_dir);
108 self
109 }
110
111 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
112 self.exclude_patterns = exclusions;
113 self
114 }
115
116 pub fn with_verbose(mut self, verbose: bool) -> Self {
117 self.verbose = verbose;
118 self
119 }
120
121 pub fn with_quiet(mut self, quiet: bool) -> Self {
122 self.quiet = quiet;
123 self
124 }
125}
126
127#[derive(Debug)]
129pub struct SearchResult {
130 pub query: String,
131 pub translation_entries: Vec<TranslationEntry>,
132 pub code_references: Vec<CodeReference>,
133}
134
135#[must_use = "this function returns a Result that should be handled"]
166pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
167 let raw_base_dir = query
169 .base_dir
170 .clone()
171 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
172
173 let (search_dir, specific_file) = if raw_base_dir.is_file() {
175 let parent_dir = raw_base_dir
177 .parent()
178 .map(|p| p.to_path_buf())
179 .unwrap_or_else(|| PathBuf::from("."));
180 (parent_dir, Some(raw_base_dir.clone()))
181 } else {
182 (raw_base_dir.clone(), None)
184 };
185
186 let project_type = config::detect_project_type(&search_dir);
188 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
189 .iter()
190 .map(|&s| s.to_string())
191 .collect();
192 exclusions.extend(query.exclude_patterns.clone());
193
194 let translation_entries = if specific_file.is_none() {
197 let mut extractor = KeyExtractor::new();
198 extractor.set_exclusions(exclusions.clone());
199 extractor.set_verbose(query.verbose);
200 extractor.set_quiet(query.quiet);
201 extractor.set_case_sensitive(query.case_sensitive);
202 extractor.extract(&search_dir, &query.text)?
203 } else {
204 Vec::new() };
206
207 let mut all_code_refs = Vec::new();
210
211 if specific_file.is_none() {
212 let mut matcher = PatternMatcher::new(search_dir.clone());
213 matcher.set_exclusions(exclusions.clone());
214
215 for entry in &translation_entries {
216 let key_variations = generate_partial_keys(&entry.key);
218
219 for key in &key_variations {
221 let code_refs = matcher.find_usages(key)?;
222 all_code_refs.extend(code_refs);
223 }
224 }
225 }
226
227 let text_searcher = TextSearcher::new(search_dir.clone())
230 .case_sensitive(query.case_sensitive)
231 .word_match(query.word_match)
232 .is_regex(query.is_regex)
233 .add_globs(query.include_patterns.clone())
234 .add_exclusions(exclusions.clone())
235 .respect_gitignore(true); if let Ok(direct_matches) = text_searcher.search(&query.text) {
238 for m in direct_matches {
239 if let Some(ref target_file) = specific_file {
241 if m.file != *target_file {
242 continue;
243 }
244 }
245
246 let path_str = m.file.to_string_lossy();
249 if specific_file.is_none()
250 && (path_str.ends_with(".yml")
251 || path_str.ends_with(".yaml")
252 || path_str.ends_with(".json")
253 || path_str.ends_with(".js"))
254 {
255 continue;
256 }
257
258 if exclusions.iter().any(|ex| path_str.contains(ex)) {
260 continue;
261 }
262
263 all_code_refs.push(CodeReference {
265 file: m.file.clone(),
266 line: m.line,
267 pattern: "Direct Match".to_string(),
268 context: m.content.clone(),
269 key_path: query.text.clone(), context_before: m.context_before.clone(),
271 context_after: m.context_after.clone(),
272 });
273 }
274 }
275
276 all_code_refs.sort_by(|a, b| {
280 a.file.cmp(&b.file).then(a.line.cmp(&b.line)).then_with(|| {
281 let a_is_direct = a.key_path == query.text;
282 let b_is_direct = b.key_path == query.text;
283 a_is_direct.cmp(&b_is_direct)
285 })
286 });
287 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
288
289 Ok(SearchResult {
290 query: query.text,
291 translation_entries,
292 code_references: all_code_refs,
293 })
294}
295
296#[must_use = "this function returns a Result that should be handled"]
309pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
310 let base_dir = query
311 .base_dir
312 .clone()
313 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
314
315 let mut finder = FunctionFinder::new(base_dir.clone());
316 if let Some(start_fn) = finder.find_function(&query.function_name) {
317 let extractor = CallExtractor::new(base_dir);
318 let mut builder =
319 CallGraphBuilder::new(query.direction, query.max_depth, &mut finder, &extractor);
320 builder.build_trace(&start_fn)
321 } else {
322 Ok(None)
323 }
324}
325
326pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
328 matches
329 .iter()
330 .filter(|m| {
331 let path = m.file.to_string_lossy();
332 path.ends_with(".yml") || path.ends_with(".yaml") || path.ends_with(".json")
333 })
334 .map(|m| m.file.clone())
335 .collect()
336}
337
338pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
345 let mut keys = Vec::new();
346
347 keys.push(full_key.to_string());
349
350 let segments: Vec<&str> = full_key.split('.').collect();
351
352 if segments.len() >= 2 {
354 for i in 1..segments.len() {
359 if segments.len() - i >= 2 {
360 keys.push(segments[i..].join("."));
361 }
362 }
363
364 if segments.len() > 1 {
367 let without_last = segments[..segments.len() - 1].join(".");
368 if !keys.contains(&without_last) {
370 keys.push(without_last);
371 }
372 }
373 }
374
375 keys
376}