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.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 if summary.error_count == 0 && summary.warning_count == 0 {
774 Label::new("No problems".to_string(), text_style.clone()).boxed()
775 } else {
776 let icon_width = theme.tab_icon_width;
777 let icon_spacing = theme.tab_icon_spacing;
778 let summary_spacing = theme.tab_summary_spacing;
779 Flex::row()
780 .with_children([
781 Svg::new("icons/diagnostic-summary-error.svg")
782 .with_color(text_style.color)
783 .constrained()
784 .with_width(icon_width)
785 .aligned()
786 .contained()
787 .with_margin_right(icon_spacing)
788 .named("no-icon"),
789 Label::new(
790 summary.error_count.to_string(),
791 LabelStyle {
792 text: text_style.clone(),
793 highlight_text: None,
794 },
795 )
796 .aligned()
797 .boxed(),
798 Svg::new("icons/diagnostic-summary-warning.svg")
799 .with_color(text_style.color)
800 .constrained()
801 .with_width(icon_width)
802 .aligned()
803 .contained()
804 .with_margin_left(summary_spacing)
805 .with_margin_right(icon_spacing)
806 .named("warn-icon"),
807 Label::new(
808 summary.warning_count.to_string(),
809 LabelStyle {
810 text: text_style.clone(),
811 highlight_text: None,
812 },
813 )
814 .aligned()
815 .boxed(),
816 ])
817 .boxed()
818 }
819}
820
821fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
822 lhs: &DiagnosticEntry<L>,
823 rhs: &DiagnosticEntry<R>,
824 snapshot: &language::BufferSnapshot,
825) -> Ordering {
826 lhs.range
827 .start
828 .to_offset(&snapshot)
829 .cmp(&rhs.range.start.to_offset(snapshot))
830 .then_with(|| {
831 lhs.range
832 .end
833 .to_offset(&snapshot)
834 .cmp(&rhs.range.end.to_offset(snapshot))
835 })
836 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
843 use gpui::TestAppContext;
844 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
845 use serde_json::json;
846 use unindent::Unindent as _;
847 use workspace::WorkspaceParams;
848
849 #[gpui::test]
850 async fn test_diagnostics(mut cx: TestAppContext) {
851 let params = cx.update(WorkspaceParams::test);
852 let project = params.project.clone();
853 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
854
855 params
856 .fs
857 .as_fake()
858 .insert_tree(
859 "/test",
860 json!({
861 "consts.rs": "
862 const a: i32 = 'a';
863 const b: i32 = c;
864 "
865 .unindent(),
866
867 "main.rs": "
868 fn main() {
869 let x = vec![];
870 let y = vec![];
871 a(x);
872 b(y);
873 // comment 1
874 // comment 2
875 c(y);
876 d(x);
877 }
878 "
879 .unindent(),
880 }),
881 )
882 .await;
883
884 project
885 .update(&mut cx, |project, cx| {
886 project.find_or_create_local_worktree("/test", false, cx)
887 })
888 .await
889 .unwrap();
890
891 // Create some diagnostics
892 project.update(&mut cx, |project, cx| {
893 project
894 .update_diagnostic_entries(
895 PathBuf::from("/test/main.rs"),
896 None,
897 vec![
898 DiagnosticEntry {
899 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
900 diagnostic: Diagnostic {
901 message:
902 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
903 .to_string(),
904 severity: DiagnosticSeverity::INFORMATION,
905 is_primary: false,
906 is_disk_based: true,
907 group_id: 1,
908 ..Default::default()
909 },
910 },
911 DiagnosticEntry {
912 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
913 diagnostic: Diagnostic {
914 message:
915 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
916 .to_string(),
917 severity: DiagnosticSeverity::INFORMATION,
918 is_primary: false,
919 is_disk_based: true,
920 group_id: 0,
921 ..Default::default()
922 },
923 },
924 DiagnosticEntry {
925 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
926 diagnostic: Diagnostic {
927 message: "value moved here".to_string(),
928 severity: DiagnosticSeverity::INFORMATION,
929 is_primary: false,
930 is_disk_based: true,
931 group_id: 1,
932 ..Default::default()
933 },
934 },
935 DiagnosticEntry {
936 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
937 diagnostic: Diagnostic {
938 message: "value moved here".to_string(),
939 severity: DiagnosticSeverity::INFORMATION,
940 is_primary: false,
941 is_disk_based: true,
942 group_id: 0,
943 ..Default::default()
944 },
945 },
946 DiagnosticEntry {
947 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
948 diagnostic: Diagnostic {
949 message: "use of moved value\nvalue used here after move".to_string(),
950 severity: DiagnosticSeverity::ERROR,
951 is_primary: true,
952 is_disk_based: true,
953 group_id: 0,
954 ..Default::default()
955 },
956 },
957 DiagnosticEntry {
958 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
959 diagnostic: Diagnostic {
960 message: "use of moved value\nvalue used here after move".to_string(),
961 severity: DiagnosticSeverity::ERROR,
962 is_primary: true,
963 is_disk_based: true,
964 group_id: 1,
965 ..Default::default()
966 },
967 },
968 ],
969 cx,
970 )
971 .unwrap();
972 });
973
974 // Open the project diagnostics view while there are already diagnostics.
975 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
976 let view = cx.add_view(0, |cx| {
977 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
978 });
979
980 view.next_notification(&cx).await;
981 view.update(&mut cx, |view, cx| {
982 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
983
984 assert_eq!(
985 editor_blocks(&editor, cx),
986 [
987 (0, "path header block".into()),
988 (2, "diagnostic header".into()),
989 (15, "diagnostic header".into()),
990 (24, "collapsed context".into()),
991 ]
992 );
993 assert_eq!(
994 editor.text(),
995 concat!(
996 //
997 // main.rs
998 //
999 "\n", // filename
1000 "\n", // padding
1001 // diagnostic group 1
1002 "\n", // primary message
1003 "\n", // padding
1004 " let x = vec![];\n",
1005 " let y = vec![];\n",
1006 "\n", // supporting diagnostic
1007 " a(x);\n",
1008 " b(y);\n",
1009 "\n", // supporting diagnostic
1010 " // comment 1\n",
1011 " // comment 2\n",
1012 " c(y);\n",
1013 "\n", // supporting diagnostic
1014 " d(x);\n",
1015 // diagnostic group 2
1016 "\n", // primary message
1017 "\n", // padding
1018 "fn main() {\n",
1019 " let x = vec![];\n",
1020 "\n", // supporting diagnostic
1021 " let y = vec![];\n",
1022 " a(x);\n",
1023 "\n", // supporting diagnostic
1024 " b(y);\n",
1025 "\n", // context ellipsis
1026 " c(y);\n",
1027 " d(x);\n",
1028 "\n", // supporting diagnostic
1029 "}"
1030 )
1031 );
1032
1033 // Cursor is at the first diagnostic
1034 view.editor.update(cx, |editor, cx| {
1035 assert_eq!(
1036 editor.selected_display_ranges(cx),
1037 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1038 );
1039 });
1040 });
1041
1042 // Diagnostics are added for another earlier path.
1043 project.update(&mut cx, |project, cx| {
1044 project.disk_based_diagnostics_started(cx);
1045 project
1046 .update_diagnostic_entries(
1047 PathBuf::from("/test/consts.rs"),
1048 None,
1049 vec![DiagnosticEntry {
1050 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1051 diagnostic: Diagnostic {
1052 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1053 severity: DiagnosticSeverity::ERROR,
1054 is_primary: true,
1055 is_disk_based: true,
1056 group_id: 0,
1057 ..Default::default()
1058 },
1059 }],
1060 cx,
1061 )
1062 .unwrap();
1063 project.disk_based_diagnostics_finished(cx);
1064 });
1065
1066 view.next_notification(&cx).await;
1067 view.update(&mut cx, |view, cx| {
1068 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1069
1070 assert_eq!(
1071 editor_blocks(&editor, cx),
1072 [
1073 (0, "path header block".into()),
1074 (2, "diagnostic header".into()),
1075 (7, "path header block".into()),
1076 (9, "diagnostic header".into()),
1077 (22, "diagnostic header".into()),
1078 (31, "collapsed context".into()),
1079 ]
1080 );
1081 assert_eq!(
1082 editor.text(),
1083 concat!(
1084 //
1085 // consts.rs
1086 //
1087 "\n", // filename
1088 "\n", // padding
1089 // diagnostic group 1
1090 "\n", // primary message
1091 "\n", // padding
1092 "const a: i32 = 'a';\n",
1093 "\n", // supporting diagnostic
1094 "const b: i32 = c;\n",
1095 //
1096 // main.rs
1097 //
1098 "\n", // filename
1099 "\n", // padding
1100 // diagnostic group 1
1101 "\n", // primary message
1102 "\n", // padding
1103 " let x = vec![];\n",
1104 " let y = vec![];\n",
1105 "\n", // supporting diagnostic
1106 " a(x);\n",
1107 " b(y);\n",
1108 "\n", // supporting diagnostic
1109 " // comment 1\n",
1110 " // comment 2\n",
1111 " c(y);\n",
1112 "\n", // supporting diagnostic
1113 " d(x);\n",
1114 // diagnostic group 2
1115 "\n", // primary message
1116 "\n", // filename
1117 "fn main() {\n",
1118 " let x = vec![];\n",
1119 "\n", // supporting diagnostic
1120 " let y = vec![];\n",
1121 " a(x);\n",
1122 "\n", // supporting diagnostic
1123 " b(y);\n",
1124 "\n", // context ellipsis
1125 " c(y);\n",
1126 " d(x);\n",
1127 "\n", // supporting diagnostic
1128 "}"
1129 )
1130 );
1131
1132 // Cursor keeps its position.
1133 view.editor.update(cx, |editor, cx| {
1134 assert_eq!(
1135 editor.selected_display_ranges(cx),
1136 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1137 );
1138 });
1139 });
1140
1141 // Diagnostics are added to the first path
1142 project.update(&mut cx, |project, cx| {
1143 project.disk_based_diagnostics_started(cx);
1144 project
1145 .update_diagnostic_entries(
1146 PathBuf::from("/test/consts.rs"),
1147 None,
1148 vec![
1149 DiagnosticEntry {
1150 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1151 diagnostic: Diagnostic {
1152 message: "mismatched types\nexpected `usize`, found `char`"
1153 .to_string(),
1154 severity: DiagnosticSeverity::ERROR,
1155 is_primary: true,
1156 is_disk_based: true,
1157 group_id: 0,
1158 ..Default::default()
1159 },
1160 },
1161 DiagnosticEntry {
1162 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1163 diagnostic: Diagnostic {
1164 message: "unresolved name `c`".to_string(),
1165 severity: DiagnosticSeverity::ERROR,
1166 is_primary: true,
1167 is_disk_based: true,
1168 group_id: 1,
1169 ..Default::default()
1170 },
1171 },
1172 ],
1173 cx,
1174 )
1175 .unwrap();
1176 project.disk_based_diagnostics_finished(cx);
1177 });
1178
1179 view.next_notification(&cx).await;
1180 view.update(&mut cx, |view, cx| {
1181 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1182
1183 assert_eq!(
1184 editor_blocks(&editor, cx),
1185 [
1186 (0, "path header block".into()),
1187 (2, "diagnostic header".into()),
1188 (7, "diagnostic header".into()),
1189 (12, "path header block".into()),
1190 (14, "diagnostic header".into()),
1191 (27, "diagnostic header".into()),
1192 (36, "collapsed context".into()),
1193 ]
1194 );
1195 assert_eq!(
1196 editor.text(),
1197 concat!(
1198 //
1199 // consts.rs
1200 //
1201 "\n", // filename
1202 "\n", // padding
1203 // diagnostic group 1
1204 "\n", // primary message
1205 "\n", // padding
1206 "const a: i32 = 'a';\n",
1207 "\n", // supporting diagnostic
1208 "const b: i32 = c;\n",
1209 // diagnostic group 2
1210 "\n", // primary message
1211 "\n", // padding
1212 "const a: i32 = 'a';\n",
1213 "const b: i32 = c;\n",
1214 "\n", // supporting diagnostic
1215 //
1216 // main.rs
1217 //
1218 "\n", // filename
1219 "\n", // padding
1220 // diagnostic group 1
1221 "\n", // primary message
1222 "\n", // padding
1223 " let x = vec![];\n",
1224 " let y = vec![];\n",
1225 "\n", // supporting diagnostic
1226 " a(x);\n",
1227 " b(y);\n",
1228 "\n", // supporting diagnostic
1229 " // comment 1\n",
1230 " // comment 2\n",
1231 " c(y);\n",
1232 "\n", // supporting diagnostic
1233 " d(x);\n",
1234 // diagnostic group 2
1235 "\n", // primary message
1236 "\n", // filename
1237 "fn main() {\n",
1238 " let x = vec![];\n",
1239 "\n", // supporting diagnostic
1240 " let y = vec![];\n",
1241 " a(x);\n",
1242 "\n", // supporting diagnostic
1243 " b(y);\n",
1244 "\n", // context ellipsis
1245 " c(y);\n",
1246 " d(x);\n",
1247 "\n", // supporting diagnostic
1248 "}"
1249 )
1250 );
1251 });
1252 }
1253
1254 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1255 editor
1256 .blocks_in_range(0..editor.max_point().row())
1257 .filter_map(|(row, block)| {
1258 block
1259 .render(&BlockContext {
1260 cx,
1261 anchor_x: 0.,
1262 scroll_x: 0.,
1263 gutter_padding: 0.,
1264 gutter_width: 0.,
1265 line_height: 0.,
1266 em_width: 0.,
1267 })
1268 .name()
1269 .map(|s| (row, s.to_string()))
1270 })
1271 .collect()
1272 }
1273}