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