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};
 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 path_ix = match self
187            .path_states
188            .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
189        {
190            Ok(ix) => ix,
191            Err(ix) => {
192                self.path_states
193                    .insert(ix, (path.clone(), Default::default()));
194                ix
195            }
196        };
197
198        let mut prev_excerpt_id = if path_ix > 0 {
199            let prev_path_last_group = &self.path_states[path_ix - 1].1.last().unwrap();
200            prev_path_last_group.excerpts.last().unwrap().clone()
201        } else {
202            ExcerptId::min()
203        };
204
205        let groups = &mut self.path_states[path_ix].1;
206        let mut groups_to_add = Vec::new();
207        let mut group_ixs_to_remove = Vec::new();
208        let mut blocks_to_add = Vec::new();
209        let mut blocks_to_remove = HashSet::default();
210        let mut diagnostic_blocks = Vec::new();
211        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
212            let mut old_groups = groups.iter().enumerate().peekable();
213            let mut new_groups = snapshot
214                .diagnostic_groups()
215                .into_iter()
216                .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
217                .peekable();
218
219            loop {
220                let mut to_insert = None;
221                let mut to_invalidate = None;
222                let mut to_keep = None;
223                match (old_groups.peek(), new_groups.peek()) {
224                    (None, None) => break,
225                    (None, Some(_)) => to_insert = new_groups.next(),
226                    (Some(_), None) => to_invalidate = old_groups.next(),
227                    (Some((_, old_group)), Some(new_group)) => {
228                        let old_primary = &old_group.primary_diagnostic;
229                        let new_primary = &new_group.entries[new_group.primary_ix];
230                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
231                            Ordering::Less => to_invalidate = old_groups.next(),
232                            Ordering::Equal => {
233                                to_keep = old_groups.next();
234                                new_groups.next();
235                            }
236                            Ordering::Greater => to_insert = new_groups.next(),
237                        }
238                    }
239                }
240
241                if let Some(group) = to_insert {
242                    let mut group_state = DiagnosticGroupState {
243                        primary_diagnostic: group.entries[group.primary_ix].clone(),
244                        excerpts: Default::default(),
245                        blocks: Default::default(),
246                        block_count: 0,
247                    };
248                    let mut pending_range: Option<(Range<Point>, usize)> = None;
249                    let mut is_first_excerpt_for_group = true;
250                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
251                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
252                        if let Some((range, start_ix)) = &mut pending_range {
253                            if let Some(entry) = resolved_entry.as_ref() {
254                                if entry.range.start.row
255                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
256                                {
257                                    range.end = range.end.max(entry.range.end);
258                                    continue;
259                                }
260                            }
261
262                            let excerpt_start =
263                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
264                            let excerpt_end = snapshot.clip_point(
265                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
266                                Bias::Left,
267                            );
268                            let excerpt_id = excerpts.insert_excerpt_after(
269                                &prev_excerpt_id,
270                                ExcerptProperties {
271                                    buffer: &buffer,
272                                    range: excerpt_start..excerpt_end,
273                                },
274                                excerpts_cx,
275                            );
276
277                            prev_excerpt_id = excerpt_id.clone();
278                            group_state.excerpts.push(excerpt_id.clone());
279                            let header_position = (excerpt_id.clone(), language::Anchor::min());
280
281                            if is_first_excerpt_for_group {
282                                is_first_excerpt_for_group = false;
283                                let primary = &group.entries[group.primary_ix].diagnostic;
284                                group_state.block_count += 1;
285                                diagnostic_blocks.push(DiagnosticBlock::Header(primary.clone()));
286                                blocks_to_add.push(BlockProperties {
287                                    position: header_position,
288                                    height: 2,
289                                    render: diagnostic_header_renderer(
290                                        buffer.clone(),
291                                        primary.clone(),
292                                        true,
293                                        self.build_settings.clone(),
294                                    ),
295                                    disposition: BlockDisposition::Above,
296                                });
297                            } else {
298                                group_state.block_count += 1;
299                                diagnostic_blocks.push(DiagnosticBlock::Context);
300                                blocks_to_add.push(BlockProperties {
301                                    position: header_position,
302                                    height: 1,
303                                    render: context_header_renderer(self.build_settings.clone()),
304                                    disposition: BlockDisposition::Above,
305                                });
306                            }
307
308                            for entry in &group.entries[*start_ix..ix] {
309                                if !entry.diagnostic.is_primary {
310                                    group_state.block_count += 1;
311                                    diagnostic_blocks
312                                        .push(DiagnosticBlock::Inline(entry.diagnostic.clone()));
313                                    blocks_to_add.push(BlockProperties {
314                                        position: (excerpt_id.clone(), entry.range.start.clone()),
315                                        height: entry.diagnostic.message.matches('\n').count()
316                                            as u8
317                                            + 1,
318                                        render: diagnostic_block_renderer(
319                                            entry.diagnostic.clone(),
320                                            true,
321                                            self.build_settings.clone(),
322                                        ),
323                                        disposition: BlockDisposition::Below,
324                                    });
325                                }
326                            }
327
328                            pending_range.take();
329                        }
330
331                        if let Some(entry) = resolved_entry {
332                            pending_range = Some((entry.range.clone(), ix));
333                        }
334                    }
335
336                    groups_to_add.push(group_state);
337                } else if let Some((group_ix, group_state)) = to_invalidate {
338                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
339                    group_ixs_to_remove.push(group_ix);
340                    blocks_to_remove.extend(group_state.blocks.keys().copied());
341                } else if let Some((_, group)) = to_keep {
342                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
343                }
344            }
345
346            excerpts.snapshot(excerpts_cx)
347        });
348
349        self.editor.update(cx, |editor, cx| {
350            editor.remove_blocks(blocks_to_remove, cx);
351            let mut block_ids = editor
352                .insert_blocks(
353                    blocks_to_add.into_iter().map(|block| {
354                        let (excerpt_id, text_anchor) = block.position;
355                        BlockProperties {
356                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
357                            height: block.height,
358                            render: block.render,
359                            disposition: block.disposition,
360                        }
361                    }),
362                    cx,
363                )
364                .into_iter()
365                .zip(diagnostic_blocks);
366
367            for group_state in &mut groups_to_add {
368                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
369            }
370
371            editor.refresh_selections(cx);
372        });
373
374        for ix in group_ixs_to_remove.into_iter().rev() {
375            groups.remove(ix);
376        }
377        groups.extend(groups_to_add);
378        groups.sort_unstable_by(|a, b| {
379            let range_a = &a.primary_diagnostic.range;
380            let range_b = &b.primary_diagnostic.range;
381            range_a
382                .start
383                .cmp(&range_b.start, &snapshot)
384                .unwrap()
385                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
386        });
387
388        if groups.is_empty() {
389            self.path_states.remove(path_ix);
390        }
391
392        if self.editor.is_focused(cx) && self.path_states.is_empty() {
393            cx.focus_self();
394        }
395        cx.notify();
396    }
397}
398
399impl workspace::Item for ProjectDiagnostics {
400    type View = ProjectDiagnosticsEditor;
401
402    fn build_view(
403        handle: ModelHandle<Self>,
404        settings: watch::Receiver<workspace::Settings>,
405        cx: &mut ViewContext<Self::View>,
406    ) -> Self::View {
407        let project = handle.read(cx).project.clone();
408        ProjectDiagnosticsEditor::new(project, settings, cx)
409    }
410
411    fn project_path(&self) -> Option<project::ProjectPath> {
412        None
413    }
414}
415
416impl workspace::ItemView for ProjectDiagnosticsEditor {
417    fn title(&self, _: &AppContext) -> String {
418        "Project Diagnostics".to_string()
419    }
420
421    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
422        None
423    }
424
425    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
426        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
427    }
428
429    fn save_as(
430        &mut self,
431        _: ModelHandle<project::Worktree>,
432        _: &std::path::Path,
433        _: &mut ViewContext<Self>,
434    ) -> Task<Result<()>> {
435        unreachable!()
436    }
437
438    fn is_dirty(&self, cx: &AppContext) -> bool {
439        self.excerpts.read(cx).read(cx).is_dirty()
440    }
441
442    fn has_conflict(&self, cx: &AppContext) -> bool {
443        self.excerpts.read(cx).read(cx).has_conflict()
444    }
445
446    fn should_update_tab_on_event(event: &Event) -> bool {
447        matches!(
448            event,
449            Event::Saved | Event::Dirtied | Event::FileHandleChanged
450        )
451    }
452
453    fn can_save(&self, _: &AppContext) -> bool {
454        true
455    }
456
457    fn can_save_as(&self, _: &AppContext) -> bool {
458        false
459    }
460}
461
462fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
463    lhs: &DiagnosticEntry<L>,
464    rhs: &DiagnosticEntry<R>,
465    snapshot: &language::BufferSnapshot,
466) -> Ordering {
467    lhs.range
468        .start
469        .to_offset(&snapshot)
470        .cmp(&rhs.range.start.to_offset(snapshot))
471        .then_with(|| {
472            lhs.range
473                .end
474                .to_offset(&snapshot)
475                .cmp(&rhs.range.end.to_offset(snapshot))
476        })
477        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
484    use gpui::TestAppContext;
485    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
486    use project::{worktree, FakeFs};
487    use serde_json::json;
488    use std::sync::Arc;
489    use unindent::Unindent as _;
490    use workspace::WorkspaceParams;
491
492    #[gpui::test]
493    async fn test_diagnostics(mut cx: TestAppContext) {
494        let settings = cx.update(WorkspaceParams::test).settings;
495        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
496        let client = Client::new(http_client.clone());
497        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
498        let fs = Arc::new(FakeFs::new());
499
500        let project = cx.update(|cx| {
501            Project::local(
502                client.clone(),
503                user_store,
504                Arc::new(LanguageRegistry::new()),
505                fs.clone(),
506                cx,
507            )
508        });
509
510        fs.insert_tree(
511            "/test",
512            json!({
513                "a.rs": "
514                    const a: i32 = 'a';
515                ".unindent(),
516
517                "main.rs": "
518                    fn main() {
519                        let x = vec![];
520                        let y = vec![];
521                        a(x);
522                        b(y);
523                        // comment 1
524                        // comment 2
525                        c(y);
526                        d(x);
527                    }
528                "
529                .unindent(),
530            }),
531        )
532        .await;
533
534        let worktree = project
535            .update(&mut cx, |project, cx| {
536                project.add_local_worktree("/test", cx)
537            })
538            .await
539            .unwrap();
540
541        worktree.update(&mut cx, |worktree, cx| {
542            worktree
543                .update_diagnostic_entries(
544                    Arc::from("/test/main.rs".as_ref()),
545                    None,
546                    vec![
547                        DiagnosticEntry {
548                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
549                            diagnostic: Diagnostic {
550                                message:
551                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
552                                        .to_string(),
553                                severity: DiagnosticSeverity::INFORMATION,
554                                is_primary: false,
555                                is_disk_based: true,
556                                group_id: 1,
557                                ..Default::default()
558                            },
559                        },
560                        DiagnosticEntry {
561                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
562                            diagnostic: Diagnostic {
563                                message:
564                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
565                                        .to_string(),
566                                severity: DiagnosticSeverity::INFORMATION,
567                                is_primary: false,
568                                is_disk_based: true,
569                                group_id: 0,
570                                ..Default::default()
571                            },
572                        },
573                        DiagnosticEntry {
574                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
575                            diagnostic: Diagnostic {
576                                message: "value moved here".to_string(),
577                                severity: DiagnosticSeverity::INFORMATION,
578                                is_primary: false,
579                                is_disk_based: true,
580                                group_id: 1,
581                                ..Default::default()
582                            },
583                        },
584                        DiagnosticEntry {
585                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
586                            diagnostic: Diagnostic {
587                                message: "value moved here".to_string(),
588                                severity: DiagnosticSeverity::INFORMATION,
589                                is_primary: false,
590                                is_disk_based: true,
591                                group_id: 0,
592                                ..Default::default()
593                            },
594                        },
595                        DiagnosticEntry {
596                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
597                            diagnostic: Diagnostic {
598                                message: "use of moved value".to_string(),
599                                severity: DiagnosticSeverity::ERROR,
600                                is_primary: true,
601                                is_disk_based: true,
602                                group_id: 0,
603                                ..Default::default()
604                            },
605                        },
606                        DiagnosticEntry {
607                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
608                            diagnostic: Diagnostic {
609                                message: "value used here after move".to_string(),
610                                severity: DiagnosticSeverity::INFORMATION,
611                                is_primary: false,
612                                is_disk_based: true,
613                                group_id: 0,
614                                ..Default::default()
615                            },
616                        },
617                        DiagnosticEntry {
618                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
619                            diagnostic: Diagnostic {
620                                message: "use of moved value".to_string(),
621                                severity: DiagnosticSeverity::ERROR,
622                                is_primary: true,
623                                is_disk_based: true,
624                                group_id: 1,
625                                ..Default::default()
626                            },
627                        },
628                        DiagnosticEntry {
629                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
630                            diagnostic: Diagnostic {
631                                message: "value used here after move".to_string(),
632                                severity: DiagnosticSeverity::INFORMATION,
633                                is_primary: false,
634                                is_disk_based: true,
635                                group_id: 1,
636                                ..Default::default()
637                            },
638                        },
639                    ],
640                    cx,
641                )
642                .unwrap();
643        });
644
645        let view = cx.add_view(Default::default(), |cx| {
646            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
647        });
648
649        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
650            .await;
651
652        view.update(&mut cx, |view, cx| {
653            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
654
655            assert_eq!(
656                editor.text(),
657                concat!(
658                    //
659                    // main.rs, diagnostic group 1
660                    //
661                    "\n", // primary message
662                    "\n", // filename
663                    "    let x = vec![];\n",
664                    "    let y = vec![];\n",
665                    "\n", // supporting diagnostic
666                    "    a(x);\n",
667                    "    b(y);\n",
668                    "\n", // supporting diagnostic
669                    "    // comment 1\n",
670                    "    // comment 2\n",
671                    "    c(y);\n",
672                    "\n", // supporting diagnostic
673                    "    d(x);\n",
674                    //
675                    // main.rs, diagnostic group 2
676                    //
677                    "\n", // primary message
678                    "\n", // filename
679                    "fn main() {\n",
680                    "    let x = vec![];\n",
681                    "\n", // supporting diagnostic
682                    "    let y = vec![];\n",
683                    "    a(x);\n",
684                    "\n", // supporting diagnostic
685                    "    b(y);\n",
686                    "\n", // context ellipsis
687                    "    c(y);\n",
688                    "    d(x);\n",
689                    "\n", // supporting diagnostic
690                    "}"
691                )
692            );
693        });
694
695        worktree.update(&mut cx, |worktree, cx| {
696            worktree
697                .update_diagnostic_entries(
698                    Arc::from("/test/a.rs".as_ref()),
699                    None,
700                    vec![
701                        DiagnosticEntry {
702                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
703                            diagnostic: Diagnostic {
704                                message: "mismatched types".to_string(),
705                                severity: DiagnosticSeverity::ERROR,
706                                is_primary: true,
707                                is_disk_based: true,
708                                group_id: 0,
709                                ..Default::default()
710                            },
711                        },
712                        DiagnosticEntry {
713                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
714                            diagnostic: Diagnostic {
715                                message: "expected `usize`, found `char`".to_string(),
716                                severity: DiagnosticSeverity::INFORMATION,
717                                is_primary: false,
718                                is_disk_based: true,
719                                group_id: 0,
720                                ..Default::default()
721                            },
722                        },
723                    ],
724                    cx,
725                )
726                .unwrap();
727            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
728        });
729
730        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
731            .await;
732
733        view.update(&mut cx, |view, cx| {
734            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
735
736            assert_eq!(
737                editor.text(),
738                concat!(
739                    //
740                    // a.rs
741                    //
742                    "\n", // primary message
743                    "\n", // filename
744                    "const a: i32 = 'a';\n",
745                    "\n", // supporting diagnostic
746                    "\n", // context line
747                    //
748                    // main.rs, diagnostic group 1
749                    //
750                    "\n", // primary message
751                    "\n", // filename
752                    "    let x = vec![];\n",
753                    "    let y = vec![];\n",
754                    "\n", // supporting diagnostic
755                    "    a(x);\n",
756                    "    b(y);\n",
757                    "\n", // supporting diagnostic
758                    "    // comment 1\n",
759                    "    // comment 2\n",
760                    "    c(y);\n",
761                    "\n", // supporting diagnostic
762                    "    d(x);\n",
763                    //
764                    // main.rs, diagnostic group 2
765                    //
766                    "\n", // primary message
767                    "\n", // filename
768                    "fn main() {\n",
769                    "    let x = vec![];\n",
770                    "\n", // supporting diagnostic
771                    "    let y = vec![];\n",
772                    "    a(x);\n",
773                    "\n", // supporting diagnostic
774                    "    b(y);\n",
775                    "\n", // context ellipsis
776                    "    c(y);\n",
777                    "    d(x);\n",
778                    "\n", // supporting diagnostic
779                    "}"
780                )
781            );
782        });
783    }
784}