diagnostics.rs

  1use anyhow::Result;
  2use collections::{HashMap, HashSet};
  3use editor::{
  4    context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
  5    display_map::{BlockDisposition, BlockId, BlockProperties},
  6    BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer,
  7};
  8use gpui::{
  9    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
 10    RenderContext, Task, View, ViewContext, ViewHandle,
 11};
 12use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
 13use postage::watch;
 14use project::{Project, ProjectPath, WorktreeId};
 15use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
 16use util::TryFutureExt;
 17use workspace::Workspace;
 18
 19action!(Toggle);
 20
 21const CONTEXT_LINE_COUNT: u32 = 1;
 22
 23pub fn init(cx: &mut MutableAppContext) {
 24    cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
 25    cx.add_action(ProjectDiagnosticsEditor::toggle);
 26}
 27
 28type Event = editor::Event;
 29
 30struct ProjectDiagnostics {
 31    project: ModelHandle<Project>,
 32}
 33
 34struct ProjectDiagnosticsEditor {
 35    project: ModelHandle<Project>,
 36    editor: ViewHandle<Editor>,
 37    excerpts: ModelHandle<MultiBuffer>,
 38    path_states: Vec<(Arc<Path>, Vec<DiagnosticGroupState>)>,
 39    paths_to_update: HashMap<WorktreeId, HashSet<ProjectPath>>,
 40    build_settings: BuildSettings,
 41    settings: watch::Receiver<workspace::Settings>,
 42}
 43
 44struct DiagnosticGroupState {
 45    primary_diagnostic: DiagnosticEntry<language::Anchor>,
 46    excerpts: Vec<ExcerptId>,
 47    blocks: HashMap<BlockId, DiagnosticBlock>,
 48    block_count: usize,
 49}
 50
 51enum DiagnosticBlock {
 52    Header(Diagnostic),
 53    Inline(Diagnostic),
 54    Context,
 55}
 56
 57impl ProjectDiagnostics {
 58    fn new(project: ModelHandle<Project>) -> Self {
 59        Self { project }
 60    }
 61}
 62
 63impl Entity for ProjectDiagnostics {
 64    type Event = ();
 65}
 66
 67impl Entity for ProjectDiagnosticsEditor {
 68    type Event = Event;
 69}
 70
 71impl View for ProjectDiagnosticsEditor {
 72    fn ui_name() -> &'static str {
 73        "ProjectDiagnosticsEditor"
 74    }
 75
 76    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 77        if self.path_states.is_empty() {
 78            let theme = &self.settings.borrow().theme.project_diagnostics;
 79            Label::new(
 80                "No problems detected in the project".to_string(),
 81                theme.empty_message.clone(),
 82            )
 83            .aligned()
 84            .contained()
 85            .with_style(theme.container)
 86            .boxed()
 87        } else {
 88            ChildView::new(self.editor.id()).boxed()
 89        }
 90    }
 91
 92    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 93        if !self.path_states.is_empty() {
 94            cx.focus(&self.editor);
 95        }
 96    }
 97}
 98
 99impl ProjectDiagnosticsEditor {
100    fn new(
101        project: ModelHandle<Project>,
102        settings: watch::Receiver<workspace::Settings>,
103        cx: &mut ViewContext<Self>,
104    ) -> Self {
105        cx.subscribe(&project, |this, _, event, cx| match event {
106            project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => {
107                if let Some(paths) = this.paths_to_update.remove(&worktree_id) {
108                    this.update_excerpts(paths, cx);
109                }
110            }
111            project::Event::DiagnosticsUpdated(path) => {
112                this.paths_to_update
113                    .entry(path.worktree_id)
114                    .or_default()
115                    .insert(path.clone());
116            }
117            _ => {}
118        })
119        .detach();
120
121        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
122        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
123        let editor =
124            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
125        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
126            .detach();
127
128        let paths_to_update = project
129            .read(cx)
130            .diagnostic_summaries(cx)
131            .map(|e| e.0)
132            .collect();
133        let this = Self {
134            project,
135            excerpts,
136            editor,
137            build_settings,
138            settings,
139            path_states: Default::default(),
140            paths_to_update: Default::default(),
141        };
142        this.update_excerpts(paths_to_update, cx);
143        this
144    }
145
146    #[cfg(test)]
147    fn text(&self, cx: &AppContext) -> String {
148        self.editor.read(cx).text(cx)
149    }
150
151    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
152        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
153        workspace.add_item(diagnostics, cx);
154    }
155
156    fn update_excerpts(&self, paths: HashSet<ProjectPath>, cx: &mut ViewContext<Self>) {
157        let project = self.project.clone();
158        cx.spawn(|this, mut cx| {
159            async move {
160                for path in paths {
161                    let buffer = project
162                        .update(&mut cx, |project, cx| project.open_buffer(path, cx))
163                        .await?;
164                    this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
165                }
166                Result::<_, anyhow::Error>::Ok(())
167            }
168            .log_err()
169        })
170        .detach();
171    }
172
173    fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
174        let snapshot;
175        let path;
176        {
177            let buffer = buffer.read(cx);
178            snapshot = buffer.snapshot();
179            if let Some(file) = buffer.file() {
180                path = file.path().clone();
181            } else {
182                return;
183            }
184        }
185
186        let was_empty = self.path_states.is_empty();
187        let path_ix = match self
188            .path_states
189            .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
190        {
191            Ok(ix) => ix,
192            Err(ix) => {
193                self.path_states
194                    .insert(ix, (path.clone(), Default::default()));
195                ix
196            }
197        };
198
199        let mut prev_excerpt_id = if path_ix > 0 {
200            let prev_path_last_group = &self.path_states[path_ix - 1].1.last().unwrap();
201            prev_path_last_group.excerpts.last().unwrap().clone()
202        } else {
203            ExcerptId::min()
204        };
205
206        let groups = &mut self.path_states[path_ix].1;
207        let mut groups_to_add = Vec::new();
208        let mut group_ixs_to_remove = Vec::new();
209        let mut blocks_to_add = Vec::new();
210        let mut blocks_to_remove = HashSet::default();
211        let mut diagnostic_blocks = Vec::new();
212        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
213            let mut old_groups = groups.iter().enumerate().peekable();
214            let mut new_groups = snapshot
215                .diagnostic_groups()
216                .into_iter()
217                .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
218                .peekable();
219
220            loop {
221                let mut to_insert = None;
222                let mut to_invalidate = None;
223                let mut to_keep = None;
224                match (old_groups.peek(), new_groups.peek()) {
225                    (None, None) => break,
226                    (None, Some(_)) => to_insert = new_groups.next(),
227                    (Some(_), None) => to_invalidate = old_groups.next(),
228                    (Some((_, old_group)), Some(new_group)) => {
229                        let old_primary = &old_group.primary_diagnostic;
230                        let new_primary = &new_group.entries[new_group.primary_ix];
231                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
232                            Ordering::Less => to_invalidate = old_groups.next(),
233                            Ordering::Equal => {
234                                to_keep = old_groups.next();
235                                new_groups.next();
236                            }
237                            Ordering::Greater => to_insert = new_groups.next(),
238                        }
239                    }
240                }
241
242                if let Some(group) = to_insert {
243                    let mut group_state = DiagnosticGroupState {
244                        primary_diagnostic: group.entries[group.primary_ix].clone(),
245                        excerpts: Default::default(),
246                        blocks: Default::default(),
247                        block_count: 0,
248                    };
249                    let mut pending_range: Option<(Range<Point>, usize)> = None;
250                    let mut is_first_excerpt_for_group = true;
251                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
252                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
253                        if let Some((range, start_ix)) = &mut pending_range {
254                            if let Some(entry) = resolved_entry.as_ref() {
255                                if entry.range.start.row
256                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
257                                {
258                                    range.end = range.end.max(entry.range.end);
259                                    continue;
260                                }
261                            }
262
263                            let excerpt_start =
264                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
265                            let excerpt_end = snapshot.clip_point(
266                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
267                                Bias::Left,
268                            );
269                            let excerpt_id = excerpts.insert_excerpt_after(
270                                &prev_excerpt_id,
271                                ExcerptProperties {
272                                    buffer: &buffer,
273                                    range: excerpt_start..excerpt_end,
274                                },
275                                excerpts_cx,
276                            );
277
278                            prev_excerpt_id = excerpt_id.clone();
279                            group_state.excerpts.push(excerpt_id.clone());
280                            let header_position = (excerpt_id.clone(), language::Anchor::min());
281
282                            if is_first_excerpt_for_group {
283                                is_first_excerpt_for_group = false;
284                                let primary = &group.entries[group.primary_ix].diagnostic;
285                                let mut header = primary.clone();
286                                header.message =
287                                    primary.message.split('\n').next().unwrap().to_string();
288                                group_state.block_count += 1;
289                                diagnostic_blocks.push(DiagnosticBlock::Header(header.clone()));
290                                blocks_to_add.push(BlockProperties {
291                                    position: header_position,
292                                    height: 3,
293                                    render: diagnostic_header_renderer(
294                                        buffer.clone(),
295                                        header,
296                                        true,
297                                        self.build_settings.clone(),
298                                    ),
299                                    disposition: BlockDisposition::Above,
300                                });
301                            } else {
302                                group_state.block_count += 1;
303                                diagnostic_blocks.push(DiagnosticBlock::Context);
304                                blocks_to_add.push(BlockProperties {
305                                    position: header_position,
306                                    height: 1,
307                                    render: context_header_renderer(self.build_settings.clone()),
308                                    disposition: BlockDisposition::Above,
309                                });
310                            }
311
312                            for entry in &group.entries[*start_ix..ix] {
313                                let mut diagnostic = entry.diagnostic.clone();
314                                if diagnostic.is_primary {
315                                    diagnostic.message =
316                                        entry.diagnostic.message.split('\n').skip(1).collect();
317                                }
318
319                                if !diagnostic.message.is_empty() {
320                                    group_state.block_count += 1;
321                                    diagnostic_blocks
322                                        .push(DiagnosticBlock::Inline(diagnostic.clone()));
323                                    blocks_to_add.push(BlockProperties {
324                                        position: (excerpt_id.clone(), entry.range.start.clone()),
325                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
326                                        render: diagnostic_block_renderer(
327                                            diagnostic,
328                                            true,
329                                            self.build_settings.clone(),
330                                        ),
331                                        disposition: BlockDisposition::Below,
332                                    });
333                                }
334                            }
335
336                            pending_range.take();
337                        }
338
339                        if let Some(entry) = resolved_entry {
340                            pending_range = Some((entry.range.clone(), ix));
341                        }
342                    }
343
344                    groups_to_add.push(group_state);
345                } else if let Some((group_ix, group_state)) = to_invalidate {
346                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
347                    group_ixs_to_remove.push(group_ix);
348                    blocks_to_remove.extend(group_state.blocks.keys().copied());
349                } else if let Some((_, group)) = to_keep {
350                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
351                }
352            }
353
354            excerpts.snapshot(excerpts_cx)
355        });
356
357        self.editor.update(cx, |editor, cx| {
358            editor.remove_blocks(blocks_to_remove, cx);
359            let mut block_ids = editor
360                .insert_blocks(
361                    blocks_to_add.into_iter().map(|block| {
362                        let (excerpt_id, text_anchor) = block.position;
363                        BlockProperties {
364                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
365                            height: block.height,
366                            render: block.render,
367                            disposition: block.disposition,
368                        }
369                    }),
370                    cx,
371                )
372                .into_iter()
373                .zip(diagnostic_blocks);
374
375            for group_state in &mut groups_to_add {
376                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
377            }
378
379            if was_empty {
380                editor.update_selections(
381                    vec![Selection {
382                        id: 0,
383                        start: 0,
384                        end: 0,
385                        reversed: false,
386                        goal: SelectionGoal::None,
387                    }],
388                    None,
389                    cx,
390                );
391            } else {
392                editor.refresh_selections(cx);
393            }
394        });
395
396        for ix in group_ixs_to_remove.into_iter().rev() {
397            groups.remove(ix);
398        }
399        groups.extend(groups_to_add);
400        groups.sort_unstable_by(|a, b| {
401            let range_a = &a.primary_diagnostic.range;
402            let range_b = &b.primary_diagnostic.range;
403            range_a
404                .start
405                .cmp(&range_b.start, &snapshot)
406                .unwrap()
407                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
408        });
409
410        if groups.is_empty() {
411            self.path_states.remove(path_ix);
412        }
413
414        if self.path_states.is_empty() {
415            if self.editor.is_focused(cx) {
416                cx.focus_self();
417            }
418        } else {
419            if cx.handle().is_focused(cx) {
420                cx.focus(&self.editor);
421            }
422        }
423        cx.notify();
424    }
425}
426
427impl workspace::Item for ProjectDiagnostics {
428    type View = ProjectDiagnosticsEditor;
429
430    fn build_view(
431        handle: ModelHandle<Self>,
432        settings: watch::Receiver<workspace::Settings>,
433        cx: &mut ViewContext<Self::View>,
434    ) -> Self::View {
435        let project = handle.read(cx).project.clone();
436        ProjectDiagnosticsEditor::new(project, settings, cx)
437    }
438
439    fn project_path(&self) -> Option<project::ProjectPath> {
440        None
441    }
442}
443
444impl workspace::ItemView for ProjectDiagnosticsEditor {
445    fn title(&self, _: &AppContext) -> String {
446        "Project Diagnostics".to_string()
447    }
448
449    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
450        None
451    }
452
453    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
454        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
455    }
456
457    fn save_as(
458        &mut self,
459        _: ModelHandle<project::Worktree>,
460        _: &std::path::Path,
461        _: &mut ViewContext<Self>,
462    ) -> Task<Result<()>> {
463        unreachable!()
464    }
465
466    fn is_dirty(&self, cx: &AppContext) -> bool {
467        self.excerpts.read(cx).read(cx).is_dirty()
468    }
469
470    fn has_conflict(&self, cx: &AppContext) -> bool {
471        self.excerpts.read(cx).read(cx).has_conflict()
472    }
473
474    fn should_update_tab_on_event(event: &Event) -> bool {
475        matches!(
476            event,
477            Event::Saved | Event::Dirtied | Event::FileHandleChanged
478        )
479    }
480
481    fn can_save(&self, _: &AppContext) -> bool {
482        true
483    }
484
485    fn can_save_as(&self, _: &AppContext) -> bool {
486        false
487    }
488}
489
490fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
491    lhs: &DiagnosticEntry<L>,
492    rhs: &DiagnosticEntry<R>,
493    snapshot: &language::BufferSnapshot,
494) -> Ordering {
495    lhs.range
496        .start
497        .to_offset(&snapshot)
498        .cmp(&rhs.range.start.to_offset(snapshot))
499        .then_with(|| {
500            lhs.range
501                .end
502                .to_offset(&snapshot)
503                .cmp(&rhs.range.end.to_offset(snapshot))
504        })
505        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
512    use gpui::TestAppContext;
513    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
514    use project::{worktree, FakeFs};
515    use serde_json::json;
516    use std::sync::Arc;
517    use unindent::Unindent as _;
518    use workspace::WorkspaceParams;
519
520    #[gpui::test]
521    async fn test_diagnostics(mut cx: TestAppContext) {
522        let settings = cx.update(WorkspaceParams::test).settings;
523        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
524        let client = Client::new(http_client.clone());
525        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
526        let fs = Arc::new(FakeFs::new());
527
528        let project = cx.update(|cx| {
529            Project::local(
530                client.clone(),
531                user_store,
532                Arc::new(LanguageRegistry::new()),
533                fs.clone(),
534                cx,
535            )
536        });
537
538        fs.insert_tree(
539            "/test",
540            json!({
541                "a.rs": "
542                    const a: i32 = 'a';
543                ".unindent(),
544
545                "main.rs": "
546                    fn main() {
547                        let x = vec![];
548                        let y = vec![];
549                        a(x);
550                        b(y);
551                        // comment 1
552                        // comment 2
553                        c(y);
554                        d(x);
555                    }
556                "
557                .unindent(),
558            }),
559        )
560        .await;
561
562        let worktree = project
563            .update(&mut cx, |project, cx| {
564                project.add_local_worktree("/test", cx)
565            })
566            .await
567            .unwrap();
568
569        worktree.update(&mut cx, |worktree, cx| {
570            worktree
571                .update_diagnostic_entries(
572                    Arc::from("/test/main.rs".as_ref()),
573                    None,
574                    vec![
575                        DiagnosticEntry {
576                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
577                            diagnostic: Diagnostic {
578                                message:
579                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
580                                        .to_string(),
581                                severity: DiagnosticSeverity::INFORMATION,
582                                is_primary: false,
583                                is_disk_based: true,
584                                group_id: 1,
585                                ..Default::default()
586                            },
587                        },
588                        DiagnosticEntry {
589                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
590                            diagnostic: Diagnostic {
591                                message:
592                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
593                                        .to_string(),
594                                severity: DiagnosticSeverity::INFORMATION,
595                                is_primary: false,
596                                is_disk_based: true,
597                                group_id: 0,
598                                ..Default::default()
599                            },
600                        },
601                        DiagnosticEntry {
602                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
603                            diagnostic: Diagnostic {
604                                message: "value moved here".to_string(),
605                                severity: DiagnosticSeverity::INFORMATION,
606                                is_primary: false,
607                                is_disk_based: true,
608                                group_id: 1,
609                                ..Default::default()
610                            },
611                        },
612                        DiagnosticEntry {
613                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
614                            diagnostic: Diagnostic {
615                                message: "value moved here".to_string(),
616                                severity: DiagnosticSeverity::INFORMATION,
617                                is_primary: false,
618                                is_disk_based: true,
619                                group_id: 0,
620                                ..Default::default()
621                            },
622                        },
623                        DiagnosticEntry {
624                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
625                            diagnostic: Diagnostic {
626                                message: "use of moved value\nvalue used here after move".to_string(),
627                                severity: DiagnosticSeverity::ERROR,
628                                is_primary: true,
629                                is_disk_based: true,
630                                group_id: 0,
631                                ..Default::default()
632                            },
633                        },
634                        DiagnosticEntry {
635                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
636                            diagnostic: Diagnostic {
637                                message: "use of moved value\nvalue used here after move".to_string(),
638                                severity: DiagnosticSeverity::ERROR,
639                                is_primary: true,
640                                is_disk_based: true,
641                                group_id: 1,
642                                ..Default::default()
643                            },
644                        },
645                    ],
646                    cx,
647                )
648                .unwrap();
649        });
650
651        let view = cx.add_view(Default::default(), |cx| {
652            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
653        });
654
655        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
656            .await;
657
658        view.update(&mut cx, |view, cx| {
659            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
660
661            assert_eq!(
662                editor.text(),
663                concat!(
664                    //
665                    // main.rs, diagnostic group 1
666                    //
667                    "\n", // primary message
668                    "\n", // filename
669                    "    let x = vec![];\n",
670                    "    let y = vec![];\n",
671                    "\n", // supporting diagnostic
672                    "    a(x);\n",
673                    "    b(y);\n",
674                    "\n", // supporting diagnostic
675                    "    // comment 1\n",
676                    "    // comment 2\n",
677                    "    c(y);\n",
678                    "\n", // supporting diagnostic
679                    "    d(x);\n",
680                    //
681                    // main.rs, diagnostic group 2
682                    //
683                    "\n", // primary message
684                    "\n", // filename
685                    "fn main() {\n",
686                    "    let x = vec![];\n",
687                    "\n", // supporting diagnostic
688                    "    let y = vec![];\n",
689                    "    a(x);\n",
690                    "\n", // supporting diagnostic
691                    "    b(y);\n",
692                    "\n", // context ellipsis
693                    "    c(y);\n",
694                    "    d(x);\n",
695                    "\n", // supporting diagnostic
696                    "}"
697                )
698            );
699
700            view.editor.update(cx, |editor, cx| {
701                assert_eq!(editor.selected_ranges::<usize>(cx), [0..0]);
702            });
703        });
704
705        worktree.update(&mut cx, |worktree, cx| {
706            worktree
707                .update_diagnostic_entries(
708                    Arc::from("/test/a.rs".as_ref()),
709                    None,
710                    vec![DiagnosticEntry {
711                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
712                        diagnostic: Diagnostic {
713                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
714                            severity: DiagnosticSeverity::ERROR,
715                            is_primary: true,
716                            is_disk_based: true,
717                            group_id: 0,
718                            ..Default::default()
719                        },
720                    }],
721                    cx,
722                )
723                .unwrap();
724            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
725        });
726
727        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
728            .await;
729
730        view.update(&mut cx, |view, cx| {
731            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
732
733            assert_eq!(
734                editor.text(),
735                concat!(
736                    //
737                    // a.rs
738                    //
739                    "\n", // primary message
740                    "\n", // filename
741                    "const a: i32 = 'a';\n",
742                    "\n", // supporting diagnostic
743                    "\n", // context line
744                    //
745                    // main.rs, diagnostic group 1
746                    //
747                    "\n", // primary message
748                    "\n", // filename
749                    "    let x = vec![];\n",
750                    "    let y = vec![];\n",
751                    "\n", // supporting diagnostic
752                    "    a(x);\n",
753                    "    b(y);\n",
754                    "\n", // supporting diagnostic
755                    "    // comment 1\n",
756                    "    // comment 2\n",
757                    "    c(y);\n",
758                    "\n", // supporting diagnostic
759                    "    d(x);\n",
760                    //
761                    // main.rs, diagnostic group 2
762                    //
763                    "\n", // primary message
764                    "\n", // filename
765                    "fn main() {\n",
766                    "    let x = vec![];\n",
767                    "\n", // supporting diagnostic
768                    "    let y = vec![];\n",
769                    "    a(x);\n",
770                    "\n", // supporting diagnostic
771                    "    b(y);\n",
772                    "\n", // context ellipsis
773                    "    c(y);\n",
774                    "    d(x);\n",
775                    "\n", // supporting diagnostic
776                    "}"
777                )
778            );
779        });
780    }
781}