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, ExcerptProperties, 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, 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.insert_excerpt_after(
337 &prev_excerpt_id,
338 ExcerptProperties {
339 buffer: &buffer,
340 range: excerpt_start..excerpt_end,
341 },
342 excerpts_cx,
343 );
344
345 prev_excerpt_id = excerpt_id.clone();
346 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
347 group_state.excerpts.push(excerpt_id.clone());
348 let header_position = (excerpt_id.clone(), language::Anchor::min());
349
350 if is_first_excerpt_for_group {
351 is_first_excerpt_for_group = false;
352 let mut primary =
353 group.entries[group.primary_ix].diagnostic.clone();
354 primary.message =
355 primary.message.split('\n').next().unwrap().to_string();
356 group_state.block_count += 1;
357 blocks_to_add.push(BlockProperties {
358 position: header_position,
359 height: 2,
360 render: diagnostic_header_renderer(
361 primary,
362 self.build_settings.clone(),
363 ),
364 disposition: BlockDisposition::Above,
365 });
366 }
367
368 for entry in &group.entries[*start_ix..ix] {
369 let mut diagnostic = entry.diagnostic.clone();
370 if diagnostic.is_primary {
371 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
372 diagnostic.message =
373 entry.diagnostic.message.split('\n').skip(1).collect();
374 }
375
376 if !diagnostic.message.is_empty() {
377 group_state.block_count += 1;
378 blocks_to_add.push(BlockProperties {
379 position: (excerpt_id.clone(), entry.range.start.clone()),
380 height: diagnostic.message.matches('\n').count() as u8 + 1,
381 render: diagnostic_block_renderer(
382 diagnostic,
383 true,
384 self.build_settings.clone(),
385 ),
386 disposition: BlockDisposition::Below,
387 });
388 }
389 }
390
391 pending_range.take();
392 }
393
394 if let Some(entry) = resolved_entry {
395 pending_range = Some((entry.range.clone(), ix));
396 }
397 }
398
399 groups_to_add.push(group_state);
400 } else if let Some((group_ix, group_state)) = to_remove {
401 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
402 group_ixs_to_remove.push(group_ix);
403 blocks_to_remove.extend(group_state.blocks.iter().copied());
404 } else if let Some((_, group)) = to_keep {
405 prev_excerpt_id = group.excerpts.last().unwrap().clone();
406 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
407 }
408 }
409
410 excerpts.snapshot(excerpts_cx)
411 });
412
413 self.editor.update(cx, |editor, cx| {
414 editor.remove_blocks(blocks_to_remove, cx);
415 let block_ids = editor.insert_blocks(
416 blocks_to_add.into_iter().map(|block| {
417 let (excerpt_id, text_anchor) = block.position;
418 BlockProperties {
419 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
420 height: block.height,
421 render: block.render,
422 disposition: block.disposition,
423 }
424 }),
425 cx,
426 );
427
428 let mut block_ids = block_ids.into_iter();
429 for group_state in &mut groups_to_add {
430 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
431 }
432 });
433
434 for ix in group_ixs_to_remove.into_iter().rev() {
435 path_state.diagnostic_groups.remove(ix);
436 }
437 path_state.diagnostic_groups.extend(groups_to_add);
438 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
439 let range_a = &a.primary_diagnostic.range;
440 let range_b = &b.primary_diagnostic.range;
441 range_a
442 .start
443 .cmp(&range_b.start, &snapshot)
444 .unwrap()
445 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
446 });
447
448 if path_state.diagnostic_groups.is_empty() {
449 self.path_states.remove(path_ix);
450 }
451
452 self.editor.update(cx, |editor, cx| {
453 let groups;
454 let mut selections;
455 let new_excerpt_ids_by_selection_id;
456 if was_empty {
457 groups = self.path_states.first()?.diagnostic_groups.as_slice();
458 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
459 selections = vec![Selection {
460 id: 0,
461 start: 0,
462 end: 0,
463 reversed: false,
464 goal: SelectionGoal::None,
465 }];
466 } else {
467 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
468 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
469 selections = editor.local_selections::<usize>(cx);
470 }
471
472 // If any selection has lost its position, move it to start of the next primary diagnostic.
473 for selection in &mut selections {
474 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
475 let group_ix = match groups.binary_search_by(|probe| {
476 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
477 }) {
478 Ok(ix) | Err(ix) => ix,
479 };
480 if let Some(group) = groups.get(group_ix) {
481 let offset = excerpts_snapshot
482 .anchor_in_excerpt(
483 group.excerpts[group.primary_excerpt_ix].clone(),
484 group.primary_diagnostic.range.start.clone(),
485 )
486 .to_offset(&excerpts_snapshot);
487 selection.start = offset;
488 selection.end = offset;
489 }
490 }
491 }
492 editor.update_selections(selections, None, cx);
493 Some(())
494 });
495
496 if self.path_states.is_empty() {
497 if self.editor.is_focused(cx) {
498 cx.focus_self();
499 }
500 } else {
501 if cx.handle().is_focused(cx) {
502 cx.focus(&self.editor);
503 }
504 }
505 cx.notify();
506 }
507
508 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
509 self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
510 cx.emit(Event::TitleChanged);
511 }
512}
513
514impl workspace::Item for ProjectDiagnostics {
515 type View = ProjectDiagnosticsEditor;
516
517 fn build_view(
518 handle: ModelHandle<Self>,
519 workspace: &Workspace,
520 nav_history: ItemNavHistory,
521 cx: &mut ViewContext<Self::View>,
522 ) -> Self::View {
523 let diagnostics = ProjectDiagnosticsEditor::new(
524 handle,
525 workspace.weak_handle(),
526 workspace.settings(),
527 cx,
528 );
529 diagnostics
530 .editor
531 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
532 diagnostics
533 }
534
535 fn project_path(&self) -> Option<project::ProjectPath> {
536 None
537 }
538}
539
540impl workspace::ItemView for ProjectDiagnosticsEditor {
541 fn item_id(&self, _: &AppContext) -> usize {
542 self.model.id()
543 }
544
545 fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
546 render_summary(
547 &self.summary,
548 &style.label.text,
549 &self.settings.borrow().theme.project_diagnostics,
550 )
551 }
552
553 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
554 None
555 }
556
557 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
558 self.editor
559 .update(cx, |editor, cx| editor.navigate(data, cx));
560 }
561
562 fn is_dirty(&self, cx: &AppContext) -> bool {
563 self.excerpts.read(cx).read(cx).is_dirty()
564 }
565
566 fn has_conflict(&self, cx: &AppContext) -> bool {
567 self.excerpts.read(cx).read(cx).has_conflict()
568 }
569
570 fn can_save(&self, _: &AppContext) -> bool {
571 true
572 }
573
574 fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
575 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
576 }
577
578 fn can_save_as(&self, _: &AppContext) -> bool {
579 false
580 }
581
582 fn save_as(
583 &mut self,
584 _: ModelHandle<Project>,
585 _: PathBuf,
586 _: &mut ViewContext<Self>,
587 ) -> Task<Result<()>> {
588 unreachable!()
589 }
590
591 fn should_activate_item_on_event(event: &Self::Event) -> bool {
592 Editor::should_activate_item_on_event(event)
593 }
594
595 fn should_update_tab_on_event(event: &Event) -> bool {
596 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
597 }
598
599 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
600 where
601 Self: Sized,
602 {
603 let diagnostics = ProjectDiagnosticsEditor::new(
604 self.model.clone(),
605 self.workspace.clone(),
606 self.settings.clone(),
607 cx,
608 );
609 diagnostics.editor.update(cx, |editor, cx| {
610 let nav_history = self
611 .editor
612 .read(cx)
613 .nav_history()
614 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
615 editor.set_nav_history(nav_history);
616 });
617 Some(diagnostics)
618 }
619
620 fn act_as_type(
621 &self,
622 type_id: TypeId,
623 self_handle: &ViewHandle<Self>,
624 _: &AppContext,
625 ) -> Option<AnyViewHandle> {
626 if type_id == TypeId::of::<Self>() {
627 Some(self_handle.into())
628 } else if type_id == TypeId::of::<Editor>() {
629 Some((&self.editor).into())
630 } else {
631 None
632 }
633 }
634
635 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
636 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
637 }
638}
639
640fn diagnostic_header_renderer(
641 diagnostic: Diagnostic,
642 build_settings: BuildSettings,
643) -> RenderBlock {
644 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
645 Arc::new(move |cx| {
646 let settings = build_settings(cx);
647 let style = &settings.style.diagnostic_header;
648 let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
649 let icon_width = cx.em_width * style.icon_width_factor;
650 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
651 Svg::new("icons/diagnostic-error-10.svg")
652 .with_color(settings.style.error_diagnostic.message.text.color)
653 } else {
654 Svg::new("icons/diagnostic-warning-10.svg")
655 .with_color(settings.style.warning_diagnostic.message.text.color)
656 };
657
658 Flex::row()
659 .with_child(
660 icon.constrained()
661 .with_width(icon_width)
662 .aligned()
663 .contained()
664 .boxed(),
665 )
666 .with_child(
667 Label::new(
668 message.clone(),
669 style.message.label.clone().with_font_size(font_size),
670 )
671 .with_highlights(highlights.clone())
672 .contained()
673 .with_style(style.message.container)
674 .with_margin_left(cx.gutter_padding)
675 .aligned()
676 .boxed(),
677 )
678 .with_children(diagnostic.code.clone().map(|code| {
679 Label::new(code, style.code.text.clone().with_font_size(font_size))
680 .contained()
681 .with_style(style.code.container)
682 .aligned()
683 .boxed()
684 }))
685 .contained()
686 .with_style(style.container)
687 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
688 .expanded()
689 .named("diagnostic header")
690 })
691}
692
693pub(crate) fn render_summary(
694 summary: &DiagnosticSummary,
695 text_style: &TextStyle,
696 theme: &theme::ProjectDiagnostics,
697) -> ElementBox {
698 if summary.error_count == 0 && summary.warning_count == 0 {
699 Label::new("No problems".to_string(), text_style.clone()).boxed()
700 } else {
701 let icon_width = theme.tab_icon_width;
702 let icon_spacing = theme.tab_icon_spacing;
703 let summary_spacing = theme.tab_summary_spacing;
704 Flex::row()
705 .with_children([
706 Svg::new("icons/diagnostic-summary-error.svg")
707 .with_color(text_style.color)
708 .constrained()
709 .with_width(icon_width)
710 .aligned()
711 .contained()
712 .with_margin_right(icon_spacing)
713 .named("no-icon"),
714 Label::new(
715 summary.error_count.to_string(),
716 LabelStyle {
717 text: text_style.clone(),
718 highlight_text: None,
719 },
720 )
721 .aligned()
722 .boxed(),
723 Svg::new("icons/diagnostic-summary-warning.svg")
724 .with_color(text_style.color)
725 .constrained()
726 .with_width(icon_width)
727 .aligned()
728 .contained()
729 .with_margin_left(summary_spacing)
730 .with_margin_right(icon_spacing)
731 .named("warn-icon"),
732 Label::new(
733 summary.warning_count.to_string(),
734 LabelStyle {
735 text: text_style.clone(),
736 highlight_text: None,
737 },
738 )
739 .aligned()
740 .boxed(),
741 ])
742 .boxed()
743 }
744}
745
746fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
747 lhs: &DiagnosticEntry<L>,
748 rhs: &DiagnosticEntry<R>,
749 snapshot: &language::BufferSnapshot,
750) -> Ordering {
751 lhs.range
752 .start
753 .to_offset(&snapshot)
754 .cmp(&rhs.range.start.to_offset(snapshot))
755 .then_with(|| {
756 lhs.range
757 .end
758 .to_offset(&snapshot)
759 .cmp(&rhs.range.end.to_offset(snapshot))
760 })
761 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use editor::{
768 display_map::{BlockContext, TransformBlock},
769 DisplayPoint, EditorSnapshot,
770 };
771 use gpui::TestAppContext;
772 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
773 use serde_json::json;
774 use unindent::Unindent as _;
775 use workspace::WorkspaceParams;
776
777 #[gpui::test]
778 async fn test_diagnostics(mut cx: TestAppContext) {
779 let params = cx.update(WorkspaceParams::test);
780 let project = params.project.clone();
781 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
782
783 params
784 .fs
785 .as_fake()
786 .insert_tree(
787 "/test",
788 json!({
789 "consts.rs": "
790 const a: i32 = 'a';
791 const b: i32 = c;
792 "
793 .unindent(),
794
795 "main.rs": "
796 fn main() {
797 let x = vec![];
798 let y = vec![];
799 a(x);
800 b(y);
801 // comment 1
802 // comment 2
803 c(y);
804 d(x);
805 }
806 "
807 .unindent(),
808 }),
809 )
810 .await;
811
812 project
813 .update(&mut cx, |project, cx| {
814 project.find_or_create_local_worktree("/test", false, cx)
815 })
816 .await
817 .unwrap();
818
819 // Create some diagnostics
820 project.update(&mut cx, |project, cx| {
821 project
822 .update_diagnostic_entries(
823 PathBuf::from("/test/main.rs"),
824 None,
825 vec![
826 DiagnosticEntry {
827 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
828 diagnostic: Diagnostic {
829 message:
830 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
831 .to_string(),
832 severity: DiagnosticSeverity::INFORMATION,
833 is_primary: false,
834 is_disk_based: true,
835 group_id: 1,
836 ..Default::default()
837 },
838 },
839 DiagnosticEntry {
840 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
841 diagnostic: Diagnostic {
842 message:
843 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
844 .to_string(),
845 severity: DiagnosticSeverity::INFORMATION,
846 is_primary: false,
847 is_disk_based: true,
848 group_id: 0,
849 ..Default::default()
850 },
851 },
852 DiagnosticEntry {
853 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
854 diagnostic: Diagnostic {
855 message: "value moved here".to_string(),
856 severity: DiagnosticSeverity::INFORMATION,
857 is_primary: false,
858 is_disk_based: true,
859 group_id: 1,
860 ..Default::default()
861 },
862 },
863 DiagnosticEntry {
864 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
865 diagnostic: Diagnostic {
866 message: "value moved here".to_string(),
867 severity: DiagnosticSeverity::INFORMATION,
868 is_primary: false,
869 is_disk_based: true,
870 group_id: 0,
871 ..Default::default()
872 },
873 },
874 DiagnosticEntry {
875 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
876 diagnostic: Diagnostic {
877 message: "use of moved value\nvalue used here after move".to_string(),
878 severity: DiagnosticSeverity::ERROR,
879 is_primary: true,
880 is_disk_based: true,
881 group_id: 0,
882 ..Default::default()
883 },
884 },
885 DiagnosticEntry {
886 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
887 diagnostic: Diagnostic {
888 message: "use of moved value\nvalue used here after move".to_string(),
889 severity: DiagnosticSeverity::ERROR,
890 is_primary: true,
891 is_disk_based: true,
892 group_id: 1,
893 ..Default::default()
894 },
895 },
896 ],
897 cx,
898 )
899 .unwrap();
900 });
901
902 // Open the project diagnostics view while there are already diagnostics.
903 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
904 let view = cx.add_view(0, |cx| {
905 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
906 });
907
908 view.next_notification(&cx).await;
909 view.update(&mut cx, |view, cx| {
910 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
911
912 assert_eq!(
913 editor_blocks(&editor, cx),
914 [
915 (0, "path header block".into()),
916 (2, "diagnostic header".into()),
917 (15, "collapsed context".into()),
918 (16, "diagnostic header".into()),
919 (25, "collapsed context".into()),
920 ]
921 );
922 assert_eq!(
923 editor.text(),
924 concat!(
925 //
926 // main.rs
927 //
928 "\n", // filename
929 "\n", // padding
930 // diagnostic group 1
931 "\n", // primary message
932 "\n", // padding
933 " let x = vec![];\n",
934 " let y = vec![];\n",
935 "\n", // supporting diagnostic
936 " a(x);\n",
937 " b(y);\n",
938 "\n", // supporting diagnostic
939 " // comment 1\n",
940 " // comment 2\n",
941 " c(y);\n",
942 "\n", // supporting diagnostic
943 " d(x);\n",
944 "\n", // context ellipsis
945 // diagnostic group 2
946 "\n", // primary message
947 "\n", // padding
948 "fn main() {\n",
949 " let x = vec![];\n",
950 "\n", // supporting diagnostic
951 " let y = vec![];\n",
952 " a(x);\n",
953 "\n", // supporting diagnostic
954 " b(y);\n",
955 "\n", // context ellipsis
956 " c(y);\n",
957 " d(x);\n",
958 "\n", // supporting diagnostic
959 "}"
960 )
961 );
962
963 // Cursor is at the first diagnostic
964 view.editor.update(cx, |editor, cx| {
965 assert_eq!(
966 editor.selected_display_ranges(cx),
967 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
968 );
969 });
970 });
971
972 // Diagnostics are added for another earlier path.
973 project.update(&mut cx, |project, cx| {
974 project.disk_based_diagnostics_started(cx);
975 project
976 .update_diagnostic_entries(
977 PathBuf::from("/test/consts.rs"),
978 None,
979 vec![DiagnosticEntry {
980 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
981 diagnostic: Diagnostic {
982 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
983 severity: DiagnosticSeverity::ERROR,
984 is_primary: true,
985 is_disk_based: true,
986 group_id: 0,
987 ..Default::default()
988 },
989 }],
990 cx,
991 )
992 .unwrap();
993 project.disk_based_diagnostics_finished(cx);
994 });
995
996 view.next_notification(&cx).await;
997 view.update(&mut cx, |view, cx| {
998 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
999
1000 assert_eq!(
1001 editor_blocks(&editor, cx),
1002 [
1003 (0, "path header block".into()),
1004 (2, "diagnostic header".into()),
1005 (7, "path header block".into()),
1006 (9, "diagnostic header".into()),
1007 (22, "collapsed context".into()),
1008 (23, "diagnostic header".into()),
1009 (32, "collapsed context".into()),
1010 ]
1011 );
1012 assert_eq!(
1013 editor.text(),
1014 concat!(
1015 //
1016 // consts.rs
1017 //
1018 "\n", // filename
1019 "\n", // padding
1020 // diagnostic group 1
1021 "\n", // primary message
1022 "\n", // padding
1023 "const a: i32 = 'a';\n",
1024 "\n", // supporting diagnostic
1025 "const b: i32 = c;\n",
1026 //
1027 // main.rs
1028 //
1029 "\n", // filename
1030 "\n", // padding
1031 // diagnostic group 1
1032 "\n", // primary message
1033 "\n", // padding
1034 " let x = vec![];\n",
1035 " let y = vec![];\n",
1036 "\n", // supporting diagnostic
1037 " a(x);\n",
1038 " b(y);\n",
1039 "\n", // supporting diagnostic
1040 " // comment 1\n",
1041 " // comment 2\n",
1042 " c(y);\n",
1043 "\n", // supporting diagnostic
1044 " d(x);\n",
1045 "\n", // collapsed context
1046 // diagnostic group 2
1047 "\n", // primary message
1048 "\n", // filename
1049 "fn main() {\n",
1050 " let x = vec![];\n",
1051 "\n", // supporting diagnostic
1052 " let y = vec![];\n",
1053 " a(x);\n",
1054 "\n", // supporting diagnostic
1055 " b(y);\n",
1056 "\n", // context ellipsis
1057 " c(y);\n",
1058 " d(x);\n",
1059 "\n", // supporting diagnostic
1060 "}"
1061 )
1062 );
1063
1064 // Cursor keeps its position.
1065 view.editor.update(cx, |editor, cx| {
1066 assert_eq!(
1067 editor.selected_display_ranges(cx),
1068 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1069 );
1070 });
1071 });
1072
1073 // Diagnostics are added to the first path
1074 project.update(&mut cx, |project, cx| {
1075 project.disk_based_diagnostics_started(cx);
1076 project
1077 .update_diagnostic_entries(
1078 PathBuf::from("/test/consts.rs"),
1079 None,
1080 vec![
1081 DiagnosticEntry {
1082 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1083 diagnostic: Diagnostic {
1084 message: "mismatched types\nexpected `usize`, found `char`"
1085 .to_string(),
1086 severity: DiagnosticSeverity::ERROR,
1087 is_primary: true,
1088 is_disk_based: true,
1089 group_id: 0,
1090 ..Default::default()
1091 },
1092 },
1093 DiagnosticEntry {
1094 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1095 diagnostic: Diagnostic {
1096 message: "unresolved name `c`".to_string(),
1097 severity: DiagnosticSeverity::ERROR,
1098 is_primary: true,
1099 is_disk_based: true,
1100 group_id: 1,
1101 ..Default::default()
1102 },
1103 },
1104 ],
1105 cx,
1106 )
1107 .unwrap();
1108 project.disk_based_diagnostics_finished(cx);
1109 });
1110
1111 view.next_notification(&cx).await;
1112 view.update(&mut cx, |view, cx| {
1113 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1114
1115 assert_eq!(
1116 editor_blocks(&editor, cx),
1117 [
1118 (0, "path header block".into()),
1119 (2, "diagnostic header".into()),
1120 (7, "collapsed context".into()),
1121 (8, "diagnostic header".into()),
1122 (13, "path header block".into()),
1123 (15, "diagnostic header".into()),
1124 (28, "collapsed context".into()),
1125 (29, "diagnostic header".into()),
1126 (38, "collapsed context".into()),
1127 ]
1128 );
1129 assert_eq!(
1130 editor.text(),
1131 concat!(
1132 //
1133 // consts.rs
1134 //
1135 "\n", // filename
1136 "\n", // padding
1137 // diagnostic group 1
1138 "\n", // primary message
1139 "\n", // padding
1140 "const a: i32 = 'a';\n",
1141 "\n", // supporting diagnostic
1142 "const b: i32 = c;\n",
1143 "\n", // context ellipsis
1144 // diagnostic group 2
1145 "\n", // primary message
1146 "\n", // padding
1147 "const a: i32 = 'a';\n",
1148 "const b: i32 = c;\n",
1149 "\n", // supporting diagnostic
1150 //
1151 // main.rs
1152 //
1153 "\n", // filename
1154 "\n", // padding
1155 // diagnostic group 1
1156 "\n", // primary message
1157 "\n", // padding
1158 " let x = vec![];\n",
1159 " let y = vec![];\n",
1160 "\n", // supporting diagnostic
1161 " a(x);\n",
1162 " b(y);\n",
1163 "\n", // supporting diagnostic
1164 " // comment 1\n",
1165 " // comment 2\n",
1166 " c(y);\n",
1167 "\n", // supporting diagnostic
1168 " d(x);\n",
1169 "\n", // context ellipsis
1170 // diagnostic group 2
1171 "\n", // primary message
1172 "\n", // filename
1173 "fn main() {\n",
1174 " let x = vec![];\n",
1175 "\n", // supporting diagnostic
1176 " let y = vec![];\n",
1177 " a(x);\n",
1178 "\n", // supporting diagnostic
1179 " b(y);\n",
1180 "\n", // context ellipsis
1181 " c(y);\n",
1182 " d(x);\n",
1183 "\n", // supporting diagnostic
1184 "}"
1185 )
1186 );
1187 });
1188 }
1189
1190 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1191 editor
1192 .blocks_in_range(0..editor.max_point().row())
1193 .filter_map(|(row, block)| {
1194 let name = match block {
1195 TransformBlock::Custom(block) => block
1196 .render(&BlockContext {
1197 cx,
1198 anchor_x: 0.,
1199 scroll_x: 0.,
1200 gutter_padding: 0.,
1201 gutter_width: 0.,
1202 line_height: 0.,
1203 em_width: 0.,
1204 })
1205 .name()?
1206 .to_string(),
1207 TransformBlock::ExcerptHeader {
1208 starts_new_buffer, ..
1209 } => {
1210 if *starts_new_buffer {
1211 "path header block".to_string()
1212 } else {
1213 "collapsed context".to_string()
1214 }
1215 }
1216 };
1217
1218 Some((row, name))
1219 })
1220 .collect()
1221 }
1222}