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 editor::DisplayPoint;
594 use gpui::TestAppContext;
595 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
596 use project::{worktree, FakeFs};
597 use serde_json::json;
598 use std::sync::Arc;
599 use unindent::Unindent as _;
600 use workspace::WorkspaceParams;
601
602 #[gpui::test]
603 async fn test_diagnostics(mut cx: TestAppContext) {
604 let workspace_params = cx.update(WorkspaceParams::test);
605 let settings = workspace_params.settings.clone();
606 let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
607 let client = Client::new(http_client.clone());
608 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
609 let fs = Arc::new(FakeFs::new());
610
611 let project = cx.update(|cx| {
612 Project::local(
613 client.clone(),
614 user_store,
615 Arc::new(LanguageRegistry::new()),
616 fs.clone(),
617 cx,
618 )
619 });
620
621 fs.insert_tree(
622 "/test",
623 json!({
624 "a.rs": "
625 const a: i32 = 'a';
626 ".unindent(),
627
628 "main.rs": "
629 fn main() {
630 let x = vec![];
631 let y = vec![];
632 a(x);
633 b(y);
634 // comment 1
635 // comment 2
636 c(y);
637 d(x);
638 }
639 "
640 .unindent(),
641 }),
642 )
643 .await;
644
645 let worktree = project
646 .update(&mut cx, |project, cx| {
647 project.add_local_worktree("/test", cx)
648 })
649 .await
650 .unwrap();
651
652 worktree.update(&mut cx, |worktree, cx| {
653 worktree
654 .update_diagnostic_entries(
655 Arc::from("/test/main.rs".as_ref()),
656 None,
657 vec![
658 DiagnosticEntry {
659 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
660 diagnostic: Diagnostic {
661 message:
662 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
663 .to_string(),
664 severity: DiagnosticSeverity::INFORMATION,
665 is_primary: false,
666 is_disk_based: true,
667 group_id: 1,
668 ..Default::default()
669 },
670 },
671 DiagnosticEntry {
672 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
673 diagnostic: Diagnostic {
674 message:
675 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
676 .to_string(),
677 severity: DiagnosticSeverity::INFORMATION,
678 is_primary: false,
679 is_disk_based: true,
680 group_id: 0,
681 ..Default::default()
682 },
683 },
684 DiagnosticEntry {
685 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
686 diagnostic: Diagnostic {
687 message: "value moved here".to_string(),
688 severity: DiagnosticSeverity::INFORMATION,
689 is_primary: false,
690 is_disk_based: true,
691 group_id: 1,
692 ..Default::default()
693 },
694 },
695 DiagnosticEntry {
696 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
697 diagnostic: Diagnostic {
698 message: "value moved here".to_string(),
699 severity: DiagnosticSeverity::INFORMATION,
700 is_primary: false,
701 is_disk_based: true,
702 group_id: 0,
703 ..Default::default()
704 },
705 },
706 DiagnosticEntry {
707 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
708 diagnostic: Diagnostic {
709 message: "use of moved value\nvalue used here after move".to_string(),
710 severity: DiagnosticSeverity::ERROR,
711 is_primary: true,
712 is_disk_based: true,
713 group_id: 0,
714 ..Default::default()
715 },
716 },
717 DiagnosticEntry {
718 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
719 diagnostic: Diagnostic {
720 message: "use of moved value\nvalue used here after move".to_string(),
721 severity: DiagnosticSeverity::ERROR,
722 is_primary: true,
723 is_disk_based: true,
724 group_id: 1,
725 ..Default::default()
726 },
727 },
728 ],
729 cx,
730 )
731 .unwrap();
732 });
733
734 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
735 let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
736
737 let view = cx.add_view(0, |cx| {
738 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx)
739 });
740
741 view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
742 .await;
743
744 view.update(&mut cx, |view, cx| {
745 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
746
747 assert_eq!(
748 editor.text(),
749 concat!(
750 //
751 // main.rs, diagnostic group 1
752 //
753 "\n", // padding
754 "\n", // primary message
755 "\n", // filename
756 " let x = vec![];\n",
757 " let y = vec![];\n",
758 "\n", // supporting diagnostic
759 " a(x);\n",
760 " b(y);\n",
761 "\n", // supporting diagnostic
762 " // comment 1\n",
763 " // comment 2\n",
764 " c(y);\n",
765 "\n", // supporting diagnostic
766 " d(x);\n",
767 //
768 // main.rs, diagnostic group 2
769 //
770 "\n", // padding
771 "\n", // primary message
772 "\n", // filename
773 "fn main() {\n",
774 " let x = vec![];\n",
775 "\n", // supporting diagnostic
776 " let y = vec![];\n",
777 " a(x);\n",
778 "\n", // supporting diagnostic
779 " b(y);\n",
780 "\n", // context ellipsis
781 " c(y);\n",
782 " d(x);\n",
783 "\n", // supporting diagnostic
784 "}"
785 )
786 );
787
788 view.editor.update(cx, |editor, cx| {
789 assert_eq!(
790 editor.selected_display_ranges(cx),
791 [DisplayPoint::new(11, 6)..DisplayPoint::new(11, 6)]
792 );
793 });
794 });
795
796 worktree.update(&mut cx, |worktree, cx| {
797 worktree
798 .update_diagnostic_entries(
799 Arc::from("/test/a.rs".as_ref()),
800 None,
801 vec![DiagnosticEntry {
802 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
803 diagnostic: Diagnostic {
804 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
805 severity: DiagnosticSeverity::ERROR,
806 is_primary: true,
807 is_disk_based: true,
808 group_id: 0,
809 ..Default::default()
810 },
811 }],
812 cx,
813 )
814 .unwrap();
815 cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
816 });
817
818 view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
819 .await;
820
821 view.update(&mut cx, |view, cx| {
822 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
823
824 assert_eq!(
825 editor.text(),
826 concat!(
827 //
828 // a.rs
829 //
830 "\n", // padding
831 "\n", // primary message
832 "\n", // filename
833 "const a: i32 = 'a';\n",
834 "\n", // supporting diagnostic
835 "\n", // context line
836 //
837 // main.rs, diagnostic group 1
838 //
839 "\n", // padding
840 "\n", // primary message
841 "\n", // filename
842 " let x = vec![];\n",
843 " let y = vec![];\n",
844 "\n", // supporting diagnostic
845 " a(x);\n",
846 " b(y);\n",
847 "\n", // supporting diagnostic
848 " // comment 1\n",
849 " // comment 2\n",
850 " c(y);\n",
851 "\n", // supporting diagnostic
852 " d(x);\n",
853 //
854 // main.rs, diagnostic group 2
855 //
856 "\n", // padding
857 "\n", // primary message
858 "\n", // filename
859 "fn main() {\n",
860 " let x = vec![];\n",
861 "\n", // supporting diagnostic
862 " let y = vec![];\n",
863 " a(x);\n",
864 "\n", // supporting diagnostic
865 " b(y);\n",
866 "\n", // context ellipsis
867 " c(y);\n",
868 " d(x);\n",
869 "\n", // supporting diagnostic
870 "}"
871 )
872 );
873 });
874 }
875}