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 + 1 {
 63        let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
 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        let mut common_mismatch = None;
 79
 80        #[derive(Eq, PartialEq)]
 81        enum Mismatch {
 82            OverIndented(String),
 83            UnderIndented(String),
 84        }
 85
 86        while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
 87        {
 88            let line_trimmed = window_line.trim_start();
 89
 90            if line_trimmed != old_line.trim_start() {
 91                continue 'windows;
 92            }
 93
 94            if line_trimmed.is_empty() {
 95                continue;
 96            }
 97
 98            let line_mismatch = if window_line.len() > old_line.len() {
 99                let prefix = window_line[..window_line.len() - old_line.len()].to_string();
100                Mismatch::UnderIndented(prefix)
101            } else {
102                let prefix = old_line[..old_line.len() - window_line.len()].to_string();
103                Mismatch::OverIndented(prefix)
104            };
105
106            match &common_mismatch {
107                Some(common_mismatch) if common_mismatch != &line_mismatch => {
108                    continue 'windows;
109                }
110                Some(_) => (),
111                None => common_mismatch = Some(line_mismatch),
112            }
113        }
114
115        if let Some(common_mismatch) = &common_mismatch {
116            let line_ending = buffer.line_ending();
117            let replacement = new_lines
118                .iter()
119                .map(|new_line| {
120                    if new_line.trim().is_empty() {
121                        new_line.to_string()
122                    } else {
123                        match common_mismatch {
124                            Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
125                            Mismatch::OverIndented(prefix) => new_line
126                                .strip_prefix(prefix)
127                                .unwrap_or(new_line)
128                                .to_string(),
129                        }
130                    }
131                })
132                .collect::<Vec<_>>()
133                .join(line_ending.as_str());
134
135            let diff = Diff {
136                base_version: buffer.version().clone(),
137                line_ending,
138                edits: vec![(range, replacement.into())],
139            };
140
141            return Some(diff);
142        }
143    }
144
145    None
146}
147
148fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {
149    lines
150        .iter()
151        .map(|line| line.get(prefix_len..).unwrap_or(""))
152        .collect()
153}
154
155fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
156    let mut lines = Vec::new();
157    let mut min_indent: Option<usize> = None;
158
159    for line in input.lines() {
160        lines.push(line);
161        if !line.trim().is_empty() {
162            let indent = line.len() - line.trim_start().len();
163            min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
164        }
165    }
166
167    (lines, min_indent.unwrap_or(0))
168}
169
170#[cfg(test)]
171mod replace_exact_tests {
172    use super::*;
173    use gpui::TestAppContext;
174    use gpui::prelude::*;
175
176    #[gpui::test]
177    async fn basic(cx: &mut TestAppContext) {
178        let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
179        assert_eq!(result, Some("let x = 42;".to_string()));
180    }
181
182    #[gpui::test]
183    async fn no_match(cx: &mut TestAppContext) {
184        let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
185        assert_eq!(result, None);
186    }
187
188    #[gpui::test]
189    async fn multi_line(cx: &mut TestAppContext) {
190        let whole = "fn example() {\n    let x = 41;\n    println!(\"x = {}\", x);\n}";
191        let old_text = "    let x = 41;\n    println!(\"x = {}\", x);";
192        let new_text = "    let x = 42;\n    println!(\"x = {}\", x);";
193        let result = test_replace_exact(cx, whole, old_text, new_text).await;
194        assert_eq!(
195            result,
196            Some("fn example() {\n    let x = 42;\n    println!(\"x = {}\", x);\n}".to_string())
197        );
198    }
199
200    #[gpui::test]
201    async fn multiple_occurrences(cx: &mut TestAppContext) {
202        let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
203        let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
204        assert_eq!(
205            result,
206            Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
207        );
208    }
209
210    #[gpui::test]
211    async fn empty_buffer(cx: &mut TestAppContext) {
212        let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
213        assert_eq!(result, None);
214    }
215
216    #[gpui::test]
217    async fn partial_match(cx: &mut TestAppContext) {
218        let whole = "let x = 41; let y = 42;";
219        let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
220        assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
221    }
222
223    #[gpui::test]
224    async fn whitespace_sensitive(cx: &mut TestAppContext) {
225        let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
226        assert_eq!(result, None);
227    }
228
229    #[gpui::test]
230    async fn entire_buffer(cx: &mut TestAppContext) {
231        let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
232        assert_eq!(result, Some("let x = 42;".to_string()));
233    }
234
235    async fn test_replace_exact(
236        cx: &mut TestAppContext,
237        whole: &str,
238        old: &str,
239        new: &str,
240    ) -> Option<String> {
241        let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
242
243        let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
244
245        let diff = replace_exact(old, new, &buffer_snapshot).await;
246        diff.map(|diff| {
247            buffer.update(cx, |buffer, cx| {
248                let _ = buffer.apply_diff(diff, cx);
249                buffer.text()
250            })
251        })
252    }
253}
254
255#[cfg(test)]
256mod flexible_indent_tests {
257    use super::*;
258    use gpui::TestAppContext;
259    use gpui::prelude::*;
260    use unindent::Unindent;
261
262    #[gpui::test]
263    fn test_underindented_single_line(cx: &mut TestAppContext) {
264        let cur = "        let a = 41;".to_string();
265        let old = "    let a = 41;".to_string();
266        let new = "    let a = 42;".to_string();
267        let exp = "        let a = 42;".to_string();
268
269        let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
270
271        assert_eq!(result, Some(exp.to_string()))
272    }
273
274    #[gpui::test]
275    fn test_overindented_single_line(cx: &mut TestAppContext) {
276        let cur = "    let a = 41;".to_string();
277        let old = "        let a = 41;".to_string();
278        let new = "        let a = 42;".to_string();
279        let exp = "    let a = 42;".to_string();
280
281        let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
282
283        assert_eq!(result, Some(exp.to_string()))
284    }
285
286    #[gpui::test]
287    fn test_underindented_multi_line(cx: &mut TestAppContext) {
288        let whole = r#"
289            fn test() {
290                let x = 5;
291                println!("x = {}", x);
292                let y = 10;
293            }
294        "#
295        .unindent();
296
297        let old = r#"
298            let x = 5;
299            println!("x = {}", x);
300        "#
301        .unindent();
302
303        let new = r#"
304            let x = 42;
305            println!("New value: {}", x);
306        "#
307        .unindent();
308
309        let expected = r#"
310            fn test() {
311                let x = 42;
312                println!("New value: {}", x);
313                let y = 10;
314            }
315        "#
316        .unindent();
317
318        assert_eq!(
319            test_replace_with_flexible_indent(cx, &whole, &old, &new),
320            Some(expected.to_string())
321        );
322    }
323
324    #[gpui::test]
325    fn test_overindented_multi_line(cx: &mut TestAppContext) {
326        let cur = r#"
327            fn foo() {
328                let a = 41;
329                let b = 3.13;
330            }
331        "#
332        .unindent();
333
334        // 6 space indent instead of 4
335        let old = "      let a = 41;\n      let b = 3.13;";
336        let new = "      let a = 42;\n      let b = 3.14;";
337
338        let expected = r#"
339            fn foo() {
340                let a = 42;
341                let b = 3.14;
342            }
343        "#
344        .unindent();
345
346        let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
347
348        assert_eq!(result, Some(expected.to_string()))
349    }
350
351    #[gpui::test]
352    fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
353        let whole = r#"
354            fn test() {
355                if condition {
356                    println!("{}", 43);
357                }
358            }
359        "#
360        .unindent();
361
362        let old = r#"
363            if condition {
364            println!("{}", 43);
365        "#
366        .unindent();
367
368        let new = r#"
369            if condition {
370            println!("{}", 42);
371        "#
372        .unindent();
373
374        assert_eq!(
375            test_replace_with_flexible_indent(cx, &whole, &old, &new),
376            None
377        );
378    }
379
380    #[gpui::test]
381    fn test_replace_with_empty_lines(cx: &mut TestAppContext) {
382        // Test with empty lines
383        let whole = r#"
384            fn test() {
385                let x = 5;
386
387                println!("x = {}", x);
388            }
389        "#
390        .unindent();
391
392        let old = r#"
393            let x = 5;
394
395            println!("x = {}", x);
396        "#
397        .unindent();
398
399        let new = r#"
400            let x = 10;
401
402            println!("New x: {}", x);
403        "#
404        .unindent();
405
406        let expected = r#"
407            fn test() {
408                let x = 10;
409
410                println!("New x: {}", x);
411            }
412        "#
413        .unindent();
414
415        assert_eq!(
416            test_replace_with_flexible_indent(cx, &whole, &old, &new),
417            Some(expected.to_string())
418        );
419    }
420
421    #[gpui::test]
422    fn test_replace_no_match(cx: &mut TestAppContext) {
423        let whole = r#"
424            fn test() {
425                let x = 5;
426            }
427        "#
428        .unindent();
429
430        let old = r#"
431            let y = 10;
432        "#
433        .unindent();
434
435        let new = r#"
436            let y = 20;
437        "#
438        .unindent();
439
440        assert_eq!(
441            test_replace_with_flexible_indent(cx, &whole, &old, &new),
442            None
443        );
444    }
445
446    #[gpui::test]
447    fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {
448        let whole = r#"
449            fn test() {
450                let x = 5;
451        "#
452        .unindent();
453
454        let old = r#"
455            let x = 5;
456            println!("x = {}", x);
457        "#
458        .unindent();
459
460        let new = r#"
461            let x = 10;
462            println!("x = {}", x);
463        "#
464        .unindent();
465
466        // Should return None because whole doesn't fully contain the old text
467        assert_eq!(
468            test_replace_with_flexible_indent(cx, &whole, &old, &new),
469            None
470        );
471    }
472
473    #[gpui::test]
474    fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
475        let whole = r#"
476            let x = 5;
477        "#
478        .unindent();
479
480        let old = r#"
481            let x = 5;
482            let y = 10;
483        "#
484        .unindent();
485
486        let new = r#"
487            let x = 5;
488            let y = 20;
489        "#
490        .unindent();
491
492        assert_eq!(
493            test_replace_with_flexible_indent(cx, &whole, &old, &new),
494            None
495        );
496    }
497
498    #[gpui::test]
499    fn test_replace_old_is_empty(cx: &mut TestAppContext) {
500        let whole = r#"
501            fn test() {
502                let x = 5;
503            }
504        "#
505        .unindent();
506
507        let old = "";
508        let new = r#"
509            let y = 10;
510        "#
511        .unindent();
512
513        assert_eq!(
514            test_replace_with_flexible_indent(cx, &whole, &old, &new),
515            None
516        );
517    }
518
519    #[gpui::test]
520    fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
521        let whole = "";
522        let old = r#"
523            let x = 5;
524        "#
525        .unindent();
526
527        let new = r#"
528            let x = 10;
529        "#
530        .unindent();
531
532        assert_eq!(
533            test_replace_with_flexible_indent(cx, &whole, &old, &new),
534            None
535        );
536    }
537
538    #[test]
539    fn test_lines_with_min_indent() {
540        // Empty string
541        assert_eq!(lines_with_min_indent(""), (vec![], 0));
542
543        // Single line without indentation
544        assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0));
545
546        // Multiple lines with no indentation
547        assert_eq!(
548            lines_with_min_indent("line1\nline2\nline3"),
549            (vec!["line1", "line2", "line3"], 0)
550        );
551
552        // Multiple lines with consistent indentation
553        assert_eq!(
554            lines_with_min_indent("  line1\n  line2\n  line3"),
555            (vec!["  line1", "  line2", "  line3"], 2)
556        );
557
558        // Multiple lines with varying indentation
559        assert_eq!(
560            lines_with_min_indent("    line1\n  line2\n      line3"),
561            (vec!["    line1", "  line2", "      line3"], 2)
562        );
563
564        // Lines with mixed indentation and empty lines
565        assert_eq!(
566            lines_with_min_indent("    line1\n\n  line2"),
567            (vec!["    line1", "", "  line2"], 2)
568        );
569    }
570
571    #[gpui::test]
572    fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) {
573        let whole = r#"
574            fn test() {
575                if true {
576                        let x = 5;
577                        println!("x = {}", x);
578                }
579            }
580        "#
581        .unindent();
582
583        let old = r#"
584            let x = 5;
585            println!("x = {}", x);
586        "#
587        .unindent();
588
589        let new = r#"
590            let x = 42;
591            println!("x = {}", x);
592        "#
593        .unindent();
594
595        let expected = r#"
596            fn test() {
597                if true {
598                        let x = 42;
599                        println!("x = {}", x);
600                }
601            }
602        "#
603        .unindent();
604
605        assert_eq!(
606            test_replace_with_flexible_indent(cx, &whole, &old, &new),
607            Some(expected.to_string())
608        );
609    }
610
611    #[gpui::test]
612    fn test_replace_big_example(cx: &mut TestAppContext) {
613        let whole = r#"
614            #[cfg(test)]
615            mod tests {
616                use super::*;
617
618                #[test]
619                fn test_is_valid_age() {
620                    assert!(is_valid_age(0));
621                    assert!(!is_valid_age(151));
622                }
623            }
624        "#
625        .unindent();
626
627        let old = r#"
628            #[test]
629            fn test_is_valid_age() {
630                assert!(is_valid_age(0));
631                assert!(!is_valid_age(151));
632            }
633        "#
634        .unindent();
635
636        let new = r#"
637            #[test]
638            fn test_is_valid_age() {
639                assert!(is_valid_age(0));
640                assert!(!is_valid_age(151));
641            }
642
643            #[test]
644            fn test_group_people_by_age() {
645                let people = vec![
646                    Person::new("Young One", 5, "young@example.com").unwrap(),
647                    Person::new("Teen One", 15, "teen@example.com").unwrap(),
648                    Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
649                    Person::new("Adult One", 25, "adult@example.com").unwrap(),
650                ];
651
652                let groups = group_people_by_age(&people);
653
654                assert_eq!(groups.get(&0).unwrap().len(), 1);  // One person in 0-9
655                assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
656                assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
657            }
658        "#
659        .unindent();
660        let expected = r#"
661            #[cfg(test)]
662            mod tests {
663                use super::*;
664
665                #[test]
666                fn test_is_valid_age() {
667                    assert!(is_valid_age(0));
668                    assert!(!is_valid_age(151));
669                }
670
671                #[test]
672                fn test_group_people_by_age() {
673                    let people = vec![
674                        Person::new("Young One", 5, "young@example.com").unwrap(),
675                        Person::new("Teen One", 15, "teen@example.com").unwrap(),
676                        Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
677                        Person::new("Adult One", 25, "adult@example.com").unwrap(),
678                    ];
679
680                    let groups = group_people_by_age(&people);
681
682                    assert_eq!(groups.get(&0).unwrap().len(), 1);  // One person in 0-9
683                    assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
684                    assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
685                }
686            }
687        "#
688        .unindent();
689        assert_eq!(
690            test_replace_with_flexible_indent(cx, &whole, &old, &new),
691            Some(expected.to_string())
692        );
693    }
694
695    #[test]
696    fn test_drop_lines_prefix() {
697        // Empty array
698        assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new());
699
700        // Zero prefix length
701        assert_eq!(
702            drop_lines_prefix(&["line1", "line2"], 0),
703            vec!["line1", "line2"]
704        );
705
706        // Normal prefix drop
707        assert_eq!(
708            drop_lines_prefix(&["  line1", "  line2"], 2),
709            vec!["line1", "line2"]
710        );
711
712        // Prefix longer than some lines
713        assert_eq!(drop_lines_prefix(&["  line1", "a"], 2), vec!["line1", ""]);
714
715        // Prefix longer than all lines
716        assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]);
717
718        // Mixed length lines
719        assert_eq!(
720            drop_lines_prefix(&["    line1", "  line2", "      line3"], 2),
721            vec!["  line1", "line2", "    line3"]
722        );
723    }
724
725    #[gpui::test]
726    async fn test_replace_exact_basic(cx: &mut TestAppContext) {
727        let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
728        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
729
730        let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
731        assert!(diff.is_some());
732
733        let diff = diff.unwrap();
734        assert_eq!(diff.edits.len(), 1);
735
736        let result = buffer.update(cx, |buffer, cx| {
737            let _ = buffer.apply_diff(diff, cx);
738            buffer.text()
739        });
740
741        assert_eq!(result, "let x = 42;");
742    }
743
744    #[gpui::test]
745    async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
746        let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
747        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
748
749        let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
750        assert!(diff.is_none());
751    }
752
753    #[gpui::test]
754    async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
755        let buffer = cx.new(|cx| {
756            language::Buffer::local(
757                "fn example() {\n    let x = 41;\n    println!(\"x = {}\", x);\n}",
758                cx,
759            )
760        });
761        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
762
763        let old_text = "    let x = 41;\n    println!(\"x = {}\", x);";
764        let new_text = "    let x = 42;\n    println!(\"x = {}\", x);";
765        let diff = replace_exact(old_text, new_text, &snapshot).await;
766        assert!(diff.is_some());
767
768        let diff = diff.unwrap();
769        let result = buffer.update(cx, |buffer, cx| {
770            let _ = buffer.apply_diff(diff, cx);
771            buffer.text()
772        });
773
774        assert_eq!(
775            result,
776            "fn example() {\n    let x = 42;\n    println!(\"x = {}\", x);\n}"
777        );
778    }
779
780    #[gpui::test]
781    async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
782        let buffer =
783            cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
784        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
785
786        // Should replace only the first occurrence
787        let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
788        assert!(diff.is_some());
789
790        let diff = diff.unwrap();
791        let result = buffer.update(cx, |buffer, cx| {
792            let _ = buffer.apply_diff(diff, cx);
793            buffer.text()
794        });
795
796        assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
797    }
798
799    #[gpui::test]
800    async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
801        let buffer = cx.new(|cx| language::Buffer::local("", cx));
802        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
803
804        let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
805        assert!(diff.is_none());
806    }
807
808    #[gpui::test]
809    async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
810        let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
811        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
812
813        // Verify substring replacement actually works
814        let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
815        assert!(diff.is_some());
816
817        let diff = diff.unwrap();
818        let result = buffer.update(cx, |buffer, cx| {
819            let _ = buffer.apply_diff(diff, cx);
820            buffer.text()
821        });
822
823        assert_eq!(result, "let x = 42; let y = 42;");
824    }
825
826    #[gpui::test]
827    async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
828        let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
829        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
830
831        let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
832        assert!(diff.is_none());
833    }
834
835    #[gpui::test]
836    async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
837        let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
838        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
839
840        let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
841        assert!(diff.is_some());
842
843        let diff = diff.unwrap();
844        let result = buffer.update(cx, |buffer, cx| {
845            let _ = buffer.apply_diff(diff, cx);
846            buffer.text()
847        });
848
849        assert_eq!(result, "let x = 42;");
850    }
851
852    fn test_replace_with_flexible_indent(
853        cx: &mut TestAppContext,
854        whole: &str,
855        old: &str,
856        new: &str,
857    ) -> Option<String> {
858        // Create a local buffer with the test content
859        let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
860
861        // Get the buffer snapshot
862        let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
863
864        // Call replace_flexible and transform the result
865        replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
866            buffer.update(cx, |buffer, cx| {
867                let _ = buffer.apply_diff(diff, cx);
868                buffer.text()
869            })
870        })
871    }
872}