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