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