use crate::delta::{apply_operation, Delta, DeltaId, DeltaOperation};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub struct ShadowFile {
pub file: String,
pub initial_snapshot: Delta,
pub deltas: Vec<Delta>,
pub current_head: DeltaId,
}
impl ShadowFile {
pub fn new(filename: &str, initial_content: String) -> Self {
let initial_snapshot = Delta::initial_snapshot(initial_content);
let current_head = initial_snapshot.id.clone();
ShadowFile {
file: filename.to_string(),
initial_snapshot,
deltas: vec![],
current_head,
}
}
pub fn apply_delta(
&mut self,
operation: DeltaOperation,
annotation: Option<String>,
) -> Result<DeltaId> {
let delta = Delta::new(self.current_head.clone(), operation, annotation);
let delta_id = delta.id.clone();
self.deltas.push(delta);
self.current_head = delta_id.clone();
Ok(delta_id)
}
pub fn set_head(&mut self, delta_id: &DeltaId) -> Result<()> {
if delta_id != "d0" && !self.deltas.iter().any(|d| &d.id == delta_id) {
return Err(anyhow!("Delta {} not found", delta_id));
}
self.current_head = delta_id.clone();
Ok(())
}
pub fn reconstruct_at(&self, target_id: &DeltaId) -> Result<String> {
let path = self
.build_path_to(target_id)
.context(format!("Failed to build path to delta {}", target_id))?;
let mut content = match &self.initial_snapshot.operation {
DeltaOperation::Snapshot { content } => content.clone(),
_ => return Err(anyhow!("Initial delta must be a snapshot")),
};
for delta_id in path {
let delta = self.find_delta(&delta_id)?;
content = apply_operation(&content, &delta.operation)
.map_err(|e| anyhow!("Failed to apply delta {}: {}", delta_id, e))?;
}
Ok(content)
}
pub fn current_content(&self) -> Result<String> {
self.reconstruct_at(&self.current_head)
}
fn build_path_to(&self, target_id: &DeltaId) -> Result<Vec<DeltaId>> {
if target_id == "d0" {
return Ok(vec![]);
}
let mut path = vec![];
let mut current = target_id.clone();
while current != "d0" {
let delta = self.find_delta(¤t)?;
path.push(current.clone());
current = delta.parent.clone();
}
path.reverse();
Ok(path)
}
fn find_delta(&self, id: &DeltaId) -> Result<&Delta> {
self.deltas
.iter()
.find(|d| &d.id == id)
.ok_or_else(|| anyhow!("Delta {} not found", id))
}
pub fn get_children(&self, parent_id: &DeltaId) -> Vec<&Delta> {
self.deltas
.iter()
.filter(|d| &d.parent == parent_id)
.collect()
}
pub fn find_branch_points(&self) -> Vec<DeltaId> {
let mut children_count: HashMap<DeltaId, usize> = HashMap::new();
children_count.insert("d0".to_string(), 0);
for delta in &self.deltas {
*children_count.entry(delta.parent.clone()).or_insert(0) += 1;
}
children_count
.into_iter()
.filter(|(_, count)| *count >= 2)
.map(|(id, _)| id)
.collect()
}
pub fn find_leaves(&self) -> Vec<&Delta> {
let all_parents: HashSet<DeltaId> = self.deltas.iter().map(|d| d.parent.clone()).collect();
self.deltas
.iter()
.filter(|d| !all_parents.contains(&d.id))
.collect()
}
pub fn get_history(&self) -> Vec<&Delta> {
let mut history = vec![];
let mut visited = HashSet::new();
let root_id = "d0".to_string();
self.traverse_from(&root_id, &mut history, &mut visited);
history
}
fn traverse_from<'a>(
&'a self,
current: &DeltaId,
history: &mut Vec<&'a Delta>,
visited: &mut HashSet<DeltaId>,
) {
if visited.contains(current) {
return;
}
visited.insert(current.clone());
let children = self.get_children(current);
for child in children {
history.push(child);
self.traverse_from(&child.id, history, visited);
}
}
pub fn save(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(self).context("Failed to serialize shadow file")?;
fs::write(path, json).context(format!("Failed to write shadow file to {:?}", path))?;
Ok(())
}
pub fn search_by_annotation(&self, query: &str) -> Vec<&Delta> {
self.deltas
.iter()
.filter(|d| {
d.annotation
.as_ref()
.map(|a| a.to_lowercase().contains(&query.to_lowercase()))
.unwrap_or(false)
})
.collect()
}
pub fn deltas_in_range(
&self,
start: chrono::DateTime<chrono::Utc>,
end: chrono::DateTime<chrono::Utc>,
) -> Vec<&Delta> {
self.deltas
.iter()
.filter(|d| d.timestamp >= start && d.timestamp <= end)
.collect()
}
pub fn deltas_by_author(&self, author: &str) -> Vec<&Delta> {
self.deltas
.iter()
.filter(|d| d.author.as_ref().map(|a| a == author).unwrap_or(false))
.collect()
}
pub fn load(path: &Path) -> Result<Self> {
let json = fs::read_to_string(path)
.context(format!("Failed to read shadow file from {:?}", path))?;
let shadow: ShadowFile =
serde_json::from_str(&json).context("Failed to deserialize shadow file")?;
Ok(shadow)
}
pub fn shadow_path(working_file: &Path) -> Result<PathBuf> {
let parent = working_file
.parent()
.ok_or_else(|| anyhow!("File has no parent directory"))?;
let filename = working_file
.file_name()
.ok_or_else(|| anyhow!("Invalid filename"))?;
let wlk_dir = parent.join(".wlk");
let shadow_name = format!("{}.wlk", filename.to_string_lossy());
Ok(wlk_dir.join(shadow_name))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_shadow_file() {
let shadow = ShadowFile::new("test.txt", "initial content".to_string());
assert_eq!(shadow.file, "test.txt");
assert_eq!(shadow.current_head, "d0");
assert_eq!(shadow.deltas.len(), 0);
}
#[test]
fn test_apply_delta() {
let mut shadow = ShadowFile::new("test.txt", "Hello".to_string());
let id = shadow
.apply_delta(
DeltaOperation::Insert {
index: 5,
value: " world".to_string(),
},
Some("Added world".to_string()),
)
.unwrap();
assert_eq!(shadow.deltas.len(), 1);
assert_eq!(shadow.current_head, id);
let content = shadow.current_content().unwrap();
assert_eq!(content, "Hello world");
}
#[test]
fn test_reconstruct_at() {
let mut shadow = ShadowFile::new("test.txt", "base".to_string());
let d1 = shadow
.apply_delta(
DeltaOperation::Insert {
index: 4,
value: "A".to_string(),
},
None,
)
.unwrap();
let d2 = shadow
.apply_delta(
DeltaOperation::Insert {
index: 5,
value: "B".to_string(),
},
None,
)
.unwrap();
assert_eq!(shadow.reconstruct_at(&"d0".to_string()).unwrap(), "base");
assert_eq!(shadow.reconstruct_at(&d1).unwrap(), "baseA");
assert_eq!(shadow.reconstruct_at(&d2).unwrap(), "baseAB");
}
#[test]
fn test_branch_creation() {
let mut shadow = ShadowFile::new("test.txt", "base".to_string());
let d1 = shadow
.apply_delta(
DeltaOperation::Insert {
index: 4,
value: "1".to_string(),
},
None,
)
.unwrap();
shadow.set_head(&"d0".to_string()).unwrap();
let d2 = shadow
.apply_delta(
DeltaOperation::Insert {
index: 4,
value: "2".to_string(),
},
None,
)
.unwrap();
assert_eq!(shadow.reconstruct_at(&d1).unwrap(), "base1");
assert_eq!(shadow.reconstruct_at(&d2).unwrap(), "base2");
let branch_points = shadow.find_branch_points();
assert!(branch_points.contains(&"d0".to_string()));
}
#[test]
fn test_find_leaves() {
let mut shadow = ShadowFile::new("test.txt", "base".to_string());
shadow
.apply_delta(
DeltaOperation::Insert {
index: 4,
value: "1".to_string(),
},
None,
)
.unwrap();
shadow.set_head(&"d0".to_string()).unwrap();
shadow
.apply_delta(
DeltaOperation::Insert {
index: 4,
value: "2".to_string(),
},
None,
)
.unwrap();
let leaves = shadow.find_leaves();
assert_eq!(leaves.len(), 2);
}
#[test]
fn test_multiple_operations() {
let mut shadow = ShadowFile::new("test.txt", "Hello World!".to_string());
shadow
.apply_delta(
DeltaOperation::Insert {
index: 6,
value: "beautiful ".to_string(),
},
Some("Add adjective".to_string()),
)
.unwrap();
shadow
.apply_delta(
DeltaOperation::Replace {
index: 16,
length: 5,
value: "Rust".to_string(),
},
Some("Change language".to_string()),
)
.unwrap();
let final_content = shadow.current_content().unwrap();
assert_eq!(final_content, "Hello beautiful Rust!");
}
#[test]
fn test_snapshot_operation() {
let mut shadow = ShadowFile::new("test.txt", "old content".to_string());
shadow
.apply_delta(
DeltaOperation::Insert {
index: 3,
value: "XXX".to_string(),
},
None,
)
.unwrap();
shadow
.apply_delta(
DeltaOperation::Snapshot {
content: "completely new".to_string(),
},
Some("Fresh start".to_string()),
)
.unwrap();
let content = shadow.current_content().unwrap();
assert_eq!(content, "completely new");
}
}