use crate::error::{Result, SpliceError};
use crate::graph::magellan_integration::MagellanIntegration;
use crate::graph::rename::{apply_replacements_in_file, group_references_by_file};
use crate::patch::replace_span;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct PatchResult {
pub file: std::path::PathBuf,
pub symbol: String,
pub byte_start: usize,
pub byte_end: usize,
pub lines_added: usize,
}
#[derive(Debug, Clone)]
pub struct RenameResult {
pub files_changed: Vec<std::path::PathBuf>,
pub replacements: usize,
}
#[derive(Debug, Clone)]
pub struct SymbolSpan {
pub name: String,
pub file: std::path::PathBuf,
pub kind: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: Option<usize>,
pub end_line: Option<usize>,
}
impl From<crate::graph::magellan_integration::SymbolInfo> for SymbolSpan {
fn from(info: crate::graph::magellan_integration::SymbolInfo) -> Self {
SymbolSpan {
name: info.name,
file: std::path::PathBuf::from(info.file_path),
kind: info.kind,
byte_start: info.byte_start,
byte_end: info.byte_end,
start_line: info.start_line,
end_line: info.end_line,
}
}
}
pub fn resolve_symbol_span(
file_path: &Path,
symbol_name: &str,
db_path: &Path,
) -> Result<SymbolSpan> {
let mut integration = MagellanIntegration::open(db_path)?;
if let Ok(Some(info)) = integration.find_symbol_by_path_and_name(file_path, symbol_name) {
return Ok(SymbolSpan::from(info));
}
let matches = integration.find_symbol_by_name(symbol_name, false)?;
match matches.len() {
0 => Err(SpliceError::symbol_not_found(symbol_name, Some(file_path))),
1 => Ok(SymbolSpan::from(
matches.into_iter().next().expect("invariant: length is 1"),
)),
_ => Err(SpliceError::Other(format!(
"Symbol '{}' is ambiguous: {} matches found. Specify a file path to disambiguate.",
symbol_name,
matches.len()
))),
}
}
pub fn patch_symbol_in_file(
file_path: &Path,
symbol_name: &str,
new_content: &str,
db_path: &Path,
) -> Result<PatchResult> {
let span = resolve_symbol_span(file_path, symbol_name, db_path)?;
replace_span(&span.file, span.byte_start, span.byte_end, new_content)?;
Ok(PatchResult {
file: span.file,
symbol: span.name,
byte_start: span.byte_start,
byte_end: span.byte_start + new_content.len(),
lines_added: new_content.lines().count(),
})
}
pub fn rename_symbol_across_files(
old_name: &str,
new_name: &str,
db_path: &Path,
) -> Result<RenameResult> {
let mut integration = MagellanIntegration::open(db_path)?;
let matches = integration.find_symbol_by_name(old_name, true)?;
if matches.is_empty() {
return Err(SpliceError::symbol_not_found(old_name, None));
}
let mut all_refs = Vec::new();
for symbol_info in &matches {
if let Ok(refs) = integration.get_all_references(symbol_info.entity_id) {
all_refs.extend(refs);
}
}
if all_refs.is_empty() {
for symbol_info in &matches {
all_refs.push(magellan::references::ReferenceFact {
file_path: std::path::PathBuf::from(&symbol_info.file_path),
referenced_symbol: old_name.to_string(),
byte_start: symbol_info.byte_start,
byte_end: symbol_info.byte_end,
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
});
}
}
let by_file = group_references_by_file(&all_refs);
let mut files_changed = Vec::new();
let mut total_replacements = 0usize;
for (file_path, refs) in by_file {
match apply_replacements_in_file(&file_path, old_name, new_name, &refs) {
Ok(count) if count > 0 => {
files_changed.push(file_path);
total_replacements += count;
}
Ok(_) => {}
Err(_) => continue,
}
}
Ok(RenameResult {
files_changed,
replacements: total_replacements,
})
}
pub fn delete_symbol_from_file(
file_path: &Path,
symbol_name: &str,
db_path: &Path,
) -> Result<PatchResult> {
let span = resolve_symbol_span(file_path, symbol_name, db_path)?;
replace_span(&span.file, span.byte_start, span.byte_end, "")?;
Ok(PatchResult {
file: span.file,
symbol: span.name,
byte_start: span.byte_start,
byte_end: span.byte_start,
lines_added: 0,
})
}