patch.rs

  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
580581
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}