feat: Tampilkan unified diff pada hasil tool edit

This commit is contained in:
asepharyana
2026-07-15 06:32:57 +07:00
parent 5dd835ff24
commit c3c0ef632a
+61 -8
View File
@@ -9,7 +9,8 @@ use super::super::Tool;
use super::super::ToolCtx; use super::super::ToolCtx;
use super::super::resolve_path; use super::super::resolve_path;
use super::super::check_graduated_checks; 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. /// Tool: replace text in a file. Requires the old string to be unique unless `replace_all` is true.
pub struct Edit; pub struct Edit;
@@ -99,11 +100,12 @@ impl Tool for Edit {
}; };
fs::write(&path, &new_content) fs::write(&path, &new_content)
.map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?;
let bytes_diff = if new_content.len() > content.len() { let text_diff = TextDiff::from_lines(content.as_str(), new_content.as_str());
new_content.len() - content.len() let diff_text = format!(
} else { "{}",
content.len() - new_content.len() 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. // 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 // Never fail the edit because of this — LSP errors are surfaced as a
// trailing annotation on the success message instead. // trailing annotation on the success message instead.
@@ -114,9 +116,60 @@ impl Tool for Edit {
String::new() String::new()
}; };
if check_matches.is_empty() { 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 { } 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();
}
}