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