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