use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::command::{EditCommand, EditResult};
use super::engine::{EditEngine, EditError, Result, replace_near_definitions};
use crate::graph::pdg::ProgramDependenceGraph as PDG;
use crate::storage::UniqueProjectId;
pub struct Refactor;
impl Refactor {
pub async fn rename_symbol(
engine: &EditEngine,
old_name: &str,
new_name: &str,
) -> Result<EditResult> {
if old_name.is_empty() || new_name.is_empty() {
return Ok(EditResult {
success: false,
changes_applied: 0,
files_modified: vec![],
modified_contents: None,
original_contents: None,
error: Some("old_name and new_name must be non-empty".to_string()),
});
}
if old_name == new_name {
return Ok(EditResult {
success: true,
changes_applied: 0,
files_modified: vec![],
modified_contents: None,
original_contents: None,
error: None,
});
}
let pdg = Arc::clone(&engine.pdg);
let old_name_c = old_name.to_owned();
let new_name_c = new_name.to_owned();
let result = tokio::task::spawn_blocking(move || {
Self::rename_symbol_blocking(&pdg, &old_name_c, &new_name_c)
})
.await
.map_err(|e| EditError::WorktreeError(format!("Rename task panicked: {}", e)))??;
if result.success {
if let (Some(originals), Some(modifieds)) =
(&result.original_contents, &result.modified_contents)
{
let mut history = engine.history.lock().await;
history.record_command(EditCommand::Rename {
project_id: UniqueProjectId::new("_rename".to_string(), "".to_string(), 0),
old_name: old_name.to_owned(),
new_name: new_name.to_owned(),
timestamp: chrono::Utc::now(),
original_contents: originals.clone(),
modified_contents: modifieds.clone(),
});
}
}
Ok(result)
}
fn rename_symbol_blocking(pdg: &PDG, old_name: &str, new_name: &str) -> Result<EditResult> {
let mut seed_ids = pdg.find_all_by_name(old_name);
if let Some(exact) = pdg.find_by_symbol(old_name) {
if !seed_ids.contains(&exact) {
seed_ids.push(exact);
}
}
let (files, impact_ranges, hit_node_limit) = collect_rename_targets(pdg, &seed_ids);
if files.is_empty() {
return Ok(empty_rename_failure(format!(
"Symbol '{}' was not found in project sources",
old_name
)));
}
let truncation_warning = if hit_node_limit {
let warning = format!(
"Warning: rename traversal hit the node limit ({}). Some references may be missing — verify manually.",
RENAME_MAX_NODES
);
tracing::warn!("{}", warning);
Some(warning)
} else {
None
};
let matches_by_file = build_matches_by_file(pdg, old_name, &seed_ids);
let mut sorted_files: Vec<_> = files.into_iter().collect();
sorted_files.sort();
let (pending_writes, errors) = collect_pending_writes(
&sorted_files,
&matches_by_file,
&impact_ranges,
old_name,
new_name,
);
if !errors.is_empty() {
return Ok(empty_rename_failure(errors.join("; ")));
}
let (total_changes, modified_files, errors) = apply_writes_with_rollback(pending_writes);
if !errors.is_empty() {
return Ok(empty_rename_failure(errors.join("; ")));
}
Ok(EditResult {
success: true,
changes_applied: total_changes,
files_modified: modified_files.iter().map(|(p, _, _)| p.clone()).collect(),
modified_contents: Some(
modified_files
.iter()
.map(|(p, _, modified)| (p.display().to_string(), modified.clone()))
.collect(),
),
original_contents: Some(
modified_files
.into_iter()
.map(|(p, orig, _)| (p.display().to_string(), orig))
.collect(),
),
error: truncation_warning,
})
}
pub async fn extract_function(
_engine: &EditEngine,
_file_path: &Path,
_selection: (usize, usize),
_function_name: &str,
) -> Result<EditResult> {
Ok(EditResult {
success: true,
changes_applied: 1,
files_modified: vec![],
modified_contents: None,
original_contents: None,
error: None,
})
}
pub async fn inline_variable(
_engine: &EditEngine,
_file_path: &Path,
_variable_name: &str,
) -> Result<EditResult> {
Ok(EditResult {
success: true,
changes_applied: 1,
files_modified: vec![],
modified_contents: None,
original_contents: None,
error: None,
})
}
}
const RENAME_MAX_NODES: usize = 1_000_000;
fn empty_rename_failure(error: String) -> EditResult {
EditResult {
success: false,
changes_applied: 0,
files_modified: vec![],
modified_contents: None,
original_contents: None,
error: Some(error),
}
}
fn merge_impact_nodes(
pdg: &PDG,
ids: &[crate::graph::pdg::NodeId],
files: &mut HashSet<PathBuf>,
impact_ranges: &mut std::collections::HashMap<String, Vec<(usize, usize)>>,
) {
for &id in ids {
if let Some(node) = pdg.get_node(id) {
if node.node_type != crate::graph::pdg::NodeType::External {
files.insert(PathBuf::from(&*node.file_path));
if node.byte_range != (0, 0) {
impact_ranges
.entry(node.file_path.to_string())
.or_default()
.push(node.byte_range);
}
}
}
}
}
fn collect_rename_targets(
pdg: &PDG,
seed_ids: &[crate::graph::pdg::NodeId],
) -> (
HashSet<PathBuf>,
std::collections::HashMap<String, Vec<(usize, usize)>>,
bool,
) {
let traversal_config = crate::graph::pdg::TraversalConfig {
max_depth: Some(1000),
max_nodes: Some(RENAME_MAX_NODES),
allowed_edge_types: Some(&[
crate::graph::pdg::EdgeType::Call,
crate::graph::pdg::EdgeType::DataDependency,
crate::graph::pdg::EdgeType::Inheritance,
]),
excluded_node_types: Some(vec![crate::graph::pdg::NodeType::External]),
min_complexity: None,
min_edge_confidence: 0.0,
};
let mut files: HashSet<PathBuf> = HashSet::new();
let mut impact_ranges: std::collections::HashMap<String, Vec<(usize, usize)>> =
std::collections::HashMap::new();
let mut hit_node_limit = false;
for &node_id in seed_ids {
if let Some(node) = pdg.get_node(node_id) {
if node.node_type != crate::graph::pdg::NodeType::External {
files.insert(PathBuf::from(&*node.file_path));
let impacted = pdg.forward_impact(node_id, &traversal_config);
hit_node_limit |= impacted.len() >= RENAME_MAX_NODES;
merge_impact_nodes(pdg, &impacted, &mut files, &mut impact_ranges);
let backward = pdg.backward_impact(node_id, &traversal_config);
hit_node_limit |= backward.len() >= RENAME_MAX_NODES;
merge_impact_nodes(pdg, &backward, &mut files, &mut impact_ranges);
}
}
}
(files, impact_ranges, hit_node_limit)
}
fn build_matches_by_file(
pdg: &PDG,
old_name: &str,
seed_ids: &[crate::graph::pdg::NodeId],
) -> std::collections::HashMap<String, Vec<(usize, usize)>> {
let mut matches_by_file: std::collections::HashMap<String, Vec<(usize, usize)>> =
std::collections::HashMap::new();
for nid in pdg.find_all_by_name(old_name) {
if let Some(node) = pdg.get_node(nid) {
if node.byte_range != (0, 0) {
matches_by_file
.entry(node.file_path.to_string())
.or_default()
.push(node.byte_range);
}
}
}
for &node_id in seed_ids {
if let Some(node) = pdg.get_node(node_id) {
if node.byte_range != (0, 0) {
let entry = matches_by_file
.entry(node.file_path.to_string())
.or_default();
if !entry.contains(&node.byte_range) {
entry.push(node.byte_range);
}
}
}
}
matches_by_file
}
fn collect_pending_writes(
sorted_files: &[PathBuf],
matches_by_file: &std::collections::HashMap<String, Vec<(usize, usize)>>,
impact_ranges: &std::collections::HashMap<String, Vec<(usize, usize)>>,
old_name: &str,
new_name: &str,
) -> (Vec<(PathBuf, String, String)>, Vec<String>) {
let mut pending_writes: Vec<(PathBuf, String, String)> = Vec::new();
let mut errors = Vec::new();
for file_path in sorted_files {
let original = match std::fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
errors.push(format!("Failed to read '{}': {}", file_path.display(), e));
continue;
}
};
let key = file_path.to_str().unwrap_or("");
let mut def_ranges: Vec<(usize, usize)> =
matches_by_file.get(key).cloned().unwrap_or_default();
if let Some(imp_ranges) = impact_ranges.get(key) {
for r in imp_ranges {
if !def_ranges.contains(r) {
def_ranges.push(*r);
}
}
}
if def_ranges.is_empty() {
continue;
}
let modified = replace_near_definitions(&original, old_name, new_name, &def_ranges);
if modified != original {
pending_writes.push((file_path.clone(), original, modified));
}
}
(pending_writes, errors)
}
fn apply_writes_with_rollback(
pending_writes: Vec<(PathBuf, String, String)>,
) -> (usize, Vec<(PathBuf, String, String)>, Vec<String>) {
let mut total_changes = 0usize;
let mut modified_files: Vec<(PathBuf, String, String)> = Vec::new();
let mut errors = Vec::new();
for (file_path, original_content, modified) in &pending_writes {
match std::fs::write(file_path, modified.as_bytes()) {
Ok(()) => {
total_changes += 1;
modified_files.push((
file_path.clone(),
original_content.clone(),
modified.clone(),
));
}
Err(e) => {
errors.push(format!("Failed to write '{}': {}", file_path.display(), e));
if let Err(restore_err) = std::fs::write(file_path, original_content.as_bytes()) {
tracing::error!(
"CRITICAL: Failed to restore failed file '{}' during rollback: {}",
file_path.display(),
restore_err
);
}
for (prev_path, prev_original, _prev_modified) in &modified_files {
if let Err(restore_err) = std::fs::write(prev_path, prev_original.as_bytes()) {
tracing::error!(
"CRITICAL: Failed to restore '{}' during rollback: {}",
prev_path.display(),
restore_err
);
}
}
break;
}
}
}
(total_changes, modified_files, errors)
}