use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::Mutation;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CodeActionKind {
QuickFix,
Refactor,
RefactorExtract,
RefactorInline,
RefactorRewrite,
Source,
SourceOrganizeImports,
Other(String),
}
impl CodeActionKind {
pub fn from_lsp(kind: &str) -> Self {
match kind {
"quickfix" => CodeActionKind::QuickFix,
"refactor" => CodeActionKind::Refactor,
"refactor.extract" => CodeActionKind::RefactorExtract,
"refactor.inline" => CodeActionKind::RefactorInline,
"refactor.rewrite" => CodeActionKind::RefactorRewrite,
"source" => CodeActionKind::Source,
"source.organizeImports" => CodeActionKind::SourceOrganizeImports,
other => CodeActionKind::Other(other.to_string()),
}
}
pub fn is_refactor(&self) -> bool {
matches!(
self,
CodeActionKind::Refactor
| CodeActionKind::RefactorExtract
| CodeActionKind::RefactorInline
| CodeActionKind::RefactorRewrite
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzerCodeAction {
pub title: String,
pub kind: CodeActionKind,
pub assist_id: Option<String>,
pub edits: Vec<TextEdit>,
pub file: PathBuf,
pub is_preferred: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextEdit {
pub file: PathBuf,
pub start: super::inlay_hints::Position,
pub end: super::inlay_hints::Position,
pub new_text: String,
}
impl AnalyzerCodeAction {
pub fn assist_id(&self) -> Option<&str> {
self.assist_id.as_deref()
}
pub fn has_mutation(&self) -> bool {
false
}
}
pub trait CodeActionToMutation {
fn to_mutation(&self) -> Option<Box<dyn Mutation>>;
}
impl CodeActionToMutation for AnalyzerCodeAction {
fn to_mutation(&self) -> Option<Box<dyn Mutation>> {
None
}
}
#[allow(dead_code)]
pub fn parse_code_actions(
response: &serde_json::Value,
file: impl Into<PathBuf>,
) -> Result<Vec<AnalyzerCodeAction>, serde_json::Error> {
let file = file.into();
let lsp_actions: Vec<LspCodeAction> = serde_json::from_value(response.clone())?;
Ok(lsp_actions
.into_iter()
.map(|a| a.into_analyzer_action(&file))
.collect())
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspCodeAction {
title: String,
kind: Option<String>,
#[serde(default)]
is_preferred: bool,
edit: Option<LspWorkspaceEdit>,
data: Option<serde_json::Value>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspWorkspaceEdit {
changes: Option<std::collections::HashMap<String, Vec<LspTextEdit>>>,
#[serde(rename = "documentChanges")]
document_changes: Option<Vec<LspDocumentChange>>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspDocumentChange {
#[serde(rename = "textDocument")]
text_document: LspVersionedTextDocument,
edits: Vec<LspTextEdit>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspVersionedTextDocument {
uri: String,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspTextEdit {
range: LspRange,
#[serde(rename = "newText")]
new_text: String,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspRange {
start: LspPosition,
end: LspPosition,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct LspPosition {
line: u32,
character: u32,
}
#[allow(dead_code)]
impl LspCodeAction {
fn into_analyzer_action(self, default_file: &Path) -> AnalyzerCodeAction {
let kind = self
.kind
.as_deref()
.map(CodeActionKind::from_lsp)
.unwrap_or(CodeActionKind::Other("unknown".to_string()));
let assist_id = self
.data
.as_ref()
.and_then(|d| d.get("id").and_then(|v| v.as_str().map(String::from)));
let mut edits = Vec::new();
if let Some(edit) = self.edit {
if let Some(changes) = edit.changes {
for (uri, text_edits) in changes {
let file = PathBuf::from(uri.strip_prefix("file://").unwrap_or(&uri));
for te in text_edits {
edits.push(TextEdit {
file: file.clone(),
start: super::inlay_hints::Position {
line: te.range.start.line,
character: te.range.start.character,
},
end: super::inlay_hints::Position {
line: te.range.end.line,
character: te.range.end.character,
},
new_text: te.new_text,
});
}
}
}
if let Some(doc_changes) = edit.document_changes {
for dc in doc_changes {
let file = PathBuf::from(
dc.text_document
.uri
.strip_prefix("file://")
.unwrap_or(&dc.text_document.uri),
);
for te in dc.edits {
edits.push(TextEdit {
file: file.clone(),
start: super::inlay_hints::Position {
line: te.range.start.line,
character: te.range.start.character,
},
end: super::inlay_hints::Position {
line: te.range.end.line,
character: te.range.end.character,
},
new_text: te.new_text,
});
}
}
}
}
AnalyzerCodeAction {
title: self.title,
kind,
assist_id,
edits,
file: default_file.to_path_buf(),
is_preferred: self.is_preferred,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_code_action_kind_from_lsp() {
assert_eq!(
CodeActionKind::from_lsp("quickfix"),
CodeActionKind::QuickFix
);
assert_eq!(
CodeActionKind::from_lsp("refactor.extract"),
CodeActionKind::RefactorExtract
);
assert!(matches!(
CodeActionKind::from_lsp("custom"),
CodeActionKind::Other(_)
));
}
#[test]
fn test_code_action_kind_is_refactor() {
assert!(CodeActionKind::Refactor.is_refactor());
assert!(CodeActionKind::RefactorExtract.is_refactor());
assert!(!CodeActionKind::QuickFix.is_refactor());
}
}