1use anyhow::{anyhow, Context as _, Result};
2use collections::HashMap;
3use editor::ProposedChangesEditor;
4use futures::{future, TryFutureExt as _};
5use gpui::{App, AsyncAppContext, 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: AsyncAppContext,
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(
425 &self,
426 project: Entity<Project>,
427 cx: &mut AsyncAppContext,
428 ) -> ResolvedPatch {
429 let mut resolve_tasks = Vec::new();
430 for (ix, edit) in self.edits.iter().enumerate() {
431 if let Ok(edit) = edit.as_ref() {
432 resolve_tasks.push(
433 edit.resolve(project.clone(), cx.clone())
434 .map_err(move |error| (ix, error)),
435 );
436 }
437 }
438
439 let edits = future::join_all(resolve_tasks).await;
440 let mut errors = Vec::new();
441 let mut edits_by_buffer = HashMap::default();
442 for entry in edits {
443 match entry {
444 Ok((buffer, edit)) => {
445 edits_by_buffer
446 .entry(buffer)
447 .or_insert_with(Vec::new)
448 .push(edit);
449 }
450 Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
451 edit_ix,
452 message: error.to_string(),
453 }),
454 }
455 }
456
457 // Expand the context ranges of each edit and group edits with overlapping context ranges.
458 let mut edit_groups_by_buffer = HashMap::default();
459 for (buffer, edits) in edits_by_buffer {
460 if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
461 edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
462 }
463 }
464
465 ResolvedPatch {
466 edit_groups: edit_groups_by_buffer,
467 errors,
468 }
469 }
470
471 fn group_edits(
472 mut edits: Vec<ResolvedEdit>,
473 snapshot: &text::BufferSnapshot,
474 ) -> Vec<ResolvedEditGroup> {
475 let mut edit_groups = Vec::<ResolvedEditGroup>::new();
476 // Sort edits by their range so that earlier, larger ranges come first
477 edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
478
479 // Merge overlapping edits
480 edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
481
482 // Create context ranges for each edit
483 for edit in edits {
484 let context_range = {
485 let edit_point_range = edit.range.to_point(&snapshot);
486 let start_row = edit_point_range.start.row.saturating_sub(5);
487 let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
488 let start = snapshot.anchor_before(Point::new(start_row, 0));
489 let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
490 start..end
491 };
492
493 if let Some(last_group) = edit_groups.last_mut() {
494 if last_group
495 .context_range
496 .end
497 .cmp(&context_range.start, &snapshot)
498 .is_ge()
499 {
500 // Merge with the previous group if context ranges overlap
501 last_group.context_range.end = context_range.end;
502 last_group.edits.push(edit);
503 } else {
504 // Create a new group
505 edit_groups.push(ResolvedEditGroup {
506 context_range,
507 edits: vec![edit],
508 });
509 }
510 } else {
511 // Create the first group
512 edit_groups.push(ResolvedEditGroup {
513 context_range,
514 edits: vec![edit],
515 });
516 }
517 }
518
519 edit_groups
520 }
521
522 pub fn path_count(&self) -> usize {
523 self.paths().count()
524 }
525
526 pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
527 let mut prev_path = None;
528 self.edits.iter().filter_map(move |edit| {
529 if let Ok(edit) = edit {
530 let path = Some(edit.path.as_str());
531 if path != prev_path {
532 prev_path = path;
533 return path;
534 }
535 }
536 None
537 })
538 }
539}
540
541impl PartialEq for AssistantPatch {
542 fn eq(&self, other: &Self) -> bool {
543 self.range == other.range
544 && self.title == other.title
545 && Arc::ptr_eq(&self.edits, &other.edits)
546 }
547}
548
549impl Eq for AssistantPatch {}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use gpui::{App, AppContext as _};
555 use language::{
556 language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
557 };
558 use settings::SettingsStore;
559 use ui::BorrowAppContext;
560 use unindent::Unindent as _;
561 use util::test::{generate_marked_text, marked_text_ranges};
562
563 #[gpui::test]
564 fn test_resolve_location(cx: &mut App) {
565 assert_location_resolution(
566 concat!(
567 " Lorem\n",
568 "« ipsum\n",
569 " dolor sit amet»\n",
570 " consecteur",
571 ),
572 "ipsum\ndolor",
573 cx,
574 );
575
576 assert_location_resolution(
577 &"
578 «fn foo1(a: usize) -> usize {
579 40
580 }»
581
582 fn foo2(b: usize) -> usize {
583 42
584 }
585 "
586 .unindent(),
587 "fn foo1(b: usize) {\n40\n}",
588 cx,
589 );
590
591 assert_location_resolution(
592 &"
593 fn main() {
594 « Foo
595 .bar()
596 .baz()
597 .qux()»
598 }
599
600 fn foo2(b: usize) -> usize {
601 42
602 }
603 "
604 .unindent(),
605 "Foo.bar.baz.qux()",
606 cx,
607 );
608
609 assert_location_resolution(
610 &"
611 class Something {
612 one() { return 1; }
613 « two() { return 2222; }
614 three() { return 333; }
615 four() { return 4444; }
616 five() { return 5555; }
617 six() { return 6666; }
618 » seven() { return 7; }
619 eight() { return 8; }
620 }
621 "
622 .unindent(),
623 &"
624 two() { return 2222; }
625 four() { return 4444; }
626 five() { return 5555; }
627 six() { return 6666; }
628 "
629 .unindent(),
630 cx,
631 );
632 }
633
634 #[gpui::test]
635 fn test_resolve_edits(cx: &mut App) {
636 init_test(cx);
637
638 assert_edits(
639 "
640 /// A person
641 struct Person {
642 name: String,
643 age: usize,
644 }
645
646 /// A dog
647 struct Dog {
648 weight: f32,
649 }
650
651 impl Person {
652 fn name(&self) -> &str {
653 &self.name
654 }
655 }
656 "
657 .unindent(),
658 vec![
659 AssistantEditKind::Update {
660 old_text: "
661 name: String,
662 "
663 .unindent(),
664 new_text: "
665 first_name: String,
666 last_name: String,
667 "
668 .unindent(),
669 description: None,
670 },
671 AssistantEditKind::Update {
672 old_text: "
673 fn name(&self) -> &str {
674 &self.name
675 }
676 "
677 .unindent(),
678 new_text: "
679 fn name(&self) -> String {
680 format!(\"{} {}\", self.first_name, self.last_name)
681 }
682 "
683 .unindent(),
684 description: None,
685 },
686 ],
687 "
688 /// A person
689 struct Person {
690 first_name: String,
691 last_name: String,
692 age: usize,
693 }
694
695 /// A dog
696 struct Dog {
697 weight: f32,
698 }
699
700 impl Person {
701 fn name(&self) -> String {
702 format!(\"{} {}\", self.first_name, self.last_name)
703 }
704 }
705 "
706 .unindent(),
707 cx,
708 );
709
710 // Ensure InsertBefore merges correctly with Update of the same text
711 assert_edits(
712 "
713 fn foo() {
714
715 }
716 "
717 .unindent(),
718 vec![
719 AssistantEditKind::InsertBefore {
720 old_text: "
721 fn foo() {"
722 .unindent(),
723 new_text: "
724 fn bar() {
725 qux();
726 }"
727 .unindent(),
728 description: Some("implement bar".into()),
729 },
730 AssistantEditKind::Update {
731 old_text: "
732 fn foo() {
733
734 }"
735 .unindent(),
736 new_text: "
737 fn foo() {
738 bar();
739 }"
740 .unindent(),
741 description: Some("call bar in foo".into()),
742 },
743 AssistantEditKind::InsertAfter {
744 old_text: "
745 fn foo() {
746
747 }
748 "
749 .unindent(),
750 new_text: "
751 fn qux() {
752 // todo
753 }
754 "
755 .unindent(),
756 description: Some("implement qux".into()),
757 },
758 ],
759 "
760 fn bar() {
761 qux();
762 }
763
764 fn foo() {
765 bar();
766 }
767
768 fn qux() {
769 // todo
770 }
771 "
772 .unindent(),
773 cx,
774 );
775
776 // Correctly indent new text when replacing multiple adjacent indented blocks.
777 assert_edits(
778 "
779 impl Numbers {
780 fn one() {
781 1
782 }
783
784 fn two() {
785 2
786 }
787
788 fn three() {
789 3
790 }
791 }
792 "
793 .unindent(),
794 vec![
795 AssistantEditKind::Update {
796 old_text: "
797 fn one() {
798 1
799 }
800 "
801 .unindent(),
802 new_text: "
803 fn one() {
804 101
805 }
806 "
807 .unindent(),
808 description: None,
809 },
810 AssistantEditKind::Update {
811 old_text: "
812 fn two() {
813 2
814 }
815 "
816 .unindent(),
817 new_text: "
818 fn two() {
819 102
820 }
821 "
822 .unindent(),
823 description: None,
824 },
825 AssistantEditKind::Update {
826 old_text: "
827 fn three() {
828 3
829 }
830 "
831 .unindent(),
832 new_text: "
833 fn three() {
834 103
835 }
836 "
837 .unindent(),
838 description: None,
839 },
840 ],
841 "
842 impl Numbers {
843 fn one() {
844 101
845 }
846
847 fn two() {
848 102
849 }
850
851 fn three() {
852 103
853 }
854 }
855 "
856 .unindent(),
857 cx,
858 );
859
860 assert_edits(
861 "
862 impl Person {
863 fn set_name(&mut self, name: String) {
864 self.name = name;
865 }
866
867 fn name(&self) -> String {
868 return self.name;
869 }
870 }
871 "
872 .unindent(),
873 vec![
874 AssistantEditKind::Update {
875 old_text: "self.name = name;".unindent(),
876 new_text: "self._name = name;".unindent(),
877 description: None,
878 },
879 AssistantEditKind::Update {
880 old_text: "return self.name;\n".unindent(),
881 new_text: "return self._name;\n".unindent(),
882 description: None,
883 },
884 ],
885 "
886 impl Person {
887 fn set_name(&mut self, name: String) {
888 self._name = name;
889 }
890
891 fn name(&self) -> String {
892 return self._name;
893 }
894 }
895 "
896 .unindent(),
897 cx,
898 );
899 }
900
901 fn init_test(cx: &mut App) {
902 let settings_store = SettingsStore::test(cx);
903 cx.set_global(settings_store);
904 language::init(cx);
905 cx.update_global::<SettingsStore, _>(|settings, cx| {
906 settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
907 });
908 }
909
910 #[track_caller]
911 fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
912 let (text, _) = marked_text_ranges(text_with_expected_range, false);
913 let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
914 let snapshot = buffer.read(cx).snapshot();
915 let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
916 let text_with_actual_range = generate_marked_text(&text, &[range], false);
917 pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
918 }
919
920 #[track_caller]
921 fn assert_edits(
922 old_text: String,
923 edits: Vec<AssistantEditKind>,
924 new_text: String,
925 cx: &mut App,
926 ) {
927 let buffer =
928 cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
929 let snapshot = buffer.read(cx).snapshot();
930 let resolved_edits = edits
931 .into_iter()
932 .map(|kind| kind.resolve(&snapshot))
933 .collect();
934 let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
935 ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
936 let actual_new_text = buffer.read(cx).text();
937 pretty_assertions::assert_eq!(actual_new_text, new_text);
938 }
939
940 fn rust_lang() -> Language {
941 Language::new(
942 LanguageConfig {
943 name: "Rust".into(),
944 matcher: LanguageMatcher {
945 path_suffixes: vec!["rs".to_string()],
946 ..Default::default()
947 },
948 ..Default::default()
949 },
950 Some(language::tree_sitter_rust::LANGUAGE.into()),
951 )
952 .with_indents_query(
953 r#"
954 (call_expression) @indent
955 (field_expression) @indent
956 (_ "(" ")" @end) @indent
957 (_ "{" "}" @end) @indent
958 "#,
959 )
960 .unwrap()
961 }
962}