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