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