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: false,
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
135pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
143 let base_dir = query
145 .base_dir
146 .clone()
147 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
148
149 let project_type = config::detect_project_type(&base_dir);
151 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
152 .iter()
153 .map(|&s| s.to_string())
154 .collect();
155 exclusions.extend(query.exclude_patterns.clone());
156
157 let mut extractor = KeyExtractor::new();
159 extractor.set_exclusions(exclusions.clone());
160 extractor.set_verbose(query.verbose);
161 extractor.set_quiet(query.quiet);
162 let translation_entries = extractor.extract(&base_dir, &query.text)?;
163
164 let mut matcher = PatternMatcher::new(base_dir.clone());
167 matcher.set_exclusions(exclusions.clone());
168 let mut all_code_refs = Vec::new();
169
170 for entry in &translation_entries {
171 let key_variations = generate_partial_keys(&entry.key);
173
174 for key in &key_variations {
176 let code_refs = matcher.find_usages(key)?;
177 all_code_refs.extend(code_refs);
178 }
179 }
180
181 let text_searcher = TextSearcher::new(base_dir.clone())
184 .case_sensitive(query.case_sensitive)
185 .word_match(query.word_match)
186 .is_regex(query.is_regex)
187 .add_globs(query.include_patterns.clone())
188 .add_exclusions(exclusions.clone())
189 .respect_gitignore(true); if let Ok(direct_matches) = text_searcher.search(&query.text) {
192 for m in direct_matches {
193 let path_str = m.file.to_string_lossy();
195 if path_str.ends_with(".yml")
196 || path_str.ends_with(".yaml")
197 || path_str.ends_with(".json")
198 {
199 continue;
200 }
201
202 if exclusions.iter().any(|ex| path_str.contains(ex)) {
204 continue;
205 }
206
207 all_code_refs.push(CodeReference {
209 file: m.file,
210 line: m.line,
211 pattern: "Direct Match".to_string(),
212 context: m.content,
213 key_path: query.text.clone(), });
215 }
216 }
217
218 all_code_refs.sort_by(|a, b| {
222 a.file.cmp(&b.file).then(a.line.cmp(&b.line)).then_with(|| {
223 let a_is_direct = a.key_path == query.text;
224 let b_is_direct = b.key_path == query.text;
225 a_is_direct.cmp(&b_is_direct)
227 })
228 });
229 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
230
231 Ok(SearchResult {
232 query: query.text,
233 translation_entries,
234 code_references: all_code_refs,
235 })
236}
237
238pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
251 let base_dir = query
252 .base_dir
253 .clone()
254 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
255
256 let mut finder = FunctionFinder::new(base_dir.clone());
257 if let Some(start_fn) = finder.find_function(&query.function_name) {
258 let extractor = CallExtractor::new(base_dir);
259 let mut builder =
260 CallGraphBuilder::new(query.direction, query.max_depth, &mut finder, &extractor);
261 builder.build_trace(&start_fn)
262 } else {
263 Ok(None)
264 }
265}
266
267pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
269 matches
270 .iter()
271 .filter(|m| {
272 let path = m.file.to_string_lossy();
273 path.ends_with(".yml") || path.ends_with(".yaml") || path.ends_with(".json")
274 })
275 .map(|m| m.file.clone())
276 .collect()
277}
278
279pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
286 let mut keys = Vec::new();
287
288 keys.push(full_key.to_string());
290
291 let segments: Vec<&str> = full_key.split('.').collect();
292
293 if segments.len() >= 2 {
295 for i in 1..segments.len() {
300 if segments.len() - i >= 2 {
301 keys.push(segments[i..].join("."));
302 }
303 }
304
305 if segments.len() > 1 {
308 let without_last = segments[..segments.len() - 1].join(".");
309 if !keys.contains(&without_last) {
311 keys.push(without_last);
312 }
313 }
314 }
315
316 keys
317}