use crate::types::CodeAction;
use perl_parser_core::ast::{Node, NodeKind};
use std::collections::HashSet;
mod error_checking;
mod extract_subroutine;
mod extract_variable;
mod helpers;
mod import_management;
mod loop_conversion;
mod postfix;
use helpers::Helpers;
pub struct EnhancedCodeActionsProvider {
source: String,
lines: Vec<String>,
}
impl EnhancedCodeActionsProvider {
pub fn new(source: String) -> Self {
let lines = source.lines().map(|s| s.to_string()).collect();
Self { source, lines }
}
pub fn get_enhanced_refactoring_actions(
&self,
ast: &Node,
range: (usize, usize),
) -> Vec<CodeAction> {
let mut actions = Vec::new();
let mut extract_var_seen: HashSet<(usize, String)> = HashSet::new();
self.collect_actions_for_range(ast, range, false, &mut actions, &mut extract_var_seen);
actions.extend(self.get_global_refactorings(ast));
actions
}
fn collect_actions_for_range(
&self,
node: &Node,
range: (usize, usize),
is_control_body: bool,
actions: &mut Vec<CodeAction>,
extract_var_seen: &mut HashSet<(usize, String)>,
) {
if node.location.start <= range.1 && node.location.end >= range.0 {
let helpers = Helpers::new(&self.source, &self.lines);
let node_reaches_selection_end = node.location.end >= range.1;
if node_reaches_selection_end && self.is_extractable_expression(node) {
let action =
extract_variable::create_extract_variable_action(node, &self.source, &helpers);
if let Some(decl) = action.edit.changes.first() {
let key = (decl.location.start, decl.new_text.clone());
if extract_var_seen.insert(key) {
actions.push(action);
}
} else {
actions.push(action);
}
}
if let Some(action) = loop_conversion::convert_loop_style(node, &self.source) {
actions.push(action);
}
if let Some(action) = error_checking::add_error_checking(node, &self.source) {
actions.push(action);
}
if let Some(action) = postfix::convert_to_postfix(node, &self.source) {
actions.push(action);
}
if !is_control_body && self.is_extractable_block(node) {
actions.push(extract_subroutine::create_extract_subroutine_action(
node,
&self.source,
&helpers,
));
}
}
match &node.kind {
NodeKind::Program { statements } => {
for stmt in statements {
self.collect_actions_for_range(stmt, range, false, actions, extract_var_seen);
}
}
NodeKind::Block { statements } => {
for stmt in statements {
self.collect_actions_for_range(stmt, range, false, actions, extract_var_seen);
}
}
NodeKind::ExpressionStatement { expression } => {
self.collect_actions_for_range(expression, range, false, actions, extract_var_seen);
}
NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
self.collect_actions_for_range(condition, range, false, actions, extract_var_seen);
self.collect_actions_for_range(
then_branch,
range,
true, actions,
extract_var_seen,
);
for (cond, branch) in elsif_branches {
self.collect_actions_for_range(cond, range, false, actions, extract_var_seen);
self.collect_actions_for_range(branch, range, true, actions, extract_var_seen);
}
if let Some(branch) = else_branch {
self.collect_actions_for_range(branch, range, true, actions, extract_var_seen);
}
}
NodeKind::FunctionCall { args, .. } => {
for arg in args {
self.collect_actions_for_range(arg, range, false, actions, extract_var_seen);
}
}
NodeKind::Binary { left, right, .. } => {
self.collect_actions_for_range(left, range, false, actions, extract_var_seen);
self.collect_actions_for_range(right, range, false, actions, extract_var_seen);
}
NodeKind::Assignment { lhs, rhs, .. } => {
self.collect_actions_for_range(lhs, range, false, actions, extract_var_seen);
self.collect_actions_for_range(rhs, range, false, actions, extract_var_seen);
}
NodeKind::VariableDeclaration { variable, initializer, .. } => {
self.collect_actions_for_range(variable, range, false, actions, extract_var_seen);
if let Some(init) = initializer {
self.collect_actions_for_range(init, range, false, actions, extract_var_seen);
}
}
NodeKind::For { init, condition, update, body, .. } => {
if let Some(init) = init {
self.collect_actions_for_range(init, range, false, actions, extract_var_seen);
}
if let Some(condition) = condition {
self.collect_actions_for_range(
condition,
range,
false,
actions,
extract_var_seen,
);
}
if let Some(update) = update {
self.collect_actions_for_range(update, range, false, actions, extract_var_seen);
}
self.collect_actions_for_range(
body,
range,
true, actions,
extract_var_seen,
);
}
NodeKind::Foreach { variable, list, body, continue_block } => {
self.collect_actions_for_range(variable, range, false, actions, extract_var_seen);
self.collect_actions_for_range(list, range, false, actions, extract_var_seen);
self.collect_actions_for_range(body, range, true, actions, extract_var_seen);
if let Some(cb) = continue_block {
self.collect_actions_for_range(cb, range, false, actions, extract_var_seen);
}
}
NodeKind::While { condition, body, .. } => {
self.collect_actions_for_range(condition, range, false, actions, extract_var_seen);
self.collect_actions_for_range(
body,
range,
true, actions,
extract_var_seen,
);
}
NodeKind::MethodCall { object, args, .. } => {
self.collect_actions_for_range(object, range, false, actions, extract_var_seen);
for arg in args {
self.collect_actions_for_range(arg, range, false, actions, extract_var_seen);
}
}
NodeKind::Subroutine { body, prototype, signature, .. } => {
self.collect_actions_for_range(
body,
range,
true, actions,
extract_var_seen,
);
if let Some(proto) = prototype {
self.collect_actions_for_range(proto, range, false, actions, extract_var_seen);
}
if let Some(sig) = signature {
self.collect_actions_for_range(sig, range, false, actions, extract_var_seen);
}
}
_ => {}
}
}
fn is_extractable_expression(&self, node: &Node) -> bool {
matches!(
&node.kind,
NodeKind::FunctionCall { .. }
| NodeKind::Binary { .. }
| NodeKind::Unary { .. }
| NodeKind::MethodCall { .. }
| NodeKind::Ternary { .. }
)
}
fn is_extractable_block(&self, node: &Node) -> bool {
matches!(&node.kind, NodeKind::Block { .. })
}
fn get_global_refactorings(&self, ast: &Node) -> Vec<CodeAction> {
let mut actions = Vec::new();
let helpers = Helpers::new(&self.source, &self.lines);
if let Some(action) = import_management::add_missing_imports(ast, &self.source, &helpers) {
actions.push(action);
}
if let Some(action) = import_management::organize_imports(ast, &self.source, &helpers) {
actions.push(action);
}
actions.extend(self.add_recommended_pragmas(&helpers));
actions
}
fn add_recommended_pragmas(&self, helpers: &Helpers<'_>) -> Vec<CodeAction> {
use crate::types::{CodeAction, CodeActionEdit, CodeActionKind};
use perl_lsp_rename::TextEdit;
use perl_parser_core::ast::SourceLocation;
let mut actions = Vec::new();
let has_strict = self.source.contains("use strict");
let has_warnings = self.source.contains("use warnings");
if !has_strict || !has_warnings {
let mut pragmas = Vec::new();
if !has_strict {
pragmas.push("use strict;");
}
if !has_warnings {
pragmas.push("use warnings;");
}
let insert_pos = helpers.find_pragma_insert_position();
actions.push(CodeAction {
title: format!("Add missing pragmas ({})", pragmas.join(", ")),
kind: CodeActionKind::QuickFix,
diagnostics: Vec::new(),
edit: CodeActionEdit {
changes: vec![TextEdit {
location: SourceLocation { start: insert_pos, end: insert_pos },
new_text: format!("{}\n", pragmas.join("\n")),
}],
},
is_preferred: true,
});
}
if !self.source.contains("use utf8") && helpers.has_non_ascii_content() {
let insert_pos = helpers.find_pragma_insert_position();
actions.push(CodeAction {
title: "Add UTF-8 support".to_string(),
kind: CodeActionKind::QuickFix,
diagnostics: Vec::new(),
edit: CodeActionEdit {
changes: vec![TextEdit {
location: SourceLocation { start: insert_pos, end: insert_pos },
new_text: "use utf8;\nuse open qw(:std :utf8);\n".to_string(),
}],
},
is_preferred: false,
});
}
actions
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_parser_core::Parser;
use perl_tdd_support::must;
#[test]
fn test_extract_variable() {
let source = "my $x = length($string) + 10;";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 23));
for action in &actions {
eprintln!("Action: {}", action.title);
}
assert!(!actions.is_empty(), "Expected at least one action");
assert!(
actions.iter().any(|a| a.title.contains("Extract")),
"Expected an Extract action, got: {:?}",
actions.iter().map(|a| &a.title).collect::<Vec<_>>()
);
}
#[test]
fn test_add_error_checking() {
let source = "open my $fh, '<', 'file.txt';";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (0, 30));
assert!(actions.iter().any(|a| a.title.contains("error checking")));
}
#[test]
fn test_convert_to_postfix() {
let source = "if ($debug) { print \"Debug\\n\"; }";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (0, source.len()));
assert!(actions.iter().any(|a| a.title.contains("postfix")));
}
}
#[cfg(test)]
mod extract_variable_tests {
use super::*;
use perl_parser_core::Parser;
use perl_tdd_support::must;
#[test]
fn test_extract_hash_access_to_variable() {
let source = "my $x = $hash{$key};";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 19));
let extract_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("Extract")).collect();
assert!(
!extract_actions.is_empty(),
"Expected an Extract action for hash access, got: {:?}",
actions.iter().map(|a| &a.title).collect::<Vec<_>>()
);
let action = &extract_actions[0];
let decl_edit = &action.edit.changes[0];
assert!(
decl_edit.new_text.contains("my $val"),
"Expected variable name '$val' for hash access, got: {}",
decl_edit.new_text
);
}
#[test]
fn test_extract_method_call_to_variable() {
let source = "print $obj->method();";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (6, 20));
let extract_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("Extract")).collect();
assert!(
!extract_actions.is_empty(),
"Expected an Extract action for method call, got: {:?}",
actions.iter().map(|a| &a.title).collect::<Vec<_>>()
);
let action = &extract_actions[0];
let decl_edit = &action.edit.changes[0];
assert!(
decl_edit.new_text.contains("my $result"),
"Expected variable name '$result' for method call, got: {}",
decl_edit.new_text
);
let replace_edit = &action.edit.changes[1];
assert!(
replace_edit.new_text.contains("$result"),
"Expected replacement with '$result', got: {}",
replace_edit.new_text
);
}
#[test]
fn test_extract_method_call_new_suggests_instance() {
let source = "my $x = Foo->new();";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 18));
let extract_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("Extract")).collect();
assert!(
!extract_actions.is_empty(),
"Expected an Extract action for constructor call, got: {:?}",
actions.iter().map(|a| &a.title).collect::<Vec<_>>()
);
let action = &extract_actions[0];
let decl_edit = &action.edit.changes[0];
assert!(
decl_edit.new_text.contains("my $instance"),
"Expected variable name '$instance' for ->new(), got: {}",
decl_edit.new_text
);
}
#[test]
fn test_extract_variable_edit_structure() {
let source = "my $x = $obj->get();";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let provider = EnhancedCodeActionsProvider::new(source.to_string());
let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 19));
let extract_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("Extract")).collect();
assert!(!extract_actions.is_empty(), "Expected at least one extract action");
let action = &extract_actions[0];
assert_eq!(action.edit.changes.len(), 2, "Expected exactly 2 edits (insert + replace)");
let insert_edit = &action.edit.changes[0];
assert!(
insert_edit.new_text.starts_with("my $"),
"First edit should be a variable declaration"
);
assert!(insert_edit.new_text.ends_with(";\n"), "Declaration should end with semicolon");
let replace_edit = &action.edit.changes[1];
assert!(
replace_edit.new_text.starts_with('$'),
"Second edit should be a variable reference"
);
}
}