use std::path::Path;
use compact_str::CompactString;
use xxhash_rust::xxh3::xxh3_64;
use crate::{
CancelSignal, FileAnalysis, FileSymbols, LanguageRegistry, ParserPool, SymbolIndex,
analyze_source, extract_symbols,
};
const PREFILTER_CANCEL_POLL_INTERVAL: usize = 128;
pub trait SourceLoader: Sync {
fn verify(&self) -> Result<(), String>;
fn probe(&self, path: &str) -> Option<u64>;
fn load(&self, path: &str) -> Option<String>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BuildOptions {
pub max_file_bytes: u64,
pub max_workers: usize,
}
impl Default for BuildOptions {
fn default() -> Self {
Self {
max_file_bytes: 2 * 1024 * 1024,
max_workers: 8,
}
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum IndexBuild {
Completed(SymbolIndex),
Cancelled {
scanned_files: usize,
},
Failed {
message: String,
},
}
#[derive(Debug)]
pub enum AnalyzeBuild {
Completed {
files: Vec<FileAnalysis>,
scanned_files: usize,
},
Cancelled {
scanned_files: usize,
},
Failed {
message: String,
},
}
pub fn build_index(
registry: &LanguageRegistry,
loader: &dyn SourceLoader,
paths: &[String],
cancel: &dyn CancelSignal,
options: &BuildOptions,
) -> IndexBuild {
match drive_paths(
registry,
loader,
paths,
cancel,
options,
supports_symbols,
analyze_symbols_only,
) {
DriverBuild::Completed {
files,
scanned_files,
} => {
let files = files
.into_iter()
.map(|analysis| FileSymbols {
path: analysis.path,
content_hash: analysis.content_hash,
symbols: analysis.symbols,
})
.collect();
let mut index = SymbolIndex::from_files(files, registry.generation());
index.set_scanned_files(scanned_files);
IndexBuild::Completed(index)
}
DriverBuild::Cancelled { scanned_files } => IndexBuild::Cancelled { scanned_files },
DriverBuild::Failed { message } => IndexBuild::Failed { message },
DriverBuild::Panicked => IndexBuild::Failed {
message: "symbol indexing worker panicked; retry the build".to_owned(),
},
}
}
pub fn analyze_paths(
registry: &LanguageRegistry,
loader: &dyn SourceLoader,
paths: &[String],
cancel: &dyn CancelSignal,
options: &BuildOptions,
) -> AnalyzeBuild {
match drive_paths(
registry,
loader,
paths,
cancel,
options,
supports_analysis,
analyze_source,
) {
DriverBuild::Completed {
files,
scanned_files,
} => AnalyzeBuild::Completed {
files,
scanned_files,
},
DriverBuild::Cancelled { scanned_files } => AnalyzeBuild::Cancelled { scanned_files },
DriverBuild::Failed { message } => AnalyzeBuild::Failed { message },
DriverBuild::Panicked => AnalyzeBuild::Failed {
message: "source analysis worker panicked; retry the analysis".to_owned(),
},
}
}
type SupportPredicate = fn(&LanguageRegistry, &Path) -> bool;
type Analyzer = fn(&str, &str, u64, &mut ParserPool<'_>) -> FileAnalysis;
enum DriverBuild {
Completed {
files: Vec<FileAnalysis>,
scanned_files: usize,
},
Cancelled {
scanned_files: usize,
},
Failed {
message: String,
},
Panicked,
}
struct ChunkOutcome {
files: Vec<FileAnalysis>,
scanned: usize,
stopped_early: bool,
}
#[allow(clippy::too_many_arguments)]
fn drive_paths(
registry: &LanguageRegistry,
loader: &dyn SourceLoader,
paths: &[String],
cancel: &dyn CancelSignal,
options: &BuildOptions,
supports: SupportPredicate,
analyzer: Analyzer,
) -> DriverBuild {
if let Err(message) = loader.verify() {
return DriverBuild::Failed { message };
}
if cancel.is_cancelled() {
return DriverBuild::Cancelled { scanned_files: 0 };
}
let mut analyzable = Vec::new();
for (position, path) in paths.iter().enumerate() {
if position != 0 && position % PREFILTER_CANCEL_POLL_INTERVAL == 0 && cancel.is_cancelled()
{
return DriverBuild::Cancelled { scanned_files: 0 };
}
if supports(registry, Path::new(path))
&& loader
.probe(path)
.is_some_and(|length| length <= options.max_file_bytes)
{
analyzable.push(path);
}
}
let workers = std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1)
.clamp(1, options.max_workers.max(1))
.min(analyzable.len().max(1));
let chunk_size = analyzable.len().div_ceil(workers).max(1);
let outcomes: Vec<std::thread::Result<ChunkOutcome>> = std::thread::scope(|scope| {
let handles: Vec<_> = analyzable
.chunks(chunk_size)
.map(|chunk| {
scope.spawn(move || analyze_chunk(registry, loader, chunk, cancel, analyzer))
})
.collect();
handles.into_iter().map(|handle| handle.join()).collect()
});
let mut files = Vec::new();
let mut scanned_files = 0;
let mut stopped_early = false;
for outcome in outcomes {
let Ok(outcome) = outcome else {
return DriverBuild::Panicked;
};
files.extend(outcome.files);
scanned_files += outcome.scanned;
stopped_early |= outcome.stopped_early;
}
if stopped_early {
return DriverBuild::Cancelled { scanned_files };
}
files.sort_by(|a, b| a.path.cmp(&b.path));
DriverBuild::Completed {
files,
scanned_files,
}
}
fn analyze_chunk(
registry: &LanguageRegistry,
loader: &dyn SourceLoader,
paths: &[&String],
cancel: &dyn CancelSignal,
analyzer: Analyzer,
) -> ChunkOutcome {
let mut pool = ParserPool::new(registry);
let mut outcome = ChunkOutcome {
files: Vec::new(),
scanned: 0,
stopped_early: false,
};
for path in paths {
if cancel.is_cancelled() {
outcome.stopped_early = true;
return outcome;
}
outcome.scanned += 1;
let Some(source) = loader.load(path) else {
continue;
};
let content_hash = xxh3_64(source.as_bytes());
outcome
.files
.push(analyzer(&source, path, content_hash, &mut pool));
}
outcome
}
fn supports_symbols(registry: &LanguageRegistry, path: &Path) -> bool {
registry.supports_symbols(path)
}
fn supports_analysis(registry: &LanguageRegistry, path: &Path) -> bool {
registry.supports_symbols(path) || registry.supports_imports(path)
}
fn analyze_symbols_only(
source: &str,
path: &str,
content_hash: u64,
pool: &mut ParserPool<'_>,
) -> FileAnalysis {
FileAnalysis {
path: CompactString::from(path),
content_hash,
language: None,
symbols: extract_symbols(source, path, pool),
imports: Vec::new(),
has_opaque_imports: false,
}
}