1use proc_macro2::LineColumn;
5use std::cmp::Ordering;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Replacement {
10 pub start: LineColumn,
12 pub end: LineColumn,
14 pub new_text: String,
16}
17
18impl Replacement {
19 pub fn new(start: LineColumn, end: LineColumn, new_text: String) -> Self {
20 Self {
21 start,
22 end,
23 new_text,
24 }
25 }
26}
27
28impl Ord for Replacement {
29 fn cmp(&self, other: &Self) -> Ordering {
30 match self.start.line.cmp(&other.start.line) {
32 Ordering::Equal => self.start.column.cmp(&other.start.column),
33 other => other,
34 }
35 }
36}
37
38impl PartialOrd for Replacement {
39 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
40 Some(self.cmp(other))
41 }
42}
43
44pub fn apply_surgical_edits(
74 original_source: &str,
75 mut replacements: Vec<Replacement>,
76) -> String {
77 if replacements.is_empty() {
78 return original_source.to_string();
79 }
80
81 replacements.sort();
83
84 for i in 1..replacements.len() {
86 let prev = &replacements[i - 1];
87 let curr = &replacements[i];
88
89 if prev.end.line > curr.start.line ||
90 (prev.end.line == curr.start.line && prev.end.column > curr.start.column) {
91 panic!("Overlapping replacements detected: {:?} and {:?}", prev, curr);
92 }
93 }
94
95 let lines: Vec<&str> = original_source.lines().collect();
96 let mut result = String::new();
97
98 let mut current_line = 1usize; let mut current_col = 0usize; for replacement in replacements {
102 while current_line < replacement.start.line {
106 if current_line <= lines.len() {
107 if let Some(line) = lines.get(current_line - 1) {
109 if current_col < line.len() {
110 result.push_str(&line[current_col..]);
111 }
112 }
113 result.push('\n');
114 }
115 current_line += 1;
116 current_col = 0;
117 }
118
119 if current_line == replacement.start.line {
121 if let Some(line) = lines.get(current_line - 1) {
122 if current_col < replacement.start.column && replacement.start.column <= line.len() {
123 result.push_str(&line[current_col..replacement.start.column]);
124 }
125 }
126 }
127
128 result.push_str(&replacement.new_text);
130
131 current_line = replacement.end.line;
133 current_col = replacement.end.column;
134 }
135
136 while current_line <= lines.len() {
138 if let Some(line) = lines.get(current_line - 1) {
139 if current_col < line.len() {
140 result.push_str(&line[current_col..]);
141 }
142 }
143 if current_line < lines.len() {
144 result.push('\n');
145 }
146 current_line += 1;
147 current_col = 0;
148 }
149
150 result
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_single_replacement() {
159 let source = "fn foo() {\n let x = 1;\n}";
160 let replacements = vec![
161 Replacement::new(
162 LineColumn { line: 2, column: 12 },
163 LineColumn { line: 2, column: 13 },
164 "42".to_string(),
165 ),
166 ];
167
168 let result = apply_surgical_edits(source, replacements);
169 assert_eq!(result, "fn foo() {\n let x = 42;\n}");
170 }
171
172 #[test]
173 fn test_multiple_replacements() {
174 let source = "let a = 1;\nlet b = 2;";
175 let replacements = vec![
176 Replacement::new(
177 LineColumn { line: 1, column: 8 },
178 LineColumn { line: 1, column: 9 },
179 "10".to_string(),
180 ),
181 Replacement::new(
182 LineColumn { line: 2, column: 8 },
183 LineColumn { line: 2, column: 9 },
184 "20".to_string(),
185 ),
186 ];
187
188 let result = apply_surgical_edits(source, replacements);
189 assert_eq!(result, "let a = 10;\nlet b = 20;");
190 }
191
192 #[test]
193 fn test_preserves_whitespace() {
194 let source = "fn foo() {\n\n // comment\n let x = old;\n}";
195 let replacements = vec![
196 Replacement::new(
197 LineColumn { line: 4, column: 12 },
198 LineColumn { line: 4, column: 15 },
199 "new".to_string(),
200 ),
201 ];
202
203 let result = apply_surgical_edits(source, replacements);
204 assert_eq!(result, "fn foo() {\n\n // comment\n let x = new;\n}");
205 }
206
207 #[test]
208 fn test_no_replacements() {
209 let source = "fn foo() {}\n";
210 let replacements = vec![];
211
212 let result = apply_surgical_edits(source, replacements);
213 assert_eq!(result, source);
214 }
215
216 #[test]
217 fn test_replacement_sorting() {
218 let source = "let a = 1; let b = 2;";
219 let replacements = vec![
221 Replacement::new(
222 LineColumn { line: 1, column: 19 },
223 LineColumn { line: 1, column: 20 },
224 "20".to_string(),
225 ),
226 Replacement::new(
227 LineColumn { line: 1, column: 8 },
228 LineColumn { line: 1, column: 9 },
229 "10".to_string(),
230 ),
231 ];
232
233 let result = apply_surgical_edits(source, replacements);
234 assert_eq!(result, "let a = 10; let b = 20;");
235 }
236}