//! Tool: `write` — write content to a file, creating parent directories on demand. use std::fs; 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: write content to a file, auto-creating parent directories as needed. pub struct Write; impl Tool for Write { fn name(&self) -> &'static str { "write" } fn description(&self) -> &'static str { "Write content to a file, creating parent directories as needed" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to write (relative to workspace root)" }, "content": { "type": "string", "description": "Content to write to the file" }, "reason": { "type": "string", "description": "Reason for the change (must be non-empty)" } }, "required": ["path", "content", "reason"] }) } /// Write content to a file, creating parent directories as needed. /// /// Flow: validate args (non-empty reason) → resolve path → create parent /// dirs → write file → report byte count (+ optional graduated checks). /// /// Why: requires a non-empty `reason` to discourage stray writes; parent /// directories are created silently so the tool works for new paths. fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = arg_str(args, "path")?; let content = arg_str(args, "content")?; let reason = arg_str(args, "reason")?; if reason.trim().is_empty() { anyhow::bail!("reason must be a non-empty string"); } let check_matches = check_graduated_checks(&rel, &content, &ctx.graduated_checks); let path = resolve_path(&ctx.workspaces, &rel)?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) .map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?; } fs::write(&path, &content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; // Notify the LSP server of the on-disk change so diagnostics stay in // sync. Never fails the write itself: a lock failure or LSP error is // folded into the returned message instead of propagated as an Err. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { lsp.did_change_file(&path); String::new() } else { String::new() }; if check_matches.is_empty() { Ok(format!("wrote {} bytes to {}{}", content.len(), rel, lsp_note)) } else { Ok(format!("wrote {} bytes to {}{}. Graduated checks matched: {}", content.len(), rel, lsp_note, check_matches.join(", "))) } } }