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