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