use crate::ast::{Node, NodeKind};
use crate::position::{Position, Range};
use lsp_types::*;
use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone)]
pub struct CodeActionProvider {
config: CodeActionConfig,
workspace_index: Option<crate::workspace::workspace_index::WorkspaceIndex>,
}
#[derive(Debug, Clone)]
pub struct CodeActionConfig {
pub enable_refactoring: bool,
pub enable_quick_fixes: bool,
pub enable_productivity: bool,
pub enable_import_optimization: bool,
pub max_actions: usize,
}
impl Default for CodeActionConfig {
fn default() -> Self {
Self {
enable_refactoring: true,
enable_quick_fixes: true,
enable_productivity: true,
enable_import_optimization: true,
max_actions: 50,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodeActionCategory {
QuickFix,
Refactor,
Productivity,
ImportOptimization,
StyleImprovement,
}
impl CodeActionProvider {
pub fn new() -> Self {
Self {
config: CodeActionConfig::default(),
workspace_index: None,
}
}
pub fn with_config(config: CodeActionConfig) -> Self {
Self {
config,
workspace_index: None,
}
}
pub fn with_index(workspace_index: crate::workspace::workspace_index::WorkspaceIndex) -> Self {
Self {
config: CodeActionConfig::default(),
workspace_index: Some(workspace_index),
}
}
pub fn provide_code_actions(&self, params: CodeActionParams) -> Option<Vec<CodeActionOrCommand>> {
let mut actions = Vec::new();
let uri = params.text_document.uri.clone();
let range = params.range;
let context = ¶ms.context;
if self.config.enable_quick_fixes {
actions.extend(self.provide_quick_fixes(&uri, range, context));
}
if self.config.enable_refactoring {
actions.extend(self.provide_refactoring_actions(&uri, range));
}
if self.config.enable_productivity {
actions.extend(self.provide_productivity_actions(&uri, range));
}
if self.config.enable_import_optimization {
actions.extend(self.provide_import_actions(&uri, range));
}
actions.sort_by(|a, b| {
let a_priority = self.get_action_priority(a);
let b_priority = self.get_action_priority(b);
b_priority.cmp(&a_priority)
});
actions.truncate(self.config.max_actions);
Some(actions)
}
fn provide_quick_fixes(
&self,
uri: &Url,
range: Range,
context: &CodeActionContext,
) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
for diagnostic in &context.diagnostics {
if let Some(code) = &diagnostic.code {
match code {
NumberOrString::String(code_str) => {
match code_str.as_str() {
"syntax-error" => {
actions.push(self.create_syntax_error_fix(uri, diagnostic));
}
"style" => {
actions.push(self.create_style_fix(uri, diagnostic));
}
"security" => {
actions.push(self.create_security_fix(uri, diagnostic));
}
_ => {}
}
}
_ => {}
}
}
}
actions
}
fn provide_refactoring_actions(&self, uri: &Url, range: Range) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
actions.push(self.create_extract_variable_action(uri, range));
actions.push(self.create_extract_subroutine_action(uri, range));
actions.push(self.create_modernize_action(uri, range));
actions.push(self.create_add_pragmas_action(uri, range));
actions
}
fn provide_productivity_actions(&self, uri: &Url, range: Range) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
actions.push(self.create_generate_docs_action(uri, range));
actions.push(self.create_add_tests_action(uri, range));
actions.push(self.create_optimize_action(uri, range));
actions
}
fn provide_import_actions(&self, uri: &Url, range: Range) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
actions.push(self.create_remove_unused_imports_action(uri, range));
actions.push(self.create_add_missing_imports_action(uri, range));
actions.push(self.create_sort_imports_action(uri, range));
actions
}
fn create_syntax_error_fix(&self, uri: &Url, diagnostic: &Diagnostic) -> CodeActionOrCommand {
let title = "Fix syntax error".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::QUICK_FIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit {
changes: Some({
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: diagnostic.range,
new_text: ";".to_string(), }],
);
changes
}),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_style_fix(&self, uri: &Url, diagnostic: &Diagnostic) -> CodeActionOrCommand {
let title = "Improve style".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::QUICK_FIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_security_fix(&self, uri: &Url, diagnostic: &Diagnostic) -> CodeActionOrCommand {
let title = "Improve security".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::QUICK_FIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_extract_variable_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Extract variable".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::REFACTOR),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_extract_subroutine_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Extract subroutine".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::REFACTOR),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_modernize_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Modernize code".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::REFACTOR),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_add_pragmas_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Add pragmas".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::REFACTOR),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_generate_docs_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Generate documentation".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::SOURCE),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_add_tests_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Add tests".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::SOURCE),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_optimize_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Optimize code".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::REFACTOR),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_remove_unused_imports_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Remove unused imports".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_add_missing_imports_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Add missing imports".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn create_sort_imports_action(&self, uri: &Url, range: Range) -> CodeActionOrCommand {
let title = "Sort imports".to_string();
let action = CodeAction {
title: title.clone(),
kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
diagnostics: None,
edit: Some(WorkspaceEdit::default()),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
};
CodeActionOrCommand::CodeAction(action)
}
fn get_action_priority(&self, action: &CodeActionOrCommand) -> u8 {
match action {
CodeActionOrCommand::CodeAction(code_action) => {
match code_action.kind {
Some(CodeActionKind::QUICK_FIX) => 100,
Some(CodeActionKind::REFACTOR) => 80,
Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS) => 70,
Some(CodeActionKind::SOURCE) => 60,
_ => 50,
}
}
CodeActionOrCommand::Command(_) => 40,
}
}
pub fn update_workspace_index(&mut self, workspace_index: crate::workspace::workspace_index::WorkspaceIndex) {
self.workspace_index = Some(workspace_index);
}
}
impl Default for CodeActionProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_code_action_provider_creation() {
let provider = CodeActionProvider::new();
assert!(provider.config.enable_refactoring);
assert!(provider.config.enable_quick_fixes);
assert!(provider.config.enable_productivity);
assert!(provider.config.enable_import_optimization);
assert_eq!(provider.config.max_actions, 50);
}
#[test]
fn test_custom_config() {
let config = CodeActionConfig {
enable_refactoring: false,
enable_quick_fixes: true,
enable_productivity: false,
enable_import_optimization: true,
max_actions: 25,
};
let provider = CodeActionProvider::with_config(config);
assert!(!provider.config.enable_refactoring);
assert!(provider.config.enable_quick_fixes);
assert!(!provider.config.enable_productivity);
assert!(provider.config.enable_import_optimization);
assert_eq!(provider.config.max_actions, 25);
}
#[test]
fn test_action_priority() {
let provider = CodeActionProvider::new();
let quick_fix = CodeActionOrCommand::CodeAction(CodeAction {
title: "Fix".to_string(),
kind: Some(CodeActionKind::QUICK_FIX),
..Default::default()
});
let refactor = CodeActionOrCommand::CodeAction(CodeAction {
title: "Refactor".to_string(),
kind: Some(CodeActionKind::REFACTOR),
..Default::default()
});
assert!(provider.get_action_priority(&quick_fix) > provider.get_action_priority(&refactor));
}
#[test]
fn test_workspace_index_update() {
let mut provider = CodeActionProvider::new();
let new_index = crate::workspace::workspace_index::WorkspaceIndex::new();
provider.update_workspace_index(new_index);
assert!(provider.workspace_index.is_some());
}
}