replace.rs

  1use language::{BufferSnapshot, Diff, Point, ToOffset};
  2use project::search::SearchQuery;
  3use util::{paths::PathMatcher, ResultExt as _};
  4
  5/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
  6/// Uses the search functionality to locate the first occurrence of the exact string.
  7/// Returns None if no exact match is found in the buffer.
  8pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option<Diff> {
  9    let query = SearchQuery::text(
 10        old,
 11        false,
 12        true,
 13        true,
 14        PathMatcher::new(&[]).ok()?,
 15        PathMatcher::new(&[]).ok()?,
 16        None,
 17    )
 18    .log_err()?;
 19
 20    let matches = query.search(&snapshot, None).await;
 21
 22    if matches.is_empty() {
 23        return None;
 24    }
 25
 26    let edit_range = matches[0].clone();
 27    let diff = language::text_diff(&old, &new);
 28
 29    let edits = diff
 30        .into_iter()
 31        .map(|(old_range, text)| {
 32            let start = edit_range.start + old_range.start;
 33            let end = edit_range.start + old_range.end;
 34            (start..end, text)
 35        })
 36        .collect::<Vec<_>>();
 37
 38    let diff = language::Diff {
 39        base_version: snapshot.version().clone(),
 40        line_ending: snapshot.line_ending(),
 41        edits,
 42    };
 43
 44    Some(diff)
 45}
 46
 47/// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences.
 48/// When replacing, preserves the indentation level found in the buffer at each matching line.
 49/// Returns None if no match found or if indentation is offset inconsistently across matched lines.
 50pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option<Diff> {
 51    let (old_lines, old_min_indent) = lines_with_min_indent(old);
 52    let (new_lines, new_min_indent) = lines_with_min_indent(new);
 53    let min_indent = old_min_indent.min(new_min_indent);
 54
 55    let old_lines = drop_lines_prefix(&old_lines, min_indent);
 56    let new_lines = drop_lines_prefix(&new_lines, min_indent);
 57
 58    let max_row = buffer.max_point().row;
 59
 60    'windows: for start_row in 0..max_row.saturating_sub(old_lines.len() as u32 - 1) {
 61        let mut common_leading = None;
 62
 63        let end_row = start_row + old_lines.len() as u32 - 1;
 64
 65        if end_row > max_row {
 66            // The buffer ends before fully matching the pattern
 67            return None;
 68        }
 69
 70        let start_point = Point::new(start_row, 0);
 71        let end_point = Point::new(end_row, buffer.line_len(end_row));
 72        let range = start_point.to_offset(buffer)..end_point.to_offset(buffer);
 73
 74        let window_text = buffer.text_for_range(range.clone());
 75        let mut window_lines = window_text.lines();
 76        let mut old_lines_iter = old_lines.iter();
 77
 78        while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
 79        {
 80            let line_trimmed = window_line.trim_start();
 81
 82            if line_trimmed != old_line.trim_start() {
 83                continue 'windows;
 84            }
 85
 86            if line_trimmed.is_empty() {
 87                continue;
 88            }
 89
 90            let line_leading = &window_line[..window_line.len() - old_line.len()];
 91
 92            match &common_leading {
 93                Some(common_leading) if common_leading != line_leading => {
 94                    continue 'windows;
 95                }
 96                Some(_) => (),
 97                None => common_leading = Some(line_leading.to_string()),
 98            }
 99        }
100
101        if let Some(common_leading) = common_leading {
102            let line_ending = buffer.line_ending();
103            let replacement = new_lines
104                .iter()
105                .map(|new_line| {
106                    if new_line.trim().is_empty() {
107                        new_line.to_string()
108                    } else {
109                        common_leading.to_string() + new_line
110                    }
111                })
112                .collect::<Vec<_>>()
113                .join(line_ending.as_str());
114
115            let diff = Diff {
116                base_version: buffer.version().clone(),
117                line_ending,
118                edits: vec![(range, replacement.into())],
119            };
120
121            return Some(diff);
122        }
123    }
124
125    None
126}
127
128fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {
129    lines
130        .iter()
131        .map(|line| line.get(prefix_len..).unwrap_or(""))
132        .collect()
133}
134
135fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
136    let mut lines = Vec::new();
137    let mut min_indent: Option<usize> = None;
138
139    for line in input.lines() {
140        lines.push(line);
141        if !line.trim().is_empty() {
142            let indent = line.len() - line.trim_start().len();
143            min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
144        }
145    }
146
147    (lines, min_indent.unwrap_or(0))
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use gpui::prelude::*;
154    use gpui::TestAppContext;
155    use unindent::Unindent;
156
157    #[gpui::test]
158    fn test_replace_consistent_indentation(cx: &mut TestAppContext) {
159        let whole = r#"
160            fn test() {
161                let x = 5;
162                println!("x = {}", x);
163                let y = 10;
164            }
165        "#
166        .unindent();
167
168        let old = r#"
169            let x = 5;
170            println!("x = {}", x);
171        "#
172        .unindent();
173
174        let new = r#"
175            let x = 42;
176            println!("New value: {}", x);
177        "#
178        .unindent();
179
180        let expected = r#"
181            fn test() {
182                let x = 42;
183                println!("New value: {}", x);
184                let y = 10;
185            }
186        "#
187        .unindent();
188
189        assert_eq!(
190            test_replace_with_flexible_indent(cx, &whole, &old, &new),
191            Some(expected.to_string())
192        );
193    }
194
195    #[gpui::test]
196    fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
197        let whole = r#"
198            fn test() {
199                if condition {
200                    println!("{}", 43);
201                }
202            }
203        "#
204        .unindent();
205
206        let old = r#"
207            if condition {
208            println!("{}", 43);
209        "#
210        .unindent();
211
212        let new = r#"
213            if condition {
214            println!("{}", 42);
215        "#
216        .unindent();
217
218        assert_eq!(
219            test_replace_with_flexible_indent(cx, &whole, &old, &new),
220            None
221        );
222    }
223
224    #[gpui::test]
225    fn test_replace_with_empty_lines(cx: &mut TestAppContext) {
226        // Test with empty lines
227        let whole = r#"
228            fn test() {
229                let x = 5;
230
231                println!("x = {}", x);
232            }
233        "#
234        .unindent();
235
236        let old = r#"
237            let x = 5;
238
239            println!("x = {}", x);
240        "#
241        .unindent();
242
243        let new = r#"
244            let x = 10;
245
246            println!("New x: {}", x);
247        "#
248        .unindent();
249
250        let expected = r#"
251            fn test() {
252                let x = 10;
253
254                println!("New x: {}", x);
255            }
256        "#
257        .unindent();
258
259        assert_eq!(
260            test_replace_with_flexible_indent(cx, &whole, &old, &new),
261            Some(expected.to_string())
262        );
263    }
264
265    #[gpui::test]
266    fn test_replace_no_match(cx: &mut TestAppContext) {
267        // Test with no match
268        let whole = r#"
269            fn test() {
270                let x = 5;
271            }
272        "#
273        .unindent();
274
275        let old = r#"
276            let y = 10;
277        "#
278        .unindent();
279
280        let new = r#"
281            let y = 20;
282        "#
283        .unindent();
284
285        assert_eq!(
286            test_replace_with_flexible_indent(cx, &whole, &old, &new),
287            None
288        );
289    }
290
291    #[gpui::test]
292    fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {
293        let whole = r#"
294            fn test() {
295                let x = 5;
296        "#
297        .unindent();
298
299        let old = r#"
300            let x = 5;
301            println!("x = {}", x);
302        "#
303        .unindent();
304
305        let new = r#"
306            let x = 10;
307            println!("x = {}", x);
308        "#
309        .unindent();
310
311        // Should return None because whole doesn't fully contain the old text
312        assert_eq!(
313            test_replace_with_flexible_indent(cx, &whole, &old, &new),
314            None
315        );
316    }
317
318    #[test]
319    fn test_lines_with_min_indent() {
320        // Empty string
321        assert_eq!(lines_with_min_indent(""), (vec![], 0));
322
323        // Single line without indentation
324        assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0));
325
326        // Multiple lines with no indentation
327        assert_eq!(
328            lines_with_min_indent("line1\nline2\nline3"),
329            (vec!["line1", "line2", "line3"], 0)
330        );
331
332        // Multiple lines with consistent indentation
333        assert_eq!(
334            lines_with_min_indent("  line1\n  line2\n  line3"),
335            (vec!["  line1", "  line2", "  line3"], 2)
336        );
337
338        // Multiple lines with varying indentation
339        assert_eq!(
340            lines_with_min_indent("    line1\n  line2\n      line3"),
341            (vec!["    line1", "  line2", "      line3"], 2)
342        );
343
344        // Lines with mixed indentation and empty lines
345        assert_eq!(
346            lines_with_min_indent("    line1\n\n  line2"),
347            (vec!["    line1", "", "  line2"], 2)
348        );
349    }
350
351    #[gpui::test]
352    fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) {
353        let whole = r#"
354            fn test() {
355                if true {
356                        let x = 5;
357                        println!("x = {}", x);
358                }
359            }
360        "#
361        .unindent();
362
363        let old = r#"
364            let x = 5;
365            println!("x = {}", x);
366        "#
367        .unindent();
368
369        let new = r#"
370            let x = 42;
371            println!("x = {}", x);
372        "#
373        .unindent();
374
375        let expected = r#"
376            fn test() {
377                if true {
378                        let x = 42;
379                        println!("x = {}", x);
380                }
381            }
382        "#
383        .unindent();
384
385        assert_eq!(
386            test_replace_with_flexible_indent(cx, &whole, &old, &new),
387            Some(expected.to_string())
388        );
389    }
390
391    #[gpui::test]
392    fn test_replace_big_example(cx: &mut TestAppContext) {
393        let whole = r#"
394            #[cfg(test)]
395            mod tests {
396                use super::*;
397
398                #[test]
399                fn test_is_valid_age() {
400                    assert!(is_valid_age(0));
401                    assert!(!is_valid_age(151));
402                }
403            }
404        "#
405        .unindent();
406
407        let old = r#"
408            #[test]
409            fn test_is_valid_age() {
410                assert!(is_valid_age(0));
411                assert!(!is_valid_age(151));
412            }
413        "#
414        .unindent();
415
416        let new = r#"
417            #[test]
418            fn test_is_valid_age() {
419                assert!(is_valid_age(0));
420                assert!(!is_valid_age(151));
421            }
422
423            #[test]
424            fn test_group_people_by_age() {
425                let people = vec![
426                    Person::new("Young One", 5, "young@example.com").unwrap(),
427                    Person::new("Teen One", 15, "teen@example.com").unwrap(),
428                    Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
429                    Person::new("Adult One", 25, "adult@example.com").unwrap(),
430                ];
431
432                let groups = group_people_by_age(&people);
433
434                assert_eq!(groups.get(&0).unwrap().len(), 1);  // One person in 0-9
435                assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
436                assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
437            }
438        "#
439        .unindent();
440        let expected = r#"
441            #[cfg(test)]
442            mod tests {
443                use super::*;
444
445                #[test]
446                fn test_is_valid_age() {
447                    assert!(is_valid_age(0));
448                    assert!(!is_valid_age(151));
449                }
450
451                #[test]
452                fn test_group_people_by_age() {
453                    let people = vec![
454                        Person::new("Young One", 5, "young@example.com").unwrap(),
455                        Person::new("Teen One", 15, "teen@example.com").unwrap(),
456                        Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
457                        Person::new("Adult One", 25, "adult@example.com").unwrap(),
458                    ];
459
460                    let groups = group_people_by_age(&people);
461
462                    assert_eq!(groups.get(&0).unwrap().len(), 1);  // One person in 0-9
463                    assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
464                    assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
465                }
466            }
467        "#
468        .unindent();
469        assert_eq!(
470            test_replace_with_flexible_indent(cx, &whole, &old, &new),
471            Some(expected.to_string())
472        );
473    }
474
475    #[test]
476    fn test_drop_lines_prefix() {
477        // Empty array
478        assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new());
479
480        // Zero prefix length
481        assert_eq!(
482            drop_lines_prefix(&["line1", "line2"], 0),
483            vec!["line1", "line2"]
484        );
485
486        // Normal prefix drop
487        assert_eq!(
488            drop_lines_prefix(&["  line1", "  line2"], 2),
489            vec!["line1", "line2"]
490        );
491
492        // Prefix longer than some lines
493        assert_eq!(drop_lines_prefix(&["  line1", "a"], 2), vec!["line1", ""]);
494
495        // Prefix longer than all lines
496        assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]);
497
498        // Mixed length lines
499        assert_eq!(
500            drop_lines_prefix(&["    line1", "  line2", "      line3"], 2),
501            vec!["  line1", "line2", "    line3"]
502        );
503    }
504
505    fn test_replace_with_flexible_indent(
506        cx: &mut TestAppContext,
507        whole: &str,
508        old: &str,
509        new: &str,
510    ) -> Option<String> {
511        // Create a local buffer with the test content
512        let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
513
514        // Get the buffer snapshot
515        let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
516
517        // Call replace_flexible and transform the result
518        replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
519            buffer.update(cx, |buffer, cx| {
520                let _ = buffer.apply_diff(diff, cx);
521                buffer.text()
522            })
523        })
524    }
525}