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::{ItemNavHistory, ItemViewHandle as _, 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.open_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::ItemView 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 project_entry(&self, _: &AppContext) -> Option<project::ProjectEntry> {
453 None
454 }
455
456 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
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.app_state::<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}