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