use super::hash::compute_file_hash;
use crate::clangd::version::ClangdVersion;
use crate::project::CompilationDatabase;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ComponentIndexError {
#[error("Index directory does not exist: {path}")]
IndexDirectoryNotFound { path: String },
#[error("Unable to access index directory: {path}")]
IndexDirectoryAccess { path: String },
#[error("File not found in index: {path}")]
FileNotFound { path: String },
#[error("Path canonicalization failed for {path}: {error}")]
PathCanonicalization { path: String, error: String },
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum FileIndexState {
#[default]
Pending,
InProgress,
Indexed,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct IndexingSummary {
pub total_files: usize,
pub indexed_count: usize,
pub pending_count: usize,
pub in_progress_count: usize,
pub failed_count: usize,
pub coverage: f32,
pub is_fully_indexed: bool,
pub has_active_indexing: bool,
pub pending_files: Vec<PathBuf>,
pub in_progress_files: Vec<PathBuf>,
pub indexed_files: Vec<PathBuf>,
pub failed_files: Vec<(PathBuf, String)>,
}
pub struct ComponentIndex {
index_dir: PathBuf,
file_to_index: HashMap<PathBuf, PathBuf>,
file_states: HashMap<PathBuf, FileIndexState>,
cdb_files: HashSet<PathBuf>,
format_version: u32,
}
impl ComponentIndex {
pub fn new(
compilation_db: &CompilationDatabase,
clangd_version: &ClangdVersion,
) -> Result<Self, ComponentIndexError> {
let compilation_db_path = compilation_db.path();
let compilation_db_dir = compilation_db_path
.parent()
.unwrap_or_else(|| Path::new("."));
let index_dir = compilation_db_dir
.join(".cache")
.join("clangd")
.join("index");
let format_version = clangd_version.index_format_version();
let mut file_to_index = HashMap::new();
let mut file_states = HashMap::new();
let canonical_files = compilation_db.canonical_source_files().map_err(|e| {
ComponentIndexError::PathCanonicalization {
path: "compilation database".to_string(),
error: e.to_string(),
}
})?;
let cdb_files: HashSet<PathBuf> = canonical_files.iter().cloned().collect();
for canonical_source_file in canonical_files {
let file_path_str = canonical_source_file.to_string_lossy();
let hash = compute_file_hash(&file_path_str, format_version);
let basename = canonical_source_file
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown");
let index_filename = format!("{basename}.{hash:016X}.idx");
let index_path = index_dir.join(&index_filename);
file_to_index.insert(canonical_source_file.clone(), index_path);
file_states.insert(canonical_source_file, FileIndexState::Pending);
}
Ok(ComponentIndex {
index_dir,
file_to_index,
file_states,
cdb_files,
format_version,
})
}
#[cfg(test)]
pub fn new_for_test(
compilation_db: &CompilationDatabase,
clangd_version: &ClangdVersion,
) -> Self {
let format_version = clangd_version.index_format_version();
let mut file_to_index = HashMap::new();
let mut file_states = HashMap::new();
let mut cdb_files = HashSet::new();
for entry in compilation_db.entries() {
let source_file = &entry.file;
cdb_files.insert(source_file.to_path_buf());
let file_path_str = source_file.to_string_lossy();
let hash = compute_file_hash(&file_path_str, format_version);
let basename = source_file
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown");
let index_filename = format!("{basename}.{hash:016X}.idx");
let fake_index_dir = PathBuf::from("/fake/test/index");
let index_path = fake_index_dir.join(&index_filename);
file_to_index.insert(source_file.to_path_buf(), index_path);
file_states.insert(source_file.to_path_buf(), FileIndexState::Pending);
}
ComponentIndex {
index_dir: PathBuf::from("/fake/test/index"),
file_to_index,
file_states,
cdb_files,
format_version,
}
}
pub fn mark_file_in_progress(&mut self, source_file: &Path) -> bool {
if let Some(state) = self.file_states.get_mut(source_file) {
*state = FileIndexState::InProgress;
true
} else {
false
}
}
pub fn mark_file_indexed(&mut self, source_file: &Path) -> bool {
if let Some(state) = self.file_states.get_mut(source_file) {
*state = FileIndexState::Indexed;
true
} else {
false
}
}
pub fn mark_file_failed(&mut self, source_file: &Path, error: String) -> bool {
if let Some(state) = self.file_states.get_mut(source_file) {
*state = FileIndexState::Failed(error);
true
} else {
false
}
}
pub fn mark_file_pending(&mut self, source_file: &Path) -> bool {
if let Some(state) = self.file_states.get_mut(source_file) {
*state = FileIndexState::Pending;
true
} else {
false
}
}
pub fn get_file_state(&self, source_file: &Path) -> Option<&FileIndexState> {
self.file_states.get(source_file)
}
pub fn is_file_indexed(&self, source_file: &Path) -> bool {
matches!(
self.file_states.get(source_file),
Some(FileIndexState::Indexed)
)
}
pub fn is_file_in_progress(&self, source_file: &Path) -> bool {
matches!(
self.file_states.get(source_file),
Some(FileIndexState::InProgress)
)
}
pub fn is_file_pending(&self, source_file: &Path) -> bool {
matches!(
self.file_states.get(source_file),
Some(FileIndexState::Pending)
)
}
pub fn is_file_failed(&self, source_file: &Path) -> bool {
matches!(
self.file_states.get(source_file),
Some(FileIndexState::Failed(_))
)
}
pub fn get_next_uncovered_file(&self) -> Option<&Path> {
self.cdb_files
.iter()
.find(|file| {
matches!(
self.file_states.get(file.as_path()),
Some(FileIndexState::Pending)
)
})
.map(|p| p.as_path())
}
pub fn get_pending_files(&self) -> Vec<&Path> {
self.cdb_files
.iter()
.filter(|file| {
matches!(
self.file_states.get(file.as_path()),
Some(FileIndexState::Pending)
)
})
.map(|p| p.as_path())
.collect()
}
pub fn get_in_progress_files(&self) -> Vec<&Path> {
self.cdb_files
.iter()
.filter(|file| {
matches!(
self.file_states.get(file.as_path()),
Some(FileIndexState::InProgress)
)
})
.map(|p| p.as_path())
.collect()
}
pub fn get_indexed_files(&self) -> Vec<&Path> {
self.cdb_files
.iter()
.filter(|file| {
matches!(
self.file_states.get(file.as_path()),
Some(FileIndexState::Indexed)
)
})
.map(|p| p.as_path())
.collect()
}
pub fn get_failed_files(&self) -> Vec<(&Path, &String)> {
self.cdb_files
.iter()
.filter_map(|file| {
if let Some(FileIndexState::Failed(error)) = self.file_states.get(file.as_path()) {
Some((file.as_path(), error))
} else {
None
}
})
.collect()
}
pub fn indexed_count(&self) -> usize {
self.file_states
.values()
.filter(|state| matches!(state, FileIndexState::Indexed))
.count()
}
pub fn pending_count(&self) -> usize {
self.file_states
.values()
.filter(|state| matches!(state, FileIndexState::Pending))
.count()
}
pub fn in_progress_count(&self) -> usize {
self.file_states
.values()
.filter(|state| matches!(state, FileIndexState::InProgress))
.count()
}
pub fn failed_count(&self) -> usize {
self.file_states
.values()
.filter(|state| matches!(state, FileIndexState::Failed(_)))
.count()
}
pub fn total_files_count(&self) -> usize {
self.cdb_files.len()
}
pub fn coverage(&self) -> f32 {
let total = self.total_files_count();
if total == 0 {
1.0
} else {
self.indexed_count() as f32 / total as f32
}
}
pub fn is_fully_indexed(&self) -> bool {
self.pending_count() == 0 && self.in_progress_count() == 0
}
pub fn has_active_indexing(&self) -> bool {
self.in_progress_count() > 0
}
pub fn get_index_file(&self, source_file: &Path) -> Option<&Path> {
self.file_to_index.get(source_file).map(|p| p.as_path())
}
pub fn source_files(&self) -> Vec<&Path> {
self.cdb_files.iter().map(|p| p.as_path()).collect()
}
pub fn index_directory(&self) -> &Path {
&self.index_dir
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn index_files(&self) -> Vec<&Path> {
self.file_to_index.values().map(|p| p.as_path()).collect()
}
pub fn get_indexing_summary(&self) -> IndexingSummary {
let pending_files: Vec<_> = self
.get_pending_files()
.iter()
.map(|p| p.to_path_buf())
.collect();
let in_progress_files: Vec<_> = self
.get_in_progress_files()
.iter()
.map(|p| p.to_path_buf())
.collect();
let indexed_files: Vec<_> = self
.get_indexed_files()
.iter()
.map(|p| p.to_path_buf())
.collect();
let failed_files: Vec<_> = self
.get_failed_files()
.iter()
.map(|(path, error)| (path.to_path_buf(), (*error).clone()))
.collect();
IndexingSummary {
total_files: self.total_files_count(),
indexed_count: self.indexed_count(),
pending_count: self.pending_count(),
in_progress_count: self.in_progress_count(),
failed_count: self.failed_count(),
coverage: self.coverage(),
is_fully_indexed: self.is_fully_indexed(),
has_active_indexing: self.has_active_indexing(),
pending_files,
in_progress_files,
indexed_files,
failed_files,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clangd::version::ClangdVersion;
use crate::project::CompilationDatabase;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
fn create_test_compilation_database(dir: &Path) -> std::io::Result<CompilationDatabase> {
let compile_commands_path = dir.join("compile_commands.json");
let mut file = fs::File::create(&compile_commands_path)?;
let content = r#"[
{
"directory": "/test/project",
"command": "clang++ -c main.cpp -o main.o",
"file": "/test/project/main.cpp"
},
{
"directory": "/test/project",
"command": "clang++ -c utils.cpp -o utils.o",
"file": "/test/project/utils.cpp"
}
]"#;
file.write_all(content.as_bytes())?;
Ok(CompilationDatabase::new(compile_commands_path).unwrap())
}
fn create_test_version() -> ClangdVersion {
ClangdVersion {
major: 18,
minor: 1,
patch: 8,
variant: None,
date: None,
}
}
#[test]
fn test_component_index_creation() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let build_dir = temp_dir.path();
let compilation_db = create_test_compilation_database(build_dir)?;
let version = create_test_version();
let component_index = ComponentIndex::new(&compilation_db, &version).unwrap();
assert_eq!(component_index.total_files_count(), 2);
assert_eq!(component_index.pending_count(), 2);
assert_eq!(component_index.indexed_count(), 0);
assert_eq!(component_index.coverage(), 0.0);
assert!(!component_index.is_fully_indexed());
Ok(())
}
#[test]
fn test_file_state_management() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let build_dir = temp_dir.path();
let compilation_db = create_test_compilation_database(build_dir)?;
let version = create_test_version();
let mut component_index = ComponentIndex::new(&compilation_db, &version).unwrap();
let main_cpp = Path::new("/test/project/main.cpp");
assert!(component_index.mark_file_in_progress(main_cpp));
assert!(component_index.is_file_in_progress(main_cpp));
assert_eq!(component_index.in_progress_count(), 1);
assert!(component_index.mark_file_indexed(main_cpp));
assert!(component_index.is_file_indexed(main_cpp));
assert_eq!(component_index.indexed_count(), 1);
assert_eq!(component_index.coverage(), 0.5);
assert!(component_index.mark_file_failed(main_cpp, "Test error".to_string()));
assert!(component_index.is_file_failed(main_cpp));
assert_eq!(component_index.failed_count(), 1);
assert!(component_index.mark_file_pending(main_cpp));
assert!(component_index.is_file_pending(main_cpp));
assert_eq!(component_index.pending_count(), 2);
Ok(())
}
#[test]
fn test_next_uncovered_file() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let build_dir = temp_dir.path();
let compilation_db = create_test_compilation_database(build_dir)?;
let version = create_test_version();
let mut component_index = ComponentIndex::new(&compilation_db, &version).unwrap();
let next_file = component_index.get_next_uncovered_file();
assert!(next_file.is_some());
let main_cpp = Path::new("/test/project/main.cpp");
component_index.mark_file_indexed(main_cpp);
let next_file = component_index.get_next_uncovered_file();
assert!(next_file.is_some());
assert_ne!(next_file.unwrap(), main_cpp);
let utils_cpp = Path::new("/test/project/utils.cpp");
component_index.mark_file_indexed(utils_cpp);
assert!(component_index.get_next_uncovered_file().is_none());
assert!(component_index.is_fully_indexed());
Ok(())
}
#[test]
fn test_file_collections() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let build_dir = temp_dir.path();
let compilation_db = create_test_compilation_database(build_dir)?;
let version = create_test_version();
let mut component_index = ComponentIndex::new(&compilation_db, &version).unwrap();
let main_cpp = Path::new("/test/project/main.cpp");
let utils_cpp = Path::new("/test/project/utils.cpp");
component_index.mark_file_in_progress(main_cpp);
component_index.mark_file_failed(utils_cpp, "Test failure".to_string());
let in_progress = component_index.get_in_progress_files();
assert_eq!(in_progress.len(), 1);
assert_eq!(in_progress[0], main_cpp);
let failed = component_index.get_failed_files();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, utils_cpp);
assert_eq!(failed[0].1, "Test failure");
let pending = component_index.get_pending_files();
assert_eq!(pending.len(), 0);
Ok(())
}
#[test]
fn test_get_indexing_summary() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let build_dir = temp_dir.path();
let compilation_db = create_test_compilation_database(build_dir)?;
let version = create_test_version();
let mut component_index = ComponentIndex::new(&compilation_db, &version).unwrap();
let main_cpp = Path::new("/test/project/main.cpp");
let utils_cpp = Path::new("/test/project/utils.cpp");
component_index.mark_file_in_progress(main_cpp);
component_index.mark_file_failed(utils_cpp, "Test error".to_string());
let summary = component_index.get_indexing_summary();
assert_eq!(summary.total_files, 2);
assert_eq!(summary.indexed_count, 0);
assert_eq!(summary.pending_count, 0);
assert_eq!(summary.in_progress_count, 1);
assert_eq!(summary.failed_count, 1);
assert_eq!(summary.coverage, 0.0);
assert!(!summary.is_fully_indexed);
assert!(summary.has_active_indexing);
assert_eq!(summary.in_progress_files.len(), 1);
assert_eq!(summary.in_progress_files[0], main_cpp);
assert_eq!(summary.failed_files.len(), 1);
assert_eq!(summary.failed_files[0].0, utils_cpp);
assert_eq!(summary.failed_files[0].1, "Test error");
Ok(())
}
}