#![allow(dead_code)]
use tower_lsp::lsp_types::*;
#[derive(Debug, Clone)]
pub struct RefactoringPreview {
pub title: String,
pub description: String,
pub changes: Vec<FileChange>,
pub is_safe: bool,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct FileChange {
pub uri: Url,
pub file_name: String,
pub edits: Vec<EditPreview>,
pub summary: String,
}
#[derive(Debug, Clone)]
pub struct EditPreview {
pub range: Range,
pub original: String,
pub replacement: String,
pub edit_type: EditType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditType {
Insert,
Delete,
Replace,
Move,
}
impl RefactoringPreview {
pub fn from_workspace_edit(
title: String,
description: String,
workspace_edit: &WorkspaceEdit,
source_content: &std::collections::HashMap<Url, String>,
) -> Self {
let mut file_changes = vec![];
let mut warnings = vec![];
if let Some(changes) = &workspace_edit.changes {
for (uri, edits) in changes {
let file_name = uri
.path()
.split('/')
.next_back()
.unwrap_or("unknown")
.to_string();
let source = source_content.get(uri);
let edit_previews: Vec<EditPreview> = edits
.iter()
.map(|edit| {
let original = if let Some(src) = source {
Self::extract_text(src, &edit.range)
} else {
String::new()
};
let edit_type = if edit.new_text.is_empty() {
EditType::Delete
} else if original.is_empty() {
EditType::Insert
} else {
EditType::Replace
};
EditPreview {
range: edit.range,
original,
replacement: edit.new_text.clone(),
edit_type,
}
})
.collect();
let summary = Self::generate_summary(&edit_previews);
file_changes.push(FileChange {
uri: uri.clone(),
file_name,
edits: edit_previews,
summary,
});
}
}
let is_safe = Self::check_safety(&file_changes, &mut warnings);
RefactoringPreview {
title,
description,
changes: file_changes,
is_safe,
warnings,
}
}
fn extract_text(source: &str, range: &Range) -> String {
let lines: Vec<&str> = source.lines().collect();
let start_line = range.start.line as usize;
let end_line = range.end.line as usize;
if start_line >= lines.len() {
return String::new();
}
if start_line == end_line {
if let Some(line) = lines.get(start_line) {
let start_char = range.start.character as usize;
let end_char = range.end.character as usize;
if start_char < line.len() {
return line[start_char..end_char.min(line.len())].to_string();
}
}
} else {
let mut result = vec![];
for (i, line) in lines.iter().enumerate().skip(start_line) {
if i > end_line {
break;
}
if i == start_line {
result.push(&line[range.start.character as usize..]);
} else if i == end_line {
result.push(&line[..range.end.character as usize]);
} else {
result.push(line);
}
}
return result.join("\n");
}
String::new()
}
fn generate_summary(edits: &[EditPreview]) -> String {
let inserts = edits
.iter()
.filter(|e| e.edit_type == EditType::Insert)
.count();
let deletes = edits
.iter()
.filter(|e| e.edit_type == EditType::Delete)
.count();
let replaces = edits
.iter()
.filter(|e| e.edit_type == EditType::Replace)
.count();
let mut parts = vec![];
if inserts > 0 {
parts.push(format!(
"{} insertion{}",
inserts,
if inserts == 1 { "" } else { "s" }
));
}
if deletes > 0 {
parts.push(format!(
"{} deletion{}",
deletes,
if deletes == 1 { "" } else { "s" }
));
}
if replaces > 0 {
parts.push(format!(
"{} replacement{}",
replaces,
if replaces == 1 { "" } else { "s" }
));
}
if parts.is_empty() {
"No changes".to_string()
} else {
parts.join(", ")
}
}
fn check_safety(file_changes: &[FileChange], warnings: &mut Vec<String>) -> bool {
let is_safe = true;
for file_change in file_changes {
let large_deletes = file_change
.edits
.iter()
.filter(|e| e.edit_type == EditType::Delete && e.original.lines().count() > 50)
.count();
if large_deletes > 0 {
warnings.push(format!(
"{}: {} large deletion(s) detected",
file_change.file_name, large_deletes
));
}
}
if file_changes.len() > 5 {
warnings.push(format!(
"Modifying {} files - consider breaking into smaller refactorings",
file_changes.len()
));
}
is_safe
}
pub fn format_diff(&self) -> String {
let mut output = vec![];
output.push(format!("=== {} ===", self.title));
output.push(self.description.clone());
output.push(String::new());
if !self.warnings.is_empty() {
output.push("⚠️ Warnings:".to_string());
for warning in &self.warnings {
output.push(format!(" - {}", warning));
}
output.push(String::new());
}
for file_change in &self.changes {
output.push(format!(
"📄 {} ({})",
file_change.file_name, file_change.summary
));
output.push(String::new());
for edit in &file_change.edits {
match edit.edit_type {
EditType::Insert => {
output.push(format!("+ INSERT at line {}", edit.range.start.line + 1));
for line in edit.replacement.lines() {
output.push(format!("+ {}", line));
}
}
EditType::Delete => {
output.push(format!("- DELETE at line {}", edit.range.start.line + 1));
for line in edit.original.lines() {
output.push(format!("- {}", line));
}
}
EditType::Replace => {
output.push(format!("~ REPLACE at line {}", edit.range.start.line + 1));
for line in edit.original.lines() {
output.push(format!("- {}", line));
}
for line in edit.replacement.lines() {
output.push(format!("+ {}", line));
}
}
EditType::Move => {
output.push(format!("→ MOVE from line {}", edit.range.start.line + 1));
}
}
output.push(String::new());
}
}
if self.is_safe {
output.push("✅ Safe to apply".to_string());
} else {
output.push("❌ Not safe to apply - review warnings".to_string());
}
output.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preview_creation() {
let mut changes = std::collections::HashMap::new();
let uri = Url::parse("file:///test.wj").unwrap();
let edits = vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
new_text: "hello".to_string(),
}];
changes.insert(uri.clone(), edits);
let workspace_edit = WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
};
let mut source_content = std::collections::HashMap::new();
source_content.insert(uri, "world".to_string());
let preview = RefactoringPreview::from_workspace_edit(
"Test Refactoring".to_string(),
"Testing preview".to_string(),
&workspace_edit,
&source_content,
);
assert_eq!(preview.title, "Test Refactoring");
assert_eq!(preview.changes.len(), 1);
assert!(preview.is_safe);
}
#[test]
fn test_diff_formatting() {
let preview = RefactoringPreview {
title: "Extract Function".to_string(),
description: "Extract code into new function".to_string(),
changes: vec![],
is_safe: true,
warnings: vec![],
};
let diff = preview.format_diff();
assert!(diff.contains("Extract Function"));
assert!(diff.contains("✅ Safe to apply"));
}
}