1pub mod items;
2
3use anyhow::Result;
4use collections::{BTreeSet, HashMap, HashSet};
5use editor::{
6 diagnostic_block_renderer,
7 display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
8 highlight_diagnostic_message,
9 items::BufferItemHandle,
10 Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
11};
12use gpui::{
13 action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
14 ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
15 WeakViewHandle,
16};
17use language::{
18 Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
19};
20use postage::watch;
21use project::{DiagnosticSummary, Project, ProjectPath};
22use std::{
23 any::{Any, TypeId},
24 cmp::Ordering,
25 mem,
26 ops::Range,
27 path::PathBuf,
28 sync::Arc,
29};
30use util::TryFutureExt;
31use workspace::{ItemNavHistory, Workspace};
32
33action!(Deploy);
34action!(OpenExcerpts);
35
36const CONTEXT_LINE_COUNT: u32 = 1;
37
38pub fn init(cx: &mut MutableAppContext) {
39 cx.add_bindings([
40 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
41 Binding::new(
42 "alt-shift-D",
43 OpenExcerpts,
44 Some("ProjectDiagnosticsEditor"),
45 ),
46 ]);
47 cx.add_action(ProjectDiagnosticsEditor::deploy);
48 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
49}
50
51type Event = editor::Event;
52
53struct ProjectDiagnostics {
54 project: ModelHandle<Project>,
55}
56
57struct ProjectDiagnosticsEditor {
58 model: ModelHandle<ProjectDiagnostics>,
59 workspace: WeakViewHandle<Workspace>,
60 editor: ViewHandle<Editor>,
61 summary: DiagnosticSummary,
62 excerpts: ModelHandle<MultiBuffer>,
63 path_states: Vec<PathState>,
64 paths_to_update: BTreeSet<ProjectPath>,
65 build_settings: BuildSettings,
66 settings: watch::Receiver<workspace::Settings>,
67}
68
69struct PathState {
70 path: ProjectPath,
71 diagnostic_groups: Vec<DiagnosticGroupState>,
72}
73
74struct DiagnosticGroupState {
75 primary_diagnostic: DiagnosticEntry<language::Anchor>,
76 primary_excerpt_ix: usize,
77 excerpts: Vec<ExcerptId>,
78 blocks: HashSet<BlockId>,
79 block_count: usize,
80}
81
82impl ProjectDiagnostics {
83 fn new(project: ModelHandle<Project>) -> Self {
84 Self { project }
85 }
86}
87
88impl Entity for ProjectDiagnostics {
89 type Event = ();
90}
91
92impl Entity for ProjectDiagnosticsEditor {
93 type Event = Event;
94}
95
96impl View for ProjectDiagnosticsEditor {
97 fn ui_name() -> &'static str {
98 "ProjectDiagnosticsEditor"
99 }
100
101 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
102 if self.path_states.is_empty() {
103 let theme = &self.settings.borrow().theme.project_diagnostics;
104 Label::new(
105 "No problems in workspace".to_string(),
106 theme.empty_message.clone(),
107 )
108 .aligned()
109 .contained()
110 .with_style(theme.container)
111 .boxed()
112 } else {
113 ChildView::new(&self.editor).boxed()
114 }
115 }
116
117 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
118 if !self.path_states.is_empty() {
119 cx.focus(&self.editor);
120 }
121 }
122}
123
124impl ProjectDiagnosticsEditor {
125 fn new(
126 model: ModelHandle<ProjectDiagnostics>,
127 workspace: WeakViewHandle<Workspace>,
128 settings: watch::Receiver<workspace::Settings>,
129 cx: &mut ViewContext<Self>,
130 ) -> Self {
131 let project = model.read(cx).project.clone();
132 cx.subscribe(&project, |this, _, event, cx| match event {
133 project::Event::DiskBasedDiagnosticsFinished => {
134 this.update_excerpts(cx);
135 this.update_title(cx);
136 }
137 project::Event::DiagnosticsUpdated(path) => {
138 this.paths_to_update.insert(path.clone());
139 }
140 _ => {}
141 })
142 .detach();
143
144 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
145 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
146 let editor = cx.add_view(|cx| {
147 let mut editor = Editor::for_buffer(
148 excerpts.clone(),
149 build_settings.clone(),
150 Some(project.clone()),
151 cx,
152 );
153 editor.set_vertical_scroll_margin(5, cx);
154 editor
155 });
156 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
157 .detach();
158
159 let project = project.read(cx);
160 let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
161 let mut this = Self {
162 model,
163 summary: project.diagnostic_summary(cx),
164 workspace,
165 excerpts,
166 editor,
167 build_settings,
168 settings,
169 path_states: Default::default(),
170 paths_to_update,
171 };
172 this.update_excerpts(cx);
173 this
174 }
175
176 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
177 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
178 workspace.activate_item(&existing, cx);
179 } else {
180 let diagnostics =
181 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
182 workspace.open_item(diagnostics, cx);
183 }
184 }
185
186 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
187 if let Some(workspace) = self.workspace.upgrade(cx) {
188 let editor = self.editor.read(cx);
189 let excerpts = self.excerpts.read(cx);
190 let mut new_selections_by_buffer = HashMap::default();
191
192 for selection in editor.local_selections::<usize>(cx) {
193 for (buffer, mut range) in
194 excerpts.excerpted_buffers(selection.start..selection.end, cx)
195 {
196 if selection.reversed {
197 mem::swap(&mut range.start, &mut range.end);
198 }
199 new_selections_by_buffer
200 .entry(buffer)
201 .or_insert(Vec::new())
202 .push(range)
203 }
204 }
205
206 // We defer the pane interaction because we ourselves are a workspace item
207 // and activating a new item causes the pane to call a method on us reentrantly,
208 // which panics if we're on the stack.
209 workspace.defer(cx, |workspace, cx| {
210 for (buffer, ranges) in new_selections_by_buffer {
211 let buffer = BufferItemHandle(buffer);
212 if !workspace.activate_pane_for_item(&buffer, cx) {
213 workspace.activate_next_pane(cx);
214 }
215 let editor = workspace
216 .open_item(buffer, cx)
217 .downcast::<Editor>()
218 .unwrap();
219 editor.update(cx, |editor, cx| {
220 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
221 });
222 }
223 });
224 }
225 }
226
227 fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
228 let paths = mem::take(&mut self.paths_to_update);
229 let project = self.model.read(cx).project.clone();
230 cx.spawn(|this, mut cx| {
231 async move {
232 for path in paths {
233 let buffer = project
234 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
235 .await?;
236 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
237 }
238 Result::<_, anyhow::Error>::Ok(())
239 }
240 .log_err()
241 })
242 .detach();
243 }
244
245 fn populate_excerpts(
246 &mut self,
247 path: ProjectPath,
248 buffer: ModelHandle<Buffer>,
249 cx: &mut ViewContext<Self>,
250 ) {
251 let was_empty = self.path_states.is_empty();
252 let snapshot = buffer.read(cx).snapshot();
253 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
254 Ok(ix) => ix,
255 Err(ix) => {
256 self.path_states.insert(
257 ix,
258 PathState {
259 path: path.clone(),
260 diagnostic_groups: Default::default(),
261 },
262 );
263 ix
264 }
265 };
266
267 let mut prev_excerpt_id = if path_ix > 0 {
268 let prev_path_last_group = &self.path_states[path_ix - 1]
269 .diagnostic_groups
270 .last()
271 .unwrap();
272 prev_path_last_group.excerpts.last().unwrap().clone()
273 } else {
274 ExcerptId::min()
275 };
276
277 let path_state = &mut self.path_states[path_ix];
278 let mut groups_to_add = Vec::new();
279 let mut group_ixs_to_remove = Vec::new();
280 let mut blocks_to_add = Vec::new();
281 let mut blocks_to_remove = HashSet::default();
282 let mut first_excerpt_id = None;
283 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
284 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
285 let mut new_groups = snapshot.diagnostic_groups().into_iter().peekable();
286 loop {
287 let mut to_insert = None;
288 let mut to_remove = None;
289 let mut to_keep = None;
290 match (old_groups.peek(), new_groups.peek()) {
291 (None, None) => break,
292 (None, Some(_)) => to_insert = new_groups.next(),
293 (Some(_), None) => to_remove = old_groups.next(),
294 (Some((_, old_group)), Some(new_group)) => {
295 let old_primary = &old_group.primary_diagnostic;
296 let new_primary = &new_group.entries[new_group.primary_ix];
297 match compare_diagnostics(old_primary, new_primary, &snapshot) {
298 Ordering::Less => to_remove = old_groups.next(),
299 Ordering::Equal => {
300 to_keep = old_groups.next();
301 new_groups.next();
302 }
303 Ordering::Greater => to_insert = new_groups.next(),
304 }
305 }
306 }
307
308 if let Some(group) = to_insert {
309 let mut group_state = DiagnosticGroupState {
310 primary_diagnostic: group.entries[group.primary_ix].clone(),
311 primary_excerpt_ix: 0,
312 excerpts: Default::default(),
313 blocks: Default::default(),
314 block_count: 0,
315 };
316 let mut pending_range: Option<(Range<Point>, usize)> = None;
317 let mut is_first_excerpt_for_group = true;
318 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
319 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
320 if let Some((range, start_ix)) = &mut pending_range {
321 if let Some(entry) = resolved_entry.as_ref() {
322 if entry.range.start.row
323 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
324 {
325 range.end = range.end.max(entry.range.end);
326 continue;
327 }
328 }
329
330 let excerpt_start =
331 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
332 let excerpt_end = snapshot.clip_point(
333 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
334 Bias::Left,
335 );
336 let excerpt_id = excerpts.insert_excerpt_after(
337 &prev_excerpt_id,
338 ExcerptProperties {
339 buffer: &buffer,
340 range: excerpt_start..excerpt_end,
341 },
342 excerpts_cx,
343 );
344
345 prev_excerpt_id = excerpt_id.clone();
346 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
347 group_state.excerpts.push(excerpt_id.clone());
348 let header_position = (excerpt_id.clone(), language::Anchor::min());
349
350 if is_first_excerpt_for_group {
351 is_first_excerpt_for_group = false;
352 let mut primary =
353 group.entries[group.primary_ix].diagnostic.clone();
354 primary.message =
355 primary.message.split('\n').next().unwrap().to_string();
356 group_state.block_count += 1;
357 blocks_to_add.push(BlockProperties {
358 position: header_position,
359 height: 2,
360 render: diagnostic_header_renderer(
361 primary,
362 self.build_settings.clone(),
363 ),
364 disposition: BlockDisposition::Above,
365 });
366 }
367
368 for entry in &group.entries[*start_ix..ix] {
369 let mut diagnostic = entry.diagnostic.clone();
370 if diagnostic.is_primary {
371 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
372 diagnostic.message =
373 entry.diagnostic.message.split('\n').skip(1).collect();
374 }
375
376 if !diagnostic.message.is_empty() {
377 group_state.block_count += 1;
378 blocks_to_add.push(BlockProperties {
379 position: (excerpt_id.clone(), entry.range.start.clone()),
380 height: diagnostic.message.matches('\n').count() as u8 + 1,
381 render: diagnostic_block_renderer(
382 diagnostic,
383 true,
384 self.build_settings.clone(),
385 ),
386 disposition: BlockDisposition::Below,
387 });
388 }
389 }
390
391 pending_range.take();
392 }
393
394 if let Some(entry) = resolved_entry {
395 pending_range = Some((entry.range.clone(), ix));
396 }
397 }
398
399 groups_to_add.push(group_state);
400 } else if let Some((group_ix, group_state)) = to_remove {
401 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
402 group_ixs_to_remove.push(group_ix);
403 blocks_to_remove.extend(group_state.blocks.iter().copied());
404 } else if let Some((_, group)) = to_keep {
405 prev_excerpt_id = group.excerpts.last().unwrap().clone();
406 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
407 }
408 }
409
410 excerpts.snapshot(excerpts_cx)
411 });
412
413 self.editor.update(cx, |editor, cx| {
414 editor.remove_blocks(blocks_to_remove, cx);
415 let block_ids = editor.insert_blocks(
416 blocks_to_add.into_iter().map(|block| {
417 let (excerpt_id, text_anchor) = block.position;
418 BlockProperties {
419 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
420 height: block.height,
421 render: block.render,
422 disposition: block.disposition,
423 }
424 }),
425 cx,
426 );
427
428 let mut block_ids = block_ids.into_iter();
429 for group_state in &mut groups_to_add {
430 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
431 }
432 });
433
434 for ix in group_ixs_to_remove.into_iter().rev() {
435 path_state.diagnostic_groups.remove(ix);
436 }
437 path_state.diagnostic_groups.extend(groups_to_add);
438 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
439 let range_a = &a.primary_diagnostic.range;
440 let range_b = &b.primary_diagnostic.range;
441 range_a
442 .start
443 .cmp(&range_b.start, &snapshot)
444 .unwrap()
445 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
446 });
447
448 if path_state.diagnostic_groups.is_empty() {
449 self.path_states.remove(path_ix);
450 }
451
452 self.editor.update(cx, |editor, cx| {
453 let groups;
454 let mut selections;
455 let new_excerpt_ids_by_selection_id;
456 if was_empty {
457 groups = self.path_states.first()?.diagnostic_groups.as_slice();
458 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
459 selections = vec![Selection {
460 id: 0,
461 start: 0,
462 end: 0,
463 reversed: false,
464 goal: SelectionGoal::None,
465 }];
466 } else {
467 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
468 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
469 selections = editor.local_selections::<usize>(cx);
470 }
471
472 // If any selection has lost its position, move it to start of the next primary diagnostic.
473 for selection in &mut selections {
474 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
475 let group_ix = match groups.binary_search_by(|probe| {
476 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
477 }) {
478 Ok(ix) | Err(ix) => ix,
479 };
480 if let Some(group) = groups.get(group_ix) {
481 let offset = excerpts_snapshot
482 .anchor_in_excerpt(
483 group.excerpts[group.primary_excerpt_ix].clone(),
484 group.primary_diagnostic.range.start.clone(),
485 )
486 .to_offset(&excerpts_snapshot);
487 selection.start = offset;
488 selection.end = offset;
489 }
490 }
491 }
492 editor.update_selections(selections, None, cx);
493 Some(())
494 });
495
496 if self.path_states.is_empty() {
497 if self.editor.is_focused(cx) {
498 cx.focus_self();
499 }
500 } else {
501 if cx.handle().is_focused(cx) {
502 cx.focus(&self.editor);
503 }
504 }
505 cx.notify();
506 }
507
508 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
509 self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
510 cx.emit(Event::TitleChanged);
511 }
512}
513
514impl workspace::Item for ProjectDiagnostics {
515 type View = ProjectDiagnosticsEditor;
516
517 fn build_view(
518 handle: ModelHandle<Self>,
519 workspace: &Workspace,
520 nav_history: ItemNavHistory,
521 cx: &mut ViewContext<Self::View>,
522 ) -> Self::View {
523 let diagnostics = ProjectDiagnosticsEditor::new(
524 handle,
525 workspace.weak_handle(),
526 workspace.settings(),
527 cx,
528 );
529 diagnostics
530 .editor
531 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
532 diagnostics
533 }
534
535 fn project_path(&self) -> Option<project::ProjectPath> {
536 None
537 }
538}
539
540impl workspace::ItemView for ProjectDiagnosticsEditor {
541 type ItemHandle = ModelHandle<ProjectDiagnostics>;
542
543 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
544 self.model.clone()
545 }
546
547 fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
548 render_summary(
549 &self.summary,
550 &style.label.text,
551 &self.settings.borrow().theme.project_diagnostics,
552 )
553 }
554
555 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
556 None
557 }
558
559 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
560 self.editor
561 .update(cx, |editor, cx| editor.navigate(data, cx));
562 }
563
564 fn is_dirty(&self, cx: &AppContext) -> bool {
565 self.excerpts.read(cx).read(cx).is_dirty()
566 }
567
568 fn has_conflict(&self, cx: &AppContext) -> bool {
569 self.excerpts.read(cx).read(cx).has_conflict()
570 }
571
572 fn can_save(&self, _: &AppContext) -> bool {
573 true
574 }
575
576 fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
577 self.excerpts.update(cx, |excerpts, cx| excerpts.save(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(&self, cx: &mut ViewContext<Self>) -> Option<Self>
602 where
603 Self: Sized,
604 {
605 let diagnostics = ProjectDiagnosticsEditor::new(
606 self.model.clone(),
607 self.workspace.clone(),
608 self.settings.clone(),
609 cx,
610 );
611 diagnostics.editor.update(cx, |editor, cx| {
612 let nav_history = self
613 .editor
614 .read(cx)
615 .nav_history()
616 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
617 editor.set_nav_history(nav_history);
618 });
619 Some(diagnostics)
620 }
621
622 fn act_as_type(
623 &self,
624 type_id: TypeId,
625 self_handle: &ViewHandle<Self>,
626 _: &AppContext,
627 ) -> Option<AnyViewHandle> {
628 if type_id == TypeId::of::<Self>() {
629 Some(self_handle.into())
630 } else if type_id == TypeId::of::<Editor>() {
631 Some((&self.editor).into())
632 } else {
633 None
634 }
635 }
636
637 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
638 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
639 }
640}
641
642fn diagnostic_header_renderer(
643 diagnostic: Diagnostic,
644 build_settings: BuildSettings,
645) -> RenderBlock {
646 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
647 Arc::new(move |cx| {
648 let settings = build_settings(cx);
649 let style = &settings.style.diagnostic_header;
650 let font_size = (style.text_scale_factor * settings.style.text.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(settings.style.error_diagnostic.message.text.color)
655 } else {
656 Svg::new("icons/diagnostic-warning-10.svg")
657 .with_color(settings.style.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}