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