use anyhow::{Context, Result};
use ignore::WalkBuilder;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use crate::cache::CacheManager;
use crate::content_store::ContentWriter;
use crate::models::{IndexConfig, IndexStats, Language};
use crate::output;
use crate::trigram::TrigramIndex;
struct FileProcessingResult {
path: PathBuf,
path_str: String,
hash: String,
content: String,
language: Language,
line_count: usize,
}
pub struct Indexer {
cache: CacheManager,
config: IndexConfig,
}
impl Indexer {
pub fn new(cache: CacheManager, config: IndexConfig) -> Self {
Self { cache, config }
}
pub fn index(&self, root: impl AsRef<Path>, show_progress: bool) -> Result<IndexStats> {
let root = root.as_ref();
log::info!("Indexing directory: {:?}", root);
let git_state = crate::git::get_git_state_optional(root)?;
let branch = git_state
.as_ref()
.map(|s| s.branch.clone())
.unwrap_or_else(|| "_default".to_string());
if let Some(ref state) = git_state {
log::info!(
"Git state: branch='{}', commit='{}', dirty={}",
state.branch,
state.commit,
state.dirty
);
} else {
log::info!("Not a git repository, using default branch");
}
let num_threads = if self.config.parallel_threads == 0 {
let available_cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
((available_cores as f64 * 0.8).ceil() as usize).max(1).min(8)
} else {
self.config.parallel_threads
};
log::info!("Using {} threads for parallel indexing (out of {} available)",
num_threads,
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4));
self.cache.init()?;
self.check_disk_space(root)?;
let existing_hashes = self.cache.load_hashes_for_branch(&branch)?;
log::debug!("Loaded {} existing file hashes for branch '{}'", existing_hashes.len(), branch);
let files = self.discover_files(root)?;
let total_files = files.len();
log::info!("Discovered {} files to index", total_files);
if !existing_hashes.is_empty() && total_files == existing_hashes.len() {
let mut any_changed = false;
for file_path in &files {
let path_str = file_path.to_string_lossy().to_string();
if let Some(existing_hash) = existing_hashes.get(&path_str) {
match std::fs::read_to_string(file_path) {
Ok(content) => {
let current_hash = self.hash_content(content.as_bytes());
if ¤t_hash != existing_hash {
any_changed = true;
log::debug!("File changed: {}", path_str);
break; }
}
Err(_) => {
any_changed = true;
break;
}
}
} else {
any_changed = true;
break;
}
}
if !any_changed {
log::info!("No files changed - skipping index rebuild");
let stats = self.cache.stats()?;
return Ok(stats);
}
} else if total_files != existing_hashes.len() {
log::info!("File count changed ({} -> {}) - full reindex required",
existing_hashes.len(), total_files);
}
let mut new_hashes = HashMap::new();
let mut files_indexed = 0;
let mut file_metadata: Vec<(String, String, String, usize)> = Vec::new();
let mut trigram_index = TrigramIndex::new();
let mut content_writer = ContentWriter::new();
if total_files > 10000 {
let temp_dir = self.cache.path().join("trigram_temp");
trigram_index.enable_batch_flush(temp_dir)
.context("Failed to enable batch-flush mode for trigram index")?;
log::info!("Enabled batch-flush mode for {} files", total_files);
}
let content_path = self.cache.path().join("content.bin");
content_writer.init(content_path.clone())
.context("Failed to initialize content writer")?;
let pb = if show_progress {
let pb = ProgressBar::new(total_files as u64);
pb.set_draw_target(ProgressDrawTarget::stderr());
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} files ({percent}%) {msg}")
.unwrap()
.progress_chars("=>-")
);
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
} else {
ProgressBar::hidden()
};
let progress_counter = Arc::new(AtomicU64::new(0));
let _start_time = Instant::now();
let counter_for_thread = Arc::clone(&progress_counter);
let pb_clone = pb.clone();
let progress_thread = if show_progress {
Some(std::thread::spawn(move || {
loop {
let count = counter_for_thread.load(Ordering::Relaxed);
pb_clone.set_position(count);
if count >= total_files as u64 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}))
} else {
None
};
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.context("Failed to create thread pool")?;
const BATCH_SIZE: usize = 5000;
let num_batches = total_files.div_ceil(BATCH_SIZE);
log::info!("Processing {} files in {} batches of up to {} files",
total_files, num_batches, BATCH_SIZE);
for (batch_idx, batch_files) in files.chunks(BATCH_SIZE).enumerate() {
log::info!("Processing batch {}/{} ({} files)",
batch_idx + 1, num_batches, batch_files.len());
let counter_clone = Arc::clone(&progress_counter);
let results: Vec<Option<FileProcessingResult>> = pool.install(|| {
batch_files
.par_iter()
.map(|file_path| {
let path_str = file_path.to_string_lossy().to_string();
let content = match std::fs::read_to_string(&file_path) {
Ok(c) => c,
Err(e) => {
log::warn!("Failed to read {}: {}", path_str, e);
counter_clone.fetch_add(1, Ordering::Relaxed);
return None;
}
};
let hash = self.hash_content(content.as_bytes());
let ext = file_path.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let language = Language::from_extension(ext);
let line_count = content.lines().count();
counter_clone.fetch_add(1, Ordering::Relaxed);
Some(FileProcessingResult {
path: file_path.clone(),
path_str,
hash,
content,
language,
line_count,
})
})
.collect()
});
for result in results.into_iter().flatten() {
let file_id = trigram_index.add_file(result.path.clone());
trigram_index.index_file(file_id, &result.content);
content_writer.add_file(result.path.clone(), &result.content);
files_indexed += 1;
file_metadata.push((
result.path_str.clone(),
result.hash.clone(),
format!("{:?}", result.language),
result.line_count
));
new_hashes.insert(result.path_str, result.hash);
}
if total_files > 10000 {
if show_progress {
pb.set_message(format!("Flushing batch {}/{}...", batch_idx + 1, num_batches));
}
trigram_index.flush_batch()
.context("Failed to flush trigram batch")?;
}
}
if let Some(thread) = progress_thread {
let _ = thread.join();
}
if show_progress {
let final_count = progress_counter.load(Ordering::Relaxed);
pb.set_position(final_count);
}
if show_progress {
pb.set_message("Finalizing trigram index...".to_string());
}
trigram_index.finalize();
if show_progress {
pb.set_message("Writing file metadata to database...".to_string());
}
if !file_metadata.is_empty() {
let files_without_hash: Vec<(String, String, usize)> = file_metadata
.iter()
.map(|(path, _hash, lang, lines)| (path.clone(), lang.clone(), *lines))
.collect();
let branch_files: Vec<(String, String)> = file_metadata
.iter()
.map(|(path, hash, _, _)| (path.clone(), hash.clone()))
.collect();
self.cache.batch_update_files_and_branch(
&files_without_hash,
&branch_files,
&branch,
git_state.as_ref().map(|s| s.commit.as_str()),
).context("Failed to batch update files and branch hashes")?;
log::info!("Wrote metadata and hashes for {} files to database", file_metadata.len());
}
self.cache.update_branch_metadata(
&branch,
git_state.as_ref().map(|s| s.commit.as_str()),
file_metadata.len(),
git_state.as_ref().map(|s| s.dirty).unwrap_or(false),
)?;
log::info!("Indexed {} files", files_indexed);
if show_progress {
pb.set_message("Writing trigram index...".to_string());
}
let trigrams_path = self.cache.path().join("trigrams.bin");
log::info!("Writing trigram index with {} trigrams to trigrams.bin",
trigram_index.trigram_count());
if show_progress {
pb.set_message("Writing trigram index...".to_string());
}
trigram_index.write(&trigrams_path)
.context("Failed to write trigram index")?;
log::info!("Wrote {} files to trigrams.bin", trigram_index.file_count());
if show_progress {
pb.set_message("Finalizing content store...".to_string());
}
content_writer.finalize_if_needed()
.context("Failed to finalize content store")?;
log::info!("Wrote {} files ({} bytes) to content.bin",
content_writer.file_count(), content_writer.content_size());
if show_progress {
pb.set_message("Updating statistics...".to_string());
}
self.cache.update_stats()?;
pb.finish_with_message("Indexing complete");
let stats = self.cache.stats()?;
log::info!("Indexing complete: {} files",
stats.total_files);
Ok(stats)
}
fn discover_files(&self, root: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let walker = WalkBuilder::new(root)
.follow_links(self.config.follow_symlinks)
.git_ignore(true) .git_global(false) .git_exclude(false) .build();
for entry in walker {
let entry = entry?;
let path = entry.path();
if !entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
continue;
}
if self.should_index(path) {
files.push(path.to_path_buf());
}
}
Ok(files)
}
fn should_index(&self, path: &Path) -> bool {
let ext = match path.extension() {
Some(ext) => ext.to_string_lossy(),
None => return false,
};
let lang = Language::from_extension(&ext);
if !lang.is_supported() {
if !matches!(lang, Language::Unknown) {
log::debug!("Skipping {} ({:?} parser not yet implemented)",
path.display(), lang);
}
return false;
}
if let Ok(metadata) = std::fs::metadata(path) {
if metadata.len() > self.config.max_file_size as u64 {
log::debug!("Skipping {} (too large: {} bytes)",
path.display(), metadata.len());
return false;
}
}
true
}
fn hash_content(&self, content: &[u8]) -> String {
let hash = blake3::hash(content);
hash.to_hex().to_string()
}
fn check_disk_space(&self, root: &Path) -> Result<()> {
let cache_path = self.cache.path();
#[cfg(unix)]
{
let test_file = cache_path.join(".space_check");
match std::fs::write(&test_file, b"test") {
Ok(_) => {
let _ = std::fs::remove_file(&test_file);
if let Ok(output) = std::process::Command::new("df")
.arg("-k")
.arg(cache_path.parent().unwrap_or(root))
.output()
{
if let Ok(df_output) = String::from_utf8(output.stdout) {
if let Some(line) = df_output.lines().nth(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
if let Ok(available_kb) = parts[3].parse::<u64>() {
let available_mb = available_kb / 1024;
if available_mb < 100 {
log::warn!("Low disk space: only {}MB available. Indexing may fail.", available_mb);
output::warn(&format!("Low disk space ({}MB available). Consider freeing up space.", available_mb));
} else {
log::debug!("Available disk space: {}MB", available_mb);
}
}
}
}
}
}
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
anyhow::bail!(
"Permission denied writing to cache directory: {}. Check file permissions.",
cache_path.display()
)
}
Err(e) => {
log::warn!("Failed to write test file (possible disk space issue): {}", e);
Err(e).context("Failed to verify disk space - indexing may fail due to insufficient space")
}
}
}
#[cfg(not(unix))]
{
let test_file = cache_path.join(".space_check");
match std::fs::write(&test_file, b"test") {
Ok(_) => {
let _ = std::fs::remove_file(&test_file);
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
anyhow::bail!(
"Permission denied writing to cache directory: {}. Check file permissions.",
cache_path.display()
)
}
Err(e) => {
log::warn!("Failed to write test file (possible disk space issue): {}", e);
Err(e).context("Failed to verify disk space - indexing may fail due to insufficient space")
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::fs;
#[test]
fn test_indexer_creation() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
assert!(indexer.cache.path().ends_with(".reflex"));
}
#[test]
fn test_hash_content() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let content1 = b"hello world";
let content2 = b"hello world";
let content3 = b"different content";
let hash1 = indexer.hash_content(content1);
let hash2 = indexer.hash_content(content2);
let hash3 = indexer.hash_content(content3);
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
assert_eq!(hash1.len(), 64); }
#[test]
fn test_should_index_rust_file() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let rust_file = temp.path().join("test.rs");
fs::write(&rust_file, "fn main() {}").unwrap();
assert!(indexer.should_index(&rust_file));
}
#[test]
fn test_should_index_unsupported_extension() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let unsupported_file = temp.path().join("test.txt");
fs::write(&unsupported_file, "plain text").unwrap();
assert!(!indexer.should_index(&unsupported_file));
}
#[test]
fn test_should_index_no_extension() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let no_ext_file = temp.path().join("Makefile");
fs::write(&no_ext_file, "all:\n\techo hello").unwrap();
assert!(!indexer.should_index(&no_ext_file));
}
#[test]
fn test_should_index_size_limit() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let mut config = IndexConfig::default();
config.max_file_size = 100;
let indexer = Indexer::new(cache, config);
let small_file = temp.path().join("small.rs");
fs::write(&small_file, "fn main() {}").unwrap();
assert!(indexer.should_index(&small_file));
let large_file = temp.path().join("large.rs");
let large_content = "a".repeat(150);
fs::write(&large_file, large_content).unwrap();
assert!(!indexer.should_index(&large_file));
}
#[test]
fn test_discover_files_empty_dir() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let files = indexer.discover_files(temp.path()).unwrap();
assert_eq!(files.len(), 0);
}
#[test]
fn test_discover_files_single_file() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let rust_file = temp.path().join("main.rs");
fs::write(&rust_file, "fn main() {}").unwrap();
let files = indexer.discover_files(temp.path()).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].ends_with("main.rs"));
}
#[test]
fn test_discover_files_multiple_languages() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(temp.path().join("main.rs"), "fn main() {}").unwrap();
fs::write(temp.path().join("script.py"), "print('hello')").unwrap();
fs::write(temp.path().join("app.js"), "console.log('hi')").unwrap();
fs::write(temp.path().join("README.md"), "# Project").unwrap();
let files = indexer.discover_files(temp.path()).unwrap();
assert_eq!(files.len(), 3); }
#[test]
fn test_discover_files_subdirectories() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let src_dir = temp.path().join("src");
fs::create_dir(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
fs::write(src_dir.join("lib.rs"), "pub mod test {}").unwrap();
let tests_dir = temp.path().join("tests");
fs::create_dir(&tests_dir).unwrap();
fs::write(tests_dir.join("test.rs"), "#[test] fn test() {}").unwrap();
let files = indexer.discover_files(temp.path()).unwrap();
assert_eq!(files.len(), 3);
}
#[test]
fn test_discover_files_respects_gitignore() {
let temp = TempDir::new().unwrap();
std::process::Command::new("git")
.arg("init")
.current_dir(temp.path())
.output()
.expect("Failed to initialize git repo");
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(temp.path().join(".gitignore"), "ignored/\n").unwrap();
fs::write(temp.path().join("included.rs"), "fn main() {}").unwrap();
fs::write(temp.path().join("also_included.py"), "print('hi')").unwrap();
let ignored_dir = temp.path().join("ignored");
fs::create_dir(&ignored_dir).unwrap();
fs::write(ignored_dir.join("excluded.rs"), "fn test() {}").unwrap();
let files = indexer.discover_files(temp.path()).unwrap();
assert!(files.iter().any(|f| f.ends_with("included.rs")), "Should find included.rs");
assert!(files.iter().any(|f| f.ends_with("also_included.py")), "Should find also_included.py");
assert!(!files.iter().any(|f| {
let path_str = f.to_string_lossy();
path_str.contains("ignored") && f.ends_with("excluded.rs")
}), "Should NOT find excluded.rs in ignored/ directory (gitignore pattern)");
assert_eq!(files.len(), 2, "Should find exactly 2 files (not including .gitignore or ignored/excluded.rs)");
}
#[test]
fn test_index_empty_directory() {
let temp = TempDir::new().unwrap();
let cache = CacheManager::new(temp.path());
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let stats = indexer.index(temp.path(), false).unwrap();
assert_eq!(stats.total_files, 0);
}
#[test]
fn test_index_single_rust_file() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(
project_root.join("main.rs"),
"fn main() { println!(\"Hello\"); }"
).unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 1);
assert!(stats.files_by_language.get("Rust").is_some());
}
#[test]
fn test_index_multiple_files() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
fs::write(project_root.join("lib.rs"), "pub fn test() {}").unwrap();
fs::write(project_root.join("script.py"), "def main(): pass").unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 3);
assert_eq!(stats.files_by_language.get("Rust"), Some(&2));
assert_eq!(stats.files_by_language.get("Python"), Some(&1));
}
#[test]
fn test_index_creates_trigram_index() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
indexer.index(&project_root, false).unwrap();
let trigrams_path = project_root.join(".reflex/trigrams.bin");
assert!(trigrams_path.exists());
}
#[test]
fn test_index_creates_content_store() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
indexer.index(&project_root, false).unwrap();
let content_path = project_root.join(".reflex/content.bin");
assert!(content_path.exists());
}
#[test]
fn test_index_incremental_no_changes() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
let stats1 = indexer.index(&project_root, false).unwrap();
assert_eq!(stats1.total_files, 1);
let stats2 = indexer.index(&project_root, false).unwrap();
assert_eq!(stats2.total_files, 1);
}
#[test]
fn test_index_incremental_with_changes() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
let main_path = project_root.join("main.rs");
fs::write(&main_path, "fn main() {}").unwrap();
indexer.index(&project_root, false).unwrap();
fs::write(&main_path, "fn main() { println!(\"changed\"); }").unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 1);
}
#[test]
fn test_index_incremental_new_file() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
let stats1 = indexer.index(&project_root, false).unwrap();
assert_eq!(stats1.total_files, 1);
fs::write(project_root.join("lib.rs"), "pub fn test() {}").unwrap();
let stats2 = indexer.index(&project_root, false).unwrap();
assert_eq!(stats2.total_files, 2);
}
#[test]
fn test_index_parallel_threads_config() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let mut config = IndexConfig::default();
config.parallel_threads = 2;
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 1);
}
#[test]
fn test_index_parallel_threads_auto() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let mut config = IndexConfig::default();
config.parallel_threads = 0;
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 1);
}
#[test]
fn test_index_respects_size_limit() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let mut config = IndexConfig::default();
config.max_file_size = 50;
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("small.rs"), "fn a() {}").unwrap();
let large_content = "fn main() {}\n".repeat(10);
fs::write(project_root.join("large.rs"), large_content).unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 1);
}
#[test]
fn test_index_mixed_languages() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
fs::write(project_root.join("test.py"), "def test(): pass").unwrap();
fs::write(project_root.join("app.js"), "function main() {}").unwrap();
fs::write(project_root.join("lib.go"), "func main() {}").unwrap();
let stats = indexer.index(&project_root, false).unwrap();
assert_eq!(stats.total_files, 4);
assert!(stats.files_by_language.contains_key("Rust"));
assert!(stats.files_by_language.contains_key("Python"));
assert!(stats.files_by_language.contains_key("JavaScript"));
assert!(stats.files_by_language.contains_key("Go"));
}
#[test]
fn test_index_updates_cache_stats() {
let temp = TempDir::new().unwrap();
let project_root = temp.path().join("project");
fs::create_dir(&project_root).unwrap();
let cache = CacheManager::new(&project_root);
let config = IndexConfig::default();
let indexer = Indexer::new(cache, config);
fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
indexer.index(&project_root, false).unwrap();
let cache = CacheManager::new(&project_root);
let stats = cache.stats().unwrap();
assert_eq!(stats.total_files, 1);
assert!(stats.index_size_bytes > 0);
}
}