use std::collections::{BTreeMap, BTreeSet, VecDeque};
use crate::ast::Language;
use super::file_table::FileId;
use super::symbol_graph::SymbolGraph;
pub const OVERLAY_CAPACITY: usize = 8;
#[derive(Debug, Clone)]
pub enum FileDelta {
Modified {
path: String,
language: Language,
source: String,
imports: Vec<String>,
},
Removed,
}
#[derive(Debug, Clone)]
pub struct BranchOverlay {
pub branch: String,
deltas: BTreeMap<FileId, FileDelta>,
materialized: SymbolGraph,
materialized_files: BTreeSet<FileId>,
}
impl BranchOverlay {
pub fn new(branch: impl Into<String>) -> Self {
Self {
branch: branch.into(),
deltas: BTreeMap::new(),
materialized: SymbolGraph::new(),
materialized_files: BTreeSet::new(),
}
}
pub fn stage(&mut self, file_id: FileId, delta: FileDelta) {
self.deltas.insert(file_id, delta);
}
pub fn changed_count(&self) -> usize {
self.deltas.len()
}
pub fn changed_files(&self) -> impl Iterator<Item = FileId> + '_ {
self.deltas.keys().copied()
}
pub fn materialize(&mut self, base: &SymbolGraph) -> usize {
self.materialized = base.clone();
self.materialized_files.clear();
for (file_id, delta) in &self.deltas {
self.materialized_files.insert(*file_id);
match delta {
FileDelta::Removed => {
self.materialized.remove_file(*file_id);
}
FileDelta::Modified {
path,
language,
source,
imports,
} => {
self.materialized
.rebuild_file(*file_id, path, *language, source, imports);
}
}
}
self.materialized.node_count()
}
pub fn graph(&self) -> &SymbolGraph {
&self.materialized
}
}
#[derive(Debug, Default, Clone)]
pub struct OverlayState {
overlays: BTreeMap<String, BranchOverlay>,
insertion_order: VecDeque<String>,
active: Option<String>,
}
impl OverlayState {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, overlay: BranchOverlay) {
let branch = overlay.branch.clone();
if self.overlays.contains_key(&branch) {
self.insertion_order.retain(|b| b != &branch);
} else if self.overlays.len() >= OVERLAY_CAPACITY {
if let Some(victim) = self.insertion_order.pop_front() {
self.overlays.remove(&victim);
if self.active.as_deref() == Some(victim.as_str()) {
self.active = None;
}
}
}
self.insertion_order.push_back(branch.clone());
self.overlays.insert(branch, overlay);
}
pub fn get(&self, branch: &str) -> Option<&BranchOverlay> {
self.overlays.get(branch)
}
pub fn get_mut(&mut self, branch: &str) -> Option<&mut BranchOverlay> {
self.overlays.get_mut(branch)
}
pub fn active(&self) -> Option<&str> {
self.active.as_deref()
}
pub fn activate(&mut self, branch: Option<String>) {
self.active = branch;
}
pub fn graph<'a>(&'a self, base: &'a SymbolGraph) -> &'a SymbolGraph {
let Some(name) = self.active.as_deref() else {
return base;
};
match self.overlays.get(name) {
Some(overlay) => overlay.graph(),
None => base,
}
}
pub fn reuse_fraction(&self, base: &SymbolGraph) -> f64 {
let total = base.file_ids().len();
if total == 0 {
return 1.0;
}
let Some(name) = self.active.as_deref() else {
return 1.0;
};
let Some(overlay) = self.overlays.get(name) else {
return 1.0;
};
let changed = overlay
.materialized_files
.iter()
.filter(|fid| base.file_ids().contains(fid))
.count();
((total - changed) as f64) / (total as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Language;
fn base_graph() -> SymbolGraph {
let mut g = SymbolGraph::new();
g.rebuild_file(1, "src/a.rs", Language::Rust, "fn a() {}\n", &[]);
g.rebuild_file(2, "src/b.rs", Language::Rust, "fn b() {}\n", &[]);
g.rebuild_file(3, "src/c.rs", Language::Rust, "fn c() {}\n", &[]);
g
}
#[test]
fn unmodified_overlay_returns_base_view() {
let base = base_graph();
let mut overlay = BranchOverlay::new("topic/x");
overlay.materialize(&base);
let mut state = OverlayState::new();
state.set(overlay);
state.activate(Some("topic/x".into()));
assert_eq!(state.graph(&base).node_count(), base.node_count());
assert!(state.reuse_fraction(&base) >= 0.99);
}
#[test]
fn modified_overlay_replaces_a_single_file() {
let base = base_graph();
let mut overlay = BranchOverlay::new("topic/y");
overlay.stage(
2,
FileDelta::Modified {
path: "src/b.rs".into(),
language: Language::Rust,
source: "fn b2() {}\nfn b3() {}\n".into(),
imports: Vec::new(),
},
);
overlay.materialize(&base);
let mut state = OverlayState::new();
state.set(overlay);
state.activate(Some("topic/y".into()));
let view = state.graph(&base);
let b_funcs: Vec<_> = view
.iter_nodes()
.filter(|n| n.kind == super::super::symbol_graph::NodeKind::Function && n.name == "b")
.collect();
assert!(b_funcs.is_empty(), "fn b should be gone after overlay");
assert!(!view.nodes_named("b2").is_empty());
let reuse = state.reuse_fraction(&base);
assert!(
(0.66..1.0).contains(&reuse),
"expected ~2/3 reuse, got {reuse}"
);
}
#[test]
fn registry_evicts_oldest_overlay_past_capacity() {
let base = base_graph();
let mut state = OverlayState::new();
for i in 0..OVERLAY_CAPACITY {
let mut overlay = BranchOverlay::new(format!("topic/{i}"));
overlay.materialize(&base);
state.set(overlay);
}
assert!(state.get("topic/0").is_some());
assert_eq!(state.overlays.len(), OVERLAY_CAPACITY);
let mut overflow = BranchOverlay::new("topic/new");
overflow.materialize(&base);
state.set(overflow);
assert_eq!(state.overlays.len(), OVERLAY_CAPACITY);
assert!(
state.get("topic/0").is_none(),
"expected oldest overlay to be evicted"
);
assert!(state.get("topic/new").is_some());
let mut refreshed = BranchOverlay::new("topic/1");
refreshed.materialize(&base);
state.set(refreshed);
assert_eq!(state.overlays.len(), OVERLAY_CAPACITY);
assert!(state.get("topic/1").is_some());
}
#[test]
fn evicting_active_overlay_clears_active_slot() {
let base = base_graph();
let mut state = OverlayState::new();
for i in 0..OVERLAY_CAPACITY {
let mut overlay = BranchOverlay::new(format!("topic/{i}"));
overlay.materialize(&base);
state.set(overlay);
}
state.activate(Some("topic/0".into()));
assert_eq!(state.active(), Some("topic/0"));
let mut overflow = BranchOverlay::new("topic/new");
overflow.materialize(&base);
state.set(overflow);
assert_eq!(
state.active(),
None,
"active slot should clear when its overlay is evicted"
);
}
#[test]
fn removed_overlay_deletes_file_slice() {
let base = base_graph();
let mut overlay = BranchOverlay::new("topic/del");
overlay.stage(3, FileDelta::Removed);
overlay.materialize(&base);
let mut state = OverlayState::new();
state.set(overlay);
state.activate(Some("topic/del".into()));
assert!(state.graph(&base).nodes_named("c").is_empty());
}
}