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}