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