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 base_dir: Option<PathBuf>,
61 pub exclude_patterns: Vec<String>,
62}
63
64impl SearchQuery {
65 pub fn new(text: String) -> Self {
66 Self {
67 text,
68 case_sensitive: false,
69 base_dir: None,
70 exclude_patterns: Vec::new(),
71 }
72 }
73
74 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
75 self.case_sensitive = case_sensitive;
76 self
77 }
78
79 pub fn with_base_dir(mut self, base_dir: PathBuf) -> Self {
80 self.base_dir = Some(base_dir);
81 self
82 }
83
84 pub fn with_exclusions(mut self, exclusions: Vec<String>) -> Self {
85 self.exclude_patterns = exclusions;
86 self
87 }
88}
89
90#[derive(Debug)]
92pub struct SearchResult {
93 pub query: String,
94 pub translation_entries: Vec<TranslationEntry>,
95 pub code_references: Vec<CodeReference>,
96}
97
98pub fn run_search(query: SearchQuery) -> Result<SearchResult> {
106 let base_dir = query
108 .base_dir
109 .clone()
110 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
111
112 let project_type = config::detect_project_type(&base_dir);
114 let mut exclusions: Vec<String> = config::get_default_exclusions(project_type)
115 .iter()
116 .map(|&s| s.to_string())
117 .collect();
118 exclusions.extend(query.exclude_patterns.clone());
119
120 let mut extractor = KeyExtractor::new();
122 extractor.set_exclusions(exclusions.clone());
123 let translation_entries = extractor.extract(&base_dir, &query.text)?;
124
125 if translation_entries.is_empty() {
126 return Ok(SearchResult {
127 query: query.text,
128 translation_entries: vec![],
129 code_references: vec![],
130 });
131 }
132
133 let mut matcher = PatternMatcher::new(base_dir);
136 matcher.set_exclusions(exclusions);
137 let mut all_code_refs = Vec::new();
138
139 for entry in &translation_entries {
140 let key_variations = generate_partial_keys(&entry.key);
142
143 for key in &key_variations {
145 let code_refs = matcher.find_usages(key)?;
146 all_code_refs.extend(code_refs);
147 }
148 }
149
150 all_code_refs.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
152 all_code_refs.dedup_by(|a, b| a.file == b.file && a.line == b.line);
153
154 Ok(SearchResult {
155 query: query.text,
156 translation_entries,
157 code_references: all_code_refs,
158 })
159}
160
161pub fn run_trace(query: TraceQuery) -> Result<Option<CallTree>> {
174 let base_dir = query
175 .base_dir
176 .clone()
177 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
178
179 let finder = FunctionFinder::new(base_dir.clone());
180 if let Some(start_fn) = finder.find_function(&query.function_name) {
181 let extractor = CallExtractor::new(base_dir);
182 let builder = CallGraphBuilder::new(query.direction, query.max_depth, &finder, &extractor);
183 builder.build_trace(&start_fn)
184 } else {
185 Ok(None)
186 }
187}
188
189pub fn filter_translation_files(matches: &[Match]) -> Vec<PathBuf> {
191 matches
192 .iter()
193 .filter(|m| {
194 let path = m.file.to_string_lossy();
195 path.ends_with(".yml") || path.ends_with(".yaml")
196 })
197 .map(|m| m.file.clone())
198 .collect()
199}
200
201pub fn generate_partial_keys(full_key: &str) -> Vec<String> {
208 let mut keys = Vec::new();
209
210 keys.push(full_key.to_string());
212
213 let segments: Vec<&str> = full_key.split('.').collect();
214
215 if segments.len() >= 2 {
217 if segments.len() > 1 {
220 let without_first = segments[1..].join(".");
221 keys.push(without_first);
222 }
223
224 if segments.len() > 1 {
227 let without_last = segments[..segments.len() - 1].join(".");
228 keys.push(without_last);
229 }
230 }
231
232 keys
233}