1use std::path::Path;
2
3use compact_str::CompactString;
4use xxhash_rust::xxh3::xxh3_64;
5
6use crate::{
7 CancelSignal, FileAnalysis, FileSymbols, LanguageRegistry, ParserPool, SymbolIndex,
8 analyze_source, extract_symbols,
9};
10
11const PREFILTER_CANCEL_POLL_INTERVAL: usize = 128;
12
13pub trait SourceLoader: Sync {
18 fn verify(&self) -> Result<(), String>;
20
21 fn probe(&self, path: &str) -> Option<u64>;
24
25 fn load(&self, path: &str) -> Option<String>;
27}
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct BuildOptions {
32 pub max_file_bytes: u64,
34 pub max_workers: usize,
36}
37
38impl Default for BuildOptions {
39 fn default() -> Self {
40 Self {
41 max_file_bytes: 2 * 1024 * 1024,
42 max_workers: 8,
43 }
44 }
45}
46
47#[derive(Debug)]
49#[allow(clippy::large_enum_variant)]
52pub enum IndexBuild {
53 Completed(SymbolIndex),
55 Cancelled {
57 scanned_files: usize,
59 },
60 Failed {
62 message: String,
64 },
65}
66
67#[derive(Debug)]
69pub enum AnalyzeBuild {
70 Completed {
72 files: Vec<FileAnalysis>,
74 scanned_files: usize,
76 },
77 Cancelled {
79 scanned_files: usize,
81 },
82 Failed {
84 message: String,
86 },
87}
88
89pub fn build_index(
102 registry: &LanguageRegistry,
103 loader: &dyn SourceLoader,
104 paths: &[String],
105 cancel: &dyn CancelSignal,
106 options: &BuildOptions,
107) -> IndexBuild {
108 match drive_paths(
112 registry,
113 loader,
114 paths,
115 cancel,
116 options,
117 supports_symbols,
118 analyze_symbols_only,
119 ) {
120 DriverBuild::Completed {
121 files,
122 scanned_files,
123 } => {
124 let files = files
125 .into_iter()
126 .map(|analysis| FileSymbols {
127 path: analysis.path,
128 content_hash: analysis.content_hash,
129 symbols: analysis.symbols,
130 })
131 .collect();
132 let mut index = SymbolIndex::from_files(files, registry.generation());
133 index.set_scanned_files(scanned_files);
134 IndexBuild::Completed(index)
135 }
136 DriverBuild::Cancelled { scanned_files } => IndexBuild::Cancelled { scanned_files },
137 DriverBuild::Failed { message } => IndexBuild::Failed { message },
138 DriverBuild::Panicked => IndexBuild::Failed {
139 message: "symbol indexing worker panicked; retry the build".to_owned(),
140 },
141 }
142}
143
144pub fn analyze_paths(
150 registry: &LanguageRegistry,
151 loader: &dyn SourceLoader,
152 paths: &[String],
153 cancel: &dyn CancelSignal,
154 options: &BuildOptions,
155) -> AnalyzeBuild {
156 match drive_paths(
157 registry,
158 loader,
159 paths,
160 cancel,
161 options,
162 supports_analysis,
163 analyze_source,
164 ) {
165 DriverBuild::Completed {
166 files,
167 scanned_files,
168 } => AnalyzeBuild::Completed {
169 files,
170 scanned_files,
171 },
172 DriverBuild::Cancelled { scanned_files } => AnalyzeBuild::Cancelled { scanned_files },
173 DriverBuild::Failed { message } => AnalyzeBuild::Failed { message },
174 DriverBuild::Panicked => AnalyzeBuild::Failed {
175 message: "source analysis worker panicked; retry the analysis".to_owned(),
176 },
177 }
178}
179
180type SupportPredicate = fn(&LanguageRegistry, &Path) -> bool;
181type Analyzer = fn(&str, &str, u64, &mut ParserPool<'_>) -> FileAnalysis;
182
183enum DriverBuild {
184 Completed {
185 files: Vec<FileAnalysis>,
186 scanned_files: usize,
187 },
188 Cancelled {
189 scanned_files: usize,
190 },
191 Failed {
192 message: String,
193 },
194 Panicked,
195}
196
197struct ChunkOutcome {
198 files: Vec<FileAnalysis>,
199 scanned: usize,
200 stopped_early: bool,
201}
202
203#[allow(clippy::too_many_arguments)]
204fn drive_paths(
205 registry: &LanguageRegistry,
206 loader: &dyn SourceLoader,
207 paths: &[String],
208 cancel: &dyn CancelSignal,
209 options: &BuildOptions,
210 supports: SupportPredicate,
211 analyzer: Analyzer,
212) -> DriverBuild {
213 if let Err(message) = loader.verify() {
214 return DriverBuild::Failed { message };
215 }
216
217 if cancel.is_cancelled() {
218 return DriverBuild::Cancelled { scanned_files: 0 };
219 }
220
221 let mut analyzable = Vec::new();
222 for (position, path) in paths.iter().enumerate() {
223 if position != 0 && position % PREFILTER_CANCEL_POLL_INTERVAL == 0 && cancel.is_cancelled()
224 {
225 return DriverBuild::Cancelled { scanned_files: 0 };
226 }
227 if supports(registry, Path::new(path))
228 && loader
229 .probe(path)
230 .is_some_and(|length| length <= options.max_file_bytes)
231 {
232 analyzable.push(path);
233 }
234 }
235
236 let workers = std::thread::available_parallelism()
237 .map(|parallelism| parallelism.get())
238 .unwrap_or(1)
239 .clamp(1, options.max_workers.max(1))
240 .min(analyzable.len().max(1));
241 let chunk_size = analyzable.len().div_ceil(workers).max(1);
242
243 let outcomes: Vec<std::thread::Result<ChunkOutcome>> = std::thread::scope(|scope| {
244 let handles: Vec<_> = analyzable
245 .chunks(chunk_size)
246 .map(|chunk| {
247 scope.spawn(move || analyze_chunk(registry, loader, chunk, cancel, analyzer))
248 })
249 .collect();
250 handles.into_iter().map(|handle| handle.join()).collect()
251 });
252
253 let mut files = Vec::new();
254 let mut scanned_files = 0;
255 let mut stopped_early = false;
256 for outcome in outcomes {
257 let Ok(outcome) = outcome else {
258 return DriverBuild::Panicked;
259 };
260 files.extend(outcome.files);
261 scanned_files += outcome.scanned;
262 stopped_early |= outcome.stopped_early;
263 }
264
265 if stopped_early {
266 return DriverBuild::Cancelled { scanned_files };
267 }
268
269 files.sort_by(|a, b| a.path.cmp(&b.path));
270 DriverBuild::Completed {
271 files,
272 scanned_files,
273 }
274}
275
276fn analyze_chunk(
277 registry: &LanguageRegistry,
278 loader: &dyn SourceLoader,
279 paths: &[&String],
280 cancel: &dyn CancelSignal,
281 analyzer: Analyzer,
282) -> ChunkOutcome {
283 let mut pool = ParserPool::new(registry);
284 let mut outcome = ChunkOutcome {
285 files: Vec::new(),
286 scanned: 0,
287 stopped_early: false,
288 };
289
290 for path in paths {
291 if cancel.is_cancelled() {
292 outcome.stopped_early = true;
293 return outcome;
294 }
295 outcome.scanned += 1;
296 let Some(source) = loader.load(path) else {
297 continue;
298 };
299 let content_hash = xxh3_64(source.as_bytes());
300 outcome
301 .files
302 .push(analyzer(&source, path, content_hash, &mut pool));
303 }
304
305 outcome
306}
307
308fn supports_symbols(registry: &LanguageRegistry, path: &Path) -> bool {
309 registry.supports_symbols(path)
310}
311
312fn supports_analysis(registry: &LanguageRegistry, path: &Path) -> bool {
313 registry.supports_symbols(path) || registry.supports_imports(path)
314}
315
316fn analyze_symbols_only(
317 source: &str,
318 path: &str,
319 content_hash: u64,
320 pool: &mut ParserPool<'_>,
321) -> FileAnalysis {
322 FileAnalysis {
323 path: CompactString::from(path),
324 content_hash,
325 language: None,
326 symbols: extract_symbols(source, path, pool),
327 imports: Vec::new(),
328 has_opaque_imports: false,
329 }
330}