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