1pub mod items;
2
3use anyhow::Result;
4use collections::{BTreeSet, HashMap, HashSet};
5use editor::{
6 diagnostic_block_renderer,
7 display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
8 highlight_diagnostic_message,
9 items::BufferItemHandle,
10 Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
11};
12use gpui::{
13 action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
14 ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
15 WeakViewHandle,
16};
17use language::{
18 Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
19};
20use postage::watch;
21use project::{DiagnosticSummary, Project, ProjectPath};
22use std::{
23 any::{Any, TypeId},
24 cmp::Ordering,
25 mem,
26 ops::Range,
27 path::PathBuf,
28 sync::Arc,
29};
30use util::TryFutureExt;
31use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace};
32
33action!(Deploy);
34action!(OpenExcerpts);
35
36const CONTEXT_LINE_COUNT: u32 = 1;
37
38pub fn init(cx: &mut MutableAppContext) {
39 cx.add_bindings([
40 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
41 Binding::new(
42 "alt-shift-D",
43 OpenExcerpts,
44 Some("ProjectDiagnosticsEditor"),
45 ),
46 ]);
47 cx.add_action(ProjectDiagnosticsEditor::deploy);
48 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
49}
50
51type Event = editor::Event;
52
53struct ProjectDiagnostics {
54 project: ModelHandle<Project>,
55}
56
57struct ProjectDiagnosticsEditor {
58 model: ModelHandle<ProjectDiagnostics>,
59 workspace: WeakViewHandle<Workspace>,
60 editor: ViewHandle<Editor>,
61 summary: DiagnosticSummary,
62 excerpts: ModelHandle<MultiBuffer>,
63 path_states: Vec<PathState>,
64 paths_to_update: BTreeSet<ProjectPath>,
65 settings: watch::Receiver<workspace::Settings>,
66}
67
68struct PathState {
69 path: ProjectPath,
70 diagnostic_groups: Vec<DiagnosticGroupState>,
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 ProjectDiagnostics {
82 fn new(project: ModelHandle<Project>) -> Self {
83 Self { project }
84 }
85}
86
87impl Entity for ProjectDiagnostics {
88 type Event = ();
89}
90
91impl Entity for ProjectDiagnosticsEditor {
92 type Event = Event;
93}
94
95impl View for ProjectDiagnosticsEditor {
96 fn ui_name() -> &'static str {
97 "ProjectDiagnosticsEditor"
98 }
99
100 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
101 if self.path_states.is_empty() {
102 let theme = &self.settings.borrow().theme.project_diagnostics;
103 Label::new(
104 "No problems in workspace".to_string(),
105 theme.empty_message.clone(),
106 )
107 .aligned()
108 .contained()
109 .with_style(theme.container)
110 .boxed()
111 } else {
112 ChildView::new(&self.editor).boxed()
113 }
114 }
115
116 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
117 if !self.path_states.is_empty() {
118 cx.focus(&self.editor);
119 }
120 }
121}
122
123impl ProjectDiagnosticsEditor {
124 fn new(
125 model: ModelHandle<ProjectDiagnostics>,
126 workspace: WeakViewHandle<Workspace>,
127 settings: watch::Receiver<workspace::Settings>,
128 cx: &mut ViewContext<Self>,
129 ) -> Self {
130 let project = model.read(cx).project.clone();
131 cx.subscribe(&project, |this, _, event, cx| match event {
132 project::Event::DiskBasedDiagnosticsFinished => {
133 this.update_excerpts(cx);
134 this.update_title(cx);
135 }
136 project::Event::DiagnosticsUpdated(path) => {
137 this.paths_to_update.insert(path.clone());
138 }
139 _ => {}
140 })
141 .detach();
142
143 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
144 let editor = cx.add_view(|cx| {
145 let mut editor = Editor::for_buffer(
146 excerpts.clone(),
147 Some(project.clone()),
148 settings.clone(),
149 cx,
150 );
151 editor.set_vertical_scroll_margin(5, cx);
152 editor
153 });
154 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
155 .detach();
156
157 let project = project.read(cx);
158 let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
159 let mut this = Self {
160 model,
161 summary: project.diagnostic_summary(cx),
162 workspace,
163 excerpts,
164 editor,
165 settings,
166 path_states: Default::default(),
167 paths_to_update,
168 };
169 this.update_excerpts(cx);
170 this
171 }
172
173 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
174 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
175 workspace.activate_item(&existing, cx);
176 } else {
177 let diagnostics =
178 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
179 workspace.open_item(diagnostics, cx);
180 }
181 }
182
183 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
184 if let Some(workspace) = self.workspace.upgrade(cx) {
185 let editor = self.editor.read(cx);
186 let excerpts = self.excerpts.read(cx);
187 let mut new_selections_by_buffer = HashMap::default();
188
189 for selection in editor.local_selections::<usize>(cx) {
190 for (buffer, mut range) in
191 excerpts.range_to_buffer_ranges(selection.start..selection.end, cx)
192 {
193 if selection.reversed {
194 mem::swap(&mut range.start, &mut range.end);
195 }
196 new_selections_by_buffer
197 .entry(buffer)
198 .or_insert(Vec::new())
199 .push(range)
200 }
201 }
202
203 // We defer the pane interaction because we ourselves are a workspace item
204 // and activating a new item causes the pane to call a method on us reentrantly,
205 // which panics if we're on the stack.
206 workspace.defer(cx, |workspace, cx| {
207 for (buffer, ranges) in new_selections_by_buffer {
208 let buffer = BufferItemHandle(buffer);
209 if !workspace.activate_pane_for_item(&buffer, cx) {
210 workspace.activate_next_pane(cx);
211 }
212 let editor = workspace
213 .open_item(buffer, cx)
214 .downcast::<Editor>()
215 .unwrap();
216 editor.update(cx, |editor, cx| {
217 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
218 });
219 }
220 });
221 }
222 }
223
224 fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
225 let paths = mem::take(&mut self.paths_to_update);
226 let project = self.model.read(cx).project.clone();
227 cx.spawn(|this, mut cx| {
228 async move {
229 for path in paths {
230 let buffer = project
231 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
232 .await?;
233 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
234 }
235 Result::<_, anyhow::Error>::Ok(())
236 }
237 .log_err()
238 })
239 .detach();
240 }
241
242 fn populate_excerpts(
243 &mut self,
244 path: ProjectPath,
245 buffer: ModelHandle<Buffer>,
246 cx: &mut ViewContext<Self>,
247 ) {
248 let was_empty = self.path_states.is_empty();
249 let snapshot = buffer.read(cx).snapshot();
250 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
251 Ok(ix) => ix,
252 Err(ix) => {
253 self.path_states.insert(
254 ix,
255 PathState {
256 path: path.clone(),
257 diagnostic_groups: Default::default(),
258 },
259 );
260 ix
261 }
262 };
263
264 let mut prev_excerpt_id = if path_ix > 0 {
265 let prev_path_last_group = &self.path_states[path_ix - 1]
266 .diagnostic_groups
267 .last()
268 .unwrap();
269 prev_path_last_group.excerpts.last().unwrap().clone()
270 } else {
271 ExcerptId::min()
272 };
273
274 let path_state = &mut self.path_states[path_ix];
275 let mut groups_to_add = Vec::new();
276 let mut group_ixs_to_remove = Vec::new();
277 let mut blocks_to_add = Vec::new();
278 let mut blocks_to_remove = HashSet::default();
279 let mut first_excerpt_id = None;
280 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
281 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
282 let mut new_groups = snapshot.diagnostic_groups().into_iter().peekable();
283 loop {
284 let mut to_insert = None;
285 let mut to_remove = None;
286 let mut to_keep = None;
287 match (old_groups.peek(), new_groups.peek()) {
288 (None, None) => break,
289 (None, Some(_)) => to_insert = new_groups.next(),
290 (Some(_), None) => to_remove = old_groups.next(),
291 (Some((_, old_group)), Some(new_group)) => {
292 let old_primary = &old_group.primary_diagnostic;
293 let new_primary = &new_group.entries[new_group.primary_ix];
294 match compare_diagnostics(old_primary, new_primary, &snapshot) {
295 Ordering::Less => to_remove = old_groups.next(),
296 Ordering::Equal => {
297 to_keep = old_groups.next();
298 new_groups.next();
299 }
300 Ordering::Greater => to_insert = new_groups.next(),
301 }
302 }
303 }
304
305 if let Some(group) = to_insert {
306 let mut group_state = DiagnosticGroupState {
307 primary_diagnostic: group.entries[group.primary_ix].clone(),
308 primary_excerpt_ix: 0,
309 excerpts: Default::default(),
310 blocks: Default::default(),
311 block_count: 0,
312 };
313 let mut pending_range: Option<(Range<Point>, usize)> = None;
314 let mut is_first_excerpt_for_group = true;
315 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
316 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
317 if let Some((range, start_ix)) = &mut pending_range {
318 if let Some(entry) = resolved_entry.as_ref() {
319 if entry.range.start.row
320 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
321 {
322 range.end = range.end.max(entry.range.end);
323 continue;
324 }
325 }
326
327 let excerpt_start =
328 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
329 let excerpt_end = snapshot.clip_point(
330 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
331 Bias::Left,
332 );
333 let excerpt_id = excerpts
334 .insert_excerpts_after(
335 &prev_excerpt_id,
336 buffer.clone(),
337 [excerpt_start..excerpt_end],
338 excerpts_cx,
339 )
340 .pop()
341 .unwrap();
342
343 prev_excerpt_id = excerpt_id.clone();
344 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
345 group_state.excerpts.push(excerpt_id.clone());
346 let header_position = (excerpt_id.clone(), language::Anchor::min());
347
348 if is_first_excerpt_for_group {
349 is_first_excerpt_for_group = false;
350 let mut primary =
351 group.entries[group.primary_ix].diagnostic.clone();
352 primary.message =
353 primary.message.split('\n').next().unwrap().to_string();
354 group_state.block_count += 1;
355 blocks_to_add.push(BlockProperties {
356 position: header_position,
357 height: 2,
358 render: diagnostic_header_renderer(
359 primary,
360 self.settings.clone(),
361 ),
362 disposition: BlockDisposition::Above,
363 });
364 }
365
366 for entry in &group.entries[*start_ix..ix] {
367 let mut diagnostic = entry.diagnostic.clone();
368 if diagnostic.is_primary {
369 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
370 diagnostic.message =
371 entry.diagnostic.message.split('\n').skip(1).collect();
372 }
373
374 if !diagnostic.message.is_empty() {
375 group_state.block_count += 1;
376 blocks_to_add.push(BlockProperties {
377 position: (excerpt_id.clone(), entry.range.start.clone()),
378 height: diagnostic.message.matches('\n').count() as u8 + 1,
379 render: diagnostic_block_renderer(
380 diagnostic,
381 true,
382 self.settings.clone(),
383 ),
384 disposition: BlockDisposition::Below,
385 });
386 }
387 }
388
389 pending_range.take();
390 }
391
392 if let Some(entry) = resolved_entry {
393 pending_range = Some((entry.range.clone(), ix));
394 }
395 }
396
397 groups_to_add.push(group_state);
398 } else if let Some((group_ix, group_state)) = to_remove {
399 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
400 group_ixs_to_remove.push(group_ix);
401 blocks_to_remove.extend(group_state.blocks.iter().copied());
402 } else if let Some((_, group)) = to_keep {
403 prev_excerpt_id = group.excerpts.last().unwrap().clone();
404 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
405 }
406 }
407
408 excerpts.snapshot(excerpts_cx)
409 });
410
411 self.editor.update(cx, |editor, cx| {
412 editor.remove_blocks(blocks_to_remove, cx);
413 let block_ids = editor.insert_blocks(
414 blocks_to_add.into_iter().map(|block| {
415 let (excerpt_id, text_anchor) = block.position;
416 BlockProperties {
417 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
418 height: block.height,
419 render: block.render,
420 disposition: block.disposition,
421 }
422 }),
423 cx,
424 );
425
426 let mut block_ids = block_ids.into_iter();
427 for group_state in &mut groups_to_add {
428 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
429 }
430 });
431
432 for ix in group_ixs_to_remove.into_iter().rev() {
433 path_state.diagnostic_groups.remove(ix);
434 }
435 path_state.diagnostic_groups.extend(groups_to_add);
436 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
437 let range_a = &a.primary_diagnostic.range;
438 let range_b = &b.primary_diagnostic.range;
439 range_a
440 .start
441 .cmp(&range_b.start, &snapshot)
442 .unwrap()
443 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
444 });
445
446 if path_state.diagnostic_groups.is_empty() {
447 self.path_states.remove(path_ix);
448 }
449
450 self.editor.update(cx, |editor, cx| {
451 let groups;
452 let mut selections;
453 let new_excerpt_ids_by_selection_id;
454 if was_empty {
455 groups = self.path_states.first()?.diagnostic_groups.as_slice();
456 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
457 selections = vec![Selection {
458 id: 0,
459 start: 0,
460 end: 0,
461 reversed: false,
462 goal: SelectionGoal::None,
463 }];
464 } else {
465 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
466 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
467 selections = editor.local_selections::<usize>(cx);
468 }
469
470 // If any selection has lost its position, move it to start of the next primary diagnostic.
471 for selection in &mut selections {
472 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
473 let group_ix = match groups.binary_search_by(|probe| {
474 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
475 }) {
476 Ok(ix) | Err(ix) => ix,
477 };
478 if let Some(group) = groups.get(group_ix) {
479 let offset = excerpts_snapshot
480 .anchor_in_excerpt(
481 group.excerpts[group.primary_excerpt_ix].clone(),
482 group.primary_diagnostic.range.start.clone(),
483 )
484 .to_offset(&excerpts_snapshot);
485 selection.start = offset;
486 selection.end = offset;
487 }
488 }
489 }
490 editor.update_selections(selections, None, cx);
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 {
499 if cx.handle().is_focused(cx) {
500 cx.focus(&self.editor);
501 }
502 }
503 cx.notify();
504 }
505
506 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
507 self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
508 cx.emit(Event::TitleChanged);
509 }
510}
511
512impl workspace::Item for ProjectDiagnostics {
513 type View = ProjectDiagnosticsEditor;
514
515 fn build_view(
516 handle: ModelHandle<Self>,
517 workspace: &Workspace,
518 nav_history: ItemNavHistory,
519 cx: &mut ViewContext<Self::View>,
520 ) -> Self::View {
521 let diagnostics = ProjectDiagnosticsEditor::new(
522 handle,
523 workspace.weak_handle(),
524 workspace.settings(),
525 cx,
526 );
527 diagnostics
528 .editor
529 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
530 diagnostics
531 }
532
533 fn project_path(&self) -> Option<project::ProjectPath> {
534 None
535 }
536}
537
538impl workspace::ItemView for ProjectDiagnosticsEditor {
539 fn item_id(&self, _: &AppContext) -> usize {
540 self.model.id()
541 }
542
543 fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
544 render_summary(
545 &self.summary,
546 &style.label.text,
547 &self.settings.borrow().theme.project_diagnostics,
548 )
549 }
550
551 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
552 None
553 }
554
555 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
556 self.editor
557 .update(cx, |editor, cx| editor.navigate(data, cx));
558 }
559
560 fn is_dirty(&self, cx: &AppContext) -> bool {
561 self.excerpts.read(cx).read(cx).is_dirty()
562 }
563
564 fn has_conflict(&self, cx: &AppContext) -> bool {
565 self.excerpts.read(cx).read(cx).has_conflict()
566 }
567
568 fn can_save(&self, _: &AppContext) -> bool {
569 true
570 }
571
572 fn save(
573 &mut self,
574 project: ModelHandle<Project>,
575 cx: &mut ViewContext<Self>,
576 ) -> Task<Result<()>> {
577 self.editor.save(project, cx)
578 }
579
580 fn can_save_as(&self, _: &AppContext) -> bool {
581 false
582 }
583
584 fn save_as(
585 &mut self,
586 _: ModelHandle<Project>,
587 _: PathBuf,
588 _: &mut ViewContext<Self>,
589 ) -> Task<Result<()>> {
590 unreachable!()
591 }
592
593 fn should_activate_item_on_event(event: &Self::Event) -> bool {
594 Editor::should_activate_item_on_event(event)
595 }
596
597 fn should_update_tab_on_event(event: &Event) -> bool {
598 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
599 }
600
601 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
602 where
603 Self: Sized,
604 {
605 let diagnostics = ProjectDiagnosticsEditor::new(
606 self.model.clone(),
607 self.workspace.clone(),
608 self.settings.clone(),
609 cx,
610 );
611 diagnostics.editor.update(cx, |editor, cx| {
612 let nav_history = self
613 .editor
614 .read(cx)
615 .nav_history()
616 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
617 editor.set_nav_history(nav_history);
618 });
619 Some(diagnostics)
620 }
621
622 fn act_as_type(
623 &self,
624 type_id: TypeId,
625 self_handle: &ViewHandle<Self>,
626 _: &AppContext,
627 ) -> Option<AnyViewHandle> {
628 if type_id == TypeId::of::<Self>() {
629 Some(self_handle.into())
630 } else if type_id == TypeId::of::<Editor>() {
631 Some((&self.editor).into())
632 } else {
633 None
634 }
635 }
636
637 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
638 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
639 }
640}
641
642fn diagnostic_header_renderer(
643 diagnostic: Diagnostic,
644 settings: watch::Receiver<workspace::Settings>,
645) -> RenderBlock {
646 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
647 Arc::new(move |cx| {
648 let settings = settings.borrow();
649 let theme = &settings.theme.editor;
650 let style = &theme.diagnostic_header;
651 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
652 let icon_width = cx.em_width * style.icon_width_factor;
653 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
654 Svg::new("icons/diagnostic-error-10.svg")
655 .with_color(theme.error_diagnostic.message.text.color)
656 } else {
657 Svg::new("icons/diagnostic-warning-10.svg")
658 .with_color(theme.warning_diagnostic.message.text.color)
659 };
660
661 Flex::row()
662 .with_child(
663 icon.constrained()
664 .with_width(icon_width)
665 .aligned()
666 .contained()
667 .boxed(),
668 )
669 .with_child(
670 Label::new(
671 message.clone(),
672 style.message.label.clone().with_font_size(font_size),
673 )
674 .with_highlights(highlights.clone())
675 .contained()
676 .with_style(style.message.container)
677 .with_margin_left(cx.gutter_padding)
678 .aligned()
679 .boxed(),
680 )
681 .with_children(diagnostic.code.clone().map(|code| {
682 Label::new(code, style.code.text.clone().with_font_size(font_size))
683 .contained()
684 .with_style(style.code.container)
685 .aligned()
686 .boxed()
687 }))
688 .contained()
689 .with_style(style.container)
690 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
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".to_string(), 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/diagnostic-summary-error.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/diagnostic-summary-warning.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, EditorSnapshot,
773 };
774 use gpui::TestAppContext;
775 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
776 use serde_json::json;
777 use unindent::Unindent as _;
778 use workspace::WorkspaceParams;
779
780 #[gpui::test]
781 async fn test_diagnostics(mut cx: TestAppContext) {
782 let params = cx.update(WorkspaceParams::test);
783 let project = params.project.clone();
784 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
785
786 params
787 .fs
788 .as_fake()
789 .insert_tree(
790 "/test",
791 json!({
792 "consts.rs": "
793 const a: i32 = 'a';
794 const b: i32 = c;
795 "
796 .unindent(),
797
798 "main.rs": "
799 fn main() {
800 let x = vec![];
801 let y = vec![];
802 a(x);
803 b(y);
804 // comment 1
805 // comment 2
806 c(y);
807 d(x);
808 }
809 "
810 .unindent(),
811 }),
812 )
813 .await;
814
815 project
816 .update(&mut cx, |project, cx| {
817 project.find_or_create_local_worktree("/test", false, cx)
818 })
819 .await
820 .unwrap();
821
822 // Create some diagnostics
823 project.update(&mut cx, |project, cx| {
824 project
825 .update_diagnostic_entries(
826 PathBuf::from("/test/main.rs"),
827 None,
828 vec![
829 DiagnosticEntry {
830 range: PointUtf16::new(1, 8)..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: PointUtf16::new(2, 8)..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: PointUtf16::new(3, 6)..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: PointUtf16::new(4, 6)..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: PointUtf16::new(7, 6)..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: PointUtf16::new(8, 6)..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 model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
907 let view = cx.add_view(0, |cx| {
908 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
909 });
910
911 view.next_notification(&cx).await;
912 view.update(&mut cx, |view, cx| {
913 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
914
915 assert_eq!(
916 editor_blocks(&editor, cx),
917 [
918 (0, "path header block".into()),
919 (2, "diagnostic header".into()),
920 (15, "collapsed context".into()),
921 (16, "diagnostic header".into()),
922 (25, "collapsed context".into()),
923 ]
924 );
925 assert_eq!(
926 editor.text(),
927 concat!(
928 //
929 // main.rs
930 //
931 "\n", // filename
932 "\n", // padding
933 // diagnostic group 1
934 "\n", // primary message
935 "\n", // padding
936 " let x = vec![];\n",
937 " let y = vec![];\n",
938 "\n", // supporting diagnostic
939 " a(x);\n",
940 " b(y);\n",
941 "\n", // supporting diagnostic
942 " // comment 1\n",
943 " // comment 2\n",
944 " c(y);\n",
945 "\n", // supporting diagnostic
946 " d(x);\n",
947 "\n", // context ellipsis
948 // diagnostic group 2
949 "\n", // primary message
950 "\n", // padding
951 "fn main() {\n",
952 " let x = vec![];\n",
953 "\n", // supporting diagnostic
954 " let y = vec![];\n",
955 " a(x);\n",
956 "\n", // supporting diagnostic
957 " b(y);\n",
958 "\n", // context ellipsis
959 " c(y);\n",
960 " d(x);\n",
961 "\n", // supporting diagnostic
962 "}"
963 )
964 );
965
966 // Cursor is at the first diagnostic
967 view.editor.update(cx, |editor, cx| {
968 assert_eq!(
969 editor.selected_display_ranges(cx),
970 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
971 );
972 });
973 });
974
975 // Diagnostics are added for another earlier path.
976 project.update(&mut cx, |project, cx| {
977 project.disk_based_diagnostics_started(cx);
978 project
979 .update_diagnostic_entries(
980 PathBuf::from("/test/consts.rs"),
981 None,
982 vec![DiagnosticEntry {
983 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
984 diagnostic: Diagnostic {
985 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
986 severity: DiagnosticSeverity::ERROR,
987 is_primary: true,
988 is_disk_based: true,
989 group_id: 0,
990 ..Default::default()
991 },
992 }],
993 cx,
994 )
995 .unwrap();
996 project.disk_based_diagnostics_finished(cx);
997 });
998
999 view.next_notification(&cx).await;
1000 view.update(&mut cx, |view, cx| {
1001 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1002
1003 assert_eq!(
1004 editor_blocks(&editor, cx),
1005 [
1006 (0, "path header block".into()),
1007 (2, "diagnostic header".into()),
1008 (7, "path header block".into()),
1009 (9, "diagnostic header".into()),
1010 (22, "collapsed context".into()),
1011 (23, "diagnostic header".into()),
1012 (32, "collapsed context".into()),
1013 ]
1014 );
1015 assert_eq!(
1016 editor.text(),
1017 concat!(
1018 //
1019 // consts.rs
1020 //
1021 "\n", // filename
1022 "\n", // padding
1023 // diagnostic group 1
1024 "\n", // primary message
1025 "\n", // padding
1026 "const a: i32 = 'a';\n",
1027 "\n", // supporting diagnostic
1028 "const b: i32 = c;\n",
1029 //
1030 // main.rs
1031 //
1032 "\n", // filename
1033 "\n", // padding
1034 // diagnostic group 1
1035 "\n", // primary message
1036 "\n", // padding
1037 " let x = vec![];\n",
1038 " let y = vec![];\n",
1039 "\n", // supporting diagnostic
1040 " a(x);\n",
1041 " b(y);\n",
1042 "\n", // supporting diagnostic
1043 " // comment 1\n",
1044 " // comment 2\n",
1045 " c(y);\n",
1046 "\n", // supporting diagnostic
1047 " d(x);\n",
1048 "\n", // collapsed context
1049 // diagnostic group 2
1050 "\n", // primary message
1051 "\n", // filename
1052 "fn main() {\n",
1053 " let x = vec![];\n",
1054 "\n", // supporting diagnostic
1055 " let y = vec![];\n",
1056 " a(x);\n",
1057 "\n", // supporting diagnostic
1058 " b(y);\n",
1059 "\n", // context ellipsis
1060 " c(y);\n",
1061 " d(x);\n",
1062 "\n", // supporting diagnostic
1063 "}"
1064 )
1065 );
1066
1067 // Cursor keeps its position.
1068 view.editor.update(cx, |editor, cx| {
1069 assert_eq!(
1070 editor.selected_display_ranges(cx),
1071 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1072 );
1073 });
1074 });
1075
1076 // Diagnostics are added to the first path
1077 project.update(&mut cx, |project, cx| {
1078 project.disk_based_diagnostics_started(cx);
1079 project
1080 .update_diagnostic_entries(
1081 PathBuf::from("/test/consts.rs"),
1082 None,
1083 vec![
1084 DiagnosticEntry {
1085 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1086 diagnostic: Diagnostic {
1087 message: "mismatched types\nexpected `usize`, found `char`"
1088 .to_string(),
1089 severity: DiagnosticSeverity::ERROR,
1090 is_primary: true,
1091 is_disk_based: true,
1092 group_id: 0,
1093 ..Default::default()
1094 },
1095 },
1096 DiagnosticEntry {
1097 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1098 diagnostic: Diagnostic {
1099 message: "unresolved name `c`".to_string(),
1100 severity: DiagnosticSeverity::ERROR,
1101 is_primary: true,
1102 is_disk_based: true,
1103 group_id: 1,
1104 ..Default::default()
1105 },
1106 },
1107 ],
1108 cx,
1109 )
1110 .unwrap();
1111 project.disk_based_diagnostics_finished(cx);
1112 });
1113
1114 view.next_notification(&cx).await;
1115 view.update(&mut cx, |view, cx| {
1116 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1117
1118 assert_eq!(
1119 editor_blocks(&editor, cx),
1120 [
1121 (0, "path header block".into()),
1122 (2, "diagnostic header".into()),
1123 (7, "collapsed context".into()),
1124 (8, "diagnostic header".into()),
1125 (13, "path header block".into()),
1126 (15, "diagnostic header".into()),
1127 (28, "collapsed context".into()),
1128 (29, "diagnostic header".into()),
1129 (38, "collapsed context".into()),
1130 ]
1131 );
1132 assert_eq!(
1133 editor.text(),
1134 concat!(
1135 //
1136 // consts.rs
1137 //
1138 "\n", // filename
1139 "\n", // padding
1140 // diagnostic group 1
1141 "\n", // primary message
1142 "\n", // padding
1143 "const a: i32 = 'a';\n",
1144 "\n", // supporting diagnostic
1145 "const b: i32 = c;\n",
1146 "\n", // context ellipsis
1147 // diagnostic group 2
1148 "\n", // primary message
1149 "\n", // padding
1150 "const a: i32 = 'a';\n",
1151 "const b: i32 = c;\n",
1152 "\n", // supporting diagnostic
1153 //
1154 // main.rs
1155 //
1156 "\n", // filename
1157 "\n", // padding
1158 // diagnostic group 1
1159 "\n", // primary message
1160 "\n", // padding
1161 " let x = vec![];\n",
1162 " let y = vec![];\n",
1163 "\n", // supporting diagnostic
1164 " a(x);\n",
1165 " b(y);\n",
1166 "\n", // supporting diagnostic
1167 " // comment 1\n",
1168 " // comment 2\n",
1169 " c(y);\n",
1170 "\n", // supporting diagnostic
1171 " d(x);\n",
1172 "\n", // context ellipsis
1173 // diagnostic group 2
1174 "\n", // primary message
1175 "\n", // filename
1176 "fn main() {\n",
1177 " let x = vec![];\n",
1178 "\n", // supporting diagnostic
1179 " let y = vec![];\n",
1180 " a(x);\n",
1181 "\n", // supporting diagnostic
1182 " b(y);\n",
1183 "\n", // context ellipsis
1184 " c(y);\n",
1185 " d(x);\n",
1186 "\n", // supporting diagnostic
1187 "}"
1188 )
1189 );
1190 });
1191 }
1192
1193 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1194 editor
1195 .blocks_in_range(0..editor.max_point().row())
1196 .filter_map(|(row, block)| {
1197 let name = match block {
1198 TransformBlock::Custom(block) => block
1199 .render(&BlockContext {
1200 cx,
1201 anchor_x: 0.,
1202 scroll_x: 0.,
1203 gutter_padding: 0.,
1204 gutter_width: 0.,
1205 line_height: 0.,
1206 em_width: 0.,
1207 })
1208 .name()?
1209 .to_string(),
1210 TransformBlock::ExcerptHeader {
1211 starts_new_buffer, ..
1212 } => {
1213 if *starts_new_buffer {
1214 "path header block".to_string()
1215 } else {
1216 "collapsed context".to_string()
1217 }
1218 }
1219 };
1220
1221 Some((row, name))
1222 })
1223 .collect()
1224 }
1225}