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