1use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8
9use crate::Mutation;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum CodeActionKind {
15 QuickFix,
17 Refactor,
19 RefactorExtract,
21 RefactorInline,
23 RefactorRewrite,
25 Source,
27 SourceOrganizeImports,
29 Other(String),
31}
32
33impl CodeActionKind {
34 pub fn from_lsp(kind: &str) -> Self {
36 match kind {
37 "quickfix" => CodeActionKind::QuickFix,
38 "refactor" => CodeActionKind::Refactor,
39 "refactor.extract" => CodeActionKind::RefactorExtract,
40 "refactor.inline" => CodeActionKind::RefactorInline,
41 "refactor.rewrite" => CodeActionKind::RefactorRewrite,
42 "source" => CodeActionKind::Source,
43 "source.organizeImports" => CodeActionKind::SourceOrganizeImports,
44 other => CodeActionKind::Other(other.to_string()),
45 }
46 }
47
48 pub fn is_refactor(&self) -> bool {
50 matches!(
51 self,
52 CodeActionKind::Refactor
53 | CodeActionKind::RefactorExtract
54 | CodeActionKind::RefactorInline
55 | CodeActionKind::RefactorRewrite
56 )
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct AnalyzerCodeAction {
63 pub title: String,
65 pub kind: CodeActionKind,
67 pub assist_id: Option<String>,
69 pub edits: Vec<TextEdit>,
71 pub file: PathBuf,
73 pub is_preferred: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TextEdit {
80 pub file: PathBuf,
82 pub start: super::inlay_hints::Position,
84 pub end: super::inlay_hints::Position,
86 pub new_text: String,
88}
89
90impl AnalyzerCodeAction {
91 pub fn assist_id(&self) -> Option<&str> {
93 self.assist_id.as_deref()
94 }
95
96 pub fn has_mutation(&self) -> bool {
104 false
105 }
106}
107
108pub trait CodeActionToMutation {
110 fn to_mutation(&self) -> Option<Box<dyn Mutation>>;
112}
113
114impl CodeActionToMutation for AnalyzerCodeAction {
115 fn to_mutation(&self) -> Option<Box<dyn Mutation>> {
118 None
119 }
120}
121
122#[allow(dead_code)]
138pub fn parse_code_actions(
139 response: &serde_json::Value,
140 file: impl Into<PathBuf>,
141) -> Result<Vec<AnalyzerCodeAction>, serde_json::Error> {
142 let file = file.into();
143 let lsp_actions: Vec<LspCodeAction> = serde_json::from_value(response.clone())?;
144
145 Ok(lsp_actions
146 .into_iter()
147 .map(|a| a.into_analyzer_action(&file))
148 .collect())
149}
150
151#[allow(dead_code)]
156#[derive(Debug, Deserialize)]
157struct LspCodeAction {
158 title: String,
159 kind: Option<String>,
160 #[serde(default)]
161 is_preferred: bool,
162 edit: Option<LspWorkspaceEdit>,
163 data: Option<serde_json::Value>,
164}
165
166#[allow(dead_code)]
167#[derive(Debug, Deserialize)]
168struct LspWorkspaceEdit {
169 changes: Option<std::collections::HashMap<String, Vec<LspTextEdit>>>,
170 #[serde(rename = "documentChanges")]
171 document_changes: Option<Vec<LspDocumentChange>>,
172}
173
174#[allow(dead_code)]
175#[derive(Debug, Deserialize)]
176struct LspDocumentChange {
177 #[serde(rename = "textDocument")]
178 text_document: LspVersionedTextDocument,
179 edits: Vec<LspTextEdit>,
180}
181
182#[allow(dead_code)]
183#[derive(Debug, Deserialize)]
184struct LspVersionedTextDocument {
185 uri: String,
186}
187
188#[allow(dead_code)]
189#[derive(Debug, Deserialize)]
190struct LspTextEdit {
191 range: LspRange,
192 #[serde(rename = "newText")]
193 new_text: String,
194}
195
196#[allow(dead_code)]
197#[derive(Debug, Deserialize)]
198struct LspRange {
199 start: LspPosition,
200 end: LspPosition,
201}
202
203#[allow(dead_code)]
204#[derive(Debug, Deserialize)]
205struct LspPosition {
206 line: u32,
207 character: u32,
208}
209
210#[allow(dead_code)]
211impl LspCodeAction {
212 fn into_analyzer_action(self, default_file: &Path) -> AnalyzerCodeAction {
213 let kind = self
214 .kind
215 .as_deref()
216 .map(CodeActionKind::from_lsp)
217 .unwrap_or(CodeActionKind::Other("unknown".to_string()));
218
219 let assist_id = self
221 .data
222 .as_ref()
223 .and_then(|d| d.get("id").and_then(|v| v.as_str().map(String::from)));
224
225 let mut edits = Vec::new();
227 if let Some(edit) = self.edit {
228 if let Some(changes) = edit.changes {
229 for (uri, text_edits) in changes {
230 let file = PathBuf::from(uri.strip_prefix("file://").unwrap_or(&uri));
231 for te in text_edits {
232 edits.push(TextEdit {
233 file: file.clone(),
234 start: super::inlay_hints::Position {
235 line: te.range.start.line,
236 character: te.range.start.character,
237 },
238 end: super::inlay_hints::Position {
239 line: te.range.end.line,
240 character: te.range.end.character,
241 },
242 new_text: te.new_text,
243 });
244 }
245 }
246 }
247 if let Some(doc_changes) = edit.document_changes {
248 for dc in doc_changes {
249 let file = PathBuf::from(
250 dc.text_document
251 .uri
252 .strip_prefix("file://")
253 .unwrap_or(&dc.text_document.uri),
254 );
255 for te in dc.edits {
256 edits.push(TextEdit {
257 file: file.clone(),
258 start: super::inlay_hints::Position {
259 line: te.range.start.line,
260 character: te.range.start.character,
261 },
262 end: super::inlay_hints::Position {
263 line: te.range.end.line,
264 character: te.range.end.character,
265 },
266 new_text: te.new_text,
267 });
268 }
269 }
270 }
271 }
272
273 AnalyzerCodeAction {
274 title: self.title,
275 kind,
276 assist_id,
277 edits,
278 file: default_file.to_path_buf(),
279 is_preferred: self.is_preferred,
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_code_action_kind_from_lsp() {
290 assert_eq!(
291 CodeActionKind::from_lsp("quickfix"),
292 CodeActionKind::QuickFix
293 );
294 assert_eq!(
295 CodeActionKind::from_lsp("refactor.extract"),
296 CodeActionKind::RefactorExtract
297 );
298 assert!(matches!(
299 CodeActionKind::from_lsp("custom"),
300 CodeActionKind::Other(_)
301 ));
302 }
303
304 #[test]
305 fn test_code_action_kind_is_refactor() {
306 assert!(CodeActionKind::Refactor.is_refactor());
307 assert!(CodeActionKind::RefactorExtract.is_refactor());
308 assert!(!CodeActionKind::QuickFix.is_refactor());
309 }
310}