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