use crate::ast::{Node, NodeKind};
use crate::position::{Position, Range};
use crate::workspace::workspace_index::{WorkspaceIndex, SymbolReference};
use lsp_types::*;
use perl_lexer::is_parser_lsp_keyword;
use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone)]
pub struct RenameProvider {
workspace_index: WorkspaceIndex,
config: RenameConfig,
}
#[derive(Debug, Clone)]
pub struct RenameConfig {
pub include_strings: bool,
pub include_comments: bool,
pub require_confirmation: bool,
pub confirmation_threshold: usize,
}
impl Default for RenameConfig {
fn default() -> Self {
Self {
include_strings: false, include_comments: false, require_confirmation: true,
confirmation_threshold: 100,
}
}
}
#[derive(Debug, Clone)]
pub struct RenameResult {
pub workspace_edit: WorkspaceEdit,
pub affected_files: usize,
pub total_references: usize,
pub warnings: Vec<String>,
pub is_safe: bool,
}
impl RenameProvider {
pub fn new() -> Self {
Self {
workspace_index: WorkspaceIndex::new(),
config: RenameConfig::default(),
}
}
pub fn with_config(config: RenameConfig) -> Self {
Self {
workspace_index: WorkspaceIndex::new(),
config,
}
}
pub fn with_index(workspace_index: WorkspaceIndex) -> Self {
Self {
workspace_index,
config: RenameConfig::default(),
}
}
pub fn prepare_rename(&self, params: RenameParams) -> Option<RenameResult> {
let position = params.text_document_position.position;
let uri = params.text_document_position.text_document.uri;
let new_name = params.new_name;
let symbol_name = self.resolve_symbol_at_position(&uri, position)?;
if let Err(warning) = self.validate_new_name(&new_name) {
return Some(RenameResult {
workspace_edit: WorkspaceEdit::default(),
affected_files: 0,
total_references: 0,
warnings: vec![warning],
is_safe: false,
});
}
let references = self.workspace_index.find_references(&symbol_name);
let conflicts = self.check_for_conflicts(&symbol_name, &new_name);
let workspace_edit = self.create_workspace_edit(&references, &new_name);
let is_safe = conflicts.is_empty() &&
(!self.config.require_confirmation || references.len() <= self.config.confirmation_threshold);
Some(RenameResult {
workspace_edit,
affected_files: self.count_affected_files(&references),
total_references: references.len(),
warnings: conflicts,
is_safe,
})
}
fn validate_new_name(&self, new_name: &str) -> Result<(), String> {
if new_name.is_empty() {
return Err("New name cannot be empty".to_string());
}
if new_name.starts_with(|c: char| c.is_ascii_digit()) {
return Err("New name cannot start with a digit".to_string());
}
if !is_valid_perl_symbol_name(new_name) {
return Err(
"New name can only contain ASCII letters/digits, underscores, and package separators (::)"
.to_string(),
);
}
if is_parser_lsp_keyword(new_name) {
return Err(format!("New name '{}' is a Perl keyword", new_name));
}
Ok(())
}
fn check_for_conflicts(&self, old_name: &str, new_name: &str) -> Vec<String> {
let mut conflicts = Vec::new();
let existing_references = self.workspace_index.find_references(new_name);
if !existing_references.is_empty() {
conflicts.push(format!(
"Symbol '{}' already exists with {} references",
new_name,
existing_references.len()
));
}
if old_name != new_name {
}
conflicts
}
fn create_workspace_edit(&self, references: &[Location], new_name: &str) -> WorkspaceEdit {
let mut changes = HashMap::new();
let mut file_changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for reference in references {
let text_edit = TextEdit {
range: reference.range,
new_text: new_name.to_string(),
};
file_changes
.entry(reference.uri.clone())
.or_default()
.push(text_edit);
}
for (uri, edits) in file_changes {
changes.insert(uri, edits);
}
WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}
}
fn count_affected_files(&self, references: &[Location]) -> usize {
let mut files = std::collections::HashSet::new();
for reference in references {
files.insert(&reference.uri);
}
files.len()
}
fn resolve_symbol_at_position(&self, uri: &Url, position: Position) -> Option<String> {
Some("example_symbol".to_string())
}
pub fn update_workspace_index(&mut self, workspace_index: WorkspaceIndex) {
self.workspace_index = workspace_index;
}
}
fn is_valid_perl_symbol_name(new_name: &str) -> bool {
if new_name.contains('\'') {
return false;
}
let mut has_any_segment = false;
for segment in new_name.split("::") {
if segment.is_empty() {
return false;
}
has_any_segment = true;
if segment.starts_with(|c: char| c.is_ascii_digit()) {
return false;
}
if !segment
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return false;
}
}
has_any_segment
}
impl Default for RenameProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rename_provider_creation() {
let provider = RenameProvider::new();
assert!(provider.config.require_confirmation);
assert_eq!(provider.config.confirmation_threshold, 100);
}
#[test]
fn test_custom_config() {
let config = RenameConfig {
include_strings: true,
include_comments: true,
require_confirmation: false,
confirmation_threshold: 50,
};
let provider = RenameProvider::with_config(config);
assert!(provider.config.include_strings);
assert!(provider.config.include_comments);
assert!(!provider.config.require_confirmation);
assert_eq!(provider.config.confirmation_threshold, 50);
}
#[test]
fn test_name_validation() {
let provider = RenameProvider::new();
assert!(provider.validate_new_name("valid_name").is_ok());
assert!(provider.validate_new_name("_underscore").is_ok());
assert!(provider.validate_new_name("name123").is_ok());
assert!(provider.validate_new_name("My::Package").is_ok());
assert!(provider.validate_new_name("My::Package_2").is_ok());
assert!(provider.validate_new_name("").is_err());
assert!(provider.validate_new_name("123invalid").is_err());
assert!(provider.validate_new_name("invalid-name").is_err());
assert!(provider.validate_new_name("My:::Package").is_err());
assert!(provider.validate_new_name("My::").is_err());
assert!(provider.validate_new_name("::My").is_err());
assert!(provider.validate_new_name("My'Package").is_err());
assert!(provider.validate_new_name("naïve").is_err());
assert!(provider.validate_new_name("if").is_err()); assert!(provider.validate_new_name("My::0Module").is_err()); assert!(provider.validate_new_name("0::Start").is_err()); }
#[test]
fn test_workspace_index_update() {
let mut provider = RenameProvider::new();
let new_index = WorkspaceIndex::new();
provider.update_workspace_index(new_index);
}
#[test]
fn test_affected_files_count() {
let provider = RenameProvider::new();
use perl_tdd_support::must;
let uri1 = must(Url::parse("file:///test1.pl"));
let uri2 = must(Url::parse("file:///test2.pl"));
let references = vec![
Location { uri: uri1.clone(), range: Range::default() },
Location { uri: uri1.clone(), range: Range::default() },
Location { uri: uri2.clone(), range: Range::default() },
];
let count = provider.count_affected_files(&references);
assert_eq!(count, 2);
}
}