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