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