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