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