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 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 = editor.refresh_selections(cx);
421 selections = editor.local_selections::<usize>(cx);
422 }
423
424 // If any selection has lost its position, move it to start of the next primary diagnostic.
425 for selection in &mut selections {
426 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
427 let group_ix = match groups.binary_search_by(|probe| {
428 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
429 }) {
430 Ok(ix) | Err(ix) => ix,
431 };
432 if let Some(group) = groups.get(group_ix) {
433 let offset = excerpts_snapshot
434 .anchor_in_excerpt(
435 group.excerpts[group.primary_excerpt_ix].clone(),
436 group.primary_diagnostic.range.start.clone(),
437 )
438 .to_offset(&excerpts_snapshot);
439 selection.start = offset;
440 selection.end = offset;
441 }
442 }
443 }
444 editor.update_selections(selections, None, cx);
445 Some(())
446 });
447
448 if self.path_states.is_empty() {
449 if self.editor.is_focused(cx) {
450 cx.focus_self();
451 }
452 } else {
453 if cx.handle().is_focused(cx) {
454 cx.focus(&self.editor);
455 }
456 }
457 cx.notify();
458 }
459
460 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
461 self.summary = self.project.read(cx).diagnostic_summary(cx);
462 cx.emit(Event::TitleChanged);
463 }
464}
465
466impl workspace::Item for ProjectDiagnosticsEditor {
467 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
468 render_summary(
469 &self.summary,
470 &style.label.text,
471 &cx.global::<Settings>().theme.project_diagnostics,
472 )
473 }
474
475 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
476 None
477 }
478
479 fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
480 None
481 }
482
483 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
484 self.editor
485 .update(cx, |editor, cx| editor.navigate(data, cx))
486 }
487
488 fn is_dirty(&self, cx: &AppContext) -> bool {
489 self.excerpts.read(cx).read(cx).is_dirty()
490 }
491
492 fn has_conflict(&self, cx: &AppContext) -> bool {
493 self.excerpts.read(cx).read(cx).has_conflict()
494 }
495
496 fn can_save(&self, _: &AppContext) -> bool {
497 true
498 }
499
500 fn save(
501 &mut self,
502 project: ModelHandle<Project>,
503 cx: &mut ViewContext<Self>,
504 ) -> Task<Result<()>> {
505 self.editor.save(project, cx)
506 }
507
508 fn reload(
509 &mut self,
510 project: ModelHandle<Project>,
511 cx: &mut ViewContext<Self>,
512 ) -> Task<Result<()>> {
513 self.editor.reload(project, cx)
514 }
515
516 fn can_save_as(&self, _: &AppContext) -> bool {
517 false
518 }
519
520 fn save_as(
521 &mut self,
522 _: ModelHandle<Project>,
523 _: PathBuf,
524 _: &mut ViewContext<Self>,
525 ) -> Task<Result<()>> {
526 unreachable!()
527 }
528
529 fn should_activate_item_on_event(event: &Self::Event) -> bool {
530 Editor::should_activate_item_on_event(event)
531 }
532
533 fn should_update_tab_on_event(event: &Event) -> bool {
534 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
535 }
536
537 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
538 self.editor.update(cx, |editor, _| {
539 editor.set_nav_history(Some(nav_history));
540 });
541 }
542
543 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
544 where
545 Self: Sized,
546 {
547 Some(ProjectDiagnosticsEditor::new(
548 self.project.clone(),
549 self.workspace.clone(),
550 cx,
551 ))
552 }
553
554 fn act_as_type(
555 &self,
556 type_id: TypeId,
557 self_handle: &ViewHandle<Self>,
558 _: &AppContext,
559 ) -> Option<AnyViewHandle> {
560 if type_id == TypeId::of::<Self>() {
561 Some(self_handle.into())
562 } else if type_id == TypeId::of::<Editor>() {
563 Some((&self.editor).into())
564 } else {
565 None
566 }
567 }
568
569 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
570 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
571 }
572}
573
574fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
575 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
576 Arc::new(move |cx| {
577 let settings = cx.global::<Settings>();
578 let theme = &settings.theme.editor;
579 let style = &theme.diagnostic_header;
580 let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
581 let icon_width = cx.em_width * style.icon_width_factor;
582 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
583 Svg::new("icons/diagnostic-error-10.svg")
584 .with_color(theme.error_diagnostic.message.text.color)
585 } else {
586 Svg::new("icons/diagnostic-warning-10.svg")
587 .with_color(theme.warning_diagnostic.message.text.color)
588 };
589
590 Flex::row()
591 .with_child(
592 icon.constrained()
593 .with_width(icon_width)
594 .aligned()
595 .contained()
596 .boxed(),
597 )
598 .with_child(
599 Label::new(
600 message.clone(),
601 style.message.label.clone().with_font_size(font_size),
602 )
603 .with_highlights(highlights.clone())
604 .contained()
605 .with_style(style.message.container)
606 .with_margin_left(cx.gutter_padding)
607 .aligned()
608 .boxed(),
609 )
610 .with_children(diagnostic.code.clone().map(|code| {
611 Label::new(code, style.code.text.clone().with_font_size(font_size))
612 .contained()
613 .with_style(style.code.container)
614 .aligned()
615 .boxed()
616 }))
617 .contained()
618 .with_style(style.container)
619 .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
620 .expanded()
621 .named("diagnostic header")
622 })
623}
624
625pub(crate) fn render_summary(
626 summary: &DiagnosticSummary,
627 text_style: &TextStyle,
628 theme: &theme::ProjectDiagnostics,
629) -> ElementBox {
630 if summary.error_count == 0 && summary.warning_count == 0 {
631 Label::new("No problems".to_string(), text_style.clone()).boxed()
632 } else {
633 let icon_width = theme.tab_icon_width;
634 let icon_spacing = theme.tab_icon_spacing;
635 let summary_spacing = theme.tab_summary_spacing;
636 Flex::row()
637 .with_children([
638 Svg::new("icons/diagnostic-summary-error.svg")
639 .with_color(text_style.color)
640 .constrained()
641 .with_width(icon_width)
642 .aligned()
643 .contained()
644 .with_margin_right(icon_spacing)
645 .named("no-icon"),
646 Label::new(
647 summary.error_count.to_string(),
648 LabelStyle {
649 text: text_style.clone(),
650 highlight_text: None,
651 },
652 )
653 .aligned()
654 .boxed(),
655 Svg::new("icons/diagnostic-summary-warning.svg")
656 .with_color(text_style.color)
657 .constrained()
658 .with_width(icon_width)
659 .aligned()
660 .contained()
661 .with_margin_left(summary_spacing)
662 .with_margin_right(icon_spacing)
663 .named("warn-icon"),
664 Label::new(
665 summary.warning_count.to_string(),
666 LabelStyle {
667 text: text_style.clone(),
668 highlight_text: None,
669 },
670 )
671 .aligned()
672 .boxed(),
673 ])
674 .boxed()
675 }
676}
677
678fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
679 lhs: &DiagnosticEntry<L>,
680 rhs: &DiagnosticEntry<R>,
681 snapshot: &language::BufferSnapshot,
682) -> Ordering {
683 lhs.range
684 .start
685 .to_offset(&snapshot)
686 .cmp(&rhs.range.start.to_offset(snapshot))
687 .then_with(|| {
688 lhs.range
689 .end
690 .to_offset(&snapshot)
691 .cmp(&rhs.range.end.to_offset(snapshot))
692 })
693 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use editor::{
700 display_map::{BlockContext, TransformBlock},
701 DisplayPoint, EditorSnapshot,
702 };
703 use gpui::TestAppContext;
704 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
705 use serde_json::json;
706 use unindent::Unindent as _;
707 use workspace::WorkspaceParams;
708
709 #[gpui::test]
710 async fn test_diagnostics(cx: &mut TestAppContext) {
711 let params = cx.update(WorkspaceParams::test);
712 let project = params.project.clone();
713 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
714
715 params
716 .fs
717 .as_fake()
718 .insert_tree(
719 "/test",
720 json!({
721 "consts.rs": "
722 const a: i32 = 'a';
723 const b: i32 = c;
724 "
725 .unindent(),
726
727 "main.rs": "
728 fn main() {
729 let x = vec![];
730 let y = vec![];
731 a(x);
732 b(y);
733 // comment 1
734 // comment 2
735 c(y);
736 d(x);
737 }
738 "
739 .unindent(),
740 }),
741 )
742 .await;
743
744 project
745 .update(cx, |project, cx| {
746 project.find_or_create_local_worktree("/test", true, cx)
747 })
748 .await
749 .unwrap();
750
751 // Create some diagnostics
752 project.update(cx, |project, cx| {
753 project
754 .update_diagnostic_entries(
755 PathBuf::from("/test/main.rs"),
756 None,
757 vec![
758 DiagnosticEntry {
759 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
760 diagnostic: Diagnostic {
761 message:
762 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
763 .to_string(),
764 severity: DiagnosticSeverity::INFORMATION,
765 is_primary: false,
766 is_disk_based: true,
767 group_id: 1,
768 ..Default::default()
769 },
770 },
771 DiagnosticEntry {
772 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
773 diagnostic: Diagnostic {
774 message:
775 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
776 .to_string(),
777 severity: DiagnosticSeverity::INFORMATION,
778 is_primary: false,
779 is_disk_based: true,
780 group_id: 0,
781 ..Default::default()
782 },
783 },
784 DiagnosticEntry {
785 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
786 diagnostic: Diagnostic {
787 message: "value moved here".to_string(),
788 severity: DiagnosticSeverity::INFORMATION,
789 is_primary: false,
790 is_disk_based: true,
791 group_id: 1,
792 ..Default::default()
793 },
794 },
795 DiagnosticEntry {
796 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
797 diagnostic: Diagnostic {
798 message: "value moved here".to_string(),
799 severity: DiagnosticSeverity::INFORMATION,
800 is_primary: false,
801 is_disk_based: true,
802 group_id: 0,
803 ..Default::default()
804 },
805 },
806 DiagnosticEntry {
807 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
808 diagnostic: Diagnostic {
809 message: "use of moved value\nvalue used here after move".to_string(),
810 severity: DiagnosticSeverity::ERROR,
811 is_primary: true,
812 is_disk_based: true,
813 group_id: 0,
814 ..Default::default()
815 },
816 },
817 DiagnosticEntry {
818 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
819 diagnostic: Diagnostic {
820 message: "use of moved value\nvalue used here after move".to_string(),
821 severity: DiagnosticSeverity::ERROR,
822 is_primary: true,
823 is_disk_based: true,
824 group_id: 1,
825 ..Default::default()
826 },
827 },
828 ],
829 cx,
830 )
831 .unwrap();
832 });
833
834 // Open the project diagnostics view while there are already diagnostics.
835 let view = cx.add_view(0, |cx| {
836 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
837 });
838
839 view.next_notification(&cx).await;
840 view.update(cx, |view, cx| {
841 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
842
843 assert_eq!(
844 editor_blocks(&editor, cx),
845 [
846 (0, "path header block".into()),
847 (2, "diagnostic header".into()),
848 (15, "collapsed context".into()),
849 (16, "diagnostic header".into()),
850 (25, "collapsed context".into()),
851 ]
852 );
853 assert_eq!(
854 editor.text(),
855 concat!(
856 //
857 // main.rs
858 //
859 "\n", // filename
860 "\n", // padding
861 // diagnostic group 1
862 "\n", // primary message
863 "\n", // padding
864 " let x = vec![];\n",
865 " let y = vec![];\n",
866 "\n", // supporting diagnostic
867 " a(x);\n",
868 " b(y);\n",
869 "\n", // supporting diagnostic
870 " // comment 1\n",
871 " // comment 2\n",
872 " c(y);\n",
873 "\n", // supporting diagnostic
874 " d(x);\n",
875 "\n", // context ellipsis
876 // diagnostic group 2
877 "\n", // primary message
878 "\n", // padding
879 "fn main() {\n",
880 " let x = vec![];\n",
881 "\n", // supporting diagnostic
882 " let y = vec![];\n",
883 " a(x);\n",
884 "\n", // supporting diagnostic
885 " b(y);\n",
886 "\n", // context ellipsis
887 " c(y);\n",
888 " d(x);\n",
889 "\n", // supporting diagnostic
890 "}"
891 )
892 );
893
894 // Cursor is at the first diagnostic
895 view.editor.update(cx, |editor, cx| {
896 assert_eq!(
897 editor.selected_display_ranges(cx),
898 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
899 );
900 });
901 });
902
903 // Diagnostics are added for another earlier path.
904 project.update(cx, |project, cx| {
905 project.disk_based_diagnostics_started(cx);
906 project
907 .update_diagnostic_entries(
908 PathBuf::from("/test/consts.rs"),
909 None,
910 vec![DiagnosticEntry {
911 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
912 diagnostic: Diagnostic {
913 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
914 severity: DiagnosticSeverity::ERROR,
915 is_primary: true,
916 is_disk_based: true,
917 group_id: 0,
918 ..Default::default()
919 },
920 }],
921 cx,
922 )
923 .unwrap();
924 project.disk_based_diagnostics_finished(cx);
925 });
926
927 view.next_notification(&cx).await;
928 view.update(cx, |view, cx| {
929 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
930
931 assert_eq!(
932 editor_blocks(&editor, cx),
933 [
934 (0, "path header block".into()),
935 (2, "diagnostic header".into()),
936 (7, "path header block".into()),
937 (9, "diagnostic header".into()),
938 (22, "collapsed context".into()),
939 (23, "diagnostic header".into()),
940 (32, "collapsed context".into()),
941 ]
942 );
943 assert_eq!(
944 editor.text(),
945 concat!(
946 //
947 // consts.rs
948 //
949 "\n", // filename
950 "\n", // padding
951 // diagnostic group 1
952 "\n", // primary message
953 "\n", // padding
954 "const a: i32 = 'a';\n",
955 "\n", // supporting diagnostic
956 "const b: i32 = c;\n",
957 //
958 // main.rs
959 //
960 "\n", // filename
961 "\n", // padding
962 // diagnostic group 1
963 "\n", // primary message
964 "\n", // padding
965 " let x = vec![];\n",
966 " let y = vec![];\n",
967 "\n", // supporting diagnostic
968 " a(x);\n",
969 " b(y);\n",
970 "\n", // supporting diagnostic
971 " // comment 1\n",
972 " // comment 2\n",
973 " c(y);\n",
974 "\n", // supporting diagnostic
975 " d(x);\n",
976 "\n", // collapsed context
977 // diagnostic group 2
978 "\n", // primary message
979 "\n", // filename
980 "fn main() {\n",
981 " let x = vec![];\n",
982 "\n", // supporting diagnostic
983 " let y = vec![];\n",
984 " a(x);\n",
985 "\n", // supporting diagnostic
986 " b(y);\n",
987 "\n", // context ellipsis
988 " c(y);\n",
989 " d(x);\n",
990 "\n", // supporting diagnostic
991 "}"
992 )
993 );
994
995 // Cursor keeps its position.
996 view.editor.update(cx, |editor, cx| {
997 assert_eq!(
998 editor.selected_display_ranges(cx),
999 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1000 );
1001 });
1002 });
1003
1004 // Diagnostics are added to the first path
1005 project.update(cx, |project, cx| {
1006 project.disk_based_diagnostics_started(cx);
1007 project
1008 .update_diagnostic_entries(
1009 PathBuf::from("/test/consts.rs"),
1010 None,
1011 vec![
1012 DiagnosticEntry {
1013 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1014 diagnostic: Diagnostic {
1015 message: "mismatched types\nexpected `usize`, found `char`"
1016 .to_string(),
1017 severity: DiagnosticSeverity::ERROR,
1018 is_primary: true,
1019 is_disk_based: true,
1020 group_id: 0,
1021 ..Default::default()
1022 },
1023 },
1024 DiagnosticEntry {
1025 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1026 diagnostic: Diagnostic {
1027 message: "unresolved name `c`".to_string(),
1028 severity: DiagnosticSeverity::ERROR,
1029 is_primary: true,
1030 is_disk_based: true,
1031 group_id: 1,
1032 ..Default::default()
1033 },
1034 },
1035 ],
1036 cx,
1037 )
1038 .unwrap();
1039 project.disk_based_diagnostics_finished(cx);
1040 });
1041
1042 view.next_notification(&cx).await;
1043 view.update(cx, |view, cx| {
1044 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1045
1046 assert_eq!(
1047 editor_blocks(&editor, cx),
1048 [
1049 (0, "path header block".into()),
1050 (2, "diagnostic header".into()),
1051 (7, "collapsed context".into()),
1052 (8, "diagnostic header".into()),
1053 (13, "path header block".into()),
1054 (15, "diagnostic header".into()),
1055 (28, "collapsed context".into()),
1056 (29, "diagnostic header".into()),
1057 (38, "collapsed context".into()),
1058 ]
1059 );
1060 assert_eq!(
1061 editor.text(),
1062 concat!(
1063 //
1064 // consts.rs
1065 //
1066 "\n", // filename
1067 "\n", // padding
1068 // diagnostic group 1
1069 "\n", // primary message
1070 "\n", // padding
1071 "const a: i32 = 'a';\n",
1072 "\n", // supporting diagnostic
1073 "const b: i32 = c;\n",
1074 "\n", // context ellipsis
1075 // diagnostic group 2
1076 "\n", // primary message
1077 "\n", // padding
1078 "const a: i32 = 'a';\n",
1079 "const b: i32 = c;\n",
1080 "\n", // supporting diagnostic
1081 //
1082 // main.rs
1083 //
1084 "\n", // filename
1085 "\n", // padding
1086 // diagnostic group 1
1087 "\n", // primary message
1088 "\n", // padding
1089 " let x = vec![];\n",
1090 " let y = vec![];\n",
1091 "\n", // supporting diagnostic
1092 " a(x);\n",
1093 " b(y);\n",
1094 "\n", // supporting diagnostic
1095 " // comment 1\n",
1096 " // comment 2\n",
1097 " c(y);\n",
1098 "\n", // supporting diagnostic
1099 " d(x);\n",
1100 "\n", // context ellipsis
1101 // diagnostic group 2
1102 "\n", // primary message
1103 "\n", // filename
1104 "fn main() {\n",
1105 " let x = vec![];\n",
1106 "\n", // supporting diagnostic
1107 " let y = vec![];\n",
1108 " a(x);\n",
1109 "\n", // supporting diagnostic
1110 " b(y);\n",
1111 "\n", // context ellipsis
1112 " c(y);\n",
1113 " d(x);\n",
1114 "\n", // supporting diagnostic
1115 "}"
1116 )
1117 );
1118 });
1119 }
1120
1121 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1122 editor
1123 .blocks_in_range(0..editor.max_point().row())
1124 .filter_map(|(row, block)| {
1125 let name = match block {
1126 TransformBlock::Custom(block) => block
1127 .render(&BlockContext {
1128 cx,
1129 anchor_x: 0.,
1130 scroll_x: 0.,
1131 gutter_padding: 0.,
1132 gutter_width: 0.,
1133 line_height: 0.,
1134 em_width: 0.,
1135 })
1136 .name()?
1137 .to_string(),
1138 TransformBlock::ExcerptHeader {
1139 starts_new_buffer, ..
1140 } => {
1141 if *starts_new_buffer {
1142 "path header block".to_string()
1143 } else {
1144 "collapsed context".to_string()
1145 }
1146 }
1147 };
1148
1149 Some((row, name))
1150 })
1151 .collect()
1152 }
1153}