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