replace.rs

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