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