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