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