use crate::project::compilation_database::CompilationDatabase;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, trace};
#[derive(Debug, Clone, PartialEq)]
pub enum FileIndexStatus {
None,
InProgress,
Done,
Stale,
Invalid(String),
}
impl FileIndexStatus {
pub fn is_valid(&self) -> bool {
matches!(self, FileIndexStatus::Done)
}
pub fn needs_indexing(&self) -> bool {
matches!(
self,
FileIndexStatus::None | FileIndexStatus::Stale | FileIndexStatus::Invalid(_)
)
}
}
#[derive(Debug, Clone)]
pub struct FileMetadata {
pub path: PathBuf,
pub is_compilation_db_entry: bool,
pub status: FileIndexStatus,
pub content_hash: Option<String>,
pub last_updated: std::time::SystemTime,
}
impl FileMetadata {
pub fn from_compilation_db(path: PathBuf) -> Self {
Self {
path,
is_compilation_db_entry: true,
status: FileIndexStatus::None,
content_hash: None,
last_updated: std::time::SystemTime::now(),
}
}
pub fn from_discovered_file(path: PathBuf) -> Self {
Self {
path,
is_compilation_db_entry: false,
status: FileIndexStatus::None,
content_hash: None,
last_updated: std::time::SystemTime::now(),
}
}
pub fn update_status(&mut self, status: FileIndexStatus) {
self.status = status;
self.last_updated = std::time::SystemTime::now();
trace!("Updated status for {:?}: {:?}", self.path, self.status);
}
}
pub struct IndexState {
files: HashMap<PathBuf, FileMetadata>,
compilation_db_file_count: usize,
last_refresh: std::time::SystemTime,
}
impl IndexState {
pub fn new() -> Self {
Self {
files: HashMap::new(),
compilation_db_file_count: 0,
last_refresh: std::time::SystemTime::now(),
}
}
pub fn from_compilation_db(comp_db: &CompilationDatabase) -> Result<Self, std::io::Error> {
let mut state = Self::new();
debug!(
"Creating index state from compilation database with {} entries",
comp_db.entries.len()
);
for entry in &comp_db.entries {
let absolute_path = entry.file.canonicalize()?;
let metadata = FileMetadata::from_compilation_db(absolute_path.clone());
state.files.insert(absolute_path, metadata);
}
state.compilation_db_file_count = comp_db.entries.len();
debug!(
"Index state created with {} compilation database files",
state.compilation_db_file_count
);
Ok(state)
}
#[cfg(test)]
pub fn from_compilation_db_test(comp_db: &CompilationDatabase) -> Self {
let mut state = Self::new();
debug!(
"Creating test index state from compilation database with {} entries",
comp_db.entries.len()
);
for entry in &comp_db.entries {
let metadata = FileMetadata::from_compilation_db(entry.file.clone());
state.files.insert(entry.file.clone(), metadata);
}
state.compilation_db_file_count = comp_db.entries.len();
debug!(
"Test index state created with {} compilation database files",
state.compilation_db_file_count
);
state
}
pub fn add_file(&mut self, path: PathBuf, is_compilation_db_entry: bool) {
let absolute_path = match path.canonicalize() {
Ok(p) => p,
Err(_) => path, };
if !self.files.contains_key(&absolute_path) {
let metadata = if is_compilation_db_entry {
FileMetadata::from_compilation_db(absolute_path.clone())
} else {
FileMetadata::from_discovered_file(absolute_path.clone())
};
self.files.insert(absolute_path.clone(), metadata);
if is_compilation_db_entry {
self.compilation_db_file_count += 1;
}
trace!(
"Added file to index state: {:?} (cdb: {})",
absolute_path, is_compilation_db_entry
);
}
}
pub fn mark_indexing(&mut self, path: &Path) {
if let Some(metadata) = self.files.get_mut(path) {
metadata.update_status(FileIndexStatus::InProgress);
}
}
pub fn mark_indexed(&mut self, path: &Path) {
if let Some(metadata) = self.files.get_mut(path) {
metadata.update_status(FileIndexStatus::Done);
debug!("Marked file as indexed: {:?}", path);
}
}
pub fn mark_stale(&mut self, path: &Path) {
if let Some(metadata) = self.files.get_mut(path) {
metadata.update_status(FileIndexStatus::Stale);
}
}
pub fn mark_invalid(&mut self, path: &Path, reason: String) {
if let Some(metadata) = self.files.get_mut(path) {
metadata.update_status(FileIndexStatus::Invalid(reason));
}
}
pub fn get_status(&self, path: &Path) -> FileIndexStatus {
self.files
.get(path)
.map(|metadata| metadata.status.clone())
.unwrap_or(FileIndexStatus::None)
}
pub fn is_indexed(&self, path: &Path) -> bool {
matches!(self.get_status(path), FileIndexStatus::Done)
}
pub fn total_files(&self) -> usize {
self.files.len()
}
pub fn indexed_files(&self) -> usize {
self.files
.values()
.filter(|metadata| metadata.status.is_valid())
.count()
}
pub fn compilation_db_files(&self) -> usize {
self.compilation_db_file_count
}
pub fn coverage(&self) -> f32 {
if self.compilation_db_file_count == 0 {
return 0.0;
}
let indexed_cdb_files = self
.files
.values()
.filter(|metadata| metadata.is_compilation_db_entry && metadata.status.is_valid())
.count();
indexed_cdb_files as f32 / self.compilation_db_file_count as f32
}
pub fn get_unindexed_files(&self) -> Vec<PathBuf> {
self.files
.values()
.filter(|metadata| metadata.is_compilation_db_entry && metadata.status.needs_indexing())
.map(|metadata| metadata.path.clone())
.collect()
}
pub fn get_stale_files(&self) -> Vec<PathBuf> {
self.files
.values()
.filter(|metadata| matches!(metadata.status, FileIndexStatus::Stale))
.map(|metadata| metadata.path.clone())
.collect()
}
pub fn get_statistics(&self) -> IndexStatistics {
let mut stats = IndexStatistics {
total_files: self.files.len(),
compilation_db_files: self.compilation_db_file_count,
..Default::default()
};
for metadata in self.files.values() {
match &metadata.status {
FileIndexStatus::None => stats.not_indexed += 1,
FileIndexStatus::InProgress => stats.in_progress += 1,
FileIndexStatus::Done => stats.indexed += 1,
FileIndexStatus::Stale => stats.stale += 1,
FileIndexStatus::Invalid(_) => stats.invalid += 1,
}
if metadata.is_compilation_db_entry && metadata.status == FileIndexStatus::Done {
stats.compilation_db_indexed += 1;
}
}
stats.coverage = if stats.compilation_db_files > 0 {
stats.compilation_db_indexed as f32 / stats.compilation_db_files as f32
} else {
0.0
};
stats
}
pub fn refresh(&mut self) {
self.last_refresh = std::time::SystemTime::now();
}
pub fn last_refresh(&self) -> std::time::SystemTime {
self.last_refresh
}
}
impl Default for IndexState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Clone)]
pub struct IndexStatistics {
pub total_files: usize,
pub compilation_db_files: usize,
pub compilation_db_indexed: usize,
pub not_indexed: usize,
pub in_progress: usize,
pub indexed: usize,
pub stale: usize,
pub invalid: usize,
pub coverage: f32,
}
impl IndexStatistics {
pub fn is_complete(&self) -> bool {
self.compilation_db_files > 0 && self.compilation_db_indexed == self.compilation_db_files
}
pub fn summary(&self) -> String {
format!(
"Index: {}/{} files ({}%), {} stale, {} invalid",
self.compilation_db_indexed,
self.compilation_db_files,
(self.coverage * 100.0) as u32,
self.stale,
self.invalid
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::compilation_database::CompilationDatabase;
use json_compilation_db::Entry;
fn create_test_compilation_db() -> CompilationDatabase {
CompilationDatabase {
path: PathBuf::from("/project/compile_commands.json"),
entries: vec![
Entry {
directory: PathBuf::from("/project"),
file: PathBuf::from("file1.cpp"),
arguments: vec!["g++".to_string(), "-c".to_string(), "file1.cpp".to_string()],
output: None,
},
Entry {
directory: PathBuf::from("/project"),
file: PathBuf::from("file2.cpp"),
arguments: vec!["g++".to_string(), "-c".to_string(), "file2.cpp".to_string()],
output: None,
},
],
}
}
#[test]
fn test_file_index_status() {
assert!(FileIndexStatus::Done.is_valid());
assert!(!FileIndexStatus::None.is_valid());
assert!(!FileIndexStatus::Stale.is_valid());
assert!(FileIndexStatus::None.needs_indexing());
assert!(FileIndexStatus::Stale.needs_indexing());
assert!(!FileIndexStatus::Done.needs_indexing());
}
#[test]
fn test_file_metadata_creation() {
let path = PathBuf::from("/test/file.cpp");
let cdb_metadata = FileMetadata::from_compilation_db(path.clone());
assert!(cdb_metadata.is_compilation_db_entry);
assert!(matches!(cdb_metadata.status, FileIndexStatus::None));
let discovered_metadata = FileMetadata::from_discovered_file(path.clone());
assert!(!discovered_metadata.is_compilation_db_entry);
}
#[test]
fn test_empty_index_state() {
let state = IndexState::new();
assert_eq!(state.total_files(), 0);
assert_eq!(state.indexed_files(), 0);
assert_eq!(state.compilation_db_files(), 0);
assert_eq!(state.coverage(), 0.0);
}
#[test]
fn test_add_files() {
let mut state = IndexState::new();
state.add_file(PathBuf::from("/test/file1.cpp"), true);
state.add_file(PathBuf::from("/test/file2.cpp"), false);
assert_eq!(state.total_files(), 2);
assert_eq!(state.compilation_db_files(), 1);
assert_eq!(state.coverage(), 0.0); }
#[test]
fn test_mark_operations() {
let mut state = IndexState::new();
let file_path = PathBuf::from("/test/file.cpp");
state.add_file(file_path.clone(), true);
state.mark_indexing(&file_path);
assert!(matches!(
state.get_status(&file_path),
FileIndexStatus::InProgress
));
state.mark_indexed(&file_path);
assert!(matches!(
state.get_status(&file_path),
FileIndexStatus::Done
));
assert!(state.is_indexed(&file_path));
state.mark_stale(&file_path);
assert!(matches!(
state.get_status(&file_path),
FileIndexStatus::Stale
));
state.mark_invalid(&file_path, "test error".to_string());
assert!(matches!(
state.get_status(&file_path),
FileIndexStatus::Invalid(_)
));
}
#[test]
fn test_coverage_calculation() {
let mut state = IndexState::new();
state.add_file(PathBuf::from("/test/file1.cpp"), true);
state.add_file(PathBuf::from("/test/file2.cpp"), true);
state.add_file(PathBuf::from("/test/file3.cpp"), true);
assert_eq!(state.coverage(), 0.0);
state.mark_indexed(&PathBuf::from("/test/file1.cpp"));
assert!((state.coverage() - 1.0 / 3.0).abs() < f32::EPSILON);
state.mark_indexed(&PathBuf::from("/test/file2.cpp"));
state.mark_indexed(&PathBuf::from("/test/file3.cpp"));
assert!((state.coverage() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_get_unindexed_files() {
let mut state = IndexState::new();
let file1 = PathBuf::from("/test/file1.cpp");
let file2 = PathBuf::from("/test/file2.cpp");
let file3 = PathBuf::from("/test/file3.cpp");
state.add_file(file1.clone(), true);
state.add_file(file2.clone(), true);
state.add_file(file3.clone(), false);
let unindexed = state.get_unindexed_files();
assert_eq!(unindexed.len(), 2);
state.mark_indexed(&file1);
let unindexed = state.get_unindexed_files();
assert_eq!(unindexed.len(), 1);
assert!(unindexed.contains(&file2));
}
#[test]
fn test_statistics() {
let mut state = IndexState::new();
state.add_file(PathBuf::from("/test/file1.cpp"), true);
state.add_file(PathBuf::from("/test/file2.cpp"), true);
state.mark_indexed(&PathBuf::from("/test/file1.cpp"));
state.mark_stale(&PathBuf::from("/test/file2.cpp"));
let stats = state.get_statistics();
assert_eq!(stats.total_files, 2);
assert_eq!(stats.compilation_db_files, 2);
assert_eq!(stats.indexed, 1);
assert_eq!(stats.stale, 1);
assert_eq!(stats.compilation_db_indexed, 1);
assert!((stats.coverage - 0.5).abs() < f32::EPSILON);
assert!(!stats.is_complete());
assert!(stats.summary().contains("50%"));
}
}