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