use crate::cli::mcp::protocol::JsonRpcError;
use crate::edit::EditChange;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tokio::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditCacheEntry {
pub file_path: PathBuf,
pub preview_token: String,
pub original_text: String,
pub modified_text: String,
pub changes: Vec<EditChange>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
pub struct EditCache {
entries: Mutex<HashMap<PathBuf, EditCacheEntry>>,
}
impl EditCache {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
async fn get_abs_path_and_cache_file(
&self,
project_storage: &Path,
file_path: &Path,
) -> (PathBuf, PathBuf) {
let abs_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
tokio::fs::canonicalize(file_path)
.await
.unwrap_or_else(|_| file_path.to_path_buf())
};
let hash = blake3::hash(abs_path.to_string_lossy().as_bytes()).to_hex();
let cache_file = project_storage
.join("edit_cache")
.join(format!("{}.json", hash));
(abs_path, cache_file)
}
pub async fn set(
&self,
project_storage: &Path,
entry: EditCacheEntry,
) -> Result<(), JsonRpcError> {
let (abs_path, cache_file) = self
.get_abs_path_and_cache_file(project_storage, &entry.file_path)
.await;
let cache_dir = cache_file.parent().unwrap();
if tokio::fs::metadata(cache_dir).await.is_err() {
tokio::fs::create_dir_all(cache_dir).await.map_err(|e| {
JsonRpcError::internal_error(format!(
"Failed to create edit cache directory: {}",
e
))
})?
}
let json = serde_json::to_string_pretty(&entry).map_err(|e| {
JsonRpcError::internal_error(format!("Failed to serialize edit cache: {}", e))
})?;
tokio::fs::write(&cache_file, json).await.map_err(|e| {
JsonRpcError::internal_error(format!("Failed to write edit cache to disk: {}", e))
})?;
{
let mut entries = self.entries.lock().await;
entries.insert(abs_path, entry);
}
Ok(())
}
pub async fn get(&self, project_storage: &Path, file_path: &Path) -> Option<EditCacheEntry> {
let (abs_path, cache_file) = self
.get_abs_path_and_cache_file(project_storage, file_path)
.await;
{
let entries = self.entries.lock().await;
if let Some(entry) = entries.get(&abs_path) {
return Some(entry.clone());
}
}
if let Ok(json) = tokio::fs::read_to_string(&cache_file).await {
if let Ok(entry) = serde_json::from_str::<EditCacheEntry>(&json) {
let mut entries = self.entries.lock().await;
entries.insert(abs_path, entry.clone());
return Some(entry);
}
}
None
}
pub async fn clear(&self, project_storage: &Path, file_path: &Path) {
let (abs_path, cache_file) = self
.get_abs_path_and_cache_file(project_storage, file_path)
.await;
{
let mut entries = self.entries.lock().await;
entries.remove(&abs_path);
}
let _ = tokio::fs::remove_file(cache_file).await;
}
}
pub static GLOBAL_EDIT_CACHE: Lazy<EditCache> = Lazy::new(EditCache::new);