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