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