1use anyhow::{Context as _, Result, anyhow};
2use collections::HashMap;
3use editor::ProposedChangesEditor;
4use futures::{TryFutureExt as _, future};
5use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
6use language::{AutoindentMode, Buffer, BufferSnapshot};
7use project::{Project, ProjectPath};
8use std::{cmp, ops::Range, path::Path, sync::Arc};
9use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
10
11#[derive(Clone, Debug)]
12pub struct AssistantPatch {
13 pub range: Range<language::Anchor>,
14 pub title: SharedString,
15 pub edits: Arc<[Result<AssistantEdit>]>,
16 pub status: AssistantPatchStatus,
17}
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
20pub enum AssistantPatchStatus {
21 Pending,
22 Ready,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct AssistantEdit {
27 pub path: String,
28 pub kind: AssistantEditKind,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub enum AssistantEditKind {
33 Update {
34 old_text: String,
35 new_text: String,
36 description: Option<String>,
37 },
38 Create {
39 new_text: String,
40 description: Option<String>,
41 },
42 InsertBefore {
43 old_text: String,
44 new_text: String,
45 description: Option<String>,
46 },
47 InsertAfter {
48 old_text: String,
49 new_text: String,
50 description: Option<String>,
51 },
52 Delete {
53 old_text: String,
54 },
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ResolvedPatch {
59 pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,
60 pub errors: Vec<AssistantPatchResolutionError>,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ResolvedEditGroup {
65 pub context_range: Range<language::Anchor>,
66 pub edits: Vec<ResolvedEdit>,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct ResolvedEdit {
71 range: Range<language::Anchor>,
72 new_text: String,
73 description: Option<String>,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct AssistantPatchResolutionError {
78 pub edit_ix: usize,
79 pub message: String,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
83enum SearchDirection {
84 Up,
85 Left,
86 Diagonal,
87}
88
89#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
90struct SearchState {
91 cost: u32,
92 direction: SearchDirection,
93}
94
95impl SearchState {
96 fn new(cost: u32, direction: SearchDirection) -> Self {
97 Self { cost, direction }
98 }
99}
100
101struct SearchMatrix {
102 cols: usize,
103 data: Vec<SearchState>,
104}
105
106impl SearchMatrix {
107 fn new(rows: usize, cols: usize) -> Self {
108 SearchMatrix {
109 cols,
110 data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
111 }
112 }
113
114 fn get(&self, row: usize, col: usize) -> SearchState {
115 self.data[row * self.cols + col]
116 }
117
118 fn set(&mut self, row: usize, col: usize, cost: SearchState) {
119 self.data[row * self.cols + col] = cost;
120 }
121}
122
123impl ResolvedPatch {
124 pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {
125 for (buffer, groups) in &self.edit_groups {
126 let branch = editor.branch_buffer_for_base(buffer).unwrap();
127 Self::apply_edit_groups(groups, &branch, cx);
128 }
129 editor.recalculate_all_buffer_diffs();
130 }
131
132 fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {
133 let mut edits = Vec::new();
134 for group in groups {
135 for suggestion in &group.edits {
136 edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
137 }
138 }
139 buffer.update(cx, |buffer, cx| {
140 buffer.edit(
141 edits,
142 Some(AutoindentMode::Block {
143 original_indent_columns: Vec::new(),
144 }),
145 cx,
146 );
147 });
148 }
149}
150
151impl ResolvedEdit {
152 pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
153 let range = &self.range;
154 let other_range = &other.range;
155
156 // Don't merge if we don't contain the other suggestion.
157 if range.start.cmp(&other_range.start, buffer).is_gt()
158 || range.end.cmp(&other_range.end, buffer).is_lt()
159 {
160 return false;
161 }
162
163 let other_offset_range = other_range.to_offset(buffer);
164 let offset_range = range.to_offset(buffer);
165
166 // If the other range is empty at the start of this edit's range, combine the new text
167 if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {
168 self.new_text = format!("{}\n{}", other.new_text, self.new_text);
169 self.range.start = other_range.start;
170
171 if let Some((description, other_description)) =
172 self.description.as_mut().zip(other.description.as_ref())
173 {
174 *description = format!("{}\n{}", other_description, description)
175 }
176 } else {
177 if let Some((description, other_description)) =
178 self.description.as_mut().zip(other.description.as_ref())
179 {
180 description.push('\n');
181 description.push_str(other_description);
182 }
183 }
184
185 true
186 }
187}
188
189impl AssistantEdit {
190 pub fn new(
191 path: Option<String>,
192 operation: Option<String>,
193 old_text: Option<String>,
194 new_text: Option<String>,
195 description: Option<String>,
196 ) -> Result<Self> {
197 let path = path.ok_or_else(|| anyhow!("missing path"))?;
198 let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
199
200 let kind = match operation.as_str() {
201 "update" => AssistantEditKind::Update {
202 old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
203 new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
204 description,
205 },
206 "insert_before" => AssistantEditKind::InsertBefore {
207 old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
208 new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
209 description,
210 },
211 "insert_after" => AssistantEditKind::InsertAfter {
212 old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
213 new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
214 description,
215 },
216 "delete" => AssistantEditKind::Delete {
217 old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
218 },
219 "create" => AssistantEditKind::Create {
220 description,
221 new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
222 },
223 _ => Err(anyhow!("unknown operation {operation:?}"))?,
224 };
225
226 Ok(Self { path, kind })
227 }
228
229 pub async fn resolve(
230 &self,
231 project: Entity<Project>,
232 mut cx: AsyncApp,
233 ) -> Result<(Entity<Buffer>, ResolvedEdit)> {
234 let path = self.path.clone();
235 let kind = self.kind.clone();
236 let buffer = project
237 .update(&mut cx, |project, cx| {
238 let project_path = project
239 .find_project_path(Path::new(&path), cx)
240 .or_else(|| {
241 // If we couldn't find a project path for it, put it in the active worktree
242 // so that when we create the buffer, it can be saved.
243 let worktree = project
244 .active_entry()
245 .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
246 .or_else(|| project.worktrees(cx).next())?;
247 let worktree = worktree.read(cx);
248
249 Some(ProjectPath {
250 worktree_id: worktree.id(),
251 path: Arc::from(Path::new(&path)),
252 })
253 })
254 .with_context(|| format!("worktree not found for {:?}", path))?;
255 anyhow::Ok(project.open_buffer(project_path, cx))
256 })??
257 .await?;
258
259 let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
260 let suggestion = cx
261 .background_spawn(async move { kind.resolve(&snapshot) })
262 .await;
263
264 Ok((buffer, suggestion))
265 }
266}
267
268impl AssistantEditKind {
269 fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
270 match self {
271 Self::Update {
272 old_text,
273 new_text,
274 description,
275 } => {
276 let range = Self::resolve_location(&snapshot, &old_text);
277 ResolvedEdit {
278 range,
279 new_text,
280 description,
281 }
282 }
283 Self::Create {
284 new_text,
285 description,
286 } => ResolvedEdit {
287 range: text::Anchor::MIN..text::Anchor::MAX,
288 description,
289 new_text,
290 },
291 Self::InsertBefore {
292 old_text,
293 mut new_text,
294 description,
295 } => {
296 let range = Self::resolve_location(&snapshot, &old_text);
297 new_text.push('\n');
298 ResolvedEdit {
299 range: range.start..range.start,
300 new_text,
301 description,
302 }
303 }
304 Self::InsertAfter {
305 old_text,
306 mut new_text,
307 description,
308 } => {
309 let range = Self::resolve_location(&snapshot, &old_text);
310 new_text.insert(0, '\n');
311 ResolvedEdit {
312 range: range.end..range.end,
313 new_text,
314 description,
315 }
316 }
317 Self::Delete { old_text } => {
318 let range = Self::resolve_location(&snapshot, &old_text);
319 ResolvedEdit {
320 range,
321 new_text: String::new(),
322 description: None,
323 }
324 }
325 }
326 }
327
328 fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
329 const INSERTION_COST: u32 = 3;
330 const DELETION_COST: u32 = 10;
331 const WHITESPACE_INSERTION_COST: u32 = 1;
332 const WHITESPACE_DELETION_COST: u32 = 1;
333
334 let buffer_len = buffer.len();
335 let query_len = search_query.len();
336 let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
337 let mut leading_deletion_cost = 0_u32;
338 for (row, query_byte) in search_query.bytes().enumerate() {
339 let deletion_cost = if query_byte.is_ascii_whitespace() {
340 WHITESPACE_DELETION_COST
341 } else {
342 DELETION_COST
343 };
344
345 leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
346 matrix.set(
347 row + 1,
348 0,
349 SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
350 );
351
352 for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
353 let insertion_cost = if buffer_byte.is_ascii_whitespace() {
354 WHITESPACE_INSERTION_COST
355 } else {
356 INSERTION_COST
357 };
358
359 let up = SearchState::new(
360 matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
361 SearchDirection::Up,
362 );
363 let left = SearchState::new(
364 matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
365 SearchDirection::Left,
366 );
367 let diagonal = SearchState::new(
368 if query_byte == *buffer_byte {
369 matrix.get(row, col).cost
370 } else {
371 matrix
372 .get(row, col)
373 .cost
374 .saturating_add(deletion_cost + insertion_cost)
375 },
376 SearchDirection::Diagonal,
377 );
378 matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
379 }
380 }
381
382 // Traceback to find the best match
383 let mut best_buffer_end = buffer_len;
384 let mut best_cost = u32::MAX;
385 for col in 1..=buffer_len {
386 let cost = matrix.get(query_len, col).cost;
387 if cost < best_cost {
388 best_cost = cost;
389 best_buffer_end = col;
390 }
391 }
392
393 let mut query_ix = query_len;
394 let mut buffer_ix = best_buffer_end;
395 while query_ix > 0 && buffer_ix > 0 {
396 let current = matrix.get(query_ix, buffer_ix);
397 match current.direction {
398 SearchDirection::Diagonal => {
399 query_ix -= 1;
400 buffer_ix -= 1;
401 }
402 SearchDirection::Up => {
403 query_ix -= 1;
404 }
405 SearchDirection::Left => {
406 buffer_ix -= 1;
407 }
408 }
409 }
410
411 let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
412 start.column = 0;
413 let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
414 if end.column > 0 {
415 end.column = buffer.line_len(end.row);
416 }
417
418 buffer.anchor_after(start)..buffer.anchor_before(end)
419 }
420}
421
422impl AssistantPatch {
423 pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
424 let mut resolve_tasks = Vec::new();
425 for (ix, edit) in self.edits.iter().enumerate() {
426 if let Ok(edit) = edit.as_ref() {
427 resolve_tasks.push(
428 edit.resolve(project.clone(), cx.clone())
429 .map_err(move |error| (ix, error)),
430 );
431 }
432 }
433
434 let edits = future::join_all(resolve_tasks).await;
435 let mut errors = Vec::new();
436 let mut edits_by_buffer = HashMap::default();
437 for entry in edits {
438 match entry {
439 Ok((buffer, edit)) => {
440 edits_by_buffer
441 .entry(buffer)
442 .or_insert_with(Vec::new)
443 .push(edit);
444 }
445 Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
446 edit_ix,
447 message: error.to_string(),
448 }),
449 }
450 }
451
452 // Expand the context ranges of each edit and group edits with overlapping context ranges.
453 let mut edit_groups_by_buffer = HashMap::default();
454 for (buffer, edits) in edits_by_buffer {
455 if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
456 edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
457 }
458 }
459
460 ResolvedPatch {
461 edit_groups: edit_groups_by_buffer,
462 errors,
463 }
464 }
465
466 fn group_edits(
467 mut edits: Vec<ResolvedEdit>,
468 snapshot: &text::BufferSnapshot,
469 ) -> Vec<ResolvedEditGroup> {
470 let mut edit_groups = Vec::<ResolvedEditGroup>::new();
471 // Sort edits by their range so that earlier, larger ranges come first
472 edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
473
474 // Merge overlapping edits
475 edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
476
477 // Create context ranges for each edit
478 for edit in edits {
479 let context_range = {
480 let edit_point_range = edit.range.to_point(&snapshot);
481 let start_row = edit_point_range.start.row.saturating_sub(5);
482 let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
483 let start = snapshot.anchor_before(Point::new(start_row, 0));
484 let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
485 start..end
486 };
487
488 if let Some(last_group) = edit_groups.last_mut() {
489 if last_group
490 .context_range
491 .end
492 .cmp(&context_range.start, &snapshot)
493 .is_ge()
494 {
495 // Merge with the previous group if context ranges overlap
496 last_group.context_range.end = context_range.end;
497 last_group.edits.push(edit);
498 } else {
499 // Create a new group
500 edit_groups.push(ResolvedEditGroup {
501 context_range,
502 edits: vec![edit],
503 });
504 }
505 } else {
506 // Create the first group
507 edit_groups.push(ResolvedEditGroup {
508 context_range,
509 edits: vec![edit],
510 });
511 }
512 }
513
514 edit_groups
515 }
516
517 pub fn path_count(&self) -> usize {
518 self.paths().count()
519 }
520
521 pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
522 let mut prev_path = None;
523 self.edits.iter().filter_map(move |edit| {
524 if let Ok(edit) = edit {
525 let path = Some(edit.path.as_str());
526 if path != prev_path {
527 prev_path = path;
528 return path;
529 }
530 }
531 None
532 })
533 }
534}
535
536impl PartialEq for AssistantPatch {
537 fn eq(&self, other: &Self) -> bool {
538 self.range == other.range
539 && self.title == other.title
540 && Arc::ptr_eq(&self.edits, &other.edits)
541 }
542}
543
544impl Eq for AssistantPatch {}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use gpui::App;
550 use language::{
551 Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
552 };
553 use settings::SettingsStore;
554 use ui::BorrowAppContext;
555 use unindent::Unindent as _;
556 use util::test::{generate_marked_text, marked_text_ranges};
557
558 #[gpui::test]
559 fn test_resolve_location(cx: &mut App) {
560 assert_location_resolution(
561 concat!(
562 " Lorem\n",
563 "« ipsum\n",
564 " dolor sit amet»\n",
565 " consecteur",
566 ),
567 "ipsum\ndolor",
568 cx,
569 );
570
571 assert_location_resolution(
572 &"
573 «fn foo1(a: usize) -> usize {
574 40
575 }»
576
577 fn foo2(b: usize) -> usize {
578 42
579 }
580 "
581 .unindent(),
582 "fn foo1(b: usize) {\n40\n}",
583 cx,
584 );
585
586 assert_location_resolution(
587 &"
588 fn main() {
589 « Foo
590 .bar()
591 .baz()
592 .qux()»
593 }
594
595 fn foo2(b: usize) -> usize {
596 42
597 }
598 "
599 .unindent(),
600 "Foo.bar.baz.qux()",
601 cx,
602 );
603
604 assert_location_resolution(
605 &"
606 class Something {
607 one() { return 1; }
608 « two() { return 2222; }
609 three() { return 333; }
610 four() { return 4444; }
611 five() { return 5555; }
612 six() { return 6666; }
613 » seven() { return 7; }
614 eight() { return 8; }
615 }
616 "
617 .unindent(),
618 &"
619 two() { return 2222; }
620 four() { return 4444; }
621 five() { return 5555; }
622 six() { return 6666; }
623 "
624 .unindent(),
625 cx,
626 );
627 }
628
629 #[gpui::test]
630 fn test_resolve_edits(cx: &mut App) {
631 init_test(cx);
632
633 assert_edits(
634 "
635 /// A person
636 struct Person {
637 name: String,
638 age: usize,
639 }
640
641 /// A dog
642 struct Dog {
643 weight: f32,
644 }
645
646 impl Person {
647 fn name(&self) -> &str {
648 &self.name
649 }
650 }
651 "
652 .unindent(),
653 vec![
654 AssistantEditKind::Update {
655 old_text: "
656 name: String,
657 "
658 .unindent(),
659 new_text: "
660 first_name: String,
661 last_name: String,
662 "
663 .unindent(),
664 description: None,
665 },
666 AssistantEditKind::Update {
667 old_text: "
668 fn name(&self) -> &str {
669 &self.name
670 }
671 "
672 .unindent(),
673 new_text: "
674 fn name(&self) -> String {
675 format!(\"{} {}\", self.first_name, self.last_name)
676 }
677 "
678 .unindent(),
679 description: None,
680 },
681 ],
682 "
683 /// A person
684 struct Person {
685 first_name: String,
686 last_name: String,
687 age: usize,
688 }
689
690 /// A dog
691 struct Dog {
692 weight: f32,
693 }
694
695 impl Person {
696 fn name(&self) -> String {
697 format!(\"{} {}\", self.first_name, self.last_name)
698 }
699 }
700 "
701 .unindent(),
702 cx,
703 );
704
705 // Ensure InsertBefore merges correctly with Update of the same text
706 assert_edits(
707 "
708 fn foo() {
709
710 }
711 "
712 .unindent(),
713 vec![
714 AssistantEditKind::InsertBefore {
715 old_text: "
716 fn foo() {"
717 .unindent(),
718 new_text: "
719 fn bar() {
720 qux();
721 }"
722 .unindent(),
723 description: Some("implement bar".into()),
724 },
725 AssistantEditKind::Update {
726 old_text: "
727 fn foo() {
728
729 }"
730 .unindent(),
731 new_text: "
732 fn foo() {
733 bar();
734 }"
735 .unindent(),
736 description: Some("call bar in foo".into()),
737 },
738 AssistantEditKind::InsertAfter {
739 old_text: "
740 fn foo() {
741
742 }
743 "
744 .unindent(),
745 new_text: "
746 fn qux() {
747 // todo
748 }
749 "
750 .unindent(),
751 description: Some("implement qux".into()),
752 },
753 ],
754 "
755 fn bar() {
756 qux();
757 }
758
759 fn foo() {
760 bar();
761 }
762
763 fn qux() {
764 // todo
765 }
766 "
767 .unindent(),
768 cx,
769 );
770
771 // Correctly indent new text when replacing multiple adjacent indented blocks.
772 assert_edits(
773 "
774 impl Numbers {
775 fn one() {
776 1
777 }
778
779 fn two() {
780 2
781 }
782
783 fn three() {
784 3
785 }
786 }
787 "
788 .unindent(),
789 vec![
790 AssistantEditKind::Update {
791 old_text: "
792 fn one() {
793 1
794 }
795 "
796 .unindent(),
797 new_text: "
798 fn one() {
799 101
800 }
801 "
802 .unindent(),
803 description: None,
804 },
805 AssistantEditKind::Update {
806 old_text: "
807 fn two() {
808 2
809 }
810 "
811 .unindent(),
812 new_text: "
813 fn two() {
814 102
815 }
816 "
817 .unindent(),
818 description: None,
819 },
820 AssistantEditKind::Update {
821 old_text: "
822 fn three() {
823 3
824 }
825 "
826 .unindent(),
827 new_text: "
828 fn three() {
829 103
830 }
831 "
832 .unindent(),
833 description: None,
834 },
835 ],
836 "
837 impl Numbers {
838 fn one() {
839 101
840 }
841
842 fn two() {
843 102
844 }
845
846 fn three() {
847 103
848 }
849 }
850 "
851 .unindent(),
852 cx,
853 );
854
855 assert_edits(
856 "
857 impl Person {
858 fn set_name(&mut self, name: String) {
859 self.name = name;
860 }
861
862 fn name(&self) -> String {
863 return self.name;
864 }
865 }
866 "
867 .unindent(),
868 vec![
869 AssistantEditKind::Update {
870 old_text: "self.name = name;".unindent(),
871 new_text: "self._name = name;".unindent(),
872 description: None,
873 },
874 AssistantEditKind::Update {
875 old_text: "return self.name;\n".unindent(),
876 new_text: "return self._name;\n".unindent(),
877 description: None,
878 },
879 ],
880 "
881 impl Person {
882 fn set_name(&mut self, name: String) {
883 self._name = name;
884 }
885
886 fn name(&self) -> String {
887 return self._name;
888 }
889 }
890 "
891 .unindent(),
892 cx,
893 );
894 }
895
896 fn init_test(cx: &mut App) {
897 let settings_store = SettingsStore::test(cx);
898 cx.set_global(settings_store);
899 language::init(cx);
900 cx.update_global::<SettingsStore, _>(|settings, cx| {
901 settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
902 });
903 }
904
905 #[track_caller]
906 fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
907 let (text, _) = marked_text_ranges(text_with_expected_range, false);
908 let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
909 let snapshot = buffer.read(cx).snapshot();
910 let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
911 let text_with_actual_range = generate_marked_text(&text, &[range], false);
912 pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
913 }
914
915 #[track_caller]
916 fn assert_edits(
917 old_text: String,
918 edits: Vec<AssistantEditKind>,
919 new_text: String,
920 cx: &mut App,
921 ) {
922 let buffer =
923 cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
924 let snapshot = buffer.read(cx).snapshot();
925 let resolved_edits = edits
926 .into_iter()
927 .map(|kind| kind.resolve(&snapshot))
928 .collect();
929 let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
930 ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
931 let actual_new_text = buffer.read(cx).text();
932 pretty_assertions::assert_eq!(actual_new_text, new_text);
933 }
934
935 fn rust_lang() -> Language {
936 Language::new(
937 LanguageConfig {
938 name: "Rust".into(),
939 matcher: LanguageMatcher {
940 path_suffixes: vec!["rs".to_string()],
941 ..Default::default()
942 },
943 ..Default::default()
944 },
945 Some(language::tree_sitter_rust::LANGUAGE.into()),
946 )
947 .with_indents_query(
948 r#"
949 (call_expression) @indent
950 (field_expression) @indent
951 (_ "(" ")" @end) @indent
952 (_ "{" "}" @end) @indent
953 "#,
954 )
955 .unwrap()
956 }
957}