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