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