use super::document::Document;
use super::types::*;
pub struct CodeActionProvider;
impl CodeActionProvider {
pub fn new() -> Self {
Self
}
pub fn provide(
&self,
doc: &Document,
range: Range,
diagnostics: &[Diagnostic],
) -> Vec<CodeAction> {
let mut actions = Vec::new();
for diagnostic in diagnostics {
actions.extend(self.quick_fixes_for_diagnostic(doc, diagnostic));
}
actions.extend(self.refactoring_actions(doc, range));
actions.extend(self.source_actions(doc));
actions
}
fn quick_fixes_for_diagnostic(
&self,
doc: &Document,
diagnostic: &Diagnostic,
) -> Vec<CodeAction> {
let mut actions = Vec::new();
let message = &diagnostic.message;
if message.contains("expected ';'") {
actions.push(self.add_semicolon_fix(doc, diagnostic));
}
if message.contains("unused variable") {
actions.push(self.prefix_underscore_fix(doc, diagnostic));
actions.push(self.remove_variable_fix(doc, diagnostic));
}
if message.contains("mismatched types") || message.contains("type mismatch") {
actions.extend(self.type_conversion_fixes(doc, diagnostic));
}
if message.contains("not found in this scope") || message.contains("cannot find") {
actions.extend(self.import_suggestions(doc, diagnostic));
}
if message.contains("unused import") {
actions.push(self.remove_import_fix(doc, diagnostic));
}
if message.contains("unreachable") || message.contains("dead code") {
actions.push(self.remove_dead_code_fix(doc, diagnostic));
}
actions
}
fn add_semicolon_fix(&self, doc: &Document, diagnostic: &Diagnostic) -> CodeAction {
let pos = diagnostic.range.end;
let mut edit = WorkspaceEdit::new();
edit.add_edit(doc.uri.clone(), TextEdit::insert(pos, ";".to_string()));
CodeAction::quick_fix("Add missing semicolon")
.with_edit(edit)
.preferred()
}
fn prefix_underscore_fix(&self, doc: &Document, diagnostic: &Diagnostic) -> CodeAction {
let start = diagnostic.range.start;
let mut edit = WorkspaceEdit::new();
edit.add_edit(doc.uri.clone(), TextEdit::insert(start, "_".to_string()));
CodeAction::quick_fix("Prefix with underscore").with_edit(edit)
}
fn remove_variable_fix(&self, doc: &Document, diagnostic: &Diagnostic) -> CodeAction {
let line = doc.line(diagnostic.range.start.line).unwrap_or("");
let start = Position::new(diagnostic.range.start.line, 0);
let end = Position::new(diagnostic.range.start.line, line.len() as u32);
let mut edit = WorkspaceEdit::new();
edit.add_edit(doc.uri.clone(), TextEdit::delete(Range::new(start, end)));
CodeAction::quick_fix("Remove unused variable").with_edit(edit)
}
fn type_conversion_fixes(&self, doc: &Document, diagnostic: &Diagnostic) -> Vec<CodeAction> {
let mut actions = Vec::new();
let conversions = [
("as i32", "Convert to i32"),
("as i64", "Convert to i64"),
("as u32", "Convert to u32"),
("as u64", "Convert to u64"),
("as usize", "Convert to usize"),
("as f32", "Convert to f32"),
("as f64", "Convert to f64"),
(".to_string()", "Convert to String"),
(".as_str()", "Convert to &str"),
];
for (suffix, title) in conversions {
let end = diagnostic.range.end;
let mut edit = WorkspaceEdit::new();
edit.add_edit(
doc.uri.clone(),
TextEdit::insert(end, format!(" {}", suffix)),
);
actions.push(CodeAction::quick_fix(title).with_edit(edit));
}
actions
}
fn import_suggestions(&self, doc: &Document, diagnostic: &Diagnostic) -> Vec<CodeAction> {
let mut actions = Vec::new();
let name = doc.text_in_range(diagnostic.range);
let suggestions = get_import_suggestions(&name);
for suggestion in suggestions {
let import_line = format!("use {};\n", suggestion);
let mut edit = WorkspaceEdit::new();
edit.add_edit(
doc.uri.clone(),
TextEdit::insert(Position::new(0, 0), import_line),
);
actions.push(CodeAction::quick_fix(format!("Import {}", suggestion)).with_edit(edit));
}
actions
}
fn remove_import_fix(&self, doc: &Document, diagnostic: &Diagnostic) -> CodeAction {
let line = diagnostic.range.start.line;
let _line_text = doc.line(line).unwrap_or("");
let start = Position::new(line, 0);
let end = Position::new(line + 1, 0);
let mut edit = WorkspaceEdit::new();
edit.add_edit(doc.uri.clone(), TextEdit::delete(Range::new(start, end)));
CodeAction::quick_fix("Remove unused import")
.with_edit(edit)
.preferred()
}
fn remove_dead_code_fix(&self, doc: &Document, diagnostic: &Diagnostic) -> CodeAction {
let mut edit = WorkspaceEdit::new();
edit.add_edit(doc.uri.clone(), TextEdit::delete(diagnostic.range));
CodeAction::quick_fix("Remove dead code").with_edit(edit)
}
fn refactoring_actions(&self, doc: &Document, range: Range) -> Vec<CodeAction> {
let mut actions = Vec::new();
if range.start != range.end {
let selected_text = doc.text_in_range(range);
if !selected_text.trim().is_empty() && !selected_text.contains('\n') {
actions.push(self.extract_variable_action(doc, range, &selected_text));
}
if selected_text.contains('\n') || selected_text.len() > 50 {
actions.push(self.extract_function_action(doc, range));
}
}
actions
}
fn extract_variable_action(&self, doc: &Document, range: Range, text: &str) -> CodeAction {
let var_name = "extracted";
let let_stmt = format!("let {} = {};\n ", var_name, text.trim());
let mut edit = WorkspaceEdit::new();
let line_start = Position::new(range.start.line, 0);
let indent = doc
.line(range.start.line)
.map(|l| l.len() - l.trim_start().len())
.unwrap_or(0);
let indented_let = format!("{}{}", " ".repeat(indent), let_stmt);
edit.add_edit(doc.uri.clone(), TextEdit::insert(line_start, indented_let));
edit.add_edit(
doc.uri.clone(),
TextEdit::replace(range, var_name.to_string()),
);
CodeAction::new("Extract to variable")
.with_kind(CodeActionKind::refactor_extract())
.with_edit(edit)
}
fn extract_function_action(&self, doc: &Document, range: Range) -> CodeAction {
let selected_text = doc.text_in_range(range);
let fn_body = selected_text.trim();
let fn_def = format!(
"\nfn extracted() {{\n {}\n}}\n",
fn_body.replace('\n', "\n ")
);
let mut edit = WorkspaceEdit::new();
let last_line = doc.line_count().saturating_sub(1) as u32;
let end_pos = Position::new(
last_line,
doc.line(last_line).map(|l| l.len() as u32).unwrap_or(0),
);
edit.add_edit(doc.uri.clone(), TextEdit::insert(end_pos, fn_def));
edit.add_edit(
doc.uri.clone(),
TextEdit::replace(range, "extracted()".to_string()),
);
CodeAction::new("Extract to function")
.with_kind(CodeActionKind::refactor_extract())
.with_edit(edit)
}
fn source_actions(&self, doc: &Document) -> Vec<CodeAction> {
let mut actions = Vec::new();
if let Some(edit) = self.organize_imports(doc) {
actions.push(
CodeAction::new("Organize imports")
.with_kind(CodeActionKind::organize_imports())
.with_edit(edit),
);
}
actions.push(CodeAction::new("Add #[derive(Debug)]").with_kind(CodeActionKind::source()));
actions
}
fn organize_imports(&self, doc: &Document) -> Option<WorkspaceEdit> {
let content = &doc.content;
let lines: Vec<&str> = content.lines().collect();
let mut imports: Vec<(usize, String)> = Vec::new();
let mut first_import_line: Option<usize> = None;
let mut last_import_line: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("use ") {
if first_import_line.is_none() {
first_import_line = Some(i);
}
last_import_line = Some(i);
imports.push((i, line.to_string()));
}
}
if imports.len() <= 1 {
return None;
}
imports.sort_by(|a, b| {
let a_trimmed = a.1.trim();
let b_trimmed = b.1.trim();
let a_priority = import_priority(a_trimmed);
let b_priority = import_priority(b_trimmed);
a_priority
.cmp(&b_priority)
.then_with(|| a_trimmed.cmp(b_trimmed))
});
let first = first_import_line?;
let last = last_import_line?;
let mut new_imports = String::new();
for (_, import) in &imports {
new_imports.push_str(import.trim());
new_imports.push('\n');
}
let mut edit = WorkspaceEdit::new();
edit.add_edit(
doc.uri.clone(),
TextEdit::replace(
Range::new(
Position::new(first as u32, 0),
Position::new(last as u32 + 1, 0),
),
new_imports,
),
);
Some(edit)
}
}
impl Default for CodeActionProvider {
fn default() -> Self {
Self::new()
}
}
fn import_priority(import: &str) -> u8 {
if import.contains("std::") {
0 } else if import.contains("crate::") || import.contains("self::") || import.contains("super::")
{
2 } else {
1 }
}
fn get_import_suggestions(name: &str) -> Vec<String> {
let suggestions: Vec<(&str, &str)> = vec![
("HashMap", "std::collections::HashMap"),
("HashSet", "std::collections::HashSet"),
("BTreeMap", "std::collections::BTreeMap"),
("BTreeSet", "std::collections::BTreeSet"),
("VecDeque", "std::collections::VecDeque"),
("LinkedList", "std::collections::LinkedList"),
("BinaryHeap", "std::collections::BinaryHeap"),
("Vec", "std::vec::Vec"),
("String", "std::string::String"),
("Arc", "std::sync::Arc"),
("Mutex", "std::sync::Mutex"),
("RwLock", "std::sync::RwLock"),
("Rc", "std::rc::Rc"),
("RefCell", "std::cell::RefCell"),
("Cell", "std::cell::Cell"),
("Path", "std::path::Path"),
("PathBuf", "std::path::PathBuf"),
("File", "std::fs::File"),
("Read", "std::io::Read"),
("Write", "std::io::Write"),
("BufReader", "std::io::BufReader"),
("BufWriter", "std::io::BufWriter"),
("Duration", "std::time::Duration"),
("Instant", "std::time::Instant"),
("SystemTime", "std::time::SystemTime"),
("thread", "std::thread"),
("fmt", "std::fmt"),
("Display", "std::fmt::Display"),
("Debug", "std::fmt::Debug"),
("Default", "std::default::Default"),
("Clone", "std::clone::Clone"),
("Copy", "std::marker::Copy"),
("Send", "std::marker::Send"),
("Sync", "std::marker::Sync"),
("Iterator", "std::iter::Iterator"),
("IntoIterator", "std::iter::IntoIterator"),
("FromIterator", "std::iter::FromIterator"),
];
suggestions
.into_iter()
.filter(|(n, _)| *n == name)
.map(|(_, path)| path.to_string())
.collect()
}