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: primary.message.matches('\n').count() as u8 + 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.path_states.is_empty() {
393            if self.editor.is_focused(cx) {
394                cx.focus_self();
395            }
396        } else {
397            if cx.handle().is_focused(cx) {
398                cx.focus(&self.editor);
399            }
400        }
401        cx.notify();
402    }
403}
404
405impl workspace::Item for ProjectDiagnostics {
406    type View = ProjectDiagnosticsEditor;
407
408    fn build_view(
409        handle: ModelHandle<Self>,
410        settings: watch::Receiver<workspace::Settings>,
411        cx: &mut ViewContext<Self::View>,
412    ) -> Self::View {
413        let project = handle.read(cx).project.clone();
414        ProjectDiagnosticsEditor::new(project, settings, cx)
415    }
416
417    fn project_path(&self) -> Option<project::ProjectPath> {
418        None
419    }
420}
421
422impl workspace::ItemView for ProjectDiagnosticsEditor {
423    fn title(&self, _: &AppContext) -> String {
424        "Project Diagnostics".to_string()
425    }
426
427    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
428        None
429    }
430
431    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
432        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
433    }
434
435    fn save_as(
436        &mut self,
437        _: ModelHandle<project::Worktree>,
438        _: &std::path::Path,
439        _: &mut ViewContext<Self>,
440    ) -> Task<Result<()>> {
441        unreachable!()
442    }
443
444    fn is_dirty(&self, cx: &AppContext) -> bool {
445        self.excerpts.read(cx).read(cx).is_dirty()
446    }
447
448    fn has_conflict(&self, cx: &AppContext) -> bool {
449        self.excerpts.read(cx).read(cx).has_conflict()
450    }
451
452    fn should_update_tab_on_event(event: &Event) -> bool {
453        matches!(
454            event,
455            Event::Saved | Event::Dirtied | Event::FileHandleChanged
456        )
457    }
458
459    fn can_save(&self, _: &AppContext) -> bool {
460        true
461    }
462
463    fn can_save_as(&self, _: &AppContext) -> bool {
464        false
465    }
466}
467
468fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
469    lhs: &DiagnosticEntry<L>,
470    rhs: &DiagnosticEntry<R>,
471    snapshot: &language::BufferSnapshot,
472) -> Ordering {
473    lhs.range
474        .start
475        .to_offset(&snapshot)
476        .cmp(&rhs.range.start.to_offset(snapshot))
477        .then_with(|| {
478            lhs.range
479                .end
480                .to_offset(&snapshot)
481                .cmp(&rhs.range.end.to_offset(snapshot))
482        })
483        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
490    use gpui::TestAppContext;
491    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
492    use project::{worktree, FakeFs};
493    use serde_json::json;
494    use std::sync::Arc;
495    use unindent::Unindent as _;
496    use workspace::WorkspaceParams;
497
498    #[gpui::test]
499    async fn test_diagnostics(mut cx: TestAppContext) {
500        let settings = cx.update(WorkspaceParams::test).settings;
501        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
502        let client = Client::new(http_client.clone());
503        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
504        let fs = Arc::new(FakeFs::new());
505
506        let project = cx.update(|cx| {
507            Project::local(
508                client.clone(),
509                user_store,
510                Arc::new(LanguageRegistry::new()),
511                fs.clone(),
512                cx,
513            )
514        });
515
516        fs.insert_tree(
517            "/test",
518            json!({
519                "a.rs": "
520                    const a: i32 = 'a';
521                ".unindent(),
522
523                "main.rs": "
524                    fn main() {
525                        let x = vec![];
526                        let y = vec![];
527                        a(x);
528                        b(y);
529                        // comment 1
530                        // comment 2
531                        c(y);
532                        d(x);
533                    }
534                "
535                .unindent(),
536            }),
537        )
538        .await;
539
540        let worktree = project
541            .update(&mut cx, |project, cx| {
542                project.add_local_worktree("/test", cx)
543            })
544            .await
545            .unwrap();
546
547        worktree.update(&mut cx, |worktree, cx| {
548            worktree
549                .update_diagnostic_entries(
550                    Arc::from("/test/main.rs".as_ref()),
551                    None,
552                    vec![
553                        DiagnosticEntry {
554                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
555                            diagnostic: Diagnostic {
556                                message:
557                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
558                                        .to_string(),
559                                severity: DiagnosticSeverity::INFORMATION,
560                                is_primary: false,
561                                is_disk_based: true,
562                                group_id: 1,
563                                ..Default::default()
564                            },
565                        },
566                        DiagnosticEntry {
567                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
568                            diagnostic: Diagnostic {
569                                message:
570                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
571                                        .to_string(),
572                                severity: DiagnosticSeverity::INFORMATION,
573                                is_primary: false,
574                                is_disk_based: true,
575                                group_id: 0,
576                                ..Default::default()
577                            },
578                        },
579                        DiagnosticEntry {
580                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
581                            diagnostic: Diagnostic {
582                                message: "value moved here".to_string(),
583                                severity: DiagnosticSeverity::INFORMATION,
584                                is_primary: false,
585                                is_disk_based: true,
586                                group_id: 1,
587                                ..Default::default()
588                            },
589                        },
590                        DiagnosticEntry {
591                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
592                            diagnostic: Diagnostic {
593                                message: "value moved here".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(7, 6)..PointUtf16::new(7, 7),
603                            diagnostic: Diagnostic {
604                                message: "use of moved value".to_string(),
605                                severity: DiagnosticSeverity::ERROR,
606                                is_primary: true,
607                                is_disk_based: true,
608                                group_id: 0,
609                                ..Default::default()
610                            },
611                        },
612                        DiagnosticEntry {
613                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
614                            diagnostic: Diagnostic {
615                                message: "value used here after move".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(8, 6)..PointUtf16::new(8, 7),
625                            diagnostic: Diagnostic {
626                                message: "use of moved value".to_string(),
627                                severity: DiagnosticSeverity::ERROR,
628                                is_primary: true,
629                                is_disk_based: true,
630                                group_id: 1,
631                                ..Default::default()
632                            },
633                        },
634                        DiagnosticEntry {
635                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
636                            diagnostic: Diagnostic {
637                                message: "value used here after move".to_string(),
638                                severity: DiagnosticSeverity::INFORMATION,
639                                is_primary: false,
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
701        worktree.update(&mut cx, |worktree, cx| {
702            worktree
703                .update_diagnostic_entries(
704                    Arc::from("/test/a.rs".as_ref()),
705                    None,
706                    vec![
707                        DiagnosticEntry {
708                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
709                            diagnostic: Diagnostic {
710                                message: "mismatched types".to_string(),
711                                severity: DiagnosticSeverity::ERROR,
712                                is_primary: true,
713                                is_disk_based: true,
714                                group_id: 0,
715                                ..Default::default()
716                            },
717                        },
718                        DiagnosticEntry {
719                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
720                            diagnostic: Diagnostic {
721                                message: "expected `usize`, found `char`".to_string(),
722                                severity: DiagnosticSeverity::INFORMATION,
723                                is_primary: false,
724                                is_disk_based: true,
725                                group_id: 0,
726                                ..Default::default()
727                            },
728                        },
729                    ],
730                    cx,
731                )
732                .unwrap();
733            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
734        });
735
736        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
737            .await;
738
739        view.update(&mut cx, |view, cx| {
740            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
741
742            assert_eq!(
743                editor.text(),
744                concat!(
745                    //
746                    // a.rs
747                    //
748                    "\n", // primary message
749                    "\n", // filename
750                    "const a: i32 = 'a';\n",
751                    "\n", // supporting diagnostic
752                    "\n", // context line
753                    //
754                    // main.rs, diagnostic group 1
755                    //
756                    "\n", // primary message
757                    "\n", // filename
758                    "    let x = vec![];\n",
759                    "    let y = vec![];\n",
760                    "\n", // supporting diagnostic
761                    "    a(x);\n",
762                    "    b(y);\n",
763                    "\n", // supporting diagnostic
764                    "    // comment 1\n",
765                    "    // comment 2\n",
766                    "    c(y);\n",
767                    "\n", // supporting diagnostic
768                    "    d(x);\n",
769                    //
770                    // main.rs, diagnostic group 2
771                    //
772                    "\n", // primary message
773                    "\n", // filename
774                    "fn main() {\n",
775                    "    let x = vec![];\n",
776                    "\n", // supporting diagnostic
777                    "    let y = vec![];\n",
778                    "    a(x);\n",
779                    "\n", // supporting diagnostic
780                    "    b(y);\n",
781                    "\n", // context ellipsis
782                    "    c(y);\n",
783                    "    d(x);\n",
784                    "\n", // supporting diagnostic
785                    "}"
786                )
787            );
788        });
789    }
790}