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 theme = &settings.theme.editor;
617 let style = theme.diagnostic_header.clone();
618 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
619 let icon_width = cx.em_width * style.icon_width_factor;
620 let icon = if entry.diagnostic.severity == DiagnosticSeverity::ERROR {
621 Svg::new("icons/diagnostic-error-10.svg")
622 .with_color(theme.error_diagnostic.message.text.color)
623 } else {
624 Svg::new("icons/diagnostic-warning-10.svg")
625 .with_color(theme.warning_diagnostic.message.text.color)
626 };
627
628 let x_padding = cx.gutter_padding + cx.scroll_x * cx.em_width;
629 Flex::row()
630 .with_child(
631 icon.constrained()
632 .with_width(icon_width)
633 .aligned()
634 .contained()
635 .boxed(),
636 )
637 .with_child(
638 Label::new(
639 message.clone(),
640 style.message.label.clone().with_font_size(font_size),
641 )
642 .with_highlights(highlights.clone())
643 .contained()
644 .with_style(style.message.container)
645 .with_margin_left(cx.gutter_padding)
646 .aligned()
647 .boxed(),
648 )
649 .with_children(entry.diagnostic.code.clone().map(|code| {
650 Label::new(code, style.code.text.clone().with_font_size(font_size))
651 .contained()
652 .with_style(style.code.container)
653 .aligned()
654 .boxed()
655 }))
656 .with_child(
657 MouseEventHandler::new::<JumpIcon, _, _>(
658 entry.diagnostic.group_id,
659 cx,
660 |state, _| {
661 let style = style.jump_icon.style_for(state, false);
662 Svg::new("icons/jump.svg")
663 .with_color(style.color)
664 .constrained()
665 .with_width(style.icon_width)
666 .aligned()
667 .contained()
668 .with_style(style.container)
669 .constrained()
670 .with_width(style.button_width)
671 .with_height(style.button_width)
672 .boxed()
673 },
674 )
675 .with_cursor_style(CursorStyle::PointingHand)
676 .on_click({
677 let entry = entry.clone();
678 let path = path.clone();
679 move |_, _, cx| {
680 cx.dispatch_action(Jump {
681 path: path.clone(),
682 range: entry.range.clone(),
683 });
684 }
685 })
686 .aligned()
687 .flex_float()
688 .boxed(),
689 )
690 .contained()
691 .with_style(style.container)
692 .with_padding_left(x_padding)
693 .with_padding_right(x_padding)
694 .expanded()
695 .named("diagnostic header")
696 })
697}
698
699pub(crate) fn render_summary(
700 summary: &DiagnosticSummary,
701 text_style: &TextStyle,
702 theme: &theme::ProjectDiagnostics,
703) -> ElementBox {
704 if summary.error_count == 0 && summary.warning_count == 0 {
705 Label::new("No problems".to_string(), text_style.clone()).boxed()
706 } else {
707 let icon_width = theme.tab_icon_width;
708 let icon_spacing = theme.tab_icon_spacing;
709 let summary_spacing = theme.tab_summary_spacing;
710 Flex::row()
711 .with_children([
712 Svg::new("icons/diagnostic-summary-error.svg")
713 .with_color(text_style.color)
714 .constrained()
715 .with_width(icon_width)
716 .aligned()
717 .contained()
718 .with_margin_right(icon_spacing)
719 .named("no-icon"),
720 Label::new(
721 summary.error_count.to_string(),
722 LabelStyle {
723 text: text_style.clone(),
724 highlight_text: None,
725 },
726 )
727 .aligned()
728 .boxed(),
729 Svg::new("icons/diagnostic-summary-warning.svg")
730 .with_color(text_style.color)
731 .constrained()
732 .with_width(icon_width)
733 .aligned()
734 .contained()
735 .with_margin_left(summary_spacing)
736 .with_margin_right(icon_spacing)
737 .named("warn-icon"),
738 Label::new(
739 summary.warning_count.to_string(),
740 LabelStyle {
741 text: text_style.clone(),
742 highlight_text: None,
743 },
744 )
745 .aligned()
746 .boxed(),
747 ])
748 .boxed()
749 }
750}
751
752fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
753 lhs: &DiagnosticEntry<L>,
754 rhs: &DiagnosticEntry<R>,
755 snapshot: &language::BufferSnapshot,
756) -> Ordering {
757 lhs.range
758 .start
759 .to_offset(&snapshot)
760 .cmp(&rhs.range.start.to_offset(snapshot))
761 .then_with(|| {
762 lhs.range
763 .end
764 .to_offset(&snapshot)
765 .cmp(&rhs.range.end.to_offset(snapshot))
766 })
767 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use editor::{
774 display_map::{BlockContext, TransformBlock},
775 DisplayPoint,
776 };
777 use gpui::TestAppContext;
778 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
779 use serde_json::json;
780 use unindent::Unindent as _;
781 use workspace::AppState;
782
783 #[gpui::test]
784 async fn test_diagnostics(cx: &mut TestAppContext) {
785 let app_state = cx.update(AppState::test);
786 app_state
787 .fs
788 .as_fake()
789 .insert_tree(
790 "/test",
791 json!({
792 "consts.rs": "
793 const a: i32 = 'a';
794 const b: i32 = c;
795 "
796 .unindent(),
797
798 "main.rs": "
799 fn main() {
800 let x = vec![];
801 let y = vec![];
802 a(x);
803 b(y);
804 // comment 1
805 // comment 2
806 c(y);
807 d(x);
808 }
809 "
810 .unindent(),
811 }),
812 )
813 .await;
814
815 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
816 let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
817
818 // Create some diagnostics
819 project.update(cx, |project, cx| {
820 project
821 .update_diagnostic_entries(
822 PathBuf::from("/test/main.rs"),
823 None,
824 vec![
825 DiagnosticEntry {
826 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
827 diagnostic: Diagnostic {
828 message:
829 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
830 .to_string(),
831 severity: DiagnosticSeverity::INFORMATION,
832 is_primary: false,
833 is_disk_based: true,
834 group_id: 1,
835 ..Default::default()
836 },
837 },
838 DiagnosticEntry {
839 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
840 diagnostic: Diagnostic {
841 message:
842 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
843 .to_string(),
844 severity: DiagnosticSeverity::INFORMATION,
845 is_primary: false,
846 is_disk_based: true,
847 group_id: 0,
848 ..Default::default()
849 },
850 },
851 DiagnosticEntry {
852 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
853 diagnostic: Diagnostic {
854 message: "value moved here".to_string(),
855 severity: DiagnosticSeverity::INFORMATION,
856 is_primary: false,
857 is_disk_based: true,
858 group_id: 1,
859 ..Default::default()
860 },
861 },
862 DiagnosticEntry {
863 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
864 diagnostic: Diagnostic {
865 message: "value moved here".to_string(),
866 severity: DiagnosticSeverity::INFORMATION,
867 is_primary: false,
868 is_disk_based: true,
869 group_id: 0,
870 ..Default::default()
871 },
872 },
873 DiagnosticEntry {
874 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
875 diagnostic: Diagnostic {
876 message: "use of moved value\nvalue used here after move".to_string(),
877 severity: DiagnosticSeverity::ERROR,
878 is_primary: true,
879 is_disk_based: true,
880 group_id: 0,
881 ..Default::default()
882 },
883 },
884 DiagnosticEntry {
885 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
886 diagnostic: Diagnostic {
887 message: "use of moved value\nvalue used here after move".to_string(),
888 severity: DiagnosticSeverity::ERROR,
889 is_primary: true,
890 is_disk_based: true,
891 group_id: 1,
892 ..Default::default()
893 },
894 },
895 ],
896 cx,
897 )
898 .unwrap();
899 });
900
901 // Open the project diagnostics view while there are already diagnostics.
902 let view = cx.add_view(0, |cx| {
903 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
904 });
905
906 view.next_notification(&cx).await;
907 view.update(cx, |view, cx| {
908 assert_eq!(
909 editor_blocks(&view.editor, cx),
910 [
911 (0, "path header block".into()),
912 (2, "diagnostic header".into()),
913 (15, "collapsed context".into()),
914 (16, "diagnostic header".into()),
915 (25, "collapsed context".into()),
916 ]
917 );
918 assert_eq!(
919 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
920 concat!(
921 //
922 // main.rs
923 //
924 "\n", // filename
925 "\n", // padding
926 // diagnostic group 1
927 "\n", // primary message
928 "\n", // padding
929 " let x = vec![];\n",
930 " let y = vec![];\n",
931 "\n", // supporting diagnostic
932 " a(x);\n",
933 " b(y);\n",
934 "\n", // supporting diagnostic
935 " // comment 1\n",
936 " // comment 2\n",
937 " c(y);\n",
938 "\n", // supporting diagnostic
939 " d(x);\n",
940 "\n", // context ellipsis
941 // diagnostic group 2
942 "\n", // primary message
943 "\n", // padding
944 "fn main() {\n",
945 " let x = vec![];\n",
946 "\n", // supporting diagnostic
947 " let y = vec![];\n",
948 " a(x);\n",
949 "\n", // supporting diagnostic
950 " b(y);\n",
951 "\n", // context ellipsis
952 " c(y);\n",
953 " d(x);\n",
954 "\n", // supporting diagnostic
955 "}"
956 )
957 );
958
959 // Cursor is at the first diagnostic
960 view.editor.update(cx, |editor, cx| {
961 assert_eq!(
962 editor.selections.display_ranges(cx),
963 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
964 );
965 });
966 });
967
968 // Diagnostics are added for another earlier path.
969 project.update(cx, |project, cx| {
970 project.disk_based_diagnostics_started(cx);
971 project
972 .update_diagnostic_entries(
973 PathBuf::from("/test/consts.rs"),
974 None,
975 vec![DiagnosticEntry {
976 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
977 diagnostic: Diagnostic {
978 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
979 severity: DiagnosticSeverity::ERROR,
980 is_primary: true,
981 is_disk_based: true,
982 group_id: 0,
983 ..Default::default()
984 },
985 }],
986 cx,
987 )
988 .unwrap();
989 project.disk_based_diagnostics_finished(cx);
990 });
991
992 view.next_notification(&cx).await;
993 view.update(cx, |view, cx| {
994 assert_eq!(
995 editor_blocks(&view.editor, cx),
996 [
997 (0, "path header block".into()),
998 (2, "diagnostic header".into()),
999 (7, "path header block".into()),
1000 (9, "diagnostic header".into()),
1001 (22, "collapsed context".into()),
1002 (23, "diagnostic header".into()),
1003 (32, "collapsed context".into()),
1004 ]
1005 );
1006 assert_eq!(
1007 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1008 concat!(
1009 //
1010 // consts.rs
1011 //
1012 "\n", // filename
1013 "\n", // padding
1014 // diagnostic group 1
1015 "\n", // primary message
1016 "\n", // padding
1017 "const a: i32 = 'a';\n",
1018 "\n", // supporting diagnostic
1019 "const b: i32 = c;\n",
1020 //
1021 // main.rs
1022 //
1023 "\n", // filename
1024 "\n", // padding
1025 // diagnostic group 1
1026 "\n", // primary message
1027 "\n", // padding
1028 " let x = vec![];\n",
1029 " let y = vec![];\n",
1030 "\n", // supporting diagnostic
1031 " a(x);\n",
1032 " b(y);\n",
1033 "\n", // supporting diagnostic
1034 " // comment 1\n",
1035 " // comment 2\n",
1036 " c(y);\n",
1037 "\n", // supporting diagnostic
1038 " d(x);\n",
1039 "\n", // collapsed context
1040 // diagnostic group 2
1041 "\n", // primary message
1042 "\n", // filename
1043 "fn main() {\n",
1044 " let x = vec![];\n",
1045 "\n", // supporting diagnostic
1046 " let y = vec![];\n",
1047 " a(x);\n",
1048 "\n", // supporting diagnostic
1049 " b(y);\n",
1050 "\n", // context ellipsis
1051 " c(y);\n",
1052 " d(x);\n",
1053 "\n", // supporting diagnostic
1054 "}"
1055 )
1056 );
1057
1058 // Cursor keeps its position.
1059 view.editor.update(cx, |editor, cx| {
1060 assert_eq!(
1061 editor.selections.display_ranges(cx),
1062 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1063 );
1064 });
1065 });
1066
1067 // Diagnostics are added to the first path
1068 project.update(cx, |project, cx| {
1069 project.disk_based_diagnostics_started(cx);
1070 project
1071 .update_diagnostic_entries(
1072 PathBuf::from("/test/consts.rs"),
1073 None,
1074 vec![
1075 DiagnosticEntry {
1076 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1077 diagnostic: Diagnostic {
1078 message: "mismatched types\nexpected `usize`, found `char`"
1079 .to_string(),
1080 severity: DiagnosticSeverity::ERROR,
1081 is_primary: true,
1082 is_disk_based: true,
1083 group_id: 0,
1084 ..Default::default()
1085 },
1086 },
1087 DiagnosticEntry {
1088 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1089 diagnostic: Diagnostic {
1090 message: "unresolved name `c`".to_string(),
1091 severity: DiagnosticSeverity::ERROR,
1092 is_primary: true,
1093 is_disk_based: true,
1094 group_id: 1,
1095 ..Default::default()
1096 },
1097 },
1098 ],
1099 cx,
1100 )
1101 .unwrap();
1102 project.disk_based_diagnostics_finished(cx);
1103 });
1104
1105 view.next_notification(&cx).await;
1106 view.update(cx, |view, cx| {
1107 assert_eq!(
1108 editor_blocks(&view.editor, cx),
1109 [
1110 (0, "path header block".into()),
1111 (2, "diagnostic header".into()),
1112 (7, "collapsed context".into()),
1113 (8, "diagnostic header".into()),
1114 (13, "path header block".into()),
1115 (15, "diagnostic header".into()),
1116 (28, "collapsed context".into()),
1117 (29, "diagnostic header".into()),
1118 (38, "collapsed context".into()),
1119 ]
1120 );
1121 assert_eq!(
1122 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1123 concat!(
1124 //
1125 // consts.rs
1126 //
1127 "\n", // filename
1128 "\n", // padding
1129 // diagnostic group 1
1130 "\n", // primary message
1131 "\n", // padding
1132 "const a: i32 = 'a';\n",
1133 "\n", // supporting diagnostic
1134 "const b: i32 = c;\n",
1135 "\n", // context ellipsis
1136 // diagnostic group 2
1137 "\n", // primary message
1138 "\n", // padding
1139 "const a: i32 = 'a';\n",
1140 "const b: i32 = c;\n",
1141 "\n", // supporting diagnostic
1142 //
1143 // main.rs
1144 //
1145 "\n", // filename
1146 "\n", // padding
1147 // diagnostic group 1
1148 "\n", // primary message
1149 "\n", // padding
1150 " let x = vec![];\n",
1151 " let y = vec![];\n",
1152 "\n", // supporting diagnostic
1153 " a(x);\n",
1154 " b(y);\n",
1155 "\n", // supporting diagnostic
1156 " // comment 1\n",
1157 " // comment 2\n",
1158 " c(y);\n",
1159 "\n", // supporting diagnostic
1160 " d(x);\n",
1161 "\n", // context ellipsis
1162 // diagnostic group 2
1163 "\n", // primary message
1164 "\n", // filename
1165 "fn main() {\n",
1166 " let x = vec![];\n",
1167 "\n", // supporting diagnostic
1168 " let y = vec![];\n",
1169 " a(x);\n",
1170 "\n", // supporting diagnostic
1171 " b(y);\n",
1172 "\n", // context ellipsis
1173 " c(y);\n",
1174 " d(x);\n",
1175 "\n", // supporting diagnostic
1176 "}"
1177 )
1178 );
1179 });
1180 }
1181
1182 fn editor_blocks(
1183 editor: &ViewHandle<Editor>,
1184 cx: &mut MutableAppContext,
1185 ) -> Vec<(u32, String)> {
1186 let mut presenter = cx.build_presenter(editor.id(), 0.);
1187 let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1188 cx.render(editor, |editor, cx| {
1189 let snapshot = editor.snapshot(cx);
1190 snapshot
1191 .blocks_in_range(0..snapshot.max_point().row())
1192 .filter_map(|(row, block)| {
1193 let name = match block {
1194 TransformBlock::Custom(block) => block
1195 .render(&mut BlockContext {
1196 cx,
1197 anchor_x: 0.,
1198 scroll_x: 0.,
1199 gutter_padding: 0.,
1200 gutter_width: 0.,
1201 line_height: 0.,
1202 em_width: 0.,
1203 })
1204 .name()?
1205 .to_string(),
1206 TransformBlock::ExcerptHeader {
1207 starts_new_buffer, ..
1208 } => {
1209 if *starts_new_buffer {
1210 "path header block".to_string()
1211 } else {
1212 "collapsed context".to_string()
1213 }
1214 }
1215 };
1216
1217 Some((row, name))
1218 })
1219 .collect()
1220 })
1221 }
1222}