1use anyhow::{anyhow, Context as _, Result};
2use collections::HashMap;
3use editor::ProposedChangesEditor;
4use futures::{future, TryFutureExt as _};
5use gpui::{App, 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_executor()
262 .spawn(async move { kind.resolve(&snapshot) })
263 .await;
264
265 Ok((buffer, suggestion))
266 }
267}
268
269impl AssistantEditKind {
270 fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
271 match self {
272 Self::Update {
273 old_text,
274 new_text,
275 description,
276 } => {
277 let range = Self::resolve_location(&snapshot, &old_text);
278 ResolvedEdit {
279 range,
280 new_text,
281 description,
282 }
283 }
284 Self::Create {
285 new_text,
286 description,
287 } => ResolvedEdit {
288 range: text::Anchor::MIN..text::Anchor::MAX,
289 description,
290 new_text,
291 },
292 Self::InsertBefore {
293 old_text,
294 mut new_text,
295 description,
296 } => {
297 let range = Self::resolve_location(&snapshot, &old_text);
298 new_text.push('\n');
299 ResolvedEdit {
300 range: range.start..range.start,
301 new_text,
302 description,
303 }
304 }
305 Self::InsertAfter {
306 old_text,
307 mut new_text,
308 description,
309 } => {
310 let range = Self::resolve_location(&snapshot, &old_text);
311 new_text.insert(0, '\n');
312 ResolvedEdit {
313 range: range.end..range.end,
314 new_text,
315 description,
316 }
317 }
318 Self::Delete { old_text } => {
319 let range = Self::resolve_location(&snapshot, &old_text);
320 ResolvedEdit {
321 range,
322 new_text: String::new(),
323 description: None,
324 }
325 }
326 }
327 }
328
329 fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
330 const INSERTION_COST: u32 = 3;
331 const DELETION_COST: u32 = 10;
332 const WHITESPACE_INSERTION_COST: u32 = 1;
333 const WHITESPACE_DELETION_COST: u32 = 1;
334
335 let buffer_len = buffer.len();
336 let query_len = search_query.len();
337 let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
338 let mut leading_deletion_cost = 0_u32;
339 for (row, query_byte) in search_query.bytes().enumerate() {
340 let deletion_cost = if query_byte.is_ascii_whitespace() {
341 WHITESPACE_DELETION_COST
342 } else {
343 DELETION_COST
344 };
345
346 leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
347 matrix.set(
348 row + 1,
349 0,
350 SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
351 );
352
353 for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
354 let insertion_cost = if buffer_byte.is_ascii_whitespace() {
355 WHITESPACE_INSERTION_COST
356 } else {
357 INSERTION_COST
358 };
359
360 let up = SearchState::new(
361 matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
362 SearchDirection::Up,
363 );
364 let left = SearchState::new(
365 matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
366 SearchDirection::Left,
367 );
368 let diagonal = SearchState::new(
369 if query_byte == *buffer_byte {
370 matrix.get(row, col).cost
371 } else {
372 matrix
373 .get(row, col)
374 .cost
375 .saturating_add(deletion_cost + insertion_cost)
376 },
377 SearchDirection::Diagonal,
378 );
379 matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
380 }
381 }
382
383 // Traceback to find the best match
384 let mut best_buffer_end = buffer_len;
385 let mut best_cost = u32::MAX;
386 for col in 1..=buffer_len {
387 let cost = matrix.get(query_len, col).cost;
388 if cost < best_cost {
389 best_cost = cost;
390 best_buffer_end = col;
391 }
392 }
393
394 let mut query_ix = query_len;
395 let mut buffer_ix = best_buffer_end;
396 while query_ix > 0 && buffer_ix > 0 {
397 let current = matrix.get(query_ix, buffer_ix);
398 match current.direction {
399 SearchDirection::Diagonal => {
400 query_ix -= 1;
401 buffer_ix -= 1;
402 }
403 SearchDirection::Up => {
404 query_ix -= 1;
405 }
406 SearchDirection::Left => {
407 buffer_ix -= 1;
408 }
409 }
410 }
411
412 let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
413 start.column = 0;
414 let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
415 if end.column > 0 {
416 end.column = buffer.line_len(end.row);
417 }
418
419 buffer.anchor_after(start)..buffer.anchor_before(end)
420 }
421}
422
423impl AssistantPatch {
424 pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
425 let mut resolve_tasks = Vec::new();
426 for (ix, edit) in self.edits.iter().enumerate() {
427 if let Ok(edit) = edit.as_ref() {
428 resolve_tasks.push(
429 edit.resolve(project.clone(), cx.clone())
430 .map_err(move |error| (ix, error)),
431 );
432 }
433 }
434
435 let edits = future::join_all(resolve_tasks).await;
436 let mut errors = Vec::new();
437 let mut edits_by_buffer = HashMap::default();
438 for entry in edits {
439 match entry {
440 Ok((buffer, edit)) => {
441 edits_by_buffer
442 .entry(buffer)
443 .or_insert_with(Vec::new)
444 .push(edit);
445 }
446 Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
447 edit_ix,
448 message: error.to_string(),
449 }),
450 }
451 }
452
453 // Expand the context ranges of each edit and group edits with overlapping context ranges.
454 let mut edit_groups_by_buffer = HashMap::default();
455 for (buffer, edits) in edits_by_buffer {
456 if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
457 edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
458 }
459 }
460
461 ResolvedPatch {
462 edit_groups: edit_groups_by_buffer,
463 errors,
464 }
465 }
466
467 fn group_edits(
468 mut edits: Vec<ResolvedEdit>,
469 snapshot: &text::BufferSnapshot,
470 ) -> Vec<ResolvedEditGroup> {
471 let mut edit_groups = Vec::<ResolvedEditGroup>::new();
472 // Sort edits by their range so that earlier, larger ranges come first
473 edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
474
475 // Merge overlapping edits
476 edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
477
478 // Create context ranges for each edit
479 for edit in edits {
480 let context_range = {
481 let edit_point_range = edit.range.to_point(&snapshot);
482 let start_row = edit_point_range.start.row.saturating_sub(5);
483 let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
484 let start = snapshot.anchor_before(Point::new(start_row, 0));
485 let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
486 start..end
487 };
488
489 if let Some(last_group) = edit_groups.last_mut() {
490 if last_group
491 .context_range
492 .end
493 .cmp(&context_range.start, &snapshot)
494 .is_ge()
495 {
496 // Merge with the previous group if context ranges overlap
497 last_group.context_range.end = context_range.end;
498 last_group.edits.push(edit);
499 } else {
500 // Create a new group
501 edit_groups.push(ResolvedEditGroup {
502 context_range,
503 edits: vec![edit],
504 });
505 }
506 } else {
507 // Create the first group
508 edit_groups.push(ResolvedEditGroup {
509 context_range,
510 edits: vec![edit],
511 });
512 }
513 }
514
515 edit_groups
516 }
517
518 pub fn path_count(&self) -> usize {
519 self.paths().count()
520 }
521
522 pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
523 let mut prev_path = None;
524 self.edits.iter().filter_map(move |edit| {
525 if let Ok(edit) = edit {
526 let path = Some(edit.path.as_str());
527 if path != prev_path {
528 prev_path = path;
529 return path;
530 }
531 }
532 None
533 })
534 }
535}
536
537impl PartialEq for AssistantPatch {
538 fn eq(&self, other: &Self) -> bool {
539 self.range == other.range
540 && self.title == other.title
541 && Arc::ptr_eq(&self.edits, &other.edits)
542 }
543}
544
545impl Eq for AssistantPatch {}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use gpui::{App, AppContext as _};
551 use language::{
552 language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
553 };
554 use settings::SettingsStore;
555 use ui::BorrowAppContext;
556 use unindent::Unindent as _;
557 use util::test::{generate_marked_text, marked_text_ranges};
558
559 #[gpui::test]
560 fn test_resolve_location(cx: &mut App) {
561 assert_location_resolution(
562 concat!(
563 " Lorem\n",
564 "« ipsum\n",
565 " dolor sit amet»\n",
566 " consecteur",
567 ),
568 "ipsum\ndolor",
569 cx,
570 );
571
572 assert_location_resolution(
573 &"
574 «fn foo1(a: usize) -> usize {
575 40
576 }»
577
578 fn foo2(b: usize) -> usize {
579 42
580 }
581 "
582 .unindent(),
583 "fn foo1(b: usize) {\n40\n}",
584 cx,
585 );
586
587 assert_location_resolution(
588 &"
589 fn main() {
590 « Foo
591 .bar()
592 .baz()
593 .qux()»
594 }
595
596 fn foo2(b: usize) -> usize {
597 42
598 }
599 "
600 .unindent(),
601 "Foo.bar.baz.qux()",
602 cx,
603 );
604
605 assert_location_resolution(
606 &"
607 class Something {
608 one() { return 1; }
609 « two() { return 2222; }
610 three() { return 333; }
611 four() { return 4444; }
612 five() { return 5555; }
613 six() { return 6666; }
614 » seven() { return 7; }
615 eight() { return 8; }
616 }
617 "
618 .unindent(),
619 &"
620 two() { return 2222; }
621 four() { return 4444; }
622 five() { return 5555; }
623 six() { return 6666; }
624 "
625 .unindent(),
626 cx,
627 );
628 }
629
630 #[gpui::test]
631 fn test_resolve_edits(cx: &mut App) {
632 init_test(cx);
633
634 assert_edits(
635 "
636 /// A person
637 struct Person {
638 name: String,
639 age: usize,
640 }
641
642 /// A dog
643 struct Dog {
644 weight: f32,
645 }
646
647 impl Person {
648 fn name(&self) -> &str {
649 &self.name
650 }
651 }
652 "
653 .unindent(),
654 vec![
655 AssistantEditKind::Update {
656 old_text: "
657 name: String,
658 "
659 .unindent(),
660 new_text: "
661 first_name: String,
662 last_name: String,
663 "
664 .unindent(),
665 description: None,
666 },
667 AssistantEditKind::Update {
668 old_text: "
669 fn name(&self) -> &str {
670 &self.name
671 }
672 "
673 .unindent(),
674 new_text: "
675 fn name(&self) -> String {
676 format!(\"{} {}\", self.first_name, self.last_name)
677 }
678 "
679 .unindent(),
680 description: None,
681 },
682 ],
683 "
684 /// A person
685 struct Person {
686 first_name: String,
687 last_name: String,
688 age: usize,
689 }
690
691 /// A dog
692 struct Dog {
693 weight: f32,
694 }
695
696 impl Person {
697 fn name(&self) -> String {
698 format!(\"{} {}\", self.first_name, self.last_name)
699 }
700 }
701 "
702 .unindent(),
703 cx,
704 );
705
706 // Ensure InsertBefore merges correctly with Update of the same text
707 assert_edits(
708 "
709 fn foo() {
710
711 }
712 "
713 .unindent(),
714 vec![
715 AssistantEditKind::InsertBefore {
716 old_text: "
717 fn foo() {"
718 .unindent(),
719 new_text: "
720 fn bar() {
721 qux();
722 }"
723 .unindent(),
724 description: Some("implement bar".into()),
725 },
726 AssistantEditKind::Update {
727 old_text: "
728 fn foo() {
729
730 }"
731 .unindent(),
732 new_text: "
733 fn foo() {
734 bar();
735 }"
736 .unindent(),
737 description: Some("call bar in foo".into()),
738 },
739 AssistantEditKind::InsertAfter {
740 old_text: "
741 fn foo() {
742
743 }
744 "
745 .unindent(),
746 new_text: "
747 fn qux() {
748 // todo
749 }
750 "
751 .unindent(),
752 description: Some("implement qux".into()),
753 },
754 ],
755 "
756 fn bar() {
757 qux();
758 }
759
760 fn foo() {
761 bar();
762 }
763
764 fn qux() {
765 // todo
766 }
767 "
768 .unindent(),
769 cx,
770 );
771
772 // Correctly indent new text when replacing multiple adjacent indented blocks.
773 assert_edits(
774 "
775 impl Numbers {
776 fn one() {
777 1
778 }
779
780 fn two() {
781 2
782 }
783
784 fn three() {
785 3
786 }
787 }
788 "
789 .unindent(),
790 vec![
791 AssistantEditKind::Update {
792 old_text: "
793 fn one() {
794 1
795 }
796 "
797 .unindent(),
798 new_text: "
799 fn one() {
800 101
801 }
802 "
803 .unindent(),
804 description: None,
805 },
806 AssistantEditKind::Update {
807 old_text: "
808 fn two() {
809 2
810 }
811 "
812 .unindent(),
813 new_text: "
814 fn two() {
815 102
816 }
817 "
818 .unindent(),
819 description: None,
820 },
821 AssistantEditKind::Update {
822 old_text: "
823 fn three() {
824 3
825 }
826 "
827 .unindent(),
828 new_text: "
829 fn three() {
830 103
831 }
832 "
833 .unindent(),
834 description: None,
835 },
836 ],
837 "
838 impl Numbers {
839 fn one() {
840 101
841 }
842
843 fn two() {
844 102
845 }
846
847 fn three() {
848 103
849 }
850 }
851 "
852 .unindent(),
853 cx,
854 );
855
856 assert_edits(
857 "
858 impl Person {
859 fn set_name(&mut self, name: String) {
860 self.name = name;
861 }
862
863 fn name(&self) -> String {
864 return self.name;
865 }
866 }
867 "
868 .unindent(),
869 vec![
870 AssistantEditKind::Update {
871 old_text: "self.name = name;".unindent(),
872 new_text: "self._name = name;".unindent(),
873 description: None,
874 },
875 AssistantEditKind::Update {
876 old_text: "return self.name;\n".unindent(),
877 new_text: "return self._name;\n".unindent(),
878 description: None,
879 },
880 ],
881 "
882 impl Person {
883 fn set_name(&mut self, name: String) {
884 self._name = name;
885 }
886
887 fn name(&self) -> String {
888 return self._name;
889 }
890 }
891 "
892 .unindent(),
893 cx,
894 );
895 }
896
897 fn init_test(cx: &mut App) {
898 let settings_store = SettingsStore::test(cx);
899 cx.set_global(settings_store);
900 language::init(cx);
901 cx.update_global::<SettingsStore, _>(|settings, cx| {
902 settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
903 });
904 }
905
906 #[track_caller]
907 fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
908 let (text, _) = marked_text_ranges(text_with_expected_range, false);
909 let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
910 let snapshot = buffer.read(cx).snapshot();
911 let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
912 let text_with_actual_range = generate_marked_text(&text, &[range], false);
913 pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
914 }
915
916 #[track_caller]
917 fn assert_edits(
918 old_text: String,
919 edits: Vec<AssistantEditKind>,
920 new_text: String,
921 cx: &mut App,
922 ) {
923 let buffer =
924 cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
925 let snapshot = buffer.read(cx).snapshot();
926 let resolved_edits = edits
927 .into_iter()
928 .map(|kind| kind.resolve(&snapshot))
929 .collect();
930 let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
931 ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
932 let actual_new_text = buffer.read(cx).text();
933 pretty_assertions::assert_eq!(actual_new_text, new_text);
934 }
935
936 fn rust_lang() -> Language {
937 Language::new(
938 LanguageConfig {
939 name: "Rust".into(),
940 matcher: LanguageMatcher {
941 path_suffixes: vec!["rs".to_string()],
942 ..Default::default()
943 },
944 ..Default::default()
945 },
946 Some(language::tree_sitter_rust::LANGUAGE.into()),
947 )
948 .with_indents_query(
949 r#"
950 (call_expression) @indent
951 (field_expression) @indent
952 (_ "(" ")" @end) @indent
953 (_ "{" "}" @end) @indent
954 "#,
955 )
956 .unwrap()
957 }
958}