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