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 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
371 });
372
373 if path_state.diagnostic_groups.is_empty() {
374 self.path_states.remove(path_ix);
375 }
376
377 self.editor.update(cx, |editor, cx| {
378 let groups;
379 let mut selections;
380 let new_excerpt_ids_by_selection_id;
381 if was_empty {
382 groups = self.path_states.first()?.diagnostic_groups.as_slice();
383 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
384 selections = vec![Selection {
385 id: 0,
386 start: 0,
387 end: 0,
388 reversed: false,
389 goal: SelectionGoal::None,
390 }];
391 } else {
392 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
393 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
394 selections = editor.local_selections::<usize>(cx);
395 }
396
397 // If any selection has lost its position, move it to start of the next primary diagnostic.
398 for selection in &mut selections {
399 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
400 let group_ix = match groups.binary_search_by(|probe| {
401 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
402 }) {
403 Ok(ix) | Err(ix) => ix,
404 };
405 if let Some(group) = groups.get(group_ix) {
406 let offset = excerpts_snapshot
407 .anchor_in_excerpt(
408 group.excerpts[group.primary_excerpt_ix].clone(),
409 group.primary_diagnostic.range.start.clone(),
410 )
411 .to_offset(&excerpts_snapshot);
412 selection.start = offset;
413 selection.end = offset;
414 }
415 }
416 }
417 editor.update_selections(selections, None, cx);
418 Some(())
419 });
420
421 if self.path_states.is_empty() {
422 if self.editor.is_focused(cx) {
423 cx.focus_self();
424 }
425 } else {
426 if cx.handle().is_focused(cx) {
427 cx.focus(&self.editor);
428 }
429 }
430 cx.notify();
431 }
432
433 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
434 self.summary = self.project.read(cx).diagnostic_summary(cx);
435 cx.emit(Event::TitleChanged);
436 }
437}
438
439impl workspace::Item for ProjectDiagnosticsEditor {
440 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
441 render_summary(
442 &self.summary,
443 &style.label.text,
444 &cx.global::<Settings>().theme.project_diagnostics,
445 )
446 }
447
448 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
449 None
450 }
451
452 fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
453 None
454 }
455
456 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
457 self.editor
458 .update(cx, |editor, cx| editor.navigate(data, cx))
459 }
460
461 fn is_dirty(&self, cx: &AppContext) -> bool {
462 self.excerpts.read(cx).read(cx).is_dirty()
463 }
464
465 fn has_conflict(&self, cx: &AppContext) -> bool {
466 self.excerpts.read(cx).read(cx).has_conflict()
467 }
468
469 fn can_save(&self, _: &AppContext) -> bool {
470 true
471 }
472
473 fn save(
474 &mut self,
475 project: ModelHandle<Project>,
476 cx: &mut ViewContext<Self>,
477 ) -> Task<Result<()>> {
478 self.editor.save(project, cx)
479 }
480
481 fn reload(
482 &mut self,
483 project: ModelHandle<Project>,
484 cx: &mut ViewContext<Self>,
485 ) -> Task<Result<()>> {
486 self.editor.reload(project, cx)
487 }
488
489 fn can_save_as(&self, _: &AppContext) -> bool {
490 false
491 }
492
493 fn save_as(
494 &mut self,
495 _: ModelHandle<Project>,
496 _: PathBuf,
497 _: &mut ViewContext<Self>,
498 ) -> Task<Result<()>> {
499 unreachable!()
500 }
501
502 fn should_activate_item_on_event(event: &Self::Event) -> bool {
503 Editor::should_activate_item_on_event(event)
504 }
505
506 fn should_update_tab_on_event(event: &Event) -> bool {
507 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
508 }
509
510 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
511 self.editor.update(cx, |editor, _| {
512 editor.set_nav_history(Some(nav_history));
513 });
514 }
515
516 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
517 where
518 Self: Sized,
519 {
520 Some(ProjectDiagnosticsEditor::new(
521 self.project.clone(),
522 self.workspace.clone(),
523 cx,
524 ))
525 }
526
527 fn act_as_type(
528 &self,
529 type_id: TypeId,
530 self_handle: &ViewHandle<Self>,
531 _: &AppContext,
532 ) -> Option<AnyViewHandle> {
533 if type_id == TypeId::of::<Self>() {
534 Some(self_handle.into())
535 } else if type_id == TypeId::of::<Editor>() {
536 Some((&self.editor).into())
537 } else {
538 None
539 }
540 }
541
542 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
543 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
544 }
545}
546
547fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
548 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
549 Arc::new(move |cx| {
550 let settings = cx.global::<Settings>();
551 let theme = &settings.theme.editor;
552 let style = &theme.diagnostic_header;
553 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
554 let icon_width = cx.em_width * style.icon_width_factor;
555 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
556 Svg::new("icons/diagnostic-error-10.svg")
557 .with_color(theme.error_diagnostic.message.text.color)
558 } else {
559 Svg::new("icons/diagnostic-warning-10.svg")
560 .with_color(theme.warning_diagnostic.message.text.color)
561 };
562
563 Flex::row()
564 .with_child(
565 icon.constrained()
566 .with_width(icon_width)
567 .aligned()
568 .contained()
569 .boxed(),
570 )
571 .with_child(
572 Label::new(
573 message.clone(),
574 style.message.label.clone().with_font_size(font_size),
575 )
576 .with_highlights(highlights.clone())
577 .contained()
578 .with_style(style.message.container)
579 .with_margin_left(cx.gutter_padding)
580 .aligned()
581 .boxed(),
582 )
583 .with_children(diagnostic.code.clone().map(|code| {
584 Label::new(code, style.code.text.clone().with_font_size(font_size))
585 .contained()
586 .with_style(style.code.container)
587 .aligned()
588 .boxed()
589 }))
590 .contained()
591 .with_style(style.container)
592 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
593 .expanded()
594 .named("diagnostic header")
595 })
596}
597
598pub(crate) fn render_summary(
599 summary: &DiagnosticSummary,
600 text_style: &TextStyle,
601 theme: &theme::ProjectDiagnostics,
602) -> ElementBox {
603 if summary.error_count == 0 && summary.warning_count == 0 {
604 Label::new("No problems".to_string(), text_style.clone()).boxed()
605 } else {
606 let icon_width = theme.tab_icon_width;
607 let icon_spacing = theme.tab_icon_spacing;
608 let summary_spacing = theme.tab_summary_spacing;
609 Flex::row()
610 .with_children([
611 Svg::new("icons/diagnostic-summary-error.svg")
612 .with_color(text_style.color)
613 .constrained()
614 .with_width(icon_width)
615 .aligned()
616 .contained()
617 .with_margin_right(icon_spacing)
618 .named("no-icon"),
619 Label::new(
620 summary.error_count.to_string(),
621 LabelStyle {
622 text: text_style.clone(),
623 highlight_text: None,
624 },
625 )
626 .aligned()
627 .boxed(),
628 Svg::new("icons/diagnostic-summary-warning.svg")
629 .with_color(text_style.color)
630 .constrained()
631 .with_width(icon_width)
632 .aligned()
633 .contained()
634 .with_margin_left(summary_spacing)
635 .with_margin_right(icon_spacing)
636 .named("warn-icon"),
637 Label::new(
638 summary.warning_count.to_string(),
639 LabelStyle {
640 text: text_style.clone(),
641 highlight_text: None,
642 },
643 )
644 .aligned()
645 .boxed(),
646 ])
647 .boxed()
648 }
649}
650
651fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
652 lhs: &DiagnosticEntry<L>,
653 rhs: &DiagnosticEntry<R>,
654 snapshot: &language::BufferSnapshot,
655) -> Ordering {
656 lhs.range
657 .start
658 .to_offset(&snapshot)
659 .cmp(&rhs.range.start.to_offset(snapshot))
660 .then_with(|| {
661 lhs.range
662 .end
663 .to_offset(&snapshot)
664 .cmp(&rhs.range.end.to_offset(snapshot))
665 })
666 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use editor::{
673 display_map::{BlockContext, TransformBlock},
674 DisplayPoint, EditorSnapshot,
675 };
676 use gpui::TestAppContext;
677 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
678 use serde_json::json;
679 use unindent::Unindent as _;
680 use workspace::WorkspaceParams;
681
682 #[gpui::test]
683 async fn test_diagnostics(cx: &mut TestAppContext) {
684 let params = cx.update(WorkspaceParams::test);
685 let project = params.project.clone();
686 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
687
688 params
689 .fs
690 .as_fake()
691 .insert_tree(
692 "/test",
693 json!({
694 "consts.rs": "
695 const a: i32 = 'a';
696 const b: i32 = c;
697 "
698 .unindent(),
699
700 "main.rs": "
701 fn main() {
702 let x = vec![];
703 let y = vec![];
704 a(x);
705 b(y);
706 // comment 1
707 // comment 2
708 c(y);
709 d(x);
710 }
711 "
712 .unindent(),
713 }),
714 )
715 .await;
716
717 project
718 .update(cx, |project, cx| {
719 project.find_or_create_local_worktree("/test", true, cx)
720 })
721 .await
722 .unwrap();
723
724 // Create some diagnostics
725 project.update(cx, |project, cx| {
726 project
727 .update_diagnostic_entries(
728 PathBuf::from("/test/main.rs"),
729 None,
730 vec![
731 DiagnosticEntry {
732 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
733 diagnostic: Diagnostic {
734 message:
735 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
736 .to_string(),
737 severity: DiagnosticSeverity::INFORMATION,
738 is_primary: false,
739 is_disk_based: true,
740 group_id: 1,
741 ..Default::default()
742 },
743 },
744 DiagnosticEntry {
745 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
746 diagnostic: Diagnostic {
747 message:
748 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
749 .to_string(),
750 severity: DiagnosticSeverity::INFORMATION,
751 is_primary: false,
752 is_disk_based: true,
753 group_id: 0,
754 ..Default::default()
755 },
756 },
757 DiagnosticEntry {
758 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
759 diagnostic: Diagnostic {
760 message: "value moved here".to_string(),
761 severity: DiagnosticSeverity::INFORMATION,
762 is_primary: false,
763 is_disk_based: true,
764 group_id: 1,
765 ..Default::default()
766 },
767 },
768 DiagnosticEntry {
769 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
770 diagnostic: Diagnostic {
771 message: "value moved here".to_string(),
772 severity: DiagnosticSeverity::INFORMATION,
773 is_primary: false,
774 is_disk_based: true,
775 group_id: 0,
776 ..Default::default()
777 },
778 },
779 DiagnosticEntry {
780 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
781 diagnostic: Diagnostic {
782 message: "use of moved value\nvalue used here after move".to_string(),
783 severity: DiagnosticSeverity::ERROR,
784 is_primary: true,
785 is_disk_based: true,
786 group_id: 0,
787 ..Default::default()
788 },
789 },
790 DiagnosticEntry {
791 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
792 diagnostic: Diagnostic {
793 message: "use of moved value\nvalue used here after move".to_string(),
794 severity: DiagnosticSeverity::ERROR,
795 is_primary: true,
796 is_disk_based: true,
797 group_id: 1,
798 ..Default::default()
799 },
800 },
801 ],
802 cx,
803 )
804 .unwrap();
805 });
806
807 // Open the project diagnostics view while there are already diagnostics.
808 let view = cx.add_view(0, |cx| {
809 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
810 });
811
812 view.next_notification(&cx).await;
813 view.update(cx, |view, cx| {
814 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
815
816 assert_eq!(
817 editor_blocks(&editor, cx),
818 [
819 (0, "path header block".into()),
820 (2, "diagnostic header".into()),
821 (15, "collapsed context".into()),
822 (16, "diagnostic header".into()),
823 (25, "collapsed context".into()),
824 ]
825 );
826 assert_eq!(
827 editor.text(),
828 concat!(
829 //
830 // main.rs
831 //
832 "\n", // filename
833 "\n", // padding
834 // diagnostic group 1
835 "\n", // primary message
836 "\n", // padding
837 " let x = vec![];\n",
838 " let y = vec![];\n",
839 "\n", // supporting diagnostic
840 " a(x);\n",
841 " b(y);\n",
842 "\n", // supporting diagnostic
843 " // comment 1\n",
844 " // comment 2\n",
845 " c(y);\n",
846 "\n", // supporting diagnostic
847 " d(x);\n",
848 "\n", // context ellipsis
849 // diagnostic group 2
850 "\n", // primary message
851 "\n", // padding
852 "fn main() {\n",
853 " let x = vec![];\n",
854 "\n", // supporting diagnostic
855 " let y = vec![];\n",
856 " a(x);\n",
857 "\n", // supporting diagnostic
858 " b(y);\n",
859 "\n", // context ellipsis
860 " c(y);\n",
861 " d(x);\n",
862 "\n", // supporting diagnostic
863 "}"
864 )
865 );
866
867 // Cursor is at the first diagnostic
868 view.editor.update(cx, |editor, cx| {
869 assert_eq!(
870 editor.selected_display_ranges(cx),
871 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
872 );
873 });
874 });
875
876 // Diagnostics are added for another earlier path.
877 project.update(cx, |project, cx| {
878 project.disk_based_diagnostics_started(cx);
879 project
880 .update_diagnostic_entries(
881 PathBuf::from("/test/consts.rs"),
882 None,
883 vec![DiagnosticEntry {
884 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
885 diagnostic: Diagnostic {
886 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
887 severity: DiagnosticSeverity::ERROR,
888 is_primary: true,
889 is_disk_based: true,
890 group_id: 0,
891 ..Default::default()
892 },
893 }],
894 cx,
895 )
896 .unwrap();
897 project.disk_based_diagnostics_finished(cx);
898 });
899
900 view.next_notification(&cx).await;
901 view.update(cx, |view, cx| {
902 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
903
904 assert_eq!(
905 editor_blocks(&editor, cx),
906 [
907 (0, "path header block".into()),
908 (2, "diagnostic header".into()),
909 (7, "path header block".into()),
910 (9, "diagnostic header".into()),
911 (22, "collapsed context".into()),
912 (23, "diagnostic header".into()),
913 (32, "collapsed context".into()),
914 ]
915 );
916 assert_eq!(
917 editor.text(),
918 concat!(
919 //
920 // consts.rs
921 //
922 "\n", // filename
923 "\n", // padding
924 // diagnostic group 1
925 "\n", // primary message
926 "\n", // padding
927 "const a: i32 = 'a';\n",
928 "\n", // supporting diagnostic
929 "const b: i32 = c;\n",
930 //
931 // main.rs
932 //
933 "\n", // filename
934 "\n", // padding
935 // diagnostic group 1
936 "\n", // primary message
937 "\n", // padding
938 " let x = vec![];\n",
939 " let y = vec![];\n",
940 "\n", // supporting diagnostic
941 " a(x);\n",
942 " b(y);\n",
943 "\n", // supporting diagnostic
944 " // comment 1\n",
945 " // comment 2\n",
946 " c(y);\n",
947 "\n", // supporting diagnostic
948 " d(x);\n",
949 "\n", // collapsed context
950 // diagnostic group 2
951 "\n", // primary message
952 "\n", // filename
953 "fn main() {\n",
954 " let x = vec![];\n",
955 "\n", // supporting diagnostic
956 " let y = vec![];\n",
957 " a(x);\n",
958 "\n", // supporting diagnostic
959 " b(y);\n",
960 "\n", // context ellipsis
961 " c(y);\n",
962 " d(x);\n",
963 "\n", // supporting diagnostic
964 "}"
965 )
966 );
967
968 // Cursor keeps its position.
969 view.editor.update(cx, |editor, cx| {
970 assert_eq!(
971 editor.selected_display_ranges(cx),
972 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
973 );
974 });
975 });
976
977 // Diagnostics are added to the first path
978 project.update(cx, |project, cx| {
979 project.disk_based_diagnostics_started(cx);
980 project
981 .update_diagnostic_entries(
982 PathBuf::from("/test/consts.rs"),
983 None,
984 vec![
985 DiagnosticEntry {
986 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
987 diagnostic: Diagnostic {
988 message: "mismatched types\nexpected `usize`, found `char`"
989 .to_string(),
990 severity: DiagnosticSeverity::ERROR,
991 is_primary: true,
992 is_disk_based: true,
993 group_id: 0,
994 ..Default::default()
995 },
996 },
997 DiagnosticEntry {
998 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
999 diagnostic: Diagnostic {
1000 message: "unresolved name `c`".to_string(),
1001 severity: DiagnosticSeverity::ERROR,
1002 is_primary: true,
1003 is_disk_based: true,
1004 group_id: 1,
1005 ..Default::default()
1006 },
1007 },
1008 ],
1009 cx,
1010 )
1011 .unwrap();
1012 project.disk_based_diagnostics_finished(cx);
1013 });
1014
1015 view.next_notification(&cx).await;
1016 view.update(cx, |view, cx| {
1017 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1018
1019 assert_eq!(
1020 editor_blocks(&editor, cx),
1021 [
1022 (0, "path header block".into()),
1023 (2, "diagnostic header".into()),
1024 (7, "collapsed context".into()),
1025 (8, "diagnostic header".into()),
1026 (13, "path header block".into()),
1027 (15, "diagnostic header".into()),
1028 (28, "collapsed context".into()),
1029 (29, "diagnostic header".into()),
1030 (38, "collapsed context".into()),
1031 ]
1032 );
1033 assert_eq!(
1034 editor.text(),
1035 concat!(
1036 //
1037 // consts.rs
1038 //
1039 "\n", // filename
1040 "\n", // padding
1041 // diagnostic group 1
1042 "\n", // primary message
1043 "\n", // padding
1044 "const a: i32 = 'a';\n",
1045 "\n", // supporting diagnostic
1046 "const b: i32 = c;\n",
1047 "\n", // context ellipsis
1048 // diagnostic group 2
1049 "\n", // primary message
1050 "\n", // padding
1051 "const a: i32 = 'a';\n",
1052 "const b: i32 = c;\n",
1053 "\n", // supporting diagnostic
1054 //
1055 // main.rs
1056 //
1057 "\n", // filename
1058 "\n", // padding
1059 // diagnostic group 1
1060 "\n", // primary message
1061 "\n", // padding
1062 " let x = vec![];\n",
1063 " let y = vec![];\n",
1064 "\n", // supporting diagnostic
1065 " a(x);\n",
1066 " b(y);\n",
1067 "\n", // supporting diagnostic
1068 " // comment 1\n",
1069 " // comment 2\n",
1070 " c(y);\n",
1071 "\n", // supporting diagnostic
1072 " d(x);\n",
1073 "\n", // context ellipsis
1074 // diagnostic group 2
1075 "\n", // primary message
1076 "\n", // filename
1077 "fn main() {\n",
1078 " let x = vec![];\n",
1079 "\n", // supporting diagnostic
1080 " let y = vec![];\n",
1081 " a(x);\n",
1082 "\n", // supporting diagnostic
1083 " b(y);\n",
1084 "\n", // context ellipsis
1085 " c(y);\n",
1086 " d(x);\n",
1087 "\n", // supporting diagnostic
1088 "}"
1089 )
1090 );
1091 });
1092 }
1093
1094 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1095 editor
1096 .blocks_in_range(0..editor.max_point().row())
1097 .filter_map(|(row, block)| {
1098 let name = match block {
1099 TransformBlock::Custom(block) => block
1100 .render(&BlockContext {
1101 cx,
1102 anchor_x: 0.,
1103 scroll_x: 0.,
1104 gutter_padding: 0.,
1105 gutter_width: 0.,
1106 line_height: 0.,
1107 em_width: 0.,
1108 })
1109 .name()?
1110 .to_string(),
1111 TransformBlock::ExcerptHeader {
1112 starts_new_buffer, ..
1113 } => {
1114 if *starts_new_buffer {
1115 "path header block".to_string()
1116 } else {
1117 "collapsed context".to_string()
1118 }
1119 }
1120 };
1121
1122 Some((row, name))
1123 })
1124 .collect()
1125 }
1126}