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