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