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