use proc_macro2::LineColumn;
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Replacement {
pub start: LineColumn,
pub end: LineColumn,
pub new_text: String,
}
impl Replacement {
pub fn new(start: LineColumn, end: LineColumn, new_text: String) -> Self {
Self {
start,
end,
new_text,
}
}
}
impl Ord for Replacement {
fn cmp(&self, other: &Self) -> Ordering {
match self.start.line.cmp(&other.start.line) {
Ordering::Equal => self.start.column.cmp(&other.start.column),
other => other,
}
}
}
impl PartialOrd for Replacement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn apply_surgical_edits(
original_source: &str,
mut replacements: Vec<Replacement>,
) -> String {
if replacements.is_empty() {
return original_source.to_string();
}
replacements.sort();
for i in 1..replacements.len() {
let prev = &replacements[i - 1];
let curr = &replacements[i];
if prev.end.line > curr.start.line ||
(prev.end.line == curr.start.line && prev.end.column > curr.start.column) {
panic!("Overlapping replacements detected: {:?} and {:?}", prev, curr);
}
}
let lines: Vec<&str> = original_source.lines().collect();
let mut result = String::new();
let mut current_line = 1usize; let mut current_col = 0usize;
for replacement in replacements {
while current_line < replacement.start.line {
if current_line <= lines.len() {
if let Some(line) = lines.get(current_line - 1) {
if current_col < line.len() {
result.push_str(&line[current_col..]);
}
}
result.push('\n');
}
current_line += 1;
current_col = 0;
}
if current_line == replacement.start.line {
if let Some(line) = lines.get(current_line - 1) {
if current_col < replacement.start.column && replacement.start.column <= line.len() {
result.push_str(&line[current_col..replacement.start.column]);
}
}
}
result.push_str(&replacement.new_text);
current_line = replacement.end.line;
current_col = replacement.end.column;
}
while current_line <= lines.len() {
if let Some(line) = lines.get(current_line - 1) {
if current_col < line.len() {
result.push_str(&line[current_col..]);
}
}
if current_line < lines.len() {
result.push('\n');
}
current_line += 1;
current_col = 0;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_replacement() {
let source = "fn foo() {\n let x = 1;\n}";
let replacements = vec![
Replacement::new(
LineColumn { line: 2, column: 12 },
LineColumn { line: 2, column: 13 },
"42".to_string(),
),
];
let result = apply_surgical_edits(source, replacements);
assert_eq!(result, "fn foo() {\n let x = 42;\n}");
}
#[test]
fn test_multiple_replacements() {
let source = "let a = 1;\nlet b = 2;";
let replacements = vec![
Replacement::new(
LineColumn { line: 1, column: 8 },
LineColumn { line: 1, column: 9 },
"10".to_string(),
),
Replacement::new(
LineColumn { line: 2, column: 8 },
LineColumn { line: 2, column: 9 },
"20".to_string(),
),
];
let result = apply_surgical_edits(source, replacements);
assert_eq!(result, "let a = 10;\nlet b = 20;");
}
#[test]
fn test_preserves_whitespace() {
let source = "fn foo() {\n\n // comment\n let x = old;\n}";
let replacements = vec![
Replacement::new(
LineColumn { line: 4, column: 12 },
LineColumn { line: 4, column: 15 },
"new".to_string(),
),
];
let result = apply_surgical_edits(source, replacements);
assert_eq!(result, "fn foo() {\n\n // comment\n let x = new;\n}");
}
#[test]
fn test_no_replacements() {
let source = "fn foo() {}\n";
let replacements = vec![];
let result = apply_surgical_edits(source, replacements);
assert_eq!(result, source);
}
#[test]
fn test_replacement_sorting() {
let source = "let a = 1; let b = 2;";
let replacements = vec![
Replacement::new(
LineColumn { line: 1, column: 19 },
LineColumn { line: 1, column: 20 },
"20".to_string(),
),
Replacement::new(
LineColumn { line: 1, column: 8 },
LineColumn { line: 1, column: 9 },
"10".to_string(),
),
];
let result = apply_surgical_edits(source, replacements);
assert_eq!(result, "let a = 10; let b = 20;");
}
}