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
11pub 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 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}