use std::collections::{BTreeMap, HashMap, VecDeque};
use std::path::{Path, PathBuf};
use crate::git;
use crate::hook::SanitizedEvent;
use crate::stream::{build_stream_files, compute_operation_diff};
use super::{App, ViewMode};
pub const DEFAULT_DIFF_SNAPSHOTS_CAP: usize = 500;
#[derive(Debug, Clone)]
pub struct DiffSnapshots {
map: HashMap<PathBuf, String>,
order: VecDeque<PathBuf>,
cap: usize,
}
impl Default for DiffSnapshots {
fn default() -> Self {
Self::with_cap(DEFAULT_DIFF_SNAPSHOTS_CAP)
}
}
impl DiffSnapshots {
pub fn with_cap(cap: usize) -> Self {
Self {
map: HashMap::new(),
order: VecDeque::new(),
cap: cap.max(1),
}
}
pub fn get(&self, path: &Path) -> Option<&String> {
self.map.get(path)
}
pub fn insert(&mut self, path: PathBuf, diff: String) {
if self.map.contains_key(&path) {
self.order.retain(|p| p != &path);
}
self.order.push_back(path.clone());
self.map.insert(path, diff);
while self.map.len() > self.cap {
if let Some(evicted) = self.order.pop_front() {
self.map.remove(&evicted);
} else {
break;
}
}
}
pub fn clear(&mut self) {
self.map.clear();
self.order.clear();
}
#[cfg(test)]
pub fn contains_key(&self, path: &Path) -> bool {
self.map.contains_key(path)
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.map.len()
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[cfg(test)]
pub fn keys(&self) -> impl Iterator<Item = &PathBuf> {
self.map.keys()
}
pub fn replace_from_map(&mut self, map: HashMap<PathBuf, String>) {
self.clear();
for (path, diff) in map {
self.insert(path, diff);
}
}
}
#[derive(Debug, Clone)]
pub struct StreamEvent {
pub metadata: SanitizedEvent,
pub per_file_diffs: BTreeMap<PathBuf, String>,
}
impl App {
fn normalize_event_path(&self, p: &Path) -> Option<PathBuf> {
let canon = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
if let Ok(rel) = canon.strip_prefix(&self.root) {
return Some(rel.to_path_buf());
}
if let Ok(rel) = p.strip_prefix(&self.root) {
return Some(rel.to_path_buf());
}
if p.is_relative()
&& p.components()
.all(|c| !matches!(c, std::path::Component::ParentDir))
&& self.root.join(p).starts_with(&self.root)
{
return Some(p.to_path_buf());
}
None
}
pub fn handle_event_log(&mut self, path: PathBuf) {
if !self.processed_event_paths.insert(path.clone()) {
return;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return,
};
let mut event: SanitizedEvent = match serde_json::from_str(&content) {
Ok(e) => e,
Err(_) => return,
};
if event.timestamp_ms < self.session_start_ms {
return;
}
if let (Some(expected), Some(sid)) =
(self.bound_session_id.as_ref(), event.session_id.as_ref())
&& expected != sid
{
return;
}
let normalized: Vec<PathBuf> = event
.file_paths
.iter()
.filter_map(|p| self.normalize_event_path(p))
.collect();
if normalized.is_empty() {
return;
}
event.file_paths = normalized;
let mut per_file_diffs: BTreeMap<PathBuf, String> = BTreeMap::new();
for file_path in &event.file_paths {
let Ok(current_diff) = git::diff_single_file(&self.root, &self.baseline_sha, file_path)
else {
continue;
};
let op_diff = if let Some(prev) = self.diff_snapshots.get(file_path) {
compute_operation_diff(prev, ¤t_diff)
} else {
current_diff.clone()
};
if !op_diff.is_empty() {
per_file_diffs.insert(file_path.clone(), op_diff);
}
self.diff_snapshots.insert(file_path.clone(), current_diff);
}
let stream_event = StreamEvent {
metadata: event,
per_file_diffs,
};
self.stream_events.push(stream_event);
if self.view_mode == ViewMode::Stream {
let stream_files = build_stream_files(&self.stream_events);
self.apply_computed_files(stream_files);
}
}
pub fn replay_events_dir(&mut self, dir: &Path) {
if !dir.is_dir() {
return;
}
let Ok(read_dir) = std::fs::read_dir(dir) else {
return;
};
let mut entries: Vec<(u64, std::time::SystemTime, PathBuf)> = Vec::new();
for entry in read_dir.flatten() {
let name = entry.file_name();
let s = name.to_string_lossy();
if s.starts_with('.') || !s.ends_with(".json") {
continue;
}
let path = entry.path();
let ts_ms = std::fs::read_to_string(&path)
.ok()
.and_then(|c| serde_json::from_str::<SanitizedEvent>(&c).ok())
.map(|e| e.timestamp_ms)
.unwrap_or(0);
let mtime = entry
.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH);
entries.push((ts_ms, mtime, path));
}
entries.sort_by(|a, b| {
a.0.cmp(&b.0)
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.cmp(&b.2))
});
for (_, _, path) in entries {
self.handle_event_log(path);
}
}
}