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