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}