1pub mod items;
2
3use anyhow::Result;
4use collections::{BTreeSet, HashSet};
5use editor::{
6 diagnostic_block_renderer,
7 display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
8 highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
9};
10use gpui::{
11 actions, elements::*, fonts::TextStyle, impl_internal_actions, platform::CursorStyle,
12 serde_json, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext,
13 Task, View, ViewContext, ViewHandle, WeakViewHandle,
14};
15use language::{
16 Bias, Buffer, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
17};
18use project::{DiagnosticSummary, Project, ProjectPath};
19use serde_json::json;
20use settings::Settings;
21use smallvec::SmallVec;
22use std::{
23 any::{Any, TypeId},
24 cmp::Ordering,
25 mem,
26 ops::Range,
27 path::PathBuf,
28 sync::Arc,
29};
30use util::{ResultExt, TryFutureExt};
31use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
32
33actions!(diagnostics, [Deploy]);
34
35impl_internal_actions!(diagnostics, [Jump]);
36
37const CONTEXT_LINE_COUNT: u32 = 1;
38
39pub fn init(cx: &mut MutableAppContext) {
40 cx.add_action(ProjectDiagnosticsEditor::deploy);
41 cx.add_action(ProjectDiagnosticsEditor::jump);
42 items::init(cx);
43}
44
45type Event = editor::Event;
46
47struct ProjectDiagnosticsEditor {
48 project: ModelHandle<Project>,
49 workspace: WeakViewHandle<Workspace>,
50 editor: ViewHandle<Editor>,
51 summary: DiagnosticSummary,
52 excerpts: ModelHandle<MultiBuffer>,
53 path_states: Vec<PathState>,
54 paths_to_update: BTreeSet<ProjectPath>,
55}
56
57struct PathState {
58 path: ProjectPath,
59 diagnostic_groups: Vec<DiagnosticGroupState>,
60}
61
62#[derive(Clone, Debug)]
63struct Jump {
64 path: ProjectPath,
65 range: Range<Point>,
66}
67
68struct DiagnosticGroupState {
69 primary_diagnostic: DiagnosticEntry<language::Anchor>,
70 primary_excerpt_ix: usize,
71 excerpts: Vec<ExcerptId>,
72 blocks: HashSet<BlockId>,
73 block_count: usize,
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, cx: &mut RenderContext<Self>) -> ElementBox {
86 if self.path_states.is_empty() {
87 let theme = &cx.global::<Settings>().theme.project_diagnostics;
88 Label::new(
89 "No problems in workspace".to_string(),
90 theme.empty_message.clone(),
91 )
92 .aligned()
93 .contained()
94 .with_style(theme.container)
95 .boxed()
96 } else {
97 ChildView::new(&self.editor).boxed()
98 }
99 }
100
101 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
102 if !self.path_states.is_empty() {
103 cx.focus(&self.editor);
104 }
105 }
106
107 fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
108 let project = self.project.read(cx);
109 json!({
110 "project": json!({
111 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
112 "summary": project.diagnostic_summary(cx),
113 }),
114 "summary": self.summary,
115 "paths_to_update": self.paths_to_update.iter().map(|path|
116 path.path.to_string_lossy()
117 ).collect::<Vec<_>>(),
118 "paths_states": self.path_states.iter().map(|state|
119 json!({
120 "path": state.path.path.to_string_lossy(),
121 "groups": state.diagnostic_groups.iter().map(|group|
122 json!({
123 "block_count": group.blocks.len(),
124 "excerpt_count": group.excerpts.len(),
125 })
126 ).collect::<Vec<_>>(),
127 })
128 ).collect::<Vec<_>>(),
129 })
130 }
131}
132
133impl ProjectDiagnosticsEditor {
134 fn new(
135 project_handle: ModelHandle<Project>,
136 workspace: WeakViewHandle<Workspace>,
137 cx: &mut ViewContext<Self>,
138 ) -> Self {
139 cx.subscribe(&project_handle, |this, _, event, cx| match event {
140 project::Event::DiskBasedDiagnosticsFinished => {
141 this.update_excerpts(cx);
142 this.update_title(cx);
143 }
144 project::Event::DiagnosticsUpdated(path) => {
145 this.paths_to_update.insert(path.clone());
146 }
147 _ => {}
148 })
149 .detach();
150
151 let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
152 let editor = cx.add_view(|cx| {
153 let mut editor =
154 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
155 editor.set_vertical_scroll_margin(5, cx);
156 editor
157 });
158 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
159 .detach();
160
161 let project = project_handle.read(cx);
162 let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
163 let summary = project.diagnostic_summary(cx);
164 let mut this = Self {
165 project: project_handle,
166 summary,
167 workspace,
168 excerpts,
169 editor,
170 path_states: Default::default(),
171 paths_to_update,
172 };
173 this.update_excerpts(cx);
174 this
175 }
176
177 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
178 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
179 workspace.activate_item(&existing, cx);
180 } else {
181 let workspace_handle = cx.weak_handle();
182 let diagnostics = cx.add_view(|cx| {
183 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
184 });
185 workspace.add_item(Box::new(diagnostics), cx);
186 }
187 }
188
189 fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
190 let editor = workspace.open_path(action.path.clone(), true, cx);
191 let range = action.range.clone();
192 cx.spawn_weak(|_, mut cx| async move {
193 let editor = editor.await.log_err()?.downcast::<Editor>()?;
194 editor.update(&mut cx, |editor, cx| {
195 let buffer = editor.buffer().read(cx).as_singleton()?;
196 let cursor = buffer.read(cx).clip_point(range.start, Bias::Left);
197 editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
198 s.select_ranges([cursor..cursor]);
199 });
200 Some(())
201 })?;
202 Some(())
203 })
204 .detach()
205 }
206
207 fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
208 let paths = mem::take(&mut self.paths_to_update);
209 let project = self.project.clone();
210 cx.spawn(|this, mut cx| {
211 async move {
212 for path in paths {
213 let buffer = project
214 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
215 .await?;
216 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
217 }
218 Result::<_, anyhow::Error>::Ok(())
219 }
220 .log_err()
221 })
222 .detach();
223 }
224
225 fn populate_excerpts(
226 &mut self,
227 path: ProjectPath,
228 buffer: ModelHandle<Buffer>,
229 cx: &mut ViewContext<Self>,
230 ) {
231 let was_empty = self.path_states.is_empty();
232 let snapshot = buffer.read(cx).snapshot();
233 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
234 Ok(ix) => ix,
235 Err(ix) => {
236 self.path_states.insert(
237 ix,
238 PathState {
239 path: path.clone(),
240 diagnostic_groups: Default::default(),
241 },
242 );
243 ix
244 }
245 };
246
247 let mut prev_excerpt_id = if path_ix > 0 {
248 let prev_path_last_group = &self.path_states[path_ix - 1]
249 .diagnostic_groups
250 .last()
251 .unwrap();
252 prev_path_last_group.excerpts.last().unwrap().clone()
253 } else {
254 ExcerptId::min()
255 };
256
257 let path_state = &mut self.path_states[path_ix];
258 let mut groups_to_add = Vec::new();
259 let mut group_ixs_to_remove = Vec::new();
260 let mut blocks_to_add = Vec::new();
261 let mut blocks_to_remove = HashSet::default();
262 let mut first_excerpt_id = None;
263 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
264 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
265 let mut new_groups = snapshot
266 .diagnostic_groups()
267 .into_iter()
268 .filter(|group| {
269 group.entries[group.primary_ix].diagnostic.severity
270 <= DiagnosticSeverity::WARNING
271 })
272 .peekable();
273 loop {
274 let mut to_insert = None;
275 let mut to_remove = None;
276 let mut to_keep = None;
277 match (old_groups.peek(), new_groups.peek()) {
278 (None, None) => break,
279 (None, Some(_)) => to_insert = new_groups.next(),
280 (Some(_), None) => to_remove = old_groups.next(),
281 (Some((_, old_group)), Some(new_group)) => {
282 let old_primary = &old_group.primary_diagnostic;
283 let new_primary = &new_group.entries[new_group.primary_ix];
284 match compare_diagnostics(old_primary, new_primary, &snapshot) {
285 Ordering::Less => to_remove = old_groups.next(),
286 Ordering::Equal => {
287 to_keep = old_groups.next();
288 new_groups.next();
289 }
290 Ordering::Greater => to_insert = new_groups.next(),
291 }
292 }
293 }
294
295 if let Some(group) = to_insert {
296 let mut group_state = DiagnosticGroupState {
297 primary_diagnostic: group.entries[group.primary_ix].clone(),
298 primary_excerpt_ix: 0,
299 excerpts: Default::default(),
300 blocks: Default::default(),
301 block_count: 0,
302 };
303 let mut pending_range: Option<(Range<Point>, usize)> = None;
304 let mut is_first_excerpt_for_group = true;
305 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
306 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
307 if let Some((range, start_ix)) = &mut pending_range {
308 if let Some(entry) = resolved_entry.as_ref() {
309 if entry.range.start.row
310 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
311 {
312 range.end = range.end.max(entry.range.end);
313 continue;
314 }
315 }
316
317 let excerpt_start =
318 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
319 let excerpt_end = snapshot.clip_point(
320 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
321 Bias::Left,
322 );
323 let excerpt_id = excerpts
324 .insert_excerpts_after(
325 &prev_excerpt_id,
326 buffer.clone(),
327 [excerpt_start..excerpt_end],
328 excerpts_cx,
329 )
330 .pop()
331 .unwrap();
332
333 prev_excerpt_id = excerpt_id.clone();
334 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
335 group_state.excerpts.push(excerpt_id.clone());
336 let header_position = (excerpt_id.clone(), language::Anchor::MIN);
337
338 if is_first_excerpt_for_group {
339 is_first_excerpt_for_group = false;
340 let mut primary =
341 group.entries[group.primary_ix].resolve::<Point>(&snapshot);
342 primary.diagnostic.message = primary
343 .diagnostic
344 .message
345 .split('\n')
346 .next()
347 .unwrap()
348 .to_string();
349 group_state.block_count += 1;
350 blocks_to_add.push(BlockProperties {
351 position: header_position,
352 height: 2,
353 render: diagnostic_header_renderer(primary, path.clone()),
354 disposition: BlockDisposition::Above,
355 });
356 }
357
358 for entry in &group.entries[*start_ix..ix] {
359 let mut diagnostic = entry.diagnostic.clone();
360 if diagnostic.is_primary {
361 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
362 diagnostic.message =
363 entry.diagnostic.message.split('\n').skip(1).collect();
364 }
365
366 if !diagnostic.message.is_empty() {
367 group_state.block_count += 1;
368 blocks_to_add.push(BlockProperties {
369 position: (excerpt_id.clone(), entry.range.start.clone()),
370 height: diagnostic.message.matches('\n').count() as u8 + 1,
371 render: diagnostic_block_renderer(diagnostic, true),
372 disposition: BlockDisposition::Below,
373 });
374 }
375 }
376
377 pending_range.take();
378 }
379
380 if let Some(entry) = resolved_entry {
381 pending_range = Some((entry.range.clone(), ix));
382 }
383 }
384
385 groups_to_add.push(group_state);
386 } else if let Some((group_ix, group_state)) = to_remove {
387 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
388 group_ixs_to_remove.push(group_ix);
389 blocks_to_remove.extend(group_state.blocks.iter().copied());
390 } else if let Some((_, group)) = to_keep {
391 prev_excerpt_id = group.excerpts.last().unwrap().clone();
392 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
393 }
394 }
395
396 excerpts.snapshot(excerpts_cx)
397 });
398
399 self.editor.update(cx, |editor, cx| {
400 editor.remove_blocks(blocks_to_remove, cx);
401 let block_ids = editor.insert_blocks(
402 blocks_to_add.into_iter().map(|block| {
403 let (excerpt_id, text_anchor) = block.position;
404 BlockProperties {
405 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
406 height: block.height,
407 render: block.render,
408 disposition: block.disposition,
409 }
410 }),
411 cx,
412 );
413
414 let mut block_ids = block_ids.into_iter();
415 for group_state in &mut groups_to_add {
416 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
417 }
418 });
419
420 for ix in group_ixs_to_remove.into_iter().rev() {
421 path_state.diagnostic_groups.remove(ix);
422 }
423 path_state.diagnostic_groups.extend(groups_to_add);
424 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
425 let range_a = &a.primary_diagnostic.range;
426 let range_b = &b.primary_diagnostic.range;
427 range_a
428 .start
429 .cmp(&range_b.start, &snapshot)
430 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
431 });
432
433 if path_state.diagnostic_groups.is_empty() {
434 self.path_states.remove(path_ix);
435 }
436
437 self.editor.update(cx, |editor, cx| {
438 let groups;
439 let mut selections;
440 let new_excerpt_ids_by_selection_id;
441 if was_empty {
442 groups = self.path_states.first()?.diagnostic_groups.as_slice();
443 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
444 selections = vec![Selection {
445 id: 0,
446 start: 0,
447 end: 0,
448 reversed: false,
449 goal: SelectionGoal::None,
450 }];
451 } else {
452 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
453 new_excerpt_ids_by_selection_id =
454 editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
455 selections = editor.selections.all::<usize>(cx);
456 }
457
458 // If any selection has lost its position, move it to start of the next primary diagnostic.
459 for selection in &mut selections {
460 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
461 let group_ix = match groups.binary_search_by(|probe| {
462 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
463 }) {
464 Ok(ix) | Err(ix) => ix,
465 };
466 if let Some(group) = groups.get(group_ix) {
467 let offset = excerpts_snapshot
468 .anchor_in_excerpt(
469 group.excerpts[group.primary_excerpt_ix].clone(),
470 group.primary_diagnostic.range.start.clone(),
471 )
472 .to_offset(&excerpts_snapshot);
473 selection.start = offset;
474 selection.end = offset;
475 }
476 }
477 }
478 editor.change_selections(None, cx, |s| {
479 s.select(selections);
480 });
481 Some(())
482 });
483
484 if self.path_states.is_empty() {
485 if self.editor.is_focused(cx) {
486 cx.focus_self();
487 }
488 } else {
489 if cx.handle().is_focused(cx) {
490 cx.focus(&self.editor);
491 }
492 }
493 cx.notify();
494 }
495
496 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
497 self.summary = self.project.read(cx).diagnostic_summary(cx);
498 cx.emit(Event::TitleChanged);
499 }
500}
501
502impl workspace::Item for ProjectDiagnosticsEditor {
503 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
504 render_summary(
505 &self.summary,
506 &style.label.text,
507 &cx.global::<Settings>().theme.project_diagnostics,
508 )
509 }
510
511 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
512 None
513 }
514
515 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
516 self.editor.project_entry_ids(cx)
517 }
518
519 fn is_singleton(&self, _: &AppContext) -> bool {
520 false
521 }
522
523 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
524 self.editor
525 .update(cx, |editor, cx| editor.navigate(data, cx))
526 }
527
528 fn is_dirty(&self, cx: &AppContext) -> bool {
529 self.excerpts.read(cx).is_dirty(cx)
530 }
531
532 fn has_conflict(&self, cx: &AppContext) -> bool {
533 self.excerpts.read(cx).has_conflict(cx)
534 }
535
536 fn can_save(&self, _: &AppContext) -> bool {
537 true
538 }
539
540 fn save(
541 &mut self,
542 project: ModelHandle<Project>,
543 cx: &mut ViewContext<Self>,
544 ) -> Task<Result<()>> {
545 self.editor.save(project, cx)
546 }
547
548 fn reload(
549 &mut self,
550 project: ModelHandle<Project>,
551 cx: &mut ViewContext<Self>,
552 ) -> Task<Result<()>> {
553 self.editor.reload(project, cx)
554 }
555
556 fn save_as(
557 &mut self,
558 _: ModelHandle<Project>,
559 _: PathBuf,
560 _: &mut ViewContext<Self>,
561 ) -> Task<Result<()>> {
562 unreachable!()
563 }
564
565 fn should_activate_item_on_event(event: &Self::Event) -> bool {
566 Editor::should_activate_item_on_event(event)
567 }
568
569 fn should_update_tab_on_event(event: &Event) -> bool {
570 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
571 }
572
573 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
574 self.editor.update(cx, |editor, _| {
575 editor.set_nav_history(Some(nav_history));
576 });
577 }
578
579 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
580 where
581 Self: Sized,
582 {
583 Some(ProjectDiagnosticsEditor::new(
584 self.project.clone(),
585 self.workspace.clone(),
586 cx,
587 ))
588 }
589
590 fn act_as_type(
591 &self,
592 type_id: TypeId,
593 self_handle: &ViewHandle<Self>,
594 _: &AppContext,
595 ) -> Option<AnyViewHandle> {
596 if type_id == TypeId::of::<Self>() {
597 Some(self_handle.into())
598 } else if type_id == TypeId::of::<Editor>() {
599 Some((&self.editor).into())
600 } else {
601 None
602 }
603 }
604
605 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
606 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
607 }
608}
609
610fn diagnostic_header_renderer(entry: DiagnosticEntry<Point>, path: ProjectPath) -> RenderBlock {
611 enum JumpIcon {}
612
613 let (message, highlights) = highlight_diagnostic_message(&entry.diagnostic.message);
614 Arc::new(move |cx| {
615 let settings = cx.global::<Settings>();
616 let tooltip_style = settings.theme.tooltip.clone();
617 let theme = &settings.theme.editor;
618 let style = theme.diagnostic_header.clone();
619 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
620 let icon_width = cx.em_width * style.icon_width_factor;
621 let icon = if entry.diagnostic.severity == DiagnosticSeverity::ERROR {
622 Svg::new("icons/diagnostic-error-10.svg")
623 .with_color(theme.error_diagnostic.message.text.color)
624 } else {
625 Svg::new("icons/diagnostic-warning-10.svg")
626 .with_color(theme.warning_diagnostic.message.text.color)
627 };
628
629 let x_padding = cx.gutter_padding + cx.scroll_x * cx.em_width;
630 Flex::row()
631 .with_child(
632 icon.constrained()
633 .with_width(icon_width)
634 .aligned()
635 .contained()
636 .boxed(),
637 )
638 .with_child(
639 Label::new(
640 message.clone(),
641 style.message.label.clone().with_font_size(font_size),
642 )
643 .with_highlights(highlights.clone())
644 .contained()
645 .with_style(style.message.container)
646 .with_margin_left(cx.gutter_padding)
647 .aligned()
648 .boxed(),
649 )
650 .with_children(entry.diagnostic.code.clone().map(|code| {
651 Label::new(code, style.code.text.clone().with_font_size(font_size))
652 .contained()
653 .with_style(style.code.container)
654 .aligned()
655 .boxed()
656 }))
657 .with_child(
658 MouseEventHandler::new::<JumpIcon, _, _>(
659 entry.diagnostic.group_id,
660 cx,
661 |state, _| {
662 let style = style.jump_icon.style_for(state, false);
663 Svg::new("icons/jump.svg")
664 .with_color(style.color)
665 .constrained()
666 .with_width(style.icon_width)
667 .aligned()
668 .contained()
669 .with_style(style.container)
670 .constrained()
671 .with_width(style.button_width)
672 .with_height(style.button_width)
673 .boxed()
674 },
675 )
676 .with_cursor_style(CursorStyle::PointingHand)
677 .on_click({
678 let entry = entry.clone();
679 let path = path.clone();
680 move |_, _, cx| {
681 cx.dispatch_action(Jump {
682 path: path.clone(),
683 range: entry.range.clone(),
684 });
685 }
686 })
687 .with_tooltip(
688 entry.diagnostic.group_id,
689 "Jump to diagnostic".to_string(),
690 Some(Box::new(editor::OpenExcerpts)),
691 tooltip_style,
692 cx,
693 )
694 .aligned()
695 .flex_float()
696 .boxed(),
697 )
698 .contained()
699 .with_style(style.container)
700 .with_padding_left(x_padding)
701 .with_padding_right(x_padding)
702 .expanded()
703 .named("diagnostic header")
704 })
705}
706
707pub(crate) fn render_summary(
708 summary: &DiagnosticSummary,
709 text_style: &TextStyle,
710 theme: &theme::ProjectDiagnostics,
711) -> ElementBox {
712 if summary.error_count == 0 && summary.warning_count == 0 {
713 Label::new("No problems".to_string(), text_style.clone()).boxed()
714 } else {
715 let icon_width = theme.tab_icon_width;
716 let icon_spacing = theme.tab_icon_spacing;
717 let summary_spacing = theme.tab_summary_spacing;
718 Flex::row()
719 .with_children([
720 Svg::new("icons/diagnostic-summary-error.svg")
721 .with_color(text_style.color)
722 .constrained()
723 .with_width(icon_width)
724 .aligned()
725 .contained()
726 .with_margin_right(icon_spacing)
727 .named("no-icon"),
728 Label::new(
729 summary.error_count.to_string(),
730 LabelStyle {
731 text: text_style.clone(),
732 highlight_text: None,
733 },
734 )
735 .aligned()
736 .boxed(),
737 Svg::new("icons/diagnostic-summary-warning.svg")
738 .with_color(text_style.color)
739 .constrained()
740 .with_width(icon_width)
741 .aligned()
742 .contained()
743 .with_margin_left(summary_spacing)
744 .with_margin_right(icon_spacing)
745 .named("warn-icon"),
746 Label::new(
747 summary.warning_count.to_string(),
748 LabelStyle {
749 text: text_style.clone(),
750 highlight_text: None,
751 },
752 )
753 .aligned()
754 .boxed(),
755 ])
756 .boxed()
757 }
758}
759
760fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
761 lhs: &DiagnosticEntry<L>,
762 rhs: &DiagnosticEntry<R>,
763 snapshot: &language::BufferSnapshot,
764) -> Ordering {
765 lhs.range
766 .start
767 .to_offset(&snapshot)
768 .cmp(&rhs.range.start.to_offset(snapshot))
769 .then_with(|| {
770 lhs.range
771 .end
772 .to_offset(&snapshot)
773 .cmp(&rhs.range.end.to_offset(snapshot))
774 })
775 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781 use editor::{
782 display_map::{BlockContext, TransformBlock},
783 DisplayPoint,
784 };
785 use gpui::TestAppContext;
786 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
787 use serde_json::json;
788 use unindent::Unindent as _;
789 use workspace::AppState;
790
791 #[gpui::test]
792 async fn test_diagnostics(cx: &mut TestAppContext) {
793 let app_state = cx.update(AppState::test);
794 app_state
795 .fs
796 .as_fake()
797 .insert_tree(
798 "/test",
799 json!({
800 "consts.rs": "
801 const a: i32 = 'a';
802 const b: i32 = c;
803 "
804 .unindent(),
805
806 "main.rs": "
807 fn main() {
808 let x = vec![];
809 let y = vec![];
810 a(x);
811 b(y);
812 // comment 1
813 // comment 2
814 c(y);
815 d(x);
816 }
817 "
818 .unindent(),
819 }),
820 )
821 .await;
822
823 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
824 let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
825
826 // Create some diagnostics
827 project.update(cx, |project, cx| {
828 project
829 .update_diagnostic_entries(
830 PathBuf::from("/test/main.rs"),
831 None,
832 vec![
833 DiagnosticEntry {
834 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
835 diagnostic: Diagnostic {
836 message:
837 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
838 .to_string(),
839 severity: DiagnosticSeverity::INFORMATION,
840 is_primary: false,
841 is_disk_based: true,
842 group_id: 1,
843 ..Default::default()
844 },
845 },
846 DiagnosticEntry {
847 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
848 diagnostic: Diagnostic {
849 message:
850 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
851 .to_string(),
852 severity: DiagnosticSeverity::INFORMATION,
853 is_primary: false,
854 is_disk_based: true,
855 group_id: 0,
856 ..Default::default()
857 },
858 },
859 DiagnosticEntry {
860 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
861 diagnostic: Diagnostic {
862 message: "value moved here".to_string(),
863 severity: DiagnosticSeverity::INFORMATION,
864 is_primary: false,
865 is_disk_based: true,
866 group_id: 1,
867 ..Default::default()
868 },
869 },
870 DiagnosticEntry {
871 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
872 diagnostic: Diagnostic {
873 message: "value moved here".to_string(),
874 severity: DiagnosticSeverity::INFORMATION,
875 is_primary: false,
876 is_disk_based: true,
877 group_id: 0,
878 ..Default::default()
879 },
880 },
881 DiagnosticEntry {
882 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
883 diagnostic: Diagnostic {
884 message: "use of moved value\nvalue used here after move".to_string(),
885 severity: DiagnosticSeverity::ERROR,
886 is_primary: true,
887 is_disk_based: true,
888 group_id: 0,
889 ..Default::default()
890 },
891 },
892 DiagnosticEntry {
893 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
894 diagnostic: Diagnostic {
895 message: "use of moved value\nvalue used here after move".to_string(),
896 severity: DiagnosticSeverity::ERROR,
897 is_primary: true,
898 is_disk_based: true,
899 group_id: 1,
900 ..Default::default()
901 },
902 },
903 ],
904 cx,
905 )
906 .unwrap();
907 });
908
909 // Open the project diagnostics view while there are already diagnostics.
910 let view = cx.add_view(0, |cx| {
911 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
912 });
913
914 view.next_notification(&cx).await;
915 view.update(cx, |view, cx| {
916 assert_eq!(
917 editor_blocks(&view.editor, cx),
918 [
919 (0, "path header block".into()),
920 (2, "diagnostic header".into()),
921 (15, "collapsed context".into()),
922 (16, "diagnostic header".into()),
923 (25, "collapsed context".into()),
924 ]
925 );
926 assert_eq!(
927 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
928 concat!(
929 //
930 // main.rs
931 //
932 "\n", // filename
933 "\n", // padding
934 // diagnostic group 1
935 "\n", // primary message
936 "\n", // padding
937 " let x = vec![];\n",
938 " let y = vec![];\n",
939 "\n", // supporting diagnostic
940 " a(x);\n",
941 " b(y);\n",
942 "\n", // supporting diagnostic
943 " // comment 1\n",
944 " // comment 2\n",
945 " c(y);\n",
946 "\n", // supporting diagnostic
947 " d(x);\n",
948 "\n", // context ellipsis
949 // diagnostic group 2
950 "\n", // primary message
951 "\n", // padding
952 "fn main() {\n",
953 " let x = vec![];\n",
954 "\n", // supporting diagnostic
955 " let y = vec![];\n",
956 " a(x);\n",
957 "\n", // supporting diagnostic
958 " b(y);\n",
959 "\n", // context ellipsis
960 " c(y);\n",
961 " d(x);\n",
962 "\n", // supporting diagnostic
963 "}"
964 )
965 );
966
967 // Cursor is at the first diagnostic
968 view.editor.update(cx, |editor, cx| {
969 assert_eq!(
970 editor.selections.display_ranges(cx),
971 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
972 );
973 });
974 });
975
976 // Diagnostics are added for another earlier path.
977 project.update(cx, |project, cx| {
978 project.disk_based_diagnostics_started(cx);
979 project
980 .update_diagnostic_entries(
981 PathBuf::from("/test/consts.rs"),
982 None,
983 vec![DiagnosticEntry {
984 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
985 diagnostic: Diagnostic {
986 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
987 severity: DiagnosticSeverity::ERROR,
988 is_primary: true,
989 is_disk_based: true,
990 group_id: 0,
991 ..Default::default()
992 },
993 }],
994 cx,
995 )
996 .unwrap();
997 project.disk_based_diagnostics_finished(cx);
998 });
999
1000 view.next_notification(&cx).await;
1001 view.update(cx, |view, cx| {
1002 assert_eq!(
1003 editor_blocks(&view.editor, cx),
1004 [
1005 (0, "path header block".into()),
1006 (2, "diagnostic header".into()),
1007 (7, "path header block".into()),
1008 (9, "diagnostic header".into()),
1009 (22, "collapsed context".into()),
1010 (23, "diagnostic header".into()),
1011 (32, "collapsed context".into()),
1012 ]
1013 );
1014 assert_eq!(
1015 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1016 concat!(
1017 //
1018 // consts.rs
1019 //
1020 "\n", // filename
1021 "\n", // padding
1022 // diagnostic group 1
1023 "\n", // primary message
1024 "\n", // padding
1025 "const a: i32 = 'a';\n",
1026 "\n", // supporting diagnostic
1027 "const b: i32 = c;\n",
1028 //
1029 // main.rs
1030 //
1031 "\n", // filename
1032 "\n", // padding
1033 // diagnostic group 1
1034 "\n", // primary message
1035 "\n", // padding
1036 " let x = vec![];\n",
1037 " let y = vec![];\n",
1038 "\n", // supporting diagnostic
1039 " a(x);\n",
1040 " b(y);\n",
1041 "\n", // supporting diagnostic
1042 " // comment 1\n",
1043 " // comment 2\n",
1044 " c(y);\n",
1045 "\n", // supporting diagnostic
1046 " d(x);\n",
1047 "\n", // collapsed context
1048 // diagnostic group 2
1049 "\n", // primary message
1050 "\n", // filename
1051 "fn main() {\n",
1052 " let x = vec![];\n",
1053 "\n", // supporting diagnostic
1054 " let y = vec![];\n",
1055 " a(x);\n",
1056 "\n", // supporting diagnostic
1057 " b(y);\n",
1058 "\n", // context ellipsis
1059 " c(y);\n",
1060 " d(x);\n",
1061 "\n", // supporting diagnostic
1062 "}"
1063 )
1064 );
1065
1066 // Cursor keeps its position.
1067 view.editor.update(cx, |editor, cx| {
1068 assert_eq!(
1069 editor.selections.display_ranges(cx),
1070 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1071 );
1072 });
1073 });
1074
1075 // Diagnostics are added to the first path
1076 project.update(cx, |project, cx| {
1077 project.disk_based_diagnostics_started(cx);
1078 project
1079 .update_diagnostic_entries(
1080 PathBuf::from("/test/consts.rs"),
1081 None,
1082 vec![
1083 DiagnosticEntry {
1084 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1085 diagnostic: Diagnostic {
1086 message: "mismatched types\nexpected `usize`, found `char`"
1087 .to_string(),
1088 severity: DiagnosticSeverity::ERROR,
1089 is_primary: true,
1090 is_disk_based: true,
1091 group_id: 0,
1092 ..Default::default()
1093 },
1094 },
1095 DiagnosticEntry {
1096 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1097 diagnostic: Diagnostic {
1098 message: "unresolved name `c`".to_string(),
1099 severity: DiagnosticSeverity::ERROR,
1100 is_primary: true,
1101 is_disk_based: true,
1102 group_id: 1,
1103 ..Default::default()
1104 },
1105 },
1106 ],
1107 cx,
1108 )
1109 .unwrap();
1110 project.disk_based_diagnostics_finished(cx);
1111 });
1112
1113 view.next_notification(&cx).await;
1114 view.update(cx, |view, cx| {
1115 assert_eq!(
1116 editor_blocks(&view.editor, cx),
1117 [
1118 (0, "path header block".into()),
1119 (2, "diagnostic header".into()),
1120 (7, "collapsed context".into()),
1121 (8, "diagnostic header".into()),
1122 (13, "path header block".into()),
1123 (15, "diagnostic header".into()),
1124 (28, "collapsed context".into()),
1125 (29, "diagnostic header".into()),
1126 (38, "collapsed context".into()),
1127 ]
1128 );
1129 assert_eq!(
1130 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1131 concat!(
1132 //
1133 // consts.rs
1134 //
1135 "\n", // filename
1136 "\n", // padding
1137 // diagnostic group 1
1138 "\n", // primary message
1139 "\n", // padding
1140 "const a: i32 = 'a';\n",
1141 "\n", // supporting diagnostic
1142 "const b: i32 = c;\n",
1143 "\n", // context ellipsis
1144 // diagnostic group 2
1145 "\n", // primary message
1146 "\n", // padding
1147 "const a: i32 = 'a';\n",
1148 "const b: i32 = c;\n",
1149 "\n", // supporting diagnostic
1150 //
1151 // main.rs
1152 //
1153 "\n", // filename
1154 "\n", // padding
1155 // diagnostic group 1
1156 "\n", // primary message
1157 "\n", // padding
1158 " let x = vec![];\n",
1159 " let y = vec![];\n",
1160 "\n", // supporting diagnostic
1161 " a(x);\n",
1162 " b(y);\n",
1163 "\n", // supporting diagnostic
1164 " // comment 1\n",
1165 " // comment 2\n",
1166 " c(y);\n",
1167 "\n", // supporting diagnostic
1168 " d(x);\n",
1169 "\n", // context ellipsis
1170 // diagnostic group 2
1171 "\n", // primary message
1172 "\n", // filename
1173 "fn main() {\n",
1174 " let x = vec![];\n",
1175 "\n", // supporting diagnostic
1176 " let y = vec![];\n",
1177 " a(x);\n",
1178 "\n", // supporting diagnostic
1179 " b(y);\n",
1180 "\n", // context ellipsis
1181 " c(y);\n",
1182 " d(x);\n",
1183 "\n", // supporting diagnostic
1184 "}"
1185 )
1186 );
1187 });
1188 }
1189
1190 fn editor_blocks(
1191 editor: &ViewHandle<Editor>,
1192 cx: &mut MutableAppContext,
1193 ) -> Vec<(u32, String)> {
1194 let mut presenter = cx.build_presenter(editor.id(), 0.);
1195 let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1196 cx.render(editor, |editor, cx| {
1197 let snapshot = editor.snapshot(cx);
1198 snapshot
1199 .blocks_in_range(0..snapshot.max_point().row())
1200 .filter_map(|(row, block)| {
1201 let name = match block {
1202 TransformBlock::Custom(block) => block
1203 .render(&mut BlockContext {
1204 cx,
1205 anchor_x: 0.,
1206 scroll_x: 0.,
1207 gutter_padding: 0.,
1208 gutter_width: 0.,
1209 line_height: 0.,
1210 em_width: 0.,
1211 })
1212 .name()?
1213 .to_string(),
1214 TransformBlock::ExcerptHeader {
1215 starts_new_buffer, ..
1216 } => {
1217 if *starts_new_buffer {
1218 "path header block".to_string()
1219 } else {
1220 "collapsed context".to_string()
1221 }
1222 }
1223 };
1224
1225 Some((row, name))
1226 })
1227 .collect()
1228 })
1229 }
1230}