use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
mod chunker;
mod enrich;
pub(crate) mod extract;
mod orphan;
pub(crate) mod sync;
#[cfg(test)]
pub(crate) use chunker::{chunk_naive, chunk_semantic};
#[cfg(test)]
pub(crate) use enrich::{enrich_fragment, enrich_orphan, orphan_header, owner_header};
#[cfg(test)]
pub(crate) use extract::extract_file_fragments;
#[cfg(test)]
pub(crate) use orphan::{OrphanInput, orphan_fragments};
#[cfg(test)]
pub(crate) use sync::{
FragmentCandidate, FragmentExtractionIdentity, FragmentFileManifest,
compute_fragment_root_hash, fragment_layer_generation_is_consistent,
incremental_sync_fragments, load_fragment_root, load_fragment_sync_manifest,
persist_fragment_root, persist_fragment_sync_manifest,
};
const LINES_PER_CHUNK: usize = 200;
#[derive(Debug, Clone)]
pub(crate) struct Fragment<'a> {
pub(crate) content: &'a str,
pub(crate) start_line: usize,
pub(crate) end_line: usize,
pub(crate) start_byte_index: usize,
pub(crate) end_byte_index: usize,
pub(crate) file_path: &'a Path,
}
impl<'a> Fragment<'a> {
fn size(&self) -> usize {
self.content.len()
}
fn append(&mut self, other: &Fragment<'a>, content: &'a str) {
self.end_line = other.end_line;
self.end_byte_index = other.end_byte_index;
self.content = &content[self.start_byte_index..other.end_byte_index];
}
}
fn coalesce_fragments<'a>(
fragments: impl DoubleEndedIterator<Item = Fragment<'a>>,
code: &'a str,
max_bytes_per_chunk: usize,
) -> Vec<Fragment<'a>> {
fragments
.rev()
.fold(
Vec::new(),
|mut acc: Vec<Fragment<'a>>, mut fragment| match acc.last_mut() {
Some(last_item) => {
let new_fragment_size =
code[fragment.start_byte_index..last_item.end_byte_index].len();
if new_fragment_size <= max_bytes_per_chunk {
fragment.append(last_item, code);
*last_item = fragment;
} else {
acc.push(fragment);
}
acc
}
None => {
acc.push(fragment);
acc
}
},
)
.into_iter()
.rev()
.collect()
}
pub(crate) fn chunk_code<'a>(
code: &'a str,
path: &'a Path,
max_bytes: usize,
naive_fallback: bool,
) -> Vec<Fragment<'a>> {
let max_bytes = if max_bytes == 0 {
code.len().max(1)
} else {
max_bytes
};
if let Some(mut fragments) = try_chunk_code_semantically(code, path, max_bytes) {
fragments.retain(|f| !f.content.is_empty());
return fragments;
}
if naive_fallback {
chunker::chunk_naive(code, path, max_bytes, LINES_PER_CHUNK)
} else {
Vec::new()
}
}
fn try_chunk_code_semantically<'a>(
code: &'a str,
path: &'a Path,
max_bytes: usize,
) -> Option<Vec<Fragment<'a>>> {
let ext = path.extension()?.to_str()?;
let language_id = crate::parse::grammar::LanguageId::from_extension(ext)?;
let language = language_id.from_cache().ok()?;
chunker::chunk_semantic(code, path, max_bytes, &language).ok()
}
const FRAGMENT_STORE_SCHEMA_VERSION: u32 = 1;
pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("fragment artifact path has no parent"))?;
std::fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("fragment artifact has invalid file name"))?;
let next = parent.join(format!("{file_name}.next"));
let mut file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&next)?;
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
#[cfg(windows)]
if path.exists() {
std::fs::remove_file(path)?;
}
std::fs::rename(&next, path)?;
#[cfg(unix)]
std::fs::File::open(parent)?.sync_all()?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct FragmentMetadata {
pub(crate) content_hash: String,
pub(crate) owner: Option<String>,
pub(crate) file_path: String,
pub(crate) byte_range: (usize, usize),
pub(crate) line_range: (usize, usize),
pub(crate) embedding_offset: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FragmentStoreState {
#[serde(default)]
schema_version: u32,
#[serde(default)]
rows: HashMap<String, Vec<FragmentMetadata>>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct FragmentStore {
rows: HashMap<String, Vec<FragmentMetadata>>,
}
impl FragmentStore {
pub(crate) fn len(&self) -> usize {
self.rows.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub(crate) fn insert(&mut self, meta: FragmentMetadata) {
self.rows
.entry(meta.content_hash.clone())
.or_default()
.push(meta);
}
pub(crate) fn get(&self, content_hash: &str) -> Option<&[FragmentMetadata]> {
self.rows.get(content_hash).map(Vec::as_slice)
}
pub(crate) fn remove_hash(&mut self, content_hash: &str) {
self.rows.remove(content_hash);
}
pub(crate) fn content_hashes(&self) -> impl Iterator<Item = &str> {
self.rows.keys().map(String::as_str)
}
fn storage_path(project_path: &Path) -> PathBuf {
project_path.join(".leindex").join("fragment_store.bin")
}
pub(crate) fn load_from_storage(project_path: &Path) -> Result<Option<Self>> {
Self::load_from_artifact_path(&project_path.join(".leindex"))
}
pub(crate) fn load_from_artifact_path(storage_path: &Path) -> Result<Option<Self>> {
let path = storage_path.join("fragment_store.bin");
if !path.exists() {
return Ok(None);
}
let bytes = std::fs::read(&path)
.with_context(|| format!("Failed to read fragment store: {}", path.display()))?;
let state: FragmentStoreState = bincode::deserialize(&bytes)
.with_context(|| format!("Failed to deserialize fragment store: {}", path.display()))?;
Ok(Self::from_persisted_state(state))
}
pub(crate) fn persist_to_storage(&self, project_path: &Path) -> Result<()> {
let path = Self::storage_path(project_path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"Failed to create fragment store directory: {}",
parent.display()
)
})?;
}
let payload = bincode::serialize(&FragmentStoreState {
schema_version: FRAGMENT_STORE_SCHEMA_VERSION,
rows: self.rows.clone(),
})
.context("Failed to serialize fragment store")?;
atomic_write(&path, &payload)
.with_context(|| format!("Failed to persist fragment store: {}", path.display()))
}
fn from_persisted_state(state: FragmentStoreState) -> Option<Self> {
if state.schema_version != FRAGMENT_STORE_SCHEMA_VERSION {
tracing::warn!(
"Persisted fragment store schema version {} != current {}; discarding",
state.schema_version,
FRAGMENT_STORE_SCHEMA_VERSION
);
return None;
}
Some(Self { rows: state.rows })
}
}
#[cfg(test)]
#[path = "fragment_test.rs"]
mod test;