From c3c0ef632a605d8bf32712049035e4fc814af9d5 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 15 Jul 2026 05:21:40 +0700 Subject: [PATCH] feat: Tampilkan unified diff pada hasil tool edit --- src/tool/fs/edit.rs | 69 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/tool/fs/edit.rs b/src/tool/fs/edit.rs index 6265bfb..67001ae 100644 --- a/src/tool/fs/edit.rs +++ b/src/tool/fs/edit.rs @@ -9,7 +9,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: replace text in a file. Requires the old string to be unique unless `replace_all` is true. pub struct Edit; @@ -99,11 +100,12 @@ impl Tool for Edit { }; fs::write(&path, &new_content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; - let bytes_diff = if new_content.len() > content.len() { - new_content.len() - content.len() - } else { - content.len() - new_content.len() - }; + let text_diff = TextDiff::from_lines(content.as_str(), new_content.as_str()); + let diff_text = format!( + "{}", + text_diff.unified_diff().context_radius(3).header(&rel, &rel) + ); + let diff_block = format!("```diff\n{}\n```", helpers::truncate_diff(&diff_text)); // Notify the LSP server of the on-disk change so diagnostics stay fresh. // Never fail the edit because of this — LSP errors are surfaced as a // trailing annotation on the success message instead. @@ -114,9 +116,60 @@ impl Tool for Edit { String::new() }; if check_matches.is_empty() { - Ok(format!("edited {} ({} byte delta){}", rel, bytes_diff as isize, lsp_note)) + Ok(format!("edited {rel}\n{diff_block}{lsp_note}")) } else { - Ok(format!("edited {} ({} byte delta). Graduated checks matched: {}{}", rel, bytes_diff as isize, check_matches.join(", "), lsp_note)) + Ok(format!("edited {rel}. Graduated checks matched: {}\n{diff_block}{lsp_note}", check_matches.join(", "))) } } } + +#[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-edit-test-{}", uuid::Uuid::new_v4())); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn edit_returns_a_diff_block_for_a_single_replace() { + let workspace = temp_workspace(); + fs::write(workspace.join("a.txt"), "line1\nline2\nline3\n").unwrap(); + let ctx = test_ctx(workspace.clone()); + let args = json!({ + "path": "a.txt", + "old": "line2", + "new": "changed", + "reason": "test edit" + }); + let result = Edit.run(&ctx, &args).unwrap(); + assert!(result.contains("```diff")); + assert!(result.contains("-line2")); + assert!(result.contains("+changed")); + fs::remove_dir_all(&workspace).ok(); + } + + #[test] + fn edit_truncates_a_very_large_diff() { + let workspace = temp_workspace(); + let old_content: String = (0..300).map(|i| format!("line{i}\n")).collect(); + let new_content: String = (0..300).map(|i| format!("changed{i}\n")).collect(); + fs::write(workspace.join("big.txt"), &old_content).unwrap(); + let ctx = test_ctx(workspace.clone()); + let args = json!({ + "path": "big.txt", + "old": &old_content, + "new": &new_content, + "reason": "test large replace" + }); + let result = Edit.run(&ctx, &args).unwrap(); + assert!(result.contains("more lines truncated")); + fs::remove_dir_all(&workspace).ok(); + } +}