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