diff --git a/src/tool/fs/write.rs b/src/tool/fs/write.rs index a3d9375..500d4b0 100644 --- a/src/tool/fs/write.rs +++ b/src/tool/fs/write.rs @@ -7,7 +7,8 @@ use super::super::Tool; use super::super::ToolCtx; use super::super::resolve_path; use super::super::check_graduated_checks; -use super::helpers::arg_str; +use super::helpers::{self, arg_str}; +use similar::TextDiff; /// Tool: write content to a file, auto-creating parent directories as needed. pub struct Write; @@ -58,6 +59,7 @@ impl Tool for Write { } let check_matches = check_graduated_checks(&rel, &content, &ctx.graduated_checks); let path = resolve_path(&ctx.workspaces, &rel)?; + let old_content = fs::read_to_string(&path).ok(); if let Some(parent) = path.parent() { fs::create_dir_all(parent) .map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?; @@ -73,10 +75,73 @@ impl Tool for Write { } else { String::new() }; - if check_matches.is_empty() { - Ok(format!("wrote {} bytes to {}{}", content.len(), rel, lsp_note)) + // Only emit a diff when the file existed before and was valid UTF-8; + // new files and binary overwrites fall back to the byte-count message. + let diff_note = if let Some(old) = old_content { + let text_diff = TextDiff::from_lines(old.as_str(), content.as_str()); + let diff_text = format!( + "{}", + text_diff.unified_diff().context_radius(3).header(&rel, &rel) + ); + format!("\n```diff\n{}\n```", helpers::truncate_diff(&diff_text)) } else { - Ok(format!("wrote {} bytes to {}{}. Graduated checks matched: {}", content.len(), rel, lsp_note, check_matches.join(", "))) + String::new() + }; + if check_matches.is_empty() { + Ok(format!("wrote {} bytes to {}{}{}", content.len(), rel, lsp_note, diff_note)) + } else { + Ok(format!("wrote {} bytes to {}{}. Graduated checks matched: {}{}", content.len(), rel, lsp_note, check_matches.join(", "), diff_note)) } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_ctx(workspace: std::path::PathBuf) -> crate::tool::ToolCtx { + crate::tool::ToolCtx::builder().workspaces(vec![workspace]).build() + } + + fn temp_workspace() -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("zesdex-write-test-{}", uuid::Uuid::new_v4())); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn write_to_a_new_file_has_no_diff_block() { + let workspace = temp_workspace(); + let ctx = test_ctx(workspace.clone()); + let args = json!({"path": "new.txt", "content": "hello\n", "reason": "test new file"}); + let result = Write.run(&ctx, &args).unwrap(); + assert!(result.contains("wrote 6 bytes")); + assert!(!result.contains("```diff")); + fs::remove_dir_all(&workspace).ok(); + } + + #[test] + fn write_overwriting_an_existing_utf8_file_includes_a_diff_block() { + let workspace = temp_workspace(); + fs::write(workspace.join("existing.txt"), "old content\n").unwrap(); + let ctx = test_ctx(workspace.clone()); + let args = json!({"path": "existing.txt", "content": "new content\n", "reason": "test overwrite"}); + let result = Write.run(&ctx, &args).unwrap(); + assert!(result.contains("```diff")); + assert!(result.contains("-old content")); + assert!(result.contains("+new content")); + fs::remove_dir_all(&workspace).ok(); + } + + #[test] + fn write_overwriting_a_non_utf8_file_has_no_diff_block() { + let workspace = temp_workspace(); + fs::write(workspace.join("binary.dat"), [0xFFu8, 0xFE, 0xFD]).unwrap(); + let ctx = test_ctx(workspace.clone()); + let args = json!({"path": "binary.dat", "content": "now text\n", "reason": "test binary overwrite"}); + let result = Write.run(&ctx, &args).unwrap(); + assert!(!result.contains("```diff")); + assert!(result.contains("wrote")); + fs::remove_dir_all(&workspace).ok(); + } +}