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 is_singleton(&self, _: &AppContext) -> bool {
488 false
489 }
490
491 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
492 self.editor
493 .update(cx, |editor, cx| editor.navigate(data, cx))
494 }
495
496 fn is_dirty(&self, cx: &AppContext) -> bool {
497 self.excerpts.read(cx).is_dirty(cx)
498 }
499
500 fn has_conflict(&self, cx: &AppContext) -> bool {
501 self.excerpts.read(cx).has_conflict(cx)
502 }
503
504 fn can_save(&self, _: &AppContext) -> bool {
505 true
506 }
507
508 fn save(
509 &mut self,
510 project: ModelHandle<Project>,
511 cx: &mut ViewContext<Self>,
512 ) -> Task<Result<()>> {
513 self.editor.save(project, cx)
514 }
515
516 fn reload(
517 &mut self,
518 project: ModelHandle<Project>,
519 cx: &mut ViewContext<Self>,
520 ) -> Task<Result<()>> {
521 self.editor.reload(project, cx)
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,
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 assert_eq!(
839 editor_blocks(&view.editor, cx),
840 [
841 (0, "path header block".into()),
842 (2, "diagnostic header".into()),
843 (15, "collapsed context".into()),
844 (16, "diagnostic header".into()),
845 (25, "collapsed context".into()),
846 ]
847 );
848 assert_eq!(
849 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
850 concat!(
851 //
852 // main.rs
853 //
854 "\n", // filename
855 "\n", // padding
856 // diagnostic group 1
857 "\n", // primary message
858 "\n", // padding
859 " let x = vec![];\n",
860 " let y = vec![];\n",
861 "\n", // supporting diagnostic
862 " a(x);\n",
863 " b(y);\n",
864 "\n", // supporting diagnostic
865 " // comment 1\n",
866 " // comment 2\n",
867 " c(y);\n",
868 "\n", // supporting diagnostic
869 " d(x);\n",
870 "\n", // context ellipsis
871 // diagnostic group 2
872 "\n", // primary message
873 "\n", // padding
874 "fn main() {\n",
875 " let x = vec![];\n",
876 "\n", // supporting diagnostic
877 " let y = vec![];\n",
878 " a(x);\n",
879 "\n", // supporting diagnostic
880 " b(y);\n",
881 "\n", // context ellipsis
882 " c(y);\n",
883 " d(x);\n",
884 "\n", // supporting diagnostic
885 "}"
886 )
887 );
888
889 // Cursor is at the first diagnostic
890 view.editor.update(cx, |editor, cx| {
891 assert_eq!(
892 editor.selections.display_ranges(cx),
893 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
894 );
895 });
896 });
897
898 // Diagnostics are added for another earlier path.
899 project.update(cx, |project, cx| {
900 project.disk_based_diagnostics_started(cx);
901 project
902 .update_diagnostic_entries(
903 PathBuf::from("/test/consts.rs"),
904 None,
905 vec![DiagnosticEntry {
906 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
907 diagnostic: Diagnostic {
908 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
909 severity: DiagnosticSeverity::ERROR,
910 is_primary: true,
911 is_disk_based: true,
912 group_id: 0,
913 ..Default::default()
914 },
915 }],
916 cx,
917 )
918 .unwrap();
919 project.disk_based_diagnostics_finished(cx);
920 });
921
922 view.next_notification(&cx).await;
923 view.update(cx, |view, cx| {
924 assert_eq!(
925 editor_blocks(&view.editor, cx),
926 [
927 (0, "path header block".into()),
928 (2, "diagnostic header".into()),
929 (7, "path header block".into()),
930 (9, "diagnostic header".into()),
931 (22, "collapsed context".into()),
932 (23, "diagnostic header".into()),
933 (32, "collapsed context".into()),
934 ]
935 );
936 assert_eq!(
937 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
938 concat!(
939 //
940 // consts.rs
941 //
942 "\n", // filename
943 "\n", // padding
944 // diagnostic group 1
945 "\n", // primary message
946 "\n", // padding
947 "const a: i32 = 'a';\n",
948 "\n", // supporting diagnostic
949 "const b: i32 = c;\n",
950 //
951 // main.rs
952 //
953 "\n", // filename
954 "\n", // padding
955 // diagnostic group 1
956 "\n", // primary message
957 "\n", // padding
958 " let x = vec![];\n",
959 " let y = vec![];\n",
960 "\n", // supporting diagnostic
961 " a(x);\n",
962 " b(y);\n",
963 "\n", // supporting diagnostic
964 " // comment 1\n",
965 " // comment 2\n",
966 " c(y);\n",
967 "\n", // supporting diagnostic
968 " d(x);\n",
969 "\n", // collapsed context
970 // diagnostic group 2
971 "\n", // primary message
972 "\n", // filename
973 "fn main() {\n",
974 " let x = vec![];\n",
975 "\n", // supporting diagnostic
976 " let y = vec![];\n",
977 " a(x);\n",
978 "\n", // supporting diagnostic
979 " b(y);\n",
980 "\n", // context ellipsis
981 " c(y);\n",
982 " d(x);\n",
983 "\n", // supporting diagnostic
984 "}"
985 )
986 );
987
988 // Cursor keeps its position.
989 view.editor.update(cx, |editor, cx| {
990 assert_eq!(
991 editor.selections.display_ranges(cx),
992 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
993 );
994 });
995 });
996
997 // Diagnostics are added to the first path
998 project.update(cx, |project, cx| {
999 project.disk_based_diagnostics_started(cx);
1000 project
1001 .update_diagnostic_entries(
1002 PathBuf::from("/test/consts.rs"),
1003 None,
1004 vec![
1005 DiagnosticEntry {
1006 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1007 diagnostic: Diagnostic {
1008 message: "mismatched types\nexpected `usize`, found `char`"
1009 .to_string(),
1010 severity: DiagnosticSeverity::ERROR,
1011 is_primary: true,
1012 is_disk_based: true,
1013 group_id: 0,
1014 ..Default::default()
1015 },
1016 },
1017 DiagnosticEntry {
1018 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1019 diagnostic: Diagnostic {
1020 message: "unresolved name `c`".to_string(),
1021 severity: DiagnosticSeverity::ERROR,
1022 is_primary: true,
1023 is_disk_based: true,
1024 group_id: 1,
1025 ..Default::default()
1026 },
1027 },
1028 ],
1029 cx,
1030 )
1031 .unwrap();
1032 project.disk_based_diagnostics_finished(cx);
1033 });
1034
1035 view.next_notification(&cx).await;
1036 view.update(cx, |view, cx| {
1037 assert_eq!(
1038 editor_blocks(&view.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 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
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(
1113 editor: &ViewHandle<Editor>,
1114 cx: &mut MutableAppContext,
1115 ) -> Vec<(u32, String)> {
1116 let mut presenter = cx.build_presenter(editor.id(), 0.);
1117 let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1118 cx.render(editor, |editor, cx| {
1119 let snapshot = editor.snapshot(cx);
1120 snapshot
1121 .blocks_in_range(0..snapshot.max_point().row())
1122 .filter_map(|(row, block)| {
1123 let name = match block {
1124 TransformBlock::Custom(block) => block
1125 .render(&mut BlockContext {
1126 cx,
1127 anchor_x: 0.,
1128 scroll_x: 0.,
1129 gutter_padding: 0.,
1130 gutter_width: 0.,
1131 line_height: 0.,
1132 em_width: 0.,
1133 })
1134 .name()?
1135 .to_string(),
1136 TransformBlock::ExcerptHeader {
1137 starts_new_buffer, ..
1138 } => {
1139 if *starts_new_buffer {
1140 "path header block".to_string()
1141 } else {
1142 "collapsed context".to_string()
1143 }
1144 }
1145 };
1146
1147 Some((row, name))
1148 })
1149 .collect()
1150 })
1151 }
1152}