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