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("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 ) -> ElementBox {
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 is_dirty(&self, cx: &AppContext) -> bool {
535 self.excerpts.read(cx).is_dirty(cx)
536 }
537
538 fn has_conflict(&self, cx: &AppContext) -> bool {
539 self.excerpts.read(cx).has_conflict(cx)
540 }
541
542 fn can_save(&self, _: &AppContext) -> bool {
543 true
544 }
545
546 fn save(
547 &mut self,
548 project: ModelHandle<Project>,
549 cx: &mut ViewContext<Self>,
550 ) -> Task<Result<()>> {
551 self.editor.save(project, cx)
552 }
553
554 fn reload(
555 &mut self,
556 project: ModelHandle<Project>,
557 cx: &mut ViewContext<Self>,
558 ) -> Task<Result<()>> {
559 self.editor.reload(project, cx)
560 }
561
562 fn save_as(
563 &mut self,
564 _: ModelHandle<Project>,
565 _: PathBuf,
566 _: &mut ViewContext<Self>,
567 ) -> Task<Result<()>> {
568 unreachable!()
569 }
570
571 fn git_diff_recalc(
572 &mut self,
573 project: ModelHandle<Project>,
574 cx: &mut ViewContext<Self>,
575 ) -> Task<Result<()>> {
576 self.editor
577 .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
578 }
579
580 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
581 Editor::to_item_events(event)
582 }
583
584 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
585 self.editor.update(cx, |editor, _| {
586 editor.set_nav_history(Some(nav_history));
587 });
588 }
589
590 fn clone_on_split(
591 &self,
592 _workspace_id: workspace::WorkspaceId,
593 cx: &mut ViewContext<Self>,
594 ) -> Option<Self>
595 where
596 Self: Sized,
597 {
598 Some(ProjectDiagnosticsEditor::new(
599 self.project.clone(),
600 self.workspace.clone(),
601 cx,
602 ))
603 }
604
605 fn act_as_type(
606 &self,
607 type_id: TypeId,
608 self_handle: &ViewHandle<Self>,
609 _: &AppContext,
610 ) -> Option<AnyViewHandle> {
611 if type_id == TypeId::of::<Self>() {
612 Some(self_handle.into())
613 } else if type_id == TypeId::of::<Editor>() {
614 Some((&self.editor).into())
615 } else {
616 None
617 }
618 }
619
620 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
621 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
622 }
623
624 fn serialized_item_kind() -> Option<&'static str> {
625 Some("diagnostics")
626 }
627
628 fn deserialize(
629 project: ModelHandle<Project>,
630 workspace: WeakViewHandle<Workspace>,
631 _workspace_id: workspace::WorkspaceId,
632 _item_id: workspace::ItemId,
633 cx: &mut ViewContext<Pane>,
634 ) -> Task<Result<ViewHandle<Self>>> {
635 Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
636 }
637}
638
639fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
640 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
641 Arc::new(move |cx| {
642 let settings = cx.global::<Settings>();
643 let theme = &settings.theme.editor;
644 let style = theme.diagnostic_header.clone();
645 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
646 let icon_width = cx.em_width * style.icon_width_factor;
647 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
648 Svg::new("icons/circle_x_mark_12.svg")
649 .with_color(theme.error_diagnostic.message.text.color)
650 } else {
651 Svg::new("icons/triangle_exclamation_12.svg")
652 .with_color(theme.warning_diagnostic.message.text.color)
653 };
654
655 Flex::row()
656 .with_child(
657 icon.constrained()
658 .with_width(icon_width)
659 .aligned()
660 .contained()
661 .boxed(),
662 )
663 .with_child(
664 Label::new(
665 message.clone(),
666 style.message.label.clone().with_font_size(font_size),
667 )
668 .with_highlights(highlights.clone())
669 .contained()
670 .with_style(style.message.container)
671 .with_margin_left(cx.gutter_padding)
672 .aligned()
673 .boxed(),
674 )
675 .with_children(diagnostic.code.clone().map(|code| {
676 Label::new(code, style.code.text.clone().with_font_size(font_size))
677 .contained()
678 .with_style(style.code.container)
679 .aligned()
680 .boxed()
681 }))
682 .contained()
683 .with_style(style.container)
684 .with_padding_left(cx.gutter_padding)
685 .with_padding_right(cx.gutter_padding)
686 .expanded()
687 .named("diagnostic header")
688 })
689}
690
691pub(crate) fn render_summary(
692 summary: &DiagnosticSummary,
693 text_style: &TextStyle,
694 theme: &theme::ProjectDiagnostics,
695) -> ElementBox {
696 if summary.error_count == 0 && summary.warning_count == 0 {
697 Label::new("No problems", text_style.clone()).boxed()
698 } else {
699 let icon_width = theme.tab_icon_width;
700 let icon_spacing = theme.tab_icon_spacing;
701 let summary_spacing = theme.tab_summary_spacing;
702 Flex::row()
703 .with_children([
704 Svg::new("icons/circle_x_mark_12.svg")
705 .with_color(text_style.color)
706 .constrained()
707 .with_width(icon_width)
708 .aligned()
709 .contained()
710 .with_margin_right(icon_spacing)
711 .named("no-icon"),
712 Label::new(
713 summary.error_count.to_string(),
714 LabelStyle {
715 text: text_style.clone(),
716 highlight_text: None,
717 },
718 )
719 .aligned()
720 .boxed(),
721 Svg::new("icons/triangle_exclamation_12.svg")
722 .with_color(text_style.color)
723 .constrained()
724 .with_width(icon_width)
725 .aligned()
726 .contained()
727 .with_margin_left(summary_spacing)
728 .with_margin_right(icon_spacing)
729 .named("warn-icon"),
730 Label::new(
731 summary.warning_count.to_string(),
732 LabelStyle {
733 text: text_style.clone(),
734 highlight_text: None,
735 },
736 )
737 .aligned()
738 .boxed(),
739 ])
740 .boxed()
741 }
742}
743
744fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
745 lhs: &DiagnosticEntry<L>,
746 rhs: &DiagnosticEntry<R>,
747 snapshot: &language::BufferSnapshot,
748) -> Ordering {
749 lhs.range
750 .start
751 .to_offset(snapshot)
752 .cmp(&rhs.range.start.to_offset(snapshot))
753 .then_with(|| {
754 lhs.range
755 .end
756 .to_offset(snapshot)
757 .cmp(&rhs.range.end.to_offset(snapshot))
758 })
759 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use editor::{
766 display_map::{BlockContext, TransformBlock},
767 DisplayPoint,
768 };
769 use gpui::TestAppContext;
770 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
771 use serde_json::json;
772 use unindent::Unindent as _;
773 use workspace::AppState;
774
775 #[gpui::test]
776 async fn test_diagnostics(cx: &mut TestAppContext) {
777 let app_state = cx.update(AppState::test);
778 app_state
779 .fs
780 .as_fake()
781 .insert_tree(
782 "/test",
783 json!({
784 "consts.rs": "
785 const a: i32 = 'a';
786 const b: i32 = c;
787 "
788 .unindent(),
789
790 "main.rs": "
791 fn main() {
792 let x = vec![];
793 let y = vec![];
794 a(x);
795 b(y);
796 // comment 1
797 // comment 2
798 c(y);
799 d(x);
800 }
801 "
802 .unindent(),
803 }),
804 )
805 .await;
806
807 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
808 let (_, workspace) = cx.add_window(|cx| {
809 Workspace::new(
810 Default::default(),
811 0,
812 project.clone(),
813 |_, _| unimplemented!(),
814 cx,
815 )
816 });
817
818 // Create some diagnostics
819 project.update(cx, |project, cx| {
820 project
821 .update_diagnostic_entries(
822 0,
823 PathBuf::from("/test/main.rs"),
824 None,
825 vec![
826 DiagnosticEntry {
827 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
828 diagnostic: Diagnostic {
829 message:
830 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
831 .to_string(),
832 severity: DiagnosticSeverity::INFORMATION,
833 is_primary: false,
834 is_disk_based: true,
835 group_id: 1,
836 ..Default::default()
837 },
838 },
839 DiagnosticEntry {
840 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
841 diagnostic: Diagnostic {
842 message:
843 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
844 .to_string(),
845 severity: DiagnosticSeverity::INFORMATION,
846 is_primary: false,
847 is_disk_based: true,
848 group_id: 0,
849 ..Default::default()
850 },
851 },
852 DiagnosticEntry {
853 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
854 diagnostic: Diagnostic {
855 message: "value moved here".to_string(),
856 severity: DiagnosticSeverity::INFORMATION,
857 is_primary: false,
858 is_disk_based: true,
859 group_id: 1,
860 ..Default::default()
861 },
862 },
863 DiagnosticEntry {
864 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
865 diagnostic: Diagnostic {
866 message: "value moved here".to_string(),
867 severity: DiagnosticSeverity::INFORMATION,
868 is_primary: false,
869 is_disk_based: true,
870 group_id: 0,
871 ..Default::default()
872 },
873 },
874 DiagnosticEntry {
875 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
876 diagnostic: Diagnostic {
877 message: "use of moved value\nvalue used here after move".to_string(),
878 severity: DiagnosticSeverity::ERROR,
879 is_primary: true,
880 is_disk_based: true,
881 group_id: 0,
882 ..Default::default()
883 },
884 },
885 DiagnosticEntry {
886 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
887 diagnostic: Diagnostic {
888 message: "use of moved value\nvalue used here after move".to_string(),
889 severity: DiagnosticSeverity::ERROR,
890 is_primary: true,
891 is_disk_based: true,
892 group_id: 1,
893 ..Default::default()
894 },
895 },
896 ],
897 cx,
898 )
899 .unwrap();
900 });
901
902 // Open the project diagnostics view while there are already diagnostics.
903 let view = cx.add_view(&workspace, |cx| {
904 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
905 });
906
907 view.next_notification(cx).await;
908 view.update(cx, |view, cx| {
909 assert_eq!(
910 editor_blocks(&view.editor, cx),
911 [
912 (0, "path header block".into()),
913 (2, "diagnostic header".into()),
914 (15, "collapsed context".into()),
915 (16, "diagnostic header".into()),
916 (25, "collapsed context".into()),
917 ]
918 );
919 assert_eq!(
920 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
921 concat!(
922 //
923 // main.rs
924 //
925 "\n", // filename
926 "\n", // padding
927 // diagnostic group 1
928 "\n", // primary message
929 "\n", // padding
930 " let x = vec![];\n",
931 " let y = vec![];\n",
932 "\n", // supporting diagnostic
933 " a(x);\n",
934 " b(y);\n",
935 "\n", // supporting diagnostic
936 " // comment 1\n",
937 " // comment 2\n",
938 " c(y);\n",
939 "\n", // supporting diagnostic
940 " d(x);\n",
941 "\n", // context ellipsis
942 // diagnostic group 2
943 "\n", // primary message
944 "\n", // padding
945 "fn main() {\n",
946 " let x = vec![];\n",
947 "\n", // supporting diagnostic
948 " let y = vec![];\n",
949 " a(x);\n",
950 "\n", // supporting diagnostic
951 " b(y);\n",
952 "\n", // context ellipsis
953 " c(y);\n",
954 " d(x);\n",
955 "\n", // supporting diagnostic
956 "}"
957 )
958 );
959
960 // Cursor is at the first diagnostic
961 view.editor.update(cx, |editor, cx| {
962 assert_eq!(
963 editor.selections.display_ranges(cx),
964 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
965 );
966 });
967 });
968
969 // Diagnostics are added for another earlier path.
970 project.update(cx, |project, cx| {
971 project.disk_based_diagnostics_started(0, cx);
972 project
973 .update_diagnostic_entries(
974 0,
975 PathBuf::from("/test/consts.rs"),
976 None,
977 vec![DiagnosticEntry {
978 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
979 diagnostic: Diagnostic {
980 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
981 severity: DiagnosticSeverity::ERROR,
982 is_primary: true,
983 is_disk_based: true,
984 group_id: 0,
985 ..Default::default()
986 },
987 }],
988 cx,
989 )
990 .unwrap();
991 project.disk_based_diagnostics_finished(0, cx);
992 });
993
994 view.next_notification(cx).await;
995 view.update(cx, |view, cx| {
996 assert_eq!(
997 editor_blocks(&view.editor, cx),
998 [
999 (0, "path header block".into()),
1000 (2, "diagnostic header".into()),
1001 (7, "path header block".into()),
1002 (9, "diagnostic header".into()),
1003 (22, "collapsed context".into()),
1004 (23, "diagnostic header".into()),
1005 (32, "collapsed context".into()),
1006 ]
1007 );
1008 assert_eq!(
1009 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1010 concat!(
1011 //
1012 // consts.rs
1013 //
1014 "\n", // filename
1015 "\n", // padding
1016 // diagnostic group 1
1017 "\n", // primary message
1018 "\n", // padding
1019 "const a: i32 = 'a';\n",
1020 "\n", // supporting diagnostic
1021 "const b: i32 = c;\n",
1022 //
1023 // main.rs
1024 //
1025 "\n", // filename
1026 "\n", // padding
1027 // diagnostic group 1
1028 "\n", // primary message
1029 "\n", // padding
1030 " let x = vec![];\n",
1031 " let y = vec![];\n",
1032 "\n", // supporting diagnostic
1033 " a(x);\n",
1034 " b(y);\n",
1035 "\n", // supporting diagnostic
1036 " // comment 1\n",
1037 " // comment 2\n",
1038 " c(y);\n",
1039 "\n", // supporting diagnostic
1040 " d(x);\n",
1041 "\n", // collapsed context
1042 // diagnostic group 2
1043 "\n", // primary message
1044 "\n", // filename
1045 "fn main() {\n",
1046 " let x = vec![];\n",
1047 "\n", // supporting diagnostic
1048 " let y = vec![];\n",
1049 " a(x);\n",
1050 "\n", // supporting diagnostic
1051 " b(y);\n",
1052 "\n", // context ellipsis
1053 " c(y);\n",
1054 " d(x);\n",
1055 "\n", // supporting diagnostic
1056 "}"
1057 )
1058 );
1059
1060 // Cursor keeps its position.
1061 view.editor.update(cx, |editor, cx| {
1062 assert_eq!(
1063 editor.selections.display_ranges(cx),
1064 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1065 );
1066 });
1067 });
1068
1069 // Diagnostics are added to the first path
1070 project.update(cx, |project, cx| {
1071 project.disk_based_diagnostics_started(0, cx);
1072 project
1073 .update_diagnostic_entries(
1074 0,
1075 PathBuf::from("/test/consts.rs"),
1076 None,
1077 vec![
1078 DiagnosticEntry {
1079 range: Unclipped(PointUtf16::new(0, 15))
1080 ..Unclipped(PointUtf16::new(0, 15)),
1081 diagnostic: Diagnostic {
1082 message: "mismatched types\nexpected `usize`, found `char`"
1083 .to_string(),
1084 severity: DiagnosticSeverity::ERROR,
1085 is_primary: true,
1086 is_disk_based: true,
1087 group_id: 0,
1088 ..Default::default()
1089 },
1090 },
1091 DiagnosticEntry {
1092 range: Unclipped(PointUtf16::new(1, 15))
1093 ..Unclipped(PointUtf16::new(1, 15)),
1094 diagnostic: Diagnostic {
1095 message: "unresolved name `c`".to_string(),
1096 severity: DiagnosticSeverity::ERROR,
1097 is_primary: true,
1098 is_disk_based: true,
1099 group_id: 1,
1100 ..Default::default()
1101 },
1102 },
1103 ],
1104 cx,
1105 )
1106 .unwrap();
1107 project.disk_based_diagnostics_finished(0, cx);
1108 });
1109
1110 view.next_notification(cx).await;
1111 view.update(cx, |view, cx| {
1112 assert_eq!(
1113 editor_blocks(&view.editor, cx),
1114 [
1115 (0, "path header block".into()),
1116 (2, "diagnostic header".into()),
1117 (7, "collapsed context".into()),
1118 (8, "diagnostic header".into()),
1119 (13, "path header block".into()),
1120 (15, "diagnostic header".into()),
1121 (28, "collapsed context".into()),
1122 (29, "diagnostic header".into()),
1123 (38, "collapsed context".into()),
1124 ]
1125 );
1126 assert_eq!(
1127 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1128 concat!(
1129 //
1130 // consts.rs
1131 //
1132 "\n", // filename
1133 "\n", // padding
1134 // diagnostic group 1
1135 "\n", // primary message
1136 "\n", // padding
1137 "const a: i32 = 'a';\n",
1138 "\n", // supporting diagnostic
1139 "const b: i32 = c;\n",
1140 "\n", // context ellipsis
1141 // diagnostic group 2
1142 "\n", // primary message
1143 "\n", // padding
1144 "const a: i32 = 'a';\n",
1145 "const b: i32 = c;\n",
1146 "\n", // supporting diagnostic
1147 //
1148 // main.rs
1149 //
1150 "\n", // filename
1151 "\n", // padding
1152 // diagnostic group 1
1153 "\n", // primary message
1154 "\n", // padding
1155 " let x = vec![];\n",
1156 " let y = vec![];\n",
1157 "\n", // supporting diagnostic
1158 " a(x);\n",
1159 " b(y);\n",
1160 "\n", // supporting diagnostic
1161 " // comment 1\n",
1162 " // comment 2\n",
1163 " c(y);\n",
1164 "\n", // supporting diagnostic
1165 " d(x);\n",
1166 "\n", // context ellipsis
1167 // diagnostic group 2
1168 "\n", // primary message
1169 "\n", // filename
1170 "fn main() {\n",
1171 " let x = vec![];\n",
1172 "\n", // supporting diagnostic
1173 " let y = vec![];\n",
1174 " a(x);\n",
1175 "\n", // supporting diagnostic
1176 " b(y);\n",
1177 "\n", // context ellipsis
1178 " c(y);\n",
1179 " d(x);\n",
1180 "\n", // supporting diagnostic
1181 "}"
1182 )
1183 );
1184 });
1185 }
1186
1187 fn editor_blocks(
1188 editor: &ViewHandle<Editor>,
1189 cx: &mut MutableAppContext,
1190 ) -> Vec<(u32, String)> {
1191 let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1192 let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1193 cx.render(editor, |editor, cx| {
1194 let snapshot = editor.snapshot(cx);
1195 snapshot
1196 .blocks_in_range(0..snapshot.max_point().row())
1197 .filter_map(|(row, block)| {
1198 let name = match block {
1199 TransformBlock::Custom(block) => block
1200 .render(&mut BlockContext {
1201 cx,
1202 anchor_x: 0.,
1203 scroll_x: 0.,
1204 gutter_padding: 0.,
1205 gutter_width: 0.,
1206 line_height: 0.,
1207 em_width: 0.,
1208 })
1209 .name()?
1210 .to_string(),
1211 TransformBlock::ExcerptHeader {
1212 starts_new_buffer, ..
1213 } => {
1214 if *starts_new_buffer {
1215 "path header block".to_string()
1216 } else {
1217 "collapsed context".to_string()
1218 }
1219 }
1220 };
1221
1222 Some((row, name))
1223 })
1224 .collect()
1225 })
1226 }
1227}