Skip to main content

aft/commands/
write.rs

1//! Handler for the `write` command: full file write with auto-backup.
2
3use std::path::Path;
4
5use crate::context::AppContext;
6use crate::edit;
7use crate::protocol::{RawRequest, Response};
8
9/// Handle a `write` request.
10///
11/// Params:
12///   - `file` (string, required) — target file path
13///   - `content` (string, required) — content to write
14///   - `create_dirs` (bool, optional, default true) — create parent dirs if missing
15///
16/// Returns: `{ file, created, syntax_valid?, backup_id? }`
17pub fn handle_write(req: &RawRequest, ctx: &AppContext) -> Response {
18    let file = match req.params.get("file").and_then(|v| v.as_str()) {
19        Some(f) => f,
20        None => {
21            return Response::error(
22                &req.id,
23                "invalid_request",
24                "write: missing required param 'file'",
25            );
26        }
27    };
28
29    let content = match req.params.get("content").and_then(|v| v.as_str()) {
30        Some(c) => c,
31        None => {
32            return Response::error(
33                &req.id,
34                "invalid_request",
35                "write: missing required param 'content'",
36            );
37        }
38    };
39
40    let create_dirs = req
41        .params
42        .get("create_dirs")
43        .and_then(|v| v.as_bool())
44        .unwrap_or(true);
45
46    if let Err(resp) = ctx.validate_path(&req.id, Path::new(file)) {
47        return resp;
48    }
49    let path = Path::new(file);
50    let existed = path.exists();
51
52    // Read original content for potential dry-run diff
53    let original = if existed {
54        std::fs::read_to_string(path).unwrap_or_default()
55    } else {
56        String::new()
57    };
58
59    // Dry-run: return diff without modifying disk
60    if edit::is_dry_run(&req.params) {
61        let dr = edit::dry_run_diff(&original, content, path);
62        return Response::success(
63            &req.id,
64            serde_json::json!({
65                "ok": true, "dry_run": true, "diff": dr.diff, "syntax_valid": dr.syntax_valid,
66            }),
67        );
68    }
69
70    // Auto-backup existing file before overwriting
71    let backup_id = match edit::auto_backup(ctx, path, "write: pre-write backup") {
72        Ok(id) => id,
73        Err(e) => {
74            return Response::error(&req.id, e.code(), e.to_string());
75        }
76    };
77
78    // Create parent directories if requested
79    if create_dirs {
80        if let Some(parent) = path.parent() {
81            if !parent.exists() {
82                if let Err(e) = std::fs::create_dir_all(parent) {
83                    return Response::error(
84                        &req.id,
85                        "invalid_request",
86                        format!("write: failed to create directories: {}", e),
87                    );
88                }
89            }
90        }
91    }
92
93    // Write, format, and validate via shared pipeline
94    let mut write_result =
95        match edit::write_format_validate(path, content, &ctx.config(), &req.params) {
96            Ok(r) => r,
97            Err(e) => {
98                return Response::error(&req.id, e.code(), e.to_string());
99            }
100        };
101
102    if let Ok(final_content) = std::fs::read_to_string(path) {
103        write_result.lsp_diagnostics = ctx.lsp_post_write(path, &final_content, &req.params);
104    }
105
106    log::debug!("write: {}", file);
107
108    let mut result = serde_json::json!({
109        "file": file,
110        "created": !existed,
111        "formatted": write_result.formatted,
112    });
113
114    if let Some(valid) = write_result.syntax_valid {
115        result["syntax_valid"] = serde_json::json!(valid);
116    }
117
118    if let Some(ref reason) = write_result.format_skipped_reason {
119        result["format_skipped_reason"] = serde_json::json!(reason);
120    }
121
122    if write_result.validate_requested {
123        result["validation_errors"] = serde_json::json!(write_result.validation_errors);
124    }
125    if let Some(ref reason) = write_result.validate_skipped_reason {
126        result["validate_skipped_reason"] = serde_json::json!(reason);
127    }
128
129    if let Some(ref id) = backup_id {
130        result["backup_id"] = serde_json::json!(id);
131    }
132
133    write_result.append_lsp_diagnostics_to(&mut result);
134
135    // Include diff info if requested (for UI metadata)
136    if edit::wants_diff(&req.params) {
137        let final_content = std::fs::read_to_string(path).unwrap_or_else(|_| content.to_string());
138        result["diff"] = edit::compute_diff_info(&original, &final_content);
139    }
140
141    Response::success(&req.id, result)
142}