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