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