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