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