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