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