use std::fs;
use std::path::Path;
use crate::FileIndex;
use crate::SimpleIndexerConfig;
use crate::atomic_write;
const SNAPSHOT_FILE: &str = "index.md";
const SNAPSHOT_HEADER: &str = "# Workspace File Index";
const HASH_HEX_LEN: usize = 64;
pub(crate) fn load_snapshot_entries(index_dir: &Path) -> Vec<FileIndex> {
let Ok(dir) = atomic_write::canonical_existing_storage_dir(index_dir) else {
return Vec::new();
};
let Ok(text) = fs::read_to_string(dir.join(SNAPSHOT_FILE)) else {
return Vec::new();
};
match parse_snapshot(&text) {
Some(entries) => entries,
None => {
tracing::debug!(
index_dir = %index_dir.display(),
"index snapshot malformed; falling back to a full re-index"
);
Vec::new()
}
}
}
pub(crate) fn parse_snapshot(text: &str) -> Option<Vec<FileIndex>> {
let mut lines = text.lines().filter(|line| !line.trim().is_empty());
if lines.next()? != SNAPSHOT_HEADER {
return None;
}
let expected: usize = bullet_value(lines.next()?, "Entries")?.parse().ok()?;
let mut entries = Vec::new();
while let Some(path_line) = lines.next() {
let path = path_line.strip_prefix("## ")?;
if path.is_empty() {
return None;
}
if bullet_value(lines.next()?, "Path")? != path {
return None;
}
let hash = bullet_value(lines.next()?, "Hash")?;
if hash.len() != HASH_HEX_LEN || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
let modified: u64 = bullet_value(lines.next()?, "Modified")?.parse().ok()?;
let size: u64 = bullet_value(lines.next()?, "Size")?.strip_suffix(" bytes")?.parse().ok()?;
let language = bullet_value(lines.next()?, "Language")?.to_owned();
let tags = match bullet_value(lines.next()?, "Tags")? {
"" => Vec::new(),
joined => joined.split(", ").map(str::to_owned).collect::<Vec<_>>(),
};
entries.push(FileIndex::from_parts(path.to_owned(), hash.to_owned(), modified, size, language, tags));
}
(entries.len() == expected).then_some(entries)
}
fn bullet_value<'a>(line: &'a str, name: &str) -> Option<&'a str> {
let rest = line.strip_prefix("- **")?;
let (field_name, value) = rest.split_once("**: ")?;
(field_name == name).then_some(value)
}
pub(crate) fn has_indexable_file_name(path: &Path, config: &SimpleIndexerConfig) -> bool {
let name = path.file_name().and_then(|name| name.to_str());
if config.ignore_hidden && name.is_some_and(|name| name.starts_with('.')) {
return false;
}
name.is_none_or(|name| {
!(vtcode_commons::exclusions::is_sensitive_file(name) || name == ".gitignore" || name == ".git")
})
}
pub(crate) fn has_parent_reference(path: &Path) -> bool {
path.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
}