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.range_to_buffer_ranges(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(
576 &mut self,
577 project: ModelHandle<Project>,
578 cx: &mut ViewContext<Self>,
579 ) -> Task<Result<()>> {
580 self.editor.save(project, cx)
581 }
582
583 fn can_save_as(&self, _: &AppContext) -> bool {
584 false
585 }
586
587 fn save_as(
588 &mut self,
589 _: ModelHandle<Project>,
590 _: PathBuf,
591 _: &mut ViewContext<Self>,
592 ) -> Task<Result<()>> {
593 unreachable!()
594 }
595
596 fn should_activate_item_on_event(event: &Self::Event) -> bool {
597 Editor::should_activate_item_on_event(event)
598 }
599
600 fn should_update_tab_on_event(event: &Event) -> bool {
601 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
602 }
603
604 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
605 where
606 Self: Sized,
607 {
608 let diagnostics = ProjectDiagnosticsEditor::new(
609 self.model.clone(),
610 self.workspace.clone(),
611 self.settings.clone(),
612 cx,
613 );
614 diagnostics.editor.update(cx, |editor, cx| {
615 let nav_history = self
616 .editor
617 .read(cx)
618 .nav_history()
619 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
620 editor.set_nav_history(nav_history);
621 });
622 Some(diagnostics)
623 }
624
625 fn act_as_type(
626 &self,
627 type_id: TypeId,
628 self_handle: &ViewHandle<Self>,
629 _: &AppContext,
630 ) -> Option<AnyViewHandle> {
631 if type_id == TypeId::of::<Self>() {
632 Some(self_handle.into())
633 } else if type_id == TypeId::of::<Editor>() {
634 Some((&self.editor).into())
635 } else {
636 None
637 }
638 }
639
640 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
641 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
642 }
643}
644
645fn diagnostic_header_renderer(
646 diagnostic: Diagnostic,
647 build_settings: BuildSettings,
648) -> RenderBlock {
649 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
650 Arc::new(move |cx| {
651 let settings = build_settings(cx);
652 let style = &settings.style.diagnostic_header;
653 let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
654 let icon_width = cx.em_width * style.icon_width_factor;
655 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
656 Svg::new("icons/diagnostic-error-10.svg")
657 .with_color(settings.style.error_diagnostic.message.text.color)
658 } else {
659 Svg::new("icons/diagnostic-warning-10.svg")
660 .with_color(settings.style.warning_diagnostic.message.text.color)
661 };
662
663 Flex::row()
664 .with_child(
665 icon.constrained()
666 .with_width(icon_width)
667 .aligned()
668 .contained()
669 .boxed(),
670 )
671 .with_child(
672 Label::new(
673 message.clone(),
674 style.message.label.clone().with_font_size(font_size),
675 )
676 .with_highlights(highlights.clone())
677 .contained()
678 .with_style(style.message.container)
679 .with_margin_left(cx.gutter_padding)
680 .aligned()
681 .boxed(),
682 )
683 .with_children(diagnostic.code.clone().map(|code| {
684 Label::new(code, style.code.text.clone().with_font_size(font_size))
685 .contained()
686 .with_style(style.code.container)
687 .aligned()
688 .boxed()
689 }))
690 .contained()
691 .with_style(style.container)
692 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
693 .expanded()
694 .named("diagnostic header")
695 })
696}
697
698pub(crate) fn render_summary(
699 summary: &DiagnosticSummary,
700 text_style: &TextStyle,
701 theme: &theme::ProjectDiagnostics,
702) -> ElementBox {
703 if summary.error_count == 0 && summary.warning_count == 0 {
704 Label::new("No problems".to_string(), text_style.clone()).boxed()
705 } else {
706 let icon_width = theme.tab_icon_width;
707 let icon_spacing = theme.tab_icon_spacing;
708 let summary_spacing = theme.tab_summary_spacing;
709 Flex::row()
710 .with_children([
711 Svg::new("icons/diagnostic-summary-error.svg")
712 .with_color(text_style.color)
713 .constrained()
714 .with_width(icon_width)
715 .aligned()
716 .contained()
717 .with_margin_right(icon_spacing)
718 .named("no-icon"),
719 Label::new(
720 summary.error_count.to_string(),
721 LabelStyle {
722 text: text_style.clone(),
723 highlight_text: None,
724 },
725 )
726 .aligned()
727 .boxed(),
728 Svg::new("icons/diagnostic-summary-warning.svg")
729 .with_color(text_style.color)
730 .constrained()
731 .with_width(icon_width)
732 .aligned()
733 .contained()
734 .with_margin_left(summary_spacing)
735 .with_margin_right(icon_spacing)
736 .named("warn-icon"),
737 Label::new(
738 summary.warning_count.to_string(),
739 LabelStyle {
740 text: text_style.clone(),
741 highlight_text: None,
742 },
743 )
744 .aligned()
745 .boxed(),
746 ])
747 .boxed()
748 }
749}
750
751fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
752 lhs: &DiagnosticEntry<L>,
753 rhs: &DiagnosticEntry<R>,
754 snapshot: &language::BufferSnapshot,
755) -> Ordering {
756 lhs.range
757 .start
758 .to_offset(&snapshot)
759 .cmp(&rhs.range.start.to_offset(snapshot))
760 .then_with(|| {
761 lhs.range
762 .end
763 .to_offset(&snapshot)
764 .cmp(&rhs.range.end.to_offset(snapshot))
765 })
766 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772 use editor::{
773 display_map::{BlockContext, TransformBlock},
774 DisplayPoint, EditorSnapshot,
775 };
776 use gpui::TestAppContext;
777 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
778 use serde_json::json;
779 use unindent::Unindent as _;
780 use workspace::WorkspaceParams;
781
782 #[gpui::test]
783 async fn test_diagnostics(mut cx: TestAppContext) {
784 let params = cx.update(WorkspaceParams::test);
785 let project = params.project.clone();
786 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
787
788 params
789 .fs
790 .as_fake()
791 .insert_tree(
792 "/test",
793 json!({
794 "consts.rs": "
795 const a: i32 = 'a';
796 const b: i32 = c;
797 "
798 .unindent(),
799
800 "main.rs": "
801 fn main() {
802 let x = vec![];
803 let y = vec![];
804 a(x);
805 b(y);
806 // comment 1
807 // comment 2
808 c(y);
809 d(x);
810 }
811 "
812 .unindent(),
813 }),
814 )
815 .await;
816
817 project
818 .update(&mut cx, |project, cx| {
819 project.find_or_create_local_worktree("/test", false, cx)
820 })
821 .await
822 .unwrap();
823
824 // Create some diagnostics
825 project.update(&mut cx, |project, cx| {
826 project
827 .update_diagnostic_entries(
828 PathBuf::from("/test/main.rs"),
829 None,
830 vec![
831 DiagnosticEntry {
832 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
833 diagnostic: Diagnostic {
834 message:
835 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
836 .to_string(),
837 severity: DiagnosticSeverity::INFORMATION,
838 is_primary: false,
839 is_disk_based: true,
840 group_id: 1,
841 ..Default::default()
842 },
843 },
844 DiagnosticEntry {
845 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
846 diagnostic: Diagnostic {
847 message:
848 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
849 .to_string(),
850 severity: DiagnosticSeverity::INFORMATION,
851 is_primary: false,
852 is_disk_based: true,
853 group_id: 0,
854 ..Default::default()
855 },
856 },
857 DiagnosticEntry {
858 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
859 diagnostic: Diagnostic {
860 message: "value moved here".to_string(),
861 severity: DiagnosticSeverity::INFORMATION,
862 is_primary: false,
863 is_disk_based: true,
864 group_id: 1,
865 ..Default::default()
866 },
867 },
868 DiagnosticEntry {
869 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
870 diagnostic: Diagnostic {
871 message: "value moved here".to_string(),
872 severity: DiagnosticSeverity::INFORMATION,
873 is_primary: false,
874 is_disk_based: true,
875 group_id: 0,
876 ..Default::default()
877 },
878 },
879 DiagnosticEntry {
880 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
881 diagnostic: Diagnostic {
882 message: "use of moved value\nvalue used here after move".to_string(),
883 severity: DiagnosticSeverity::ERROR,
884 is_primary: true,
885 is_disk_based: true,
886 group_id: 0,
887 ..Default::default()
888 },
889 },
890 DiagnosticEntry {
891 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
892 diagnostic: Diagnostic {
893 message: "use of moved value\nvalue used here after move".to_string(),
894 severity: DiagnosticSeverity::ERROR,
895 is_primary: true,
896 is_disk_based: true,
897 group_id: 1,
898 ..Default::default()
899 },
900 },
901 ],
902 cx,
903 )
904 .unwrap();
905 });
906
907 // Open the project diagnostics view while there are already diagnostics.
908 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
909 let view = cx.add_view(0, |cx| {
910 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
911 });
912
913 view.next_notification(&cx).await;
914 view.update(&mut cx, |view, cx| {
915 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
916
917 assert_eq!(
918 editor_blocks(&editor, cx),
919 [
920 (0, "path header block".into()),
921 (2, "diagnostic header".into()),
922 (15, "collapsed context".into()),
923 (16, "diagnostic header".into()),
924 (25, "collapsed context".into()),
925 ]
926 );
927 assert_eq!(
928 editor.text(),
929 concat!(
930 //
931 // main.rs
932 //
933 "\n", // filename
934 "\n", // padding
935 // diagnostic group 1
936 "\n", // primary message
937 "\n", // padding
938 " let x = vec![];\n",
939 " let y = vec![];\n",
940 "\n", // supporting diagnostic
941 " a(x);\n",
942 " b(y);\n",
943 "\n", // supporting diagnostic
944 " // comment 1\n",
945 " // comment 2\n",
946 " c(y);\n",
947 "\n", // supporting diagnostic
948 " d(x);\n",
949 "\n", // context ellipsis
950 // diagnostic group 2
951 "\n", // primary message
952 "\n", // padding
953 "fn main() {\n",
954 " let x = vec![];\n",
955 "\n", // supporting diagnostic
956 " let y = vec![];\n",
957 " a(x);\n",
958 "\n", // supporting diagnostic
959 " b(y);\n",
960 "\n", // context ellipsis
961 " c(y);\n",
962 " d(x);\n",
963 "\n", // supporting diagnostic
964 "}"
965 )
966 );
967
968 // Cursor is at the first diagnostic
969 view.editor.update(cx, |editor, cx| {
970 assert_eq!(
971 editor.selected_display_ranges(cx),
972 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
973 );
974 });
975 });
976
977 // Diagnostics are added for another earlier path.
978 project.update(&mut cx, |project, cx| {
979 project.disk_based_diagnostics_started(cx);
980 project
981 .update_diagnostic_entries(
982 PathBuf::from("/test/consts.rs"),
983 None,
984 vec![DiagnosticEntry {
985 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
986 diagnostic: Diagnostic {
987 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
988 severity: DiagnosticSeverity::ERROR,
989 is_primary: true,
990 is_disk_based: true,
991 group_id: 0,
992 ..Default::default()
993 },
994 }],
995 cx,
996 )
997 .unwrap();
998 project.disk_based_diagnostics_finished(cx);
999 });
1000
1001 view.next_notification(&cx).await;
1002 view.update(&mut cx, |view, cx| {
1003 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1004
1005 assert_eq!(
1006 editor_blocks(&editor, cx),
1007 [
1008 (0, "path header block".into()),
1009 (2, "diagnostic header".into()),
1010 (7, "path header block".into()),
1011 (9, "diagnostic header".into()),
1012 (22, "collapsed context".into()),
1013 (23, "diagnostic header".into()),
1014 (32, "collapsed context".into()),
1015 ]
1016 );
1017 assert_eq!(
1018 editor.text(),
1019 concat!(
1020 //
1021 // consts.rs
1022 //
1023 "\n", // filename
1024 "\n", // padding
1025 // diagnostic group 1
1026 "\n", // primary message
1027 "\n", // padding
1028 "const a: i32 = 'a';\n",
1029 "\n", // supporting diagnostic
1030 "const b: i32 = c;\n",
1031 //
1032 // main.rs
1033 //
1034 "\n", // filename
1035 "\n", // padding
1036 // diagnostic group 1
1037 "\n", // primary message
1038 "\n", // padding
1039 " let x = vec![];\n",
1040 " let y = vec![];\n",
1041 "\n", // supporting diagnostic
1042 " a(x);\n",
1043 " b(y);\n",
1044 "\n", // supporting diagnostic
1045 " // comment 1\n",
1046 " // comment 2\n",
1047 " c(y);\n",
1048 "\n", // supporting diagnostic
1049 " d(x);\n",
1050 "\n", // collapsed context
1051 // diagnostic group 2
1052 "\n", // primary message
1053 "\n", // filename
1054 "fn main() {\n",
1055 " let x = vec![];\n",
1056 "\n", // supporting diagnostic
1057 " let y = vec![];\n",
1058 " a(x);\n",
1059 "\n", // supporting diagnostic
1060 " b(y);\n",
1061 "\n", // context ellipsis
1062 " c(y);\n",
1063 " d(x);\n",
1064 "\n", // supporting diagnostic
1065 "}"
1066 )
1067 );
1068
1069 // Cursor keeps its position.
1070 view.editor.update(cx, |editor, cx| {
1071 assert_eq!(
1072 editor.selected_display_ranges(cx),
1073 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1074 );
1075 });
1076 });
1077
1078 // Diagnostics are added to the first path
1079 project.update(&mut cx, |project, cx| {
1080 project.disk_based_diagnostics_started(cx);
1081 project
1082 .update_diagnostic_entries(
1083 PathBuf::from("/test/consts.rs"),
1084 None,
1085 vec![
1086 DiagnosticEntry {
1087 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1088 diagnostic: Diagnostic {
1089 message: "mismatched types\nexpected `usize`, found `char`"
1090 .to_string(),
1091 severity: DiagnosticSeverity::ERROR,
1092 is_primary: true,
1093 is_disk_based: true,
1094 group_id: 0,
1095 ..Default::default()
1096 },
1097 },
1098 DiagnosticEntry {
1099 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1100 diagnostic: Diagnostic {
1101 message: "unresolved name `c`".to_string(),
1102 severity: DiagnosticSeverity::ERROR,
1103 is_primary: true,
1104 is_disk_based: true,
1105 group_id: 1,
1106 ..Default::default()
1107 },
1108 },
1109 ],
1110 cx,
1111 )
1112 .unwrap();
1113 project.disk_based_diagnostics_finished(cx);
1114 });
1115
1116 view.next_notification(&cx).await;
1117 view.update(&mut cx, |view, cx| {
1118 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1119
1120 assert_eq!(
1121 editor_blocks(&editor, cx),
1122 [
1123 (0, "path header block".into()),
1124 (2, "diagnostic header".into()),
1125 (7, "collapsed context".into()),
1126 (8, "diagnostic header".into()),
1127 (13, "path header block".into()),
1128 (15, "diagnostic header".into()),
1129 (28, "collapsed context".into()),
1130 (29, "diagnostic header".into()),
1131 (38, "collapsed context".into()),
1132 ]
1133 );
1134 assert_eq!(
1135 editor.text(),
1136 concat!(
1137 //
1138 // consts.rs
1139 //
1140 "\n", // filename
1141 "\n", // padding
1142 // diagnostic group 1
1143 "\n", // primary message
1144 "\n", // padding
1145 "const a: i32 = 'a';\n",
1146 "\n", // supporting diagnostic
1147 "const b: i32 = c;\n",
1148 "\n", // context ellipsis
1149 // diagnostic group 2
1150 "\n", // primary message
1151 "\n", // padding
1152 "const a: i32 = 'a';\n",
1153 "const b: i32 = c;\n",
1154 "\n", // supporting diagnostic
1155 //
1156 // main.rs
1157 //
1158 "\n", // filename
1159 "\n", // padding
1160 // diagnostic group 1
1161 "\n", // primary message
1162 "\n", // padding
1163 " let x = vec![];\n",
1164 " let y = vec![];\n",
1165 "\n", // supporting diagnostic
1166 " a(x);\n",
1167 " b(y);\n",
1168 "\n", // supporting diagnostic
1169 " // comment 1\n",
1170 " // comment 2\n",
1171 " c(y);\n",
1172 "\n", // supporting diagnostic
1173 " d(x);\n",
1174 "\n", // context ellipsis
1175 // diagnostic group 2
1176 "\n", // primary message
1177 "\n", // filename
1178 "fn main() {\n",
1179 " let x = vec![];\n",
1180 "\n", // supporting diagnostic
1181 " let y = vec![];\n",
1182 " a(x);\n",
1183 "\n", // supporting diagnostic
1184 " b(y);\n",
1185 "\n", // context ellipsis
1186 " c(y);\n",
1187 " d(x);\n",
1188 "\n", // supporting diagnostic
1189 "}"
1190 )
1191 );
1192 });
1193 }
1194
1195 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1196 editor
1197 .blocks_in_range(0..editor.max_point().row())
1198 .filter_map(|(row, block)| {
1199 let name = match block {
1200 TransformBlock::Custom(block) => block
1201 .render(&BlockContext {
1202 cx,
1203 anchor_x: 0.,
1204 scroll_x: 0.,
1205 gutter_padding: 0.,
1206 gutter_width: 0.,
1207 line_height: 0.,
1208 em_width: 0.,
1209 })
1210 .name()?
1211 .to_string(),
1212 TransformBlock::ExcerptHeader {
1213 starts_new_buffer, ..
1214 } => {
1215 if *starts_new_buffer {
1216 "path header block".to_string()
1217 } else {
1218 "collapsed context".to_string()
1219 }
1220 }
1221 };
1222
1223 Some((row, name))
1224 })
1225 .collect()
1226 }
1227}