use std::collections::HashMap;
use lsp_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
Position, Range, TextEdit, Uri, WorkspaceEdit,
};
use crate::ir::ast::{Expression, Variability};
use crate::lsp::utils::parse_document;
pub fn handle_code_action(
documents: &HashMap<Uri, String>,
params: CodeActionParams,
) -> Option<CodeActionResponse> {
let uri = ¶ms.text_document.uri;
let text = documents.get(uri)?;
let path = uri.path().as_str();
let range = params.range;
let mut actions = Vec::new();
if let Some(ast) = parse_document(text, path) {
for class in ast.class_list.values() {
for (comp_name, comp) in &class.components {
if matches!(comp.variability, Variability::Parameter(_)) {
let has_default = !matches!(comp.start, Expression::Empty);
if !has_default {
let comp_line = comp
.type_name
.name
.first()
.map(|t| t.location.start_line.saturating_sub(1))
.unwrap_or(0);
if comp_line >= range.start.line && comp_line <= range.end.line {
if let Some(action) =
create_add_default_action(uri, text, comp_name, comp_line)
{
actions.push(action);
}
}
}
}
}
for (comp_name, comp) in &class.components {
let comp_line = comp
.type_name
.name
.first()
.map(|t| t.location.start_line.saturating_sub(1))
.unwrap_or(0);
if comp_line >= range.start.line && comp_line <= range.end.line {
if !matches!(comp.variability, Variability::Parameter(_))
&& let Some(action) =
create_remove_variable_action(uri, text, comp_name, comp_line)
{
actions.push(action);
}
}
}
}
}
for diag in ¶ms.context.diagnostics {
if let Some(action) = create_quick_fix_for_diagnostic(uri, text, diag, &range) {
actions.push(action);
}
}
if actions.is_empty() {
None
} else {
let response: CodeActionResponse = actions
.into_iter()
.map(CodeActionOrCommand::CodeAction)
.collect();
Some(response)
}
}
fn create_add_default_action(
uri: &Uri,
text: &str,
param_name: &str,
line: u32,
) -> Option<CodeAction> {
let lines: Vec<&str> = text.lines().collect();
let line_text = lines.get(line as usize)?;
let semicolon_pos = line_text.rfind(';')?;
let default_text = " = 0";
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position {
line,
character: semicolon_pos as u32,
},
end: Position {
line,
character: semicolon_pos as u32,
},
},
new_text: default_text.to_string(),
}],
);
Some(CodeAction {
title: format!("Add default value to parameter '{}'", param_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: None,
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
})
}
fn create_remove_variable_action(
uri: &Uri,
text: &str,
var_name: &str,
line: u32,
) -> Option<CodeAction> {
let lines: Vec<&str> = text.lines().collect();
let _line_text = lines.get(line as usize)?;
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position { line, character: 0 },
end: Position {
line: line + 1,
character: 0,
},
},
new_text: String::new(),
}],
);
Some(CodeAction {
title: format!("Remove unused variable '{}'", var_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: None,
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
})
}
fn create_quick_fix_for_diagnostic(
uri: &Uri,
text: &str,
diagnostic: &lsp_types::Diagnostic,
_range: &Range,
) -> Option<CodeAction> {
let message = &diagnostic.message;
if message.starts_with("Undefined variable:") {
let var_name = message
.strip_prefix("Undefined variable: '")?
.strip_suffix('\'')?;
return create_declare_variable_action(uri, text, var_name, diagnostic);
}
if message.starts_with("Unused variable:") {
let var_name = message
.strip_prefix("Unused variable: '")?
.strip_suffix("' is declared but never used")?;
return create_prefix_underscore_action(uri, var_name, diagnostic);
}
if message.contains("has no default value") {
return create_add_default_from_diagnostic(uri, text, diagnostic);
}
None
}
fn create_declare_variable_action(
uri: &Uri,
text: &str,
var_name: &str,
diagnostic: &lsp_types::Diagnostic,
) -> Option<CodeAction> {
let lines: Vec<&str> = text.lines().collect();
let mut insert_line = 0u32;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed == "equation" || trimmed.starts_with("equation ") {
insert_line = i as u32;
break;
}
}
if insert_line == 0 {
return None;
}
let indent = lines
.get(insert_line as usize)
.map(|l| l.len() - l.trim_start().len())
.unwrap_or(2);
let indent_str = " ".repeat(indent);
let declaration = format!("{}Real {};\n", indent_str, var_name);
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position {
line: insert_line,
character: 0,
},
end: Position {
line: insert_line,
character: 0,
},
},
new_text: declaration,
}],
);
Some(CodeAction {
title: format!("Declare variable '{}' as Real", var_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
})
}
fn create_prefix_underscore_action(
uri: &Uri,
var_name: &str,
diagnostic: &lsp_types::Diagnostic,
) -> Option<CodeAction> {
let line = diagnostic.range.start.line;
let start_char = diagnostic.range.start.character;
let end_char = diagnostic.range.end.character;
let new_name = format!("_{}", var_name);
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position {
line,
character: start_char,
},
end: Position {
line,
character: end_char,
},
},
new_text: new_name.clone(),
}],
);
Some(CodeAction {
title: format!("Rename to '{}' to suppress warning", new_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
})
}
fn create_add_default_from_diagnostic(
uri: &Uri,
text: &str,
diagnostic: &lsp_types::Diagnostic,
) -> Option<CodeAction> {
let line = diagnostic.range.start.line;
let lines: Vec<&str> = text.lines().collect();
let line_text = lines.get(line as usize)?;
let semicolon_pos = line_text.rfind(';')?;
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position {
line,
character: semicolon_pos as u32,
},
end: Position {
line,
character: semicolon_pos as u32,
},
},
new_text: " = 0".to_string(),
}],
);
Some(CodeAction {
title: "Add default value".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
})
}