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, Editor, ExcerptId, MultiBuffer, ToOffset,
11};
12use gpui::{
13 action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
14 ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
15 WeakViewHandle,
16};
17use language::{
18 Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
19};
20use postage::watch;
21use project::{DiagnosticSummary, Project, ProjectPath};
22use std::{
23 any::{Any, TypeId},
24 cmp::Ordering,
25 mem,
26 ops::Range,
27 path::PathBuf,
28 sync::Arc,
29};
30use util::TryFutureExt;
31use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace};
32
33action!(Deploy);
34action!(OpenExcerpts);
35
36const CONTEXT_LINE_COUNT: u32 = 1;
37
38pub fn init(cx: &mut MutableAppContext) {
39 cx.add_bindings([
40 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
41 Binding::new(
42 "alt-shift-D",
43 OpenExcerpts,
44 Some("ProjectDiagnosticsEditor"),
45 ),
46 ]);
47 cx.add_action(ProjectDiagnosticsEditor::deploy);
48 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
49}
50
51type Event = editor::Event;
52
53struct ProjectDiagnostics {
54 project: ModelHandle<Project>,
55}
56
57struct ProjectDiagnosticsEditor {
58 model: ModelHandle<ProjectDiagnostics>,
59 workspace: WeakViewHandle<Workspace>,
60 editor: ViewHandle<Editor>,
61 summary: DiagnosticSummary,
62 excerpts: ModelHandle<MultiBuffer>,
63 path_states: Vec<PathState>,
64 paths_to_update: BTreeSet<ProjectPath>,
65 settings: watch::Receiver<workspace::Settings>,
66}
67
68struct PathState {
69 path: ProjectPath,
70 diagnostic_groups: Vec<DiagnosticGroupState>,
71}
72
73struct DiagnosticGroupState {
74 primary_diagnostic: DiagnosticEntry<language::Anchor>,
75 primary_excerpt_ix: usize,
76 excerpts: Vec<ExcerptId>,
77 blocks: HashSet<BlockId>,
78 block_count: usize,
79}
80
81impl ProjectDiagnostics {
82 fn new(project: ModelHandle<Project>) -> Self {
83 Self { project }
84 }
85}
86
87impl Entity for ProjectDiagnostics {
88 type Event = ();
89}
90
91impl Entity for ProjectDiagnosticsEditor {
92 type Event = Event;
93}
94
95impl View for ProjectDiagnosticsEditor {
96 fn ui_name() -> &'static str {
97 "ProjectDiagnosticsEditor"
98 }
99
100 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
101 if self.path_states.is_empty() {
102 let theme = &self.settings.borrow().theme.project_diagnostics;
103 Label::new(
104 "No problems in workspace".to_string(),
105 theme.empty_message.clone(),
106 )
107 .aligned()
108 .contained()
109 .with_style(theme.container)
110 .boxed()
111 } else {
112 ChildView::new(&self.editor).boxed()
113 }
114 }
115
116 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
117 if !self.path_states.is_empty() {
118 cx.focus(&self.editor);
119 }
120 }
121}
122
123impl ProjectDiagnosticsEditor {
124 fn new(
125 model: ModelHandle<ProjectDiagnostics>,
126 workspace: WeakViewHandle<Workspace>,
127 settings: watch::Receiver<workspace::Settings>,
128 cx: &mut ViewContext<Self>,
129 ) -> Self {
130 let project = model.read(cx).project.clone();
131 cx.subscribe(&project, |this, _, event, cx| match event {
132 project::Event::DiskBasedDiagnosticsFinished => {
133 this.update_excerpts(cx);
134 this.update_title(cx);
135 }
136 project::Event::DiagnosticsUpdated(path) => {
137 this.paths_to_update.insert(path.clone());
138 }
139 _ => {}
140 })
141 .detach();
142
143 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
144 let editor = cx.add_view(|cx| {
145 let mut editor = Editor::for_buffer(
146 excerpts.clone(),
147 Some(project.clone()),
148 settings.clone(),
149 cx,
150 );
151 editor.set_vertical_scroll_margin(5, cx);
152 editor
153 });
154 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
155 .detach();
156
157 let project = project.read(cx);
158 let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
159 let mut this = Self {
160 model,
161 summary: project.diagnostic_summary(cx),
162 workspace,
163 excerpts,
164 editor,
165 settings,
166 path_states: Default::default(),
167 paths_to_update,
168 };
169 this.update_excerpts(cx);
170 this
171 }
172
173 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
174 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
175 workspace.activate_item(&existing, cx);
176 } else {
177 let diagnostics =
178 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
179 workspace.open_item(diagnostics, cx);
180 }
181 }
182
183 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
184 if let Some(workspace) = self.workspace.upgrade(cx) {
185 let editor = self.editor.read(cx);
186 let excerpts = self.excerpts.read(cx);
187 let mut new_selections_by_buffer = HashMap::default();
188
189 for selection in editor.local_selections::<usize>(cx) {
190 for (buffer, mut range) in
191 excerpts.range_to_buffer_ranges(selection.start..selection.end, cx)
192 {
193 if selection.reversed {
194 mem::swap(&mut range.start, &mut range.end);
195 }
196 new_selections_by_buffer
197 .entry(buffer)
198 .or_insert(Vec::new())
199 .push(range)
200 }
201 }
202
203 // We defer the pane interaction because we ourselves are a workspace item
204 // and activating a new item causes the pane to call a method on us reentrantly,
205 // which panics if we're on the stack.
206 workspace.defer(cx, |workspace, cx| {
207 for (buffer, ranges) in new_selections_by_buffer {
208 let buffer = BufferItemHandle(buffer);
209 if !workspace.activate_pane_for_item(&buffer, cx) {
210 workspace.activate_next_pane(cx);
211 }
212 let editor = workspace
213 .open_item(buffer, cx)
214 .downcast::<Editor>()
215 .unwrap();
216 editor.update(cx, |editor, cx| {
217 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
218 });
219 }
220 });
221 }
222 }
223
224 fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
225 let paths = mem::take(&mut self.paths_to_update);
226 let project = self.model.read(cx).project.clone();
227 cx.spawn(|this, mut cx| {
228 async move {
229 for path in paths {
230 let buffer = project
231 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
232 .await?;
233 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
234 }
235 Result::<_, anyhow::Error>::Ok(())
236 }
237 .log_err()
238 })
239 .detach();
240 }
241
242 fn populate_excerpts(
243 &mut self,
244 path: ProjectPath,
245 buffer: ModelHandle<Buffer>,
246 cx: &mut ViewContext<Self>,
247 ) {
248 let was_empty = self.path_states.is_empty();
249 let snapshot = buffer.read(cx).snapshot();
250 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
251 Ok(ix) => ix,
252 Err(ix) => {
253 self.path_states.insert(
254 ix,
255 PathState {
256 path: path.clone(),
257 diagnostic_groups: Default::default(),
258 },
259 );
260 ix
261 }
262 };
263
264 let mut prev_excerpt_id = if path_ix > 0 {
265 let prev_path_last_group = &self.path_states[path_ix - 1]
266 .diagnostic_groups
267 .last()
268 .unwrap();
269 prev_path_last_group.excerpts.last().unwrap().clone()
270 } else {
271 ExcerptId::min()
272 };
273
274 let path_state = &mut self.path_states[path_ix];
275 let mut groups_to_add = Vec::new();
276 let mut group_ixs_to_remove = Vec::new();
277 let mut blocks_to_add = Vec::new();
278 let mut blocks_to_remove = HashSet::default();
279 let mut first_excerpt_id = None;
280 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
281 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
282 let mut new_groups = snapshot.diagnostic_groups().into_iter().peekable();
283 loop {
284 let mut to_insert = None;
285 let mut to_remove = None;
286 let mut to_keep = None;
287 match (old_groups.peek(), new_groups.peek()) {
288 (None, None) => break,
289 (None, Some(_)) => to_insert = new_groups.next(),
290 (Some(_), None) => to_remove = old_groups.next(),
291 (Some((_, old_group)), Some(new_group)) => {
292 let old_primary = &old_group.primary_diagnostic;
293 let new_primary = &new_group.entries[new_group.primary_ix];
294 match compare_diagnostics(old_primary, new_primary, &snapshot) {
295 Ordering::Less => to_remove = old_groups.next(),
296 Ordering::Equal => {
297 to_keep = old_groups.next();
298 new_groups.next();
299 }
300 Ordering::Greater => to_insert = new_groups.next(),
301 }
302 }
303 }
304
305 if let Some(group) = to_insert {
306 let mut group_state = DiagnosticGroupState {
307 primary_diagnostic: group.entries[group.primary_ix].clone(),
308 primary_excerpt_ix: 0,
309 excerpts: Default::default(),
310 blocks: Default::default(),
311 block_count: 0,
312 };
313 let mut pending_range: Option<(Range<Point>, usize)> = None;
314 let mut is_first_excerpt_for_group = true;
315 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
316 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
317 if let Some((range, start_ix)) = &mut pending_range {
318 if let Some(entry) = resolved_entry.as_ref() {
319 if entry.range.start.row
320 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
321 {
322 range.end = range.end.max(entry.range.end);
323 continue;
324 }
325 }
326
327 let excerpt_start =
328 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
329 let excerpt_end = snapshot.clip_point(
330 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
331 Bias::Left,
332 );
333 let excerpt_id = excerpts
334 .insert_excerpts_after(
335 &prev_excerpt_id,
336 buffer.clone(),
337 [excerpt_start..excerpt_end],
338 excerpts_cx,
339 )
340 .pop()
341 .unwrap();
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.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.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(
573 &mut self,
574 project: ModelHandle<Project>,
575 cx: &mut ViewContext<Self>,
576 ) -> Task<Result<()>> {
577 self.editor.save(project, cx)
578 }
579
580 fn can_save_as(&self, _: &AppContext) -> bool {
581 false
582 }
583
584 fn save_as(
585 &mut self,
586 _: ModelHandle<Project>,
587 _: PathBuf,
588 _: &mut ViewContext<Self>,
589 ) -> Task<Result<()>> {
590 unreachable!()
591 }
592
593 fn should_activate_item_on_event(event: &Self::Event) -> bool {
594 Editor::should_activate_item_on_event(event)
595 }
596
597 fn should_update_tab_on_event(event: &Event) -> bool {
598 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
599 }
600
601 fn clone_on_split(
602 &self,
603 nav_history: ItemNavHistory,
604 cx: &mut ViewContext<Self>,
605 ) -> Option<Self>
606 where
607 Self: Sized,
608 {
609 let diagnostics = ProjectDiagnosticsEditor::new(
610 self.model.clone(),
611 self.workspace.clone(),
612 self.settings.clone(),
613 cx,
614 );
615 diagnostics.editor.update(cx, |editor, _| {
616 editor.set_nav_history(Some(nav_history));
617 });
618 Some(diagnostics)
619 }
620
621 fn act_as_type(
622 &self,
623 type_id: TypeId,
624 self_handle: &ViewHandle<Self>,
625 _: &AppContext,
626 ) -> Option<AnyViewHandle> {
627 if type_id == TypeId::of::<Self>() {
628 Some(self_handle.into())
629 } else if type_id == TypeId::of::<Editor>() {
630 Some((&self.editor).into())
631 } else {
632 None
633 }
634 }
635
636 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
637 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
638 }
639}
640
641fn diagnostic_header_renderer(
642 diagnostic: Diagnostic,
643 settings: watch::Receiver<workspace::Settings>,
644) -> RenderBlock {
645 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
646 Arc::new(move |cx| {
647 let settings = settings.borrow();
648 let theme = &settings.theme.editor;
649 let style = &theme.diagnostic_header;
650 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
651 let icon_width = cx.em_width * style.icon_width_factor;
652 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
653 Svg::new("icons/diagnostic-error-10.svg")
654 .with_color(theme.error_diagnostic.message.text.color)
655 } else {
656 Svg::new("icons/diagnostic-warning-10.svg")
657 .with_color(theme.warning_diagnostic.message.text.color)
658 };
659
660 Flex::row()
661 .with_child(
662 icon.constrained()
663 .with_width(icon_width)
664 .aligned()
665 .contained()
666 .boxed(),
667 )
668 .with_child(
669 Label::new(
670 message.clone(),
671 style.message.label.clone().with_font_size(font_size),
672 )
673 .with_highlights(highlights.clone())
674 .contained()
675 .with_style(style.message.container)
676 .with_margin_left(cx.gutter_padding)
677 .aligned()
678 .boxed(),
679 )
680 .with_children(diagnostic.code.clone().map(|code| {
681 Label::new(code, style.code.text.clone().with_font_size(font_size))
682 .contained()
683 .with_style(style.code.container)
684 .aligned()
685 .boxed()
686 }))
687 .contained()
688 .with_style(style.container)
689 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
690 .expanded()
691 .named("diagnostic header")
692 })
693}
694
695pub(crate) fn render_summary(
696 summary: &DiagnosticSummary,
697 text_style: &TextStyle,
698 theme: &theme::ProjectDiagnostics,
699) -> ElementBox {
700 if summary.error_count == 0 && summary.warning_count == 0 {
701 Label::new("No problems".to_string(), text_style.clone()).boxed()
702 } else {
703 let icon_width = theme.tab_icon_width;
704 let icon_spacing = theme.tab_icon_spacing;
705 let summary_spacing = theme.tab_summary_spacing;
706 Flex::row()
707 .with_children([
708 Svg::new("icons/diagnostic-summary-error.svg")
709 .with_color(text_style.color)
710 .constrained()
711 .with_width(icon_width)
712 .aligned()
713 .contained()
714 .with_margin_right(icon_spacing)
715 .named("no-icon"),
716 Label::new(
717 summary.error_count.to_string(),
718 LabelStyle {
719 text: text_style.clone(),
720 highlight_text: None,
721 },
722 )
723 .aligned()
724 .boxed(),
725 Svg::new("icons/diagnostic-summary-warning.svg")
726 .with_color(text_style.color)
727 .constrained()
728 .with_width(icon_width)
729 .aligned()
730 .contained()
731 .with_margin_left(summary_spacing)
732 .with_margin_right(icon_spacing)
733 .named("warn-icon"),
734 Label::new(
735 summary.warning_count.to_string(),
736 LabelStyle {
737 text: text_style.clone(),
738 highlight_text: None,
739 },
740 )
741 .aligned()
742 .boxed(),
743 ])
744 .boxed()
745 }
746}
747
748fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
749 lhs: &DiagnosticEntry<L>,
750 rhs: &DiagnosticEntry<R>,
751 snapshot: &language::BufferSnapshot,
752) -> Ordering {
753 lhs.range
754 .start
755 .to_offset(&snapshot)
756 .cmp(&rhs.range.start.to_offset(snapshot))
757 .then_with(|| {
758 lhs.range
759 .end
760 .to_offset(&snapshot)
761 .cmp(&rhs.range.end.to_offset(snapshot))
762 })
763 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use editor::{
770 display_map::{BlockContext, TransformBlock},
771 DisplayPoint, EditorSnapshot,
772 };
773 use gpui::TestAppContext;
774 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
775 use serde_json::json;
776 use unindent::Unindent as _;
777 use workspace::WorkspaceParams;
778
779 #[gpui::test]
780 async fn test_diagnostics(mut cx: TestAppContext) {
781 let params = cx.update(WorkspaceParams::test);
782 let project = params.project.clone();
783 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
784
785 params
786 .fs
787 .as_fake()
788 .insert_tree(
789 "/test",
790 json!({
791 "consts.rs": "
792 const a: i32 = 'a';
793 const b: i32 = c;
794 "
795 .unindent(),
796
797 "main.rs": "
798 fn main() {
799 let x = vec![];
800 let y = vec![];
801 a(x);
802 b(y);
803 // comment 1
804 // comment 2
805 c(y);
806 d(x);
807 }
808 "
809 .unindent(),
810 }),
811 )
812 .await;
813
814 project
815 .update(&mut cx, |project, cx| {
816 project.find_or_create_local_worktree("/test", false, cx)
817 })
818 .await
819 .unwrap();
820
821 // Create some diagnostics
822 project.update(&mut cx, |project, cx| {
823 project
824 .update_diagnostic_entries(
825 PathBuf::from("/test/main.rs"),
826 None,
827 vec![
828 DiagnosticEntry {
829 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
830 diagnostic: Diagnostic {
831 message:
832 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
833 .to_string(),
834 severity: DiagnosticSeverity::INFORMATION,
835 is_primary: false,
836 is_disk_based: true,
837 group_id: 1,
838 ..Default::default()
839 },
840 },
841 DiagnosticEntry {
842 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
843 diagnostic: Diagnostic {
844 message:
845 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
846 .to_string(),
847 severity: DiagnosticSeverity::INFORMATION,
848 is_primary: false,
849 is_disk_based: true,
850 group_id: 0,
851 ..Default::default()
852 },
853 },
854 DiagnosticEntry {
855 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
856 diagnostic: Diagnostic {
857 message: "value moved here".to_string(),
858 severity: DiagnosticSeverity::INFORMATION,
859 is_primary: false,
860 is_disk_based: true,
861 group_id: 1,
862 ..Default::default()
863 },
864 },
865 DiagnosticEntry {
866 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
867 diagnostic: Diagnostic {
868 message: "value moved here".to_string(),
869 severity: DiagnosticSeverity::INFORMATION,
870 is_primary: false,
871 is_disk_based: true,
872 group_id: 0,
873 ..Default::default()
874 },
875 },
876 DiagnosticEntry {
877 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
878 diagnostic: Diagnostic {
879 message: "use of moved value\nvalue used here after move".to_string(),
880 severity: DiagnosticSeverity::ERROR,
881 is_primary: true,
882 is_disk_based: true,
883 group_id: 0,
884 ..Default::default()
885 },
886 },
887 DiagnosticEntry {
888 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
889 diagnostic: Diagnostic {
890 message: "use of moved value\nvalue used here after move".to_string(),
891 severity: DiagnosticSeverity::ERROR,
892 is_primary: true,
893 is_disk_based: true,
894 group_id: 1,
895 ..Default::default()
896 },
897 },
898 ],
899 cx,
900 )
901 .unwrap();
902 });
903
904 // Open the project diagnostics view while there are already diagnostics.
905 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
906 let view = cx.add_view(0, |cx| {
907 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
908 });
909
910 view.next_notification(&cx).await;
911 view.update(&mut cx, |view, cx| {
912 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
913
914 assert_eq!(
915 editor_blocks(&editor, cx),
916 [
917 (0, "path header block".into()),
918 (2, "diagnostic header".into()),
919 (15, "collapsed context".into()),
920 (16, "diagnostic header".into()),
921 (25, "collapsed context".into()),
922 ]
923 );
924 assert_eq!(
925 editor.text(),
926 concat!(
927 //
928 // main.rs
929 //
930 "\n", // filename
931 "\n", // padding
932 // diagnostic group 1
933 "\n", // primary message
934 "\n", // padding
935 " let x = vec![];\n",
936 " let y = vec![];\n",
937 "\n", // supporting diagnostic
938 " a(x);\n",
939 " b(y);\n",
940 "\n", // supporting diagnostic
941 " // comment 1\n",
942 " // comment 2\n",
943 " c(y);\n",
944 "\n", // supporting diagnostic
945 " d(x);\n",
946 "\n", // context ellipsis
947 // diagnostic group 2
948 "\n", // primary message
949 "\n", // padding
950 "fn main() {\n",
951 " let x = vec![];\n",
952 "\n", // supporting diagnostic
953 " let y = vec![];\n",
954 " a(x);\n",
955 "\n", // supporting diagnostic
956 " b(y);\n",
957 "\n", // context ellipsis
958 " c(y);\n",
959 " d(x);\n",
960 "\n", // supporting diagnostic
961 "}"
962 )
963 );
964
965 // Cursor is at the first diagnostic
966 view.editor.update(cx, |editor, cx| {
967 assert_eq!(
968 editor.selected_display_ranges(cx),
969 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
970 );
971 });
972 });
973
974 // Diagnostics are added for another earlier path.
975 project.update(&mut cx, |project, cx| {
976 project.disk_based_diagnostics_started(cx);
977 project
978 .update_diagnostic_entries(
979 PathBuf::from("/test/consts.rs"),
980 None,
981 vec![DiagnosticEntry {
982 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
983 diagnostic: Diagnostic {
984 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
985 severity: DiagnosticSeverity::ERROR,
986 is_primary: true,
987 is_disk_based: true,
988 group_id: 0,
989 ..Default::default()
990 },
991 }],
992 cx,
993 )
994 .unwrap();
995 project.disk_based_diagnostics_finished(cx);
996 });
997
998 view.next_notification(&cx).await;
999 view.update(&mut cx, |view, cx| {
1000 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1001
1002 assert_eq!(
1003 editor_blocks(&editor, cx),
1004 [
1005 (0, "path header block".into()),
1006 (2, "diagnostic header".into()),
1007 (7, "path header block".into()),
1008 (9, "diagnostic header".into()),
1009 (22, "collapsed context".into()),
1010 (23, "diagnostic header".into()),
1011 (32, "collapsed context".into()),
1012 ]
1013 );
1014 assert_eq!(
1015 editor.text(),
1016 concat!(
1017 //
1018 // consts.rs
1019 //
1020 "\n", // filename
1021 "\n", // padding
1022 // diagnostic group 1
1023 "\n", // primary message
1024 "\n", // padding
1025 "const a: i32 = 'a';\n",
1026 "\n", // supporting diagnostic
1027 "const b: i32 = c;\n",
1028 //
1029 // main.rs
1030 //
1031 "\n", // filename
1032 "\n", // padding
1033 // diagnostic group 1
1034 "\n", // primary message
1035 "\n", // padding
1036 " let x = vec![];\n",
1037 " let y = vec![];\n",
1038 "\n", // supporting diagnostic
1039 " a(x);\n",
1040 " b(y);\n",
1041 "\n", // supporting diagnostic
1042 " // comment 1\n",
1043 " // comment 2\n",
1044 " c(y);\n",
1045 "\n", // supporting diagnostic
1046 " d(x);\n",
1047 "\n", // collapsed context
1048 // diagnostic group 2
1049 "\n", // primary message
1050 "\n", // filename
1051 "fn main() {\n",
1052 " let x = vec![];\n",
1053 "\n", // supporting diagnostic
1054 " let y = vec![];\n",
1055 " a(x);\n",
1056 "\n", // supporting diagnostic
1057 " b(y);\n",
1058 "\n", // context ellipsis
1059 " c(y);\n",
1060 " d(x);\n",
1061 "\n", // supporting diagnostic
1062 "}"
1063 )
1064 );
1065
1066 // Cursor keeps its position.
1067 view.editor.update(cx, |editor, cx| {
1068 assert_eq!(
1069 editor.selected_display_ranges(cx),
1070 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1071 );
1072 });
1073 });
1074
1075 // Diagnostics are added to the first path
1076 project.update(&mut cx, |project, cx| {
1077 project.disk_based_diagnostics_started(cx);
1078 project
1079 .update_diagnostic_entries(
1080 PathBuf::from("/test/consts.rs"),
1081 None,
1082 vec![
1083 DiagnosticEntry {
1084 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1085 diagnostic: Diagnostic {
1086 message: "mismatched types\nexpected `usize`, found `char`"
1087 .to_string(),
1088 severity: DiagnosticSeverity::ERROR,
1089 is_primary: true,
1090 is_disk_based: true,
1091 group_id: 0,
1092 ..Default::default()
1093 },
1094 },
1095 DiagnosticEntry {
1096 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1097 diagnostic: Diagnostic {
1098 message: "unresolved name `c`".to_string(),
1099 severity: DiagnosticSeverity::ERROR,
1100 is_primary: true,
1101 is_disk_based: true,
1102 group_id: 1,
1103 ..Default::default()
1104 },
1105 },
1106 ],
1107 cx,
1108 )
1109 .unwrap();
1110 project.disk_based_diagnostics_finished(cx);
1111 });
1112
1113 view.next_notification(&cx).await;
1114 view.update(&mut cx, |view, cx| {
1115 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1116
1117 assert_eq!(
1118 editor_blocks(&editor, cx),
1119 [
1120 (0, "path header block".into()),
1121 (2, "diagnostic header".into()),
1122 (7, "collapsed context".into()),
1123 (8, "diagnostic header".into()),
1124 (13, "path header block".into()),
1125 (15, "diagnostic header".into()),
1126 (28, "collapsed context".into()),
1127 (29, "diagnostic header".into()),
1128 (38, "collapsed context".into()),
1129 ]
1130 );
1131 assert_eq!(
1132 editor.text(),
1133 concat!(
1134 //
1135 // consts.rs
1136 //
1137 "\n", // filename
1138 "\n", // padding
1139 // diagnostic group 1
1140 "\n", // primary message
1141 "\n", // padding
1142 "const a: i32 = 'a';\n",
1143 "\n", // supporting diagnostic
1144 "const b: i32 = c;\n",
1145 "\n", // context ellipsis
1146 // diagnostic group 2
1147 "\n", // primary message
1148 "\n", // padding
1149 "const a: i32 = 'a';\n",
1150 "const b: i32 = c;\n",
1151 "\n", // supporting diagnostic
1152 //
1153 // main.rs
1154 //
1155 "\n", // filename
1156 "\n", // padding
1157 // diagnostic group 1
1158 "\n", // primary message
1159 "\n", // padding
1160 " let x = vec![];\n",
1161 " let y = vec![];\n",
1162 "\n", // supporting diagnostic
1163 " a(x);\n",
1164 " b(y);\n",
1165 "\n", // supporting diagnostic
1166 " // comment 1\n",
1167 " // comment 2\n",
1168 " c(y);\n",
1169 "\n", // supporting diagnostic
1170 " d(x);\n",
1171 "\n", // context ellipsis
1172 // diagnostic group 2
1173 "\n", // primary message
1174 "\n", // filename
1175 "fn main() {\n",
1176 " let x = vec![];\n",
1177 "\n", // supporting diagnostic
1178 " let y = vec![];\n",
1179 " a(x);\n",
1180 "\n", // supporting diagnostic
1181 " b(y);\n",
1182 "\n", // context ellipsis
1183 " c(y);\n",
1184 " d(x);\n",
1185 "\n", // supporting diagnostic
1186 "}"
1187 )
1188 );
1189 });
1190 }
1191
1192 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1193 editor
1194 .blocks_in_range(0..editor.max_point().row())
1195 .filter_map(|(row, block)| {
1196 let name = match block {
1197 TransformBlock::Custom(block) => block
1198 .render(&BlockContext {
1199 cx,
1200 anchor_x: 0.,
1201 scroll_x: 0.,
1202 gutter_padding: 0.,
1203 gutter_width: 0.,
1204 line_height: 0.,
1205 em_width: 0.,
1206 })
1207 .name()?
1208 .to_string(),
1209 TransformBlock::ExcerptHeader {
1210 starts_new_buffer, ..
1211 } => {
1212 if *starts_new_buffer {
1213 "path header block".to_string()
1214 } else {
1215 "collapsed context".to_string()
1216 }
1217 }
1218 };
1219
1220 Some((row, name))
1221 })
1222 .collect()
1223 }
1224}