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::WorkspaceParams;
711
712 #[gpui::test]
713 async fn test_diagnostics(cx: &mut TestAppContext) {
714 let params = cx.update(WorkspaceParams::test);
715 let project = params.project.clone();
716 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
717
718 params
719 .fs
720 .as_fake()
721 .insert_tree(
722 "/test",
723 json!({
724 "consts.rs": "
725 const a: i32 = 'a';
726 const b: i32 = c;
727 "
728 .unindent(),
729
730 "main.rs": "
731 fn main() {
732 let x = vec![];
733 let y = vec![];
734 a(x);
735 b(y);
736 // comment 1
737 // comment 2
738 c(y);
739 d(x);
740 }
741 "
742 .unindent(),
743 }),
744 )
745 .await;
746
747 project
748 .update(cx, |project, cx| {
749 project.find_or_create_local_worktree("/test", true, cx)
750 })
751 .await
752 .unwrap();
753
754 // Create some diagnostics
755 project.update(cx, |project, cx| {
756 project
757 .update_diagnostic_entries(
758 PathBuf::from("/test/main.rs"),
759 None,
760 vec![
761 DiagnosticEntry {
762 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
763 diagnostic: Diagnostic {
764 message:
765 "move occurs because `x` 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: 1,
771 ..Default::default()
772 },
773 },
774 DiagnosticEntry {
775 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
776 diagnostic: Diagnostic {
777 message:
778 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
779 .to_string(),
780 severity: DiagnosticSeverity::INFORMATION,
781 is_primary: false,
782 is_disk_based: true,
783 group_id: 0,
784 ..Default::default()
785 },
786 },
787 DiagnosticEntry {
788 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
789 diagnostic: Diagnostic {
790 message: "value moved here".to_string(),
791 severity: DiagnosticSeverity::INFORMATION,
792 is_primary: false,
793 is_disk_based: true,
794 group_id: 1,
795 ..Default::default()
796 },
797 },
798 DiagnosticEntry {
799 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
800 diagnostic: Diagnostic {
801 message: "value moved here".to_string(),
802 severity: DiagnosticSeverity::INFORMATION,
803 is_primary: false,
804 is_disk_based: true,
805 group_id: 0,
806 ..Default::default()
807 },
808 },
809 DiagnosticEntry {
810 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
811 diagnostic: Diagnostic {
812 message: "use of moved value\nvalue used here after move".to_string(),
813 severity: DiagnosticSeverity::ERROR,
814 is_primary: true,
815 is_disk_based: true,
816 group_id: 0,
817 ..Default::default()
818 },
819 },
820 DiagnosticEntry {
821 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
822 diagnostic: Diagnostic {
823 message: "use of moved value\nvalue used here after move".to_string(),
824 severity: DiagnosticSeverity::ERROR,
825 is_primary: true,
826 is_disk_based: true,
827 group_id: 1,
828 ..Default::default()
829 },
830 },
831 ],
832 cx,
833 )
834 .unwrap();
835 });
836
837 // Open the project diagnostics view while there are already diagnostics.
838 let view = cx.add_view(0, |cx| {
839 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
840 });
841
842 view.next_notification(&cx).await;
843 view.update(cx, |view, cx| {
844 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
845
846 assert_eq!(
847 editor_blocks(&editor, cx),
848 [
849 (0, "path header block".into()),
850 (2, "diagnostic header".into()),
851 (15, "collapsed context".into()),
852 (16, "diagnostic header".into()),
853 (25, "collapsed context".into()),
854 ]
855 );
856 assert_eq!(
857 editor.text(),
858 concat!(
859 //
860 // main.rs
861 //
862 "\n", // filename
863 "\n", // padding
864 // diagnostic group 1
865 "\n", // primary message
866 "\n", // padding
867 " let x = vec![];\n",
868 " let y = vec![];\n",
869 "\n", // supporting diagnostic
870 " a(x);\n",
871 " b(y);\n",
872 "\n", // supporting diagnostic
873 " // comment 1\n",
874 " // comment 2\n",
875 " c(y);\n",
876 "\n", // supporting diagnostic
877 " d(x);\n",
878 "\n", // context ellipsis
879 // diagnostic group 2
880 "\n", // primary message
881 "\n", // padding
882 "fn main() {\n",
883 " let x = vec![];\n",
884 "\n", // supporting diagnostic
885 " let y = vec![];\n",
886 " a(x);\n",
887 "\n", // supporting diagnostic
888 " b(y);\n",
889 "\n", // context ellipsis
890 " c(y);\n",
891 " d(x);\n",
892 "\n", // supporting diagnostic
893 "}"
894 )
895 );
896
897 // Cursor is at the first diagnostic
898 view.editor.update(cx, |editor, cx| {
899 assert_eq!(
900 editor.selections.display_ranges(cx),
901 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
902 );
903 });
904 });
905
906 // Diagnostics are added for another earlier path.
907 project.update(cx, |project, cx| {
908 project.disk_based_diagnostics_started(cx);
909 project
910 .update_diagnostic_entries(
911 PathBuf::from("/test/consts.rs"),
912 None,
913 vec![DiagnosticEntry {
914 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
915 diagnostic: Diagnostic {
916 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
917 severity: DiagnosticSeverity::ERROR,
918 is_primary: true,
919 is_disk_based: true,
920 group_id: 0,
921 ..Default::default()
922 },
923 }],
924 cx,
925 )
926 .unwrap();
927 project.disk_based_diagnostics_finished(cx);
928 });
929
930 view.next_notification(&cx).await;
931 view.update(cx, |view, cx| {
932 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
933
934 assert_eq!(
935 editor_blocks(&editor, cx),
936 [
937 (0, "path header block".into()),
938 (2, "diagnostic header".into()),
939 (7, "path header block".into()),
940 (9, "diagnostic header".into()),
941 (22, "collapsed context".into()),
942 (23, "diagnostic header".into()),
943 (32, "collapsed context".into()),
944 ]
945 );
946 assert_eq!(
947 editor.text(),
948 concat!(
949 //
950 // consts.rs
951 //
952 "\n", // filename
953 "\n", // padding
954 // diagnostic group 1
955 "\n", // primary message
956 "\n", // padding
957 "const a: i32 = 'a';\n",
958 "\n", // supporting diagnostic
959 "const b: i32 = c;\n",
960 //
961 // main.rs
962 //
963 "\n", // filename
964 "\n", // padding
965 // diagnostic group 1
966 "\n", // primary message
967 "\n", // padding
968 " let x = vec![];\n",
969 " let y = vec![];\n",
970 "\n", // supporting diagnostic
971 " a(x);\n",
972 " b(y);\n",
973 "\n", // supporting diagnostic
974 " // comment 1\n",
975 " // comment 2\n",
976 " c(y);\n",
977 "\n", // supporting diagnostic
978 " d(x);\n",
979 "\n", // collapsed context
980 // diagnostic group 2
981 "\n", // primary message
982 "\n", // filename
983 "fn main() {\n",
984 " let x = vec![];\n",
985 "\n", // supporting diagnostic
986 " let y = vec![];\n",
987 " a(x);\n",
988 "\n", // supporting diagnostic
989 " b(y);\n",
990 "\n", // context ellipsis
991 " c(y);\n",
992 " d(x);\n",
993 "\n", // supporting diagnostic
994 "}"
995 )
996 );
997
998 // Cursor keeps its position.
999 view.editor.update(cx, |editor, cx| {
1000 assert_eq!(
1001 editor.selections.display_ranges(cx),
1002 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1003 );
1004 });
1005 });
1006
1007 // Diagnostics are added to the first path
1008 project.update(cx, |project, cx| {
1009 project.disk_based_diagnostics_started(cx);
1010 project
1011 .update_diagnostic_entries(
1012 PathBuf::from("/test/consts.rs"),
1013 None,
1014 vec![
1015 DiagnosticEntry {
1016 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1017 diagnostic: Diagnostic {
1018 message: "mismatched types\nexpected `usize`, found `char`"
1019 .to_string(),
1020 severity: DiagnosticSeverity::ERROR,
1021 is_primary: true,
1022 is_disk_based: true,
1023 group_id: 0,
1024 ..Default::default()
1025 },
1026 },
1027 DiagnosticEntry {
1028 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1029 diagnostic: Diagnostic {
1030 message: "unresolved name `c`".to_string(),
1031 severity: DiagnosticSeverity::ERROR,
1032 is_primary: true,
1033 is_disk_based: true,
1034 group_id: 1,
1035 ..Default::default()
1036 },
1037 },
1038 ],
1039 cx,
1040 )
1041 .unwrap();
1042 project.disk_based_diagnostics_finished(cx);
1043 });
1044
1045 view.next_notification(&cx).await;
1046 view.update(cx, |view, cx| {
1047 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1048
1049 assert_eq!(
1050 editor_blocks(&editor, cx),
1051 [
1052 (0, "path header block".into()),
1053 (2, "diagnostic header".into()),
1054 (7, "collapsed context".into()),
1055 (8, "diagnostic header".into()),
1056 (13, "path header block".into()),
1057 (15, "diagnostic header".into()),
1058 (28, "collapsed context".into()),
1059 (29, "diagnostic header".into()),
1060 (38, "collapsed context".into()),
1061 ]
1062 );
1063 assert_eq!(
1064 editor.text(),
1065 concat!(
1066 //
1067 // consts.rs
1068 //
1069 "\n", // filename
1070 "\n", // padding
1071 // diagnostic group 1
1072 "\n", // primary message
1073 "\n", // padding
1074 "const a: i32 = 'a';\n",
1075 "\n", // supporting diagnostic
1076 "const b: i32 = c;\n",
1077 "\n", // context ellipsis
1078 // diagnostic group 2
1079 "\n", // primary message
1080 "\n", // padding
1081 "const a: i32 = 'a';\n",
1082 "const b: i32 = c;\n",
1083 "\n", // supporting diagnostic
1084 //
1085 // main.rs
1086 //
1087 "\n", // filename
1088 "\n", // padding
1089 // diagnostic group 1
1090 "\n", // primary message
1091 "\n", // padding
1092 " let x = vec![];\n",
1093 " let y = vec![];\n",
1094 "\n", // supporting diagnostic
1095 " a(x);\n",
1096 " b(y);\n",
1097 "\n", // supporting diagnostic
1098 " // comment 1\n",
1099 " // comment 2\n",
1100 " c(y);\n",
1101 "\n", // supporting diagnostic
1102 " d(x);\n",
1103 "\n", // context ellipsis
1104 // diagnostic group 2
1105 "\n", // primary message
1106 "\n", // filename
1107 "fn main() {\n",
1108 " let x = vec![];\n",
1109 "\n", // supporting diagnostic
1110 " let y = vec![];\n",
1111 " a(x);\n",
1112 "\n", // supporting diagnostic
1113 " b(y);\n",
1114 "\n", // context ellipsis
1115 " c(y);\n",
1116 " d(x);\n",
1117 "\n", // supporting diagnostic
1118 "}"
1119 )
1120 );
1121 });
1122 }
1123
1124 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1125 editor
1126 .blocks_in_range(0..editor.max_point().row())
1127 .filter_map(|(row, block)| {
1128 let name = match block {
1129 TransformBlock::Custom(block) => block
1130 .render(&BlockContext {
1131 cx,
1132 anchor_x: 0.,
1133 scroll_x: 0.,
1134 gutter_padding: 0.,
1135 gutter_width: 0.,
1136 line_height: 0.,
1137 em_width: 0.,
1138 })
1139 .name()?
1140 .to_string(),
1141 TransformBlock::ExcerptHeader {
1142 starts_new_buffer, ..
1143 } => {
1144 if *starts_new_buffer {
1145 "path header block".to_string()
1146 } else {
1147 "collapsed context".to_string()
1148 }
1149 }
1150 };
1151
1152 Some((row, name))
1153 })
1154 .collect()
1155 }
1156}