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