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