Skip to main content

krait/commands/
fix.rs

1use std::path::Path;
2
3use anyhow::Context;
4use serde_json::{json, Value};
5
6use crate::commands::workspace_edit::apply_workspace_edit;
7use crate::lsp::client::{path_to_uri, LspClient};
8use crate::lsp::diagnostics::{DiagSeverity, DiagnosticStore};
9use crate::lsp::files::FileTracker;
10
11/// Apply LSP quick-fix code actions for current diagnostics.
12///
13/// If `path` is given, only fixes diagnostics for that file.
14/// Otherwise fixes all files that have diagnostics in the store.
15///
16/// For each diagnostic, sends `textDocument/codeAction` with `only: ["quickfix"]`
17/// and applies any returned actions that carry an embedded `WorkspaceEdit`.
18///
19/// # Errors
20/// Returns an error if any LSP request or file write fails.
21pub async fn handle_fix(
22    path: Option<&Path>,
23    client: &mut LspClient,
24    file_tracker: &mut FileTracker,
25    project_root: &Path,
26    diagnostic_store: &DiagnosticStore,
27) -> anyhow::Result<Value> {
28    // Resolve the target file(s)
29    let all_diags = diagnostic_store.get_all();
30    let file_diags = if let Some(p) = path {
31        let abs = if p.is_absolute() {
32            p.to_path_buf()
33        } else {
34            project_root.join(p)
35        };
36        all_diags
37            .into_iter()
38            .filter(|(fp, _)| *fp == abs)
39            .collect::<Vec<_>>()
40    } else {
41        all_diags
42    };
43
44    if file_diags.is_empty() {
45        return Ok(json!({
46            "fixes_applied": 0,
47            "files": [],
48        }));
49    }
50
51    let mut total_fixes = 0usize;
52    let mut fixed_files: Vec<String> = Vec::new();
53
54    for (file_path, diags) in &file_diags {
55        file_tracker
56            .ensure_open(file_path, client.transport_mut())
57            .await?;
58        let uri = path_to_uri(file_path)?;
59
60        for diag in diags {
61            let line = diag.line;
62            let col = diag.col;
63            let severity_num: u64 = match diag.severity {
64                DiagSeverity::Error => 1,
65                DiagSeverity::Warning => 2,
66                DiagSeverity::Information => 3,
67                DiagSeverity::Hint => 4,
68            };
69            let code_val: Value = diag
70                .code
71                .as_deref()
72                .map_or(Value::Null, |c| Value::String(c.to_string()));
73
74            let lsp_diag = json!({
75                "range": {
76                    "start": { "line": line, "character": col },
77                    "end": { "line": line, "character": col }
78                },
79                "message": diag.message,
80                "severity": severity_num,
81                "code": code_val,
82            });
83
84            let params = json!({
85                "textDocument": { "uri": uri.as_str() },
86                "range": {
87                    "start": { "line": line, "character": col },
88                    "end": { "line": line, "character": col }
89                },
90                "context": {
91                    "diagnostics": [lsp_diag],
92                    "only": ["quickfix"]
93                }
94            });
95
96            let request_id = client
97                .transport_mut()
98                .send_request("textDocument/codeAction", params)
99                .await?;
100
101            let response = client
102                .wait_for_response_public(request_id)
103                .await
104                .context("textDocument/codeAction request failed")?;
105
106            let actions = response.as_array().cloned().unwrap_or_default();
107            for action in &actions {
108                if let Some(edit) = action.get("edit") {
109                    if !edit.is_null() {
110                        apply_workspace_edit(edit, project_root)?;
111                        total_fixes += 1;
112                        let rel = file_path
113                            .strip_prefix(project_root)
114                            .unwrap_or(file_path)
115                            .to_string_lossy()
116                            .to_string();
117                        if !fixed_files.contains(&rel) {
118                            fixed_files.push(rel);
119                        }
120                    }
121                }
122            }
123        }
124    }
125
126    Ok(json!({
127        "fixes_applied": total_fixes,
128        "files": fixed_files,
129    }))
130}
131
132#[cfg(test)]
133mod tests {
134    use serde_json::json;
135
136    #[test]
137    fn fix_response_shape() {
138        let data = json!({ "fixes_applied": 3, "files": ["src/lib.rs", "src/main.rs"] });
139        assert_eq!(data["fixes_applied"].as_u64(), Some(3));
140        assert_eq!(data["files"].as_array().unwrap().len(), 2);
141    }
142
143    #[test]
144    fn fix_no_diagnostics_shape() {
145        let data = json!({ "fixes_applied": 0, "files": [] });
146        assert_eq!(data["fixes_applied"].as_u64(), Some(0));
147    }
148}