Files
zesdex/src/tool/fs/edit.rs
T
asepharyana e78813ecdb feat(lsp): implement auto-provisioning for language servers
- Added LSP auto-provisioning functionality to automatically install and connect language servers.
- Introduced `shutdown_lsp` method to cleanly shut down LSP servers on application exit.
- Enhanced `AppStateRest` to spawn a background thread for provisioning language servers.
- Updated `builtin_agents` to include new LSP-related agents.
- Modified settings to include options for LSP auto-provisioning and supported languages.
- Updated file editing and writing tools to notify LSP servers of changes.
- Enhanced LSP tools to support auto-detection of servers based on file extensions.
- Added utility functions for managing known file extensions and resolving server names.
- Created a new `provisioner` module to handle the provisioning logic for various language servers.
2026-07-12 14:47:01 +07:00

125 lines
4.8 KiB
Rust

//! Tool: `edit` — replace a substring in a file with a new string.
use std::fs;
use std::path::PathBuf;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
use super::super::resolve_path;
use super::super::check_graduated_checks;
use super::helpers::arg_str;
/// Tool: replace text in a file. Requires the old string to be unique unless `replace_all` is true.
pub struct Edit;
impl Tool for Edit {
fn name(&self) -> &'static str {
"edit"
}
fn description(&self) -> &'static str {
"Replace a string in a file with a new string. The old string must be unique unless replace_all is true."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit (relative to workspace root)"
},
"old": {
"type": "string",
"description": "The exact text to replace"
},
"new": {
"type": "string",
"description": "The replacement text"
},
"replace_all": {
"type": "boolean",
"description": "Replace all occurrences instead of requiring uniqueness"
},
"reason": {
"type": "string",
"description": "Reason for the change (must be non-empty)"
}
},
"required": ["path", "old", "new", "reason"]
})
}
/// Perform the in-file string replacement.
///
/// Flow: validate args → resolve path → read file → count occurrences →
/// replace one or all → write back → report byte delta (+ optional graduated checks).
///
/// Why: requires a non-empty `reason` and a non-empty `old` string to prevent
/// accidental identity edits. Enforces uniqueness unless `replace_all` is set.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let old = arg_str(args, "old")?;
let new_str = arg_str(args, "new")?;
let reason = arg_str(args, "reason")?;
if reason.trim().is_empty() {
anyhow::bail!("reason must be a non-empty string");
}
if old.is_empty() {
anyhow::bail!("'old' must be a non-empty string; use 'write' to replace entire file contents");
}
let check_matches = check_graduated_checks(&rel, &new_str, &ctx.graduated_checks);
let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false);
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
if !path.exists() {
anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display());
}
if path.is_dir() {
anyhow::bail!("'{}' is a directory, not a file", rel);
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
if !content.contains(&old) {
anyhow::bail!("old string not found in '{}'", rel);
}
if !replace_all {
let count = content.matches(&old).count();
if count > 1 {
anyhow::bail!(
"old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.",
count, rel
);
}
}
let new_content = if replace_all {
content.replace(&old, &new_str)
} else {
content.replacen(&old, &new_str, 1)
};
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()
};
// 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.
let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() {
match lsp.did_change_file(&path) {
Ok(()) => String::new(),
Err(e) => format!(" (LSP: {})", e),
}
} else {
String::new()
};
if check_matches.is_empty() {
Ok(format!("edited {} ({} byte delta){}", rel, bytes_diff as isize, lsp_note))
} else {
Ok(format!("edited {} ({} byte delta). Graduated checks matched: {}{}", rel, bytes_diff as isize, check_matches.join(", "), lsp_note))
}
}
}