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 base_dir = query
169 .base_dir
170 .clone()
171 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
172
173 let project_type = config::detect_project_type(&base_dir);
175 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
176 .iter()
177 .map(|&s| s.to_string())
178 .collect();
179 exclusions.extend(query.exclude_patterns.clone());
180
181 let mut extractor = KeyExtractor::new();
183 extractor.set_exclusions(exclusions.clone());
184 extractor.set_verbose(query.verbose);
185 extractor.set_quiet(query.quiet);
186 extractor.set_case_sensitive(query.case_sensitive);
187 let translation_entries = extractor.extract(&base_dir, &query.text)?;
188
189 let mut matcher = PatternMatcher::new(base_dir.clone());
192 matcher.set_exclusions(exclusions.clone());
193 let mut all_code_refs = Vec::new();
194
195 for entry in &translation_entries {
196 let key_variations = generate_partial_keys(&entry.key);
198
199 for key in &key_variations {
201 let code_refs = matcher.find_usages(key)?;
202 all_code_refs.extend(code_refs);
203 }
204 }
205
206 let text_searcher = TextSearcher::new(base_dir.clone())
209 .case_sensitive(query.case_sensitive)
210 .word_match(query.word_match)
211 .is_regex(query.is_regex)
212 .add_globs(query.include_patterns.clone())
213 .add_exclusions(exclusions.clone())
214 .respect_gitignore(true); if let Ok(direct_matches) = text_searcher.search(&query.text) {
217 for m in direct_matches {
218 let path_str = m.file.to_string_lossy();
220 if path_str.ends_with(".yml")
221 || path_str.ends_with(".yaml")
222 || path_str.ends_with(".json")
223 {
224 continue;
225 }
226
227 if exclusions.iter().any(|ex| path_str.contains(ex)) {
229 continue;
230 }
231
232 all_code_refs.push(CodeReference {
234 file: m.file,
235 line: m.line,
236 pattern: "Direct Match".to_string(),
237 context: m.content,
238 key_path: query.text.clone(), });
240 }
241 }
242
243 all_code_refs.sort_by(|a, b| {
247 a.file.cmp(&b.file).then(a.line.cmp(&b.line)).then_with(|| {
248 let a_is_direct = a.key_path == query.text;
249 let b_is_direct = b.key_path == query.text;
250 a_is_direct.cmp(&b_is_direct)
252 })
253 });
254 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
255
256 Ok(SearchResult {
257 query: query.text,
258 translation_entries,
259 code_references: all_code_refs,
260 })
261}
262
263#[must_use = "this function returns a Result that should be handled"]
276pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
277 let base_dir = query
278 .base_dir
279 .clone()
280 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
281
282 let mut finder = FunctionFinder::new(base_dir.clone());
283 if let Some(start_fn) = finder.find_function(&query.function_name) {
284 let extractor = CallExtractor::new(base_dir);
285 let mut builder =
286 CallGraphBuilder::new(query.direction, query.max_depth, &mut finder, &extractor);
287 builder.build_trace(&start_fn)
288 } else {
289 Ok(None)
290 }
291}
292
293pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
295 matches
296 .iter()
297 .filter(|m| {
298 let path = m.file.to_string_lossy();
299 path.ends_with(".yml") || path.ends_with(".yaml") || path.ends_with(".json")
300 })
301 .map(|m| m.file.clone())
302 .collect()
303}
304
305pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
312 let mut keys = Vec::new();
313
314 keys.push(full_key.to_string());
316
317 let segments: Vec<&str> = full_key.split('.').collect();
318
319 if segments.len() >= 2 {
321 for i in 1..segments.len() {
326 if segments.len() - i >= 2 {
327 keys.push(segments[i..].join("."));
328 }
329 }
330
331 if segments.len() > 1 {
334 let without_last = segments[..segments.len() - 1].join(".");
335 if !keys.contains(&without_last) {
337 keys.push(without_last);
338 }
339 }
340 }
341
342 keys
343}