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