diagnostics.rs

  1pub mod items;
  2mod project_diagnostics_settings;
  3mod toolbar_controls;
  4
  5#[cfg(test)]
  6mod diagnostics_tests;
  7mod grouped_diagnostics;
  8
  9use anyhow::Result;
 10use collections::{BTreeSet, HashSet};
 11use editor::{
 12    diagnostic_block_renderer,
 13    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
 14    highlight_diagnostic_message,
 15    scroll::Autoscroll,
 16    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
 17};
 18use feature_flags::FeatureFlagAppExt;
 19use futures::{
 20    channel::mpsc::{self, UnboundedSender},
 21    StreamExt as _,
 22};
 23use gpui::{
 24    actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
 25    FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
 26    SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
 27    WeakView, WindowContext,
 28};
 29use language::{
 30    Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
 31};
 32use lsp::LanguageServerId;
 33use project::{DiagnosticSummary, Project, ProjectPath};
 34use project_diagnostics_settings::ProjectDiagnosticsSettings;
 35use settings::Settings;
 36use std::{
 37    any::{Any, TypeId},
 38    cmp::Ordering,
 39    mem,
 40    ops::Range,
 41};
 42use theme::ActiveTheme;
 43pub use toolbar_controls::ToolbarControls;
 44use ui::{h_flex, prelude::*, Icon, IconName, Label};
 45use util::ResultExt;
 46use workspace::{
 47    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
 48    ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
 49};
 50
 51actions!(diagnostics, [Deploy, ToggleWarnings]);
 52
 53pub fn init(cx: &mut AppContext) {
 54    ProjectDiagnosticsSettings::register(cx);
 55    cx.observe_new_views(ProjectDiagnosticsEditor::register)
 56        .detach();
 57    if !cx.has_flag::<feature_flags::GroupedDiagnostics>() {
 58        grouped_diagnostics::init(cx);
 59    }
 60}
 61
 62struct ProjectDiagnosticsEditor {
 63    project: Model<Project>,
 64    workspace: WeakView<Workspace>,
 65    focus_handle: FocusHandle,
 66    editor: View<Editor>,
 67    summary: DiagnosticSummary,
 68    excerpts: Model<MultiBuffer>,
 69    path_states: Vec<PathState>,
 70    paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
 71    include_warnings: bool,
 72    context: u32,
 73    update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
 74    _update_excerpts_task: Task<Result<()>>,
 75    _subscription: Subscription,
 76}
 77
 78struct PathState {
 79    path: ProjectPath,
 80    diagnostic_groups: Vec<DiagnosticGroupState>,
 81}
 82
 83struct DiagnosticGroupState {
 84    language_server_id: LanguageServerId,
 85    primary_diagnostic: DiagnosticEntry<language::Anchor>,
 86    primary_excerpt_ix: usize,
 87    excerpts: Vec<ExcerptId>,
 88    blocks: HashSet<BlockId>,
 89    block_count: usize,
 90}
 91
 92impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 93
 94impl Render for ProjectDiagnosticsEditor {
 95    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 96        let child = if self.path_states.is_empty() {
 97            div()
 98                .bg(cx.theme().colors().editor_background)
 99                .flex()
100                .items_center()
101                .justify_center()
102                .size_full()
103                .child(Label::new("No problems in workspace"))
104        } else {
105            div().size_full().child(self.editor.clone())
106        };
107
108        div()
109            .track_focus(&self.focus_handle)
110            .when(self.path_states.is_empty(), |el| {
111                el.key_context("EmptyPane")
112            })
113            .size_full()
114            .on_action(cx.listener(Self::toggle_warnings))
115            .child(child)
116    }
117}
118
119impl ProjectDiagnosticsEditor {
120    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
121        workspace.register_action(Self::deploy);
122    }
123
124    fn new_with_context(
125        context: u32,
126        project_handle: Model<Project>,
127        workspace: WeakView<Workspace>,
128        cx: &mut ViewContext<Self>,
129    ) -> Self {
130        let project_event_subscription =
131            cx.subscribe(&project_handle, |this, project, event, cx| match event {
132                project::Event::DiskBasedDiagnosticsStarted { .. } => {
133                    cx.notify();
134                }
135                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
136                    log::debug!("disk based diagnostics finished for server {language_server_id}");
137                    this.enqueue_update_stale_excerpts(Some(*language_server_id));
138                }
139                project::Event::DiagnosticsUpdated {
140                    language_server_id,
141                    path,
142                } => {
143                    this.paths_to_update
144                        .insert((path.clone(), *language_server_id));
145                    this.summary = project.read(cx).diagnostic_summary(false, cx);
146                    cx.emit(EditorEvent::TitleChanged);
147
148                    if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
149                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
150                    } else {
151                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
152                        this.enqueue_update_stale_excerpts(Some(*language_server_id));
153                    }
154                }
155                _ => {}
156            });
157
158        let focus_handle = cx.focus_handle();
159        cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
160            .detach();
161        cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
162            .detach();
163
164        let excerpts = cx.new_model(|cx| {
165            MultiBuffer::new(
166                project_handle.read(cx).replica_id(),
167                project_handle.read(cx).capability(),
168            )
169        });
170        let editor = cx.new_view(|cx| {
171            let mut editor =
172                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
173            editor.set_vertical_scroll_margin(5, cx);
174            editor
175        });
176        cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
177            cx.emit(event.clone());
178            match event {
179                EditorEvent::Focused => {
180                    if this.path_states.is_empty() {
181                        cx.focus(&this.focus_handle);
182                    }
183                }
184                EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
185                _ => {}
186            }
187        })
188        .detach();
189
190        let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
191
192        let project = project_handle.read(cx);
193        let mut this = Self {
194            project: project_handle.clone(),
195            context,
196            summary: project.diagnostic_summary(false, cx),
197            workspace,
198            excerpts,
199            focus_handle,
200            editor,
201            path_states: Default::default(),
202            paths_to_update: Default::default(),
203            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
204            update_paths_tx: update_excerpts_tx,
205            _update_excerpts_task: cx.spawn(move |this, mut cx| async move {
206                while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
207                    if let Some(buffer) = project_handle
208                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
209                        .await
210                        .log_err()
211                    {
212                        this.update(&mut cx, |this, cx| {
213                            this.update_excerpts(path, language_server_id, buffer, cx);
214                        })?;
215                    }
216                }
217                anyhow::Ok(())
218            }),
219            _subscription: project_event_subscription,
220        };
221        this.enqueue_update_all_excerpts(cx);
222        this
223    }
224
225    fn new(
226        project_handle: Model<Project>,
227        workspace: WeakView<Workspace>,
228        cx: &mut ViewContext<Self>,
229    ) -> Self {
230        Self::new_with_context(
231            editor::DEFAULT_MULTIBUFFER_CONTEXT,
232            project_handle,
233            workspace,
234            cx,
235        )
236    }
237
238    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
239        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
240            workspace.activate_item(&existing, cx);
241        } else {
242            let workspace_handle = cx.view().downgrade();
243            let diagnostics = cx.new_view(|cx| {
244                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
245            });
246            workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
247        }
248    }
249
250    fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
251        self.include_warnings = !self.include_warnings;
252        self.enqueue_update_all_excerpts(cx);
253        cx.notify();
254    }
255
256    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
257        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
258            self.editor.focus_handle(cx).focus(cx)
259        }
260    }
261
262    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
263        if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
264            self.enqueue_update_stale_excerpts(None);
265        }
266    }
267
268    /// Enqueue an update of all excerpts. Updates all paths that either
269    /// currently have diagnostics or are currently present in this view.
270    fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
271        self.project.update(cx, |project, cx| {
272            let mut paths = project
273                .diagnostic_summaries(false, cx)
274                .map(|(path, _, _)| path)
275                .collect::<BTreeSet<_>>();
276            paths.extend(self.path_states.iter().map(|state| state.path.clone()));
277            for path in paths {
278                self.update_paths_tx.unbounded_send((path, None)).unwrap();
279            }
280        });
281    }
282
283    /// Enqueue an update of the excerpts for any path whose diagnostics are known
284    /// to have changed. If a language server id is passed, then only the excerpts for
285    /// that language server's diagnostics will be updated. Otherwise, all stale excerpts
286    /// will be refreshed.
287    fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
288        for (path, server_id) in &self.paths_to_update {
289            if language_server_id.map_or(true, |id| id == *server_id) {
290                self.update_paths_tx
291                    .unbounded_send((path.clone(), Some(*server_id)))
292                    .unwrap();
293            }
294        }
295    }
296
297    fn update_excerpts(
298        &mut self,
299        path_to_update: ProjectPath,
300        server_to_update: Option<LanguageServerId>,
301        buffer: Model<Buffer>,
302        cx: &mut ViewContext<Self>,
303    ) {
304        self.paths_to_update.retain(|(path, server_id)| {
305            *path != path_to_update
306                || server_to_update.map_or(false, |to_update| *server_id != to_update)
307        });
308
309        let was_empty = self.path_states.is_empty();
310        let snapshot = buffer.read(cx).snapshot();
311        let path_ix = match self
312            .path_states
313            .binary_search_by_key(&&path_to_update, |e| &e.path)
314        {
315            Ok(ix) => ix,
316            Err(ix) => {
317                self.path_states.insert(
318                    ix,
319                    PathState {
320                        path: path_to_update.clone(),
321                        diagnostic_groups: Default::default(),
322                    },
323                );
324                ix
325            }
326        };
327
328        let mut prev_excerpt_id = if path_ix > 0 {
329            let prev_path_last_group = &self.path_states[path_ix - 1]
330                .diagnostic_groups
331                .last()
332                .unwrap();
333            *prev_path_last_group.excerpts.last().unwrap()
334        } else {
335            ExcerptId::min()
336        };
337
338        let path_state = &mut self.path_states[path_ix];
339        let mut new_group_ixs = Vec::new();
340        let mut blocks_to_add = Vec::new();
341        let mut blocks_to_remove = HashSet::default();
342        let mut first_excerpt_id = None;
343        let max_severity = if self.include_warnings {
344            DiagnosticSeverity::WARNING
345        } else {
346            DiagnosticSeverity::ERROR
347        };
348        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
349            let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
350                .into_iter()
351                .enumerate()
352                .peekable();
353            let mut new_groups = snapshot
354                .diagnostic_groups(server_to_update)
355                .into_iter()
356                .filter(|(_, group)| {
357                    group.entries[group.primary_ix].diagnostic.severity <= max_severity
358                })
359                .peekable();
360            loop {
361                let mut to_insert = None;
362                let mut to_remove = None;
363                let mut to_keep = None;
364                match (old_groups.peek(), new_groups.peek()) {
365                    (None, None) => break,
366                    (None, Some(_)) => to_insert = new_groups.next(),
367                    (Some((_, old_group)), None) => {
368                        if server_to_update.map_or(true, |id| id == old_group.language_server_id) {
369                            to_remove = old_groups.next();
370                        } else {
371                            to_keep = old_groups.next();
372                        }
373                    }
374                    (Some((_, old_group)), Some((new_language_server_id, new_group))) => {
375                        let old_primary = &old_group.primary_diagnostic;
376                        let new_primary = &new_group.entries[new_group.primary_ix];
377                        match compare_diagnostics(old_primary, new_primary, &snapshot)
378                            .then_with(|| old_group.language_server_id.cmp(new_language_server_id))
379                        {
380                            Ordering::Less => {
381                                if server_to_update
382                                    .map_or(true, |id| id == old_group.language_server_id)
383                                {
384                                    to_remove = old_groups.next();
385                                } else {
386                                    to_keep = old_groups.next();
387                                }
388                            }
389                            Ordering::Equal => {
390                                to_keep = old_groups.next();
391                                new_groups.next();
392                            }
393                            Ordering::Greater => to_insert = new_groups.next(),
394                        }
395                    }
396                }
397
398                if let Some((language_server_id, group)) = to_insert {
399                    let mut group_state = DiagnosticGroupState {
400                        language_server_id,
401                        primary_diagnostic: group.entries[group.primary_ix].clone(),
402                        primary_excerpt_ix: 0,
403                        excerpts: Default::default(),
404                        blocks: Default::default(),
405                        block_count: 0,
406                    };
407                    let mut pending_range: Option<(Range<Point>, usize)> = None;
408                    let mut is_first_excerpt_for_group = true;
409                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
410                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
411                        if let Some((range, start_ix)) = &mut pending_range {
412                            if let Some(entry) = resolved_entry.as_ref() {
413                                if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
414                                    range.end = range.end.max(entry.range.end);
415                                    continue;
416                                }
417                            }
418
419                            let excerpt_start =
420                                Point::new(range.start.row.saturating_sub(self.context), 0);
421                            let excerpt_end = snapshot.clip_point(
422                                Point::new(range.end.row + self.context, u32::MAX),
423                                Bias::Left,
424                            );
425
426                            let excerpt_id = excerpts
427                                .insert_excerpts_after(
428                                    prev_excerpt_id,
429                                    buffer.clone(),
430                                    [ExcerptRange {
431                                        context: excerpt_start..excerpt_end,
432                                        primary: Some(range.clone()),
433                                    }],
434                                    cx,
435                                )
436                                .pop()
437                                .unwrap();
438
439                            prev_excerpt_id = excerpt_id;
440                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);
441                            group_state.excerpts.push(excerpt_id);
442                            let header_position = (excerpt_id, language::Anchor::MIN);
443
444                            if is_first_excerpt_for_group {
445                                is_first_excerpt_for_group = false;
446                                let mut primary =
447                                    group.entries[group.primary_ix].diagnostic.clone();
448                                primary.message =
449                                    primary.message.split('\n').next().unwrap().to_string();
450                                group_state.block_count += 1;
451                                blocks_to_add.push(BlockProperties {
452                                    position: header_position,
453                                    height: 2,
454                                    style: BlockStyle::Sticky,
455                                    render: diagnostic_header_renderer(primary),
456                                    disposition: BlockDisposition::Above,
457                                });
458                            }
459
460                            for entry in &group.entries[*start_ix..ix] {
461                                let mut diagnostic = entry.diagnostic.clone();
462                                if diagnostic.is_primary {
463                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
464                                    diagnostic.message =
465                                        entry.diagnostic.message.split('\n').skip(1).collect();
466                                }
467
468                                if !diagnostic.message.is_empty() {
469                                    group_state.block_count += 1;
470                                    blocks_to_add.push(BlockProperties {
471                                        position: (excerpt_id, entry.range.start),
472                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
473                                        style: BlockStyle::Fixed,
474                                        render: diagnostic_block_renderer(
475                                            diagnostic, None, true, true,
476                                        ),
477                                        disposition: BlockDisposition::Below,
478                                    });
479                                }
480                            }
481
482                            pending_range.take();
483                        }
484
485                        if let Some(entry) = resolved_entry {
486                            pending_range = Some((entry.range.clone(), ix));
487                        }
488                    }
489
490                    new_group_ixs.push(path_state.diagnostic_groups.len());
491                    path_state.diagnostic_groups.push(group_state);
492                } else if let Some((_, group_state)) = to_remove {
493                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx);
494                    blocks_to_remove.extend(group_state.blocks.iter().copied());
495                } else if let Some((_, group_state)) = to_keep {
496                    prev_excerpt_id = *group_state.excerpts.last().unwrap();
497                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);
498                    path_state.diagnostic_groups.push(group_state);
499                }
500            }
501
502            excerpts.snapshot(cx)
503        });
504
505        self.editor.update(cx, |editor, cx| {
506            editor.remove_blocks(blocks_to_remove, None, cx);
507            let block_ids = editor.insert_blocks(
508                blocks_to_add.into_iter().flat_map(|block| {
509                    let (excerpt_id, text_anchor) = block.position;
510                    Some(BlockProperties {
511                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
512                        height: block.height,
513                        style: block.style,
514                        render: block.render,
515                        disposition: block.disposition,
516                    })
517                }),
518                Some(Autoscroll::fit()),
519                cx,
520            );
521
522            let mut block_ids = block_ids.into_iter();
523            for ix in new_group_ixs {
524                let group_state = &mut path_state.diagnostic_groups[ix];
525                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
526            }
527        });
528
529        if path_state.diagnostic_groups.is_empty() {
530            self.path_states.remove(path_ix);
531        }
532
533        self.editor.update(cx, |editor, cx| {
534            let groups;
535            let mut selections;
536            let new_excerpt_ids_by_selection_id;
537            if was_empty {
538                groups = self.path_states.first()?.diagnostic_groups.as_slice();
539                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
540                selections = vec![Selection {
541                    id: 0,
542                    start: 0,
543                    end: 0,
544                    reversed: false,
545                    goal: SelectionGoal::None,
546                }];
547            } else {
548                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
549                new_excerpt_ids_by_selection_id =
550                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
551                selections = editor.selections.all::<usize>(cx);
552            }
553
554            // If any selection has lost its position, move it to start of the next primary diagnostic.
555            let snapshot = editor.snapshot(cx);
556            for selection in &mut selections {
557                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
558                    let group_ix = match groups.binary_search_by(|probe| {
559                        probe
560                            .excerpts
561                            .last()
562                            .unwrap()
563                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
564                    }) {
565                        Ok(ix) | Err(ix) => ix,
566                    };
567                    if let Some(group) = groups.get(group_ix) {
568                        if let Some(offset) = excerpts_snapshot
569                            .anchor_in_excerpt(
570                                group.excerpts[group.primary_excerpt_ix],
571                                group.primary_diagnostic.range.start,
572                            )
573                            .map(|anchor| anchor.to_offset(&excerpts_snapshot))
574                        {
575                            selection.start = offset;
576                            selection.end = offset;
577                        }
578                    }
579                }
580            }
581            editor.change_selections(None, cx, |s| {
582                s.select(selections);
583            });
584            Some(())
585        });
586
587        if self.path_states.is_empty() {
588            if self.editor.focus_handle(cx).is_focused(cx) {
589                cx.focus(&self.focus_handle);
590            }
591        } else if self.focus_handle.is_focused(cx) {
592            let focus_handle = self.editor.focus_handle(cx);
593            cx.focus(&focus_handle);
594        }
595
596        #[cfg(test)]
597        self.check_invariants(cx);
598
599        cx.notify();
600    }
601
602    #[cfg(test)]
603    fn check_invariants(&self, cx: &mut ViewContext<Self>) {
604        let mut excerpts = Vec::new();
605        for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
606            if let Some(file) = buffer.file() {
607                excerpts.push((id, file.path().clone()));
608            }
609        }
610
611        let mut prev_path = None;
612        for (_, path) in &excerpts {
613            if let Some(prev_path) = prev_path {
614                if path < prev_path {
615                    panic!("excerpts are not sorted by path {:?}", excerpts);
616                }
617            }
618            prev_path = Some(path);
619        }
620    }
621}
622
623impl FocusableView for ProjectDiagnosticsEditor {
624    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
625        self.focus_handle.clone()
626    }
627}
628
629impl Item for ProjectDiagnosticsEditor {
630    type Event = EditorEvent;
631
632    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
633        Editor::to_item_events(event, f)
634    }
635
636    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
637        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
638    }
639
640    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
641        self.editor
642            .update(cx, |editor, cx| editor.navigate(data, cx))
643    }
644
645    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
646        Some("Project Diagnostics".into())
647    }
648
649    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
650        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
651            Label::new("No problems")
652                .color(if params.selected {
653                    Color::Default
654                } else {
655                    Color::Muted
656                })
657                .into_any_element()
658        } else {
659            h_flex()
660                .gap_1()
661                .when(self.summary.error_count > 0, |then| {
662                    then.child(
663                        h_flex()
664                            .gap_1()
665                            .child(Icon::new(IconName::XCircle).color(Color::Error))
666                            .child(Label::new(self.summary.error_count.to_string()).color(
667                                if params.selected {
668                                    Color::Default
669                                } else {
670                                    Color::Muted
671                                },
672                            )),
673                    )
674                })
675                .when(self.summary.warning_count > 0, |then| {
676                    then.child(
677                        h_flex()
678                            .gap_1()
679                            .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
680                            .child(Label::new(self.summary.warning_count.to_string()).color(
681                                if params.selected {
682                                    Color::Default
683                                } else {
684                                    Color::Muted
685                                },
686                            )),
687                    )
688                })
689                .into_any_element()
690        }
691    }
692
693    fn telemetry_event_text(&self) -> Option<&'static str> {
694        Some("project diagnostics")
695    }
696
697    fn for_each_project_item(
698        &self,
699        cx: &AppContext,
700        f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
701    ) {
702        self.editor.for_each_project_item(cx, f)
703    }
704
705    fn is_singleton(&self, _: &AppContext) -> bool {
706        false
707    }
708
709    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
710        self.editor.update(cx, |editor, _| {
711            editor.set_nav_history(Some(nav_history));
712        });
713    }
714
715    fn clone_on_split(
716        &self,
717        _workspace_id: Option<workspace::WorkspaceId>,
718        cx: &mut ViewContext<Self>,
719    ) -> Option<View<Self>>
720    where
721        Self: Sized,
722    {
723        Some(cx.new_view(|cx| {
724            ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
725        }))
726    }
727
728    fn is_dirty(&self, cx: &AppContext) -> bool {
729        self.excerpts.read(cx).is_dirty(cx)
730    }
731
732    fn has_conflict(&self, cx: &AppContext) -> bool {
733        self.excerpts.read(cx).has_conflict(cx)
734    }
735
736    fn can_save(&self, _: &AppContext) -> bool {
737        true
738    }
739
740    fn save(
741        &mut self,
742        format: bool,
743        project: Model<Project>,
744        cx: &mut ViewContext<Self>,
745    ) -> Task<Result<()>> {
746        self.editor.save(format, project, cx)
747    }
748
749    fn save_as(
750        &mut self,
751        _: Model<Project>,
752        _: ProjectPath,
753        _: &mut ViewContext<Self>,
754    ) -> Task<Result<()>> {
755        unreachable!()
756    }
757
758    fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
759        self.editor.reload(project, cx)
760    }
761
762    fn act_as_type<'a>(
763        &'a self,
764        type_id: TypeId,
765        self_handle: &'a View<Self>,
766        _: &'a AppContext,
767    ) -> Option<AnyView> {
768        if type_id == TypeId::of::<Self>() {
769            Some(self_handle.to_any())
770        } else if type_id == TypeId::of::<Editor>() {
771            Some(self.editor.to_any())
772        } else {
773            None
774        }
775    }
776
777    fn breadcrumb_location(&self) -> ToolbarItemLocation {
778        ToolbarItemLocation::PrimaryLeft
779    }
780
781    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
782        self.editor.breadcrumbs(theme, cx)
783    }
784
785    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
786        self.editor
787            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
788    }
789
790    fn serialized_item_kind() -> Option<&'static str> {
791        Some("diagnostics")
792    }
793
794    fn deserialize(
795        project: Model<Project>,
796        workspace: WeakView<Workspace>,
797        _workspace_id: workspace::WorkspaceId,
798        _item_id: workspace::ItemId,
799        cx: &mut ViewContext<Pane>,
800    ) -> Task<Result<View<Self>>> {
801        Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
802    }
803}
804
805const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
806
807fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
808    let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
809    let message: SharedString = message;
810    Box::new(move |cx| {
811        let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
812        h_flex()
813            .id(DIAGNOSTIC_HEADER)
814            .py_2()
815            .pl_10()
816            .pr_5()
817            .w_full()
818            .justify_between()
819            .gap_2()
820            .child(
821                h_flex()
822                    .gap_3()
823                    .map(|stack| {
824                        stack.child(
825                            svg()
826                                .size(cx.text_style().font_size)
827                                .flex_none()
828                                .map(|icon| {
829                                    if diagnostic.severity == DiagnosticSeverity::ERROR {
830                                        icon.path(IconName::XCircle.path())
831                                            .text_color(Color::Error.color(cx))
832                                    } else {
833                                        icon.path(IconName::ExclamationTriangle.path())
834                                            .text_color(Color::Warning.color(cx))
835                                    }
836                                }),
837                        )
838                    })
839                    .child(
840                        h_flex()
841                            .gap_1()
842                            .child(
843                                StyledText::new(message.clone()).with_highlights(
844                                    &cx.text_style(),
845                                    code_ranges
846                                        .iter()
847                                        .map(|range| (range.clone(), highlight_style)),
848                                ),
849                            )
850                            .when_some(diagnostic.code.as_ref(), |stack, code| {
851                                stack.child(
852                                    div()
853                                        .child(SharedString::from(format!("({code})")))
854                                        .text_color(cx.theme().colors().text_muted),
855                                )
856                            }),
857                    ),
858            )
859            .child(
860                h_flex()
861                    .gap_1()
862                    .when_some(diagnostic.source.as_ref(), |stack, source| {
863                        stack.child(
864                            div()
865                                .child(SharedString::from(source.clone()))
866                                .text_color(cx.theme().colors().text_muted),
867                        )
868                    }),
869            )
870            .into_any_element()
871    })
872}
873
874fn compare_diagnostics(
875    old: &DiagnosticEntry<language::Anchor>,
876    new: &DiagnosticEntry<language::Anchor>,
877    snapshot: &language::BufferSnapshot,
878) -> Ordering {
879    use language::ToOffset;
880
881    // The diagnostics may point to a previously open Buffer for this file.
882    if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
883        return Ordering::Greater;
884    }
885
886    old.range
887        .start
888        .to_offset(snapshot)
889        .cmp(&new.range.start.to_offset(snapshot))
890        .then_with(|| {
891            old.range
892                .end
893                .to_offset(snapshot)
894                .cmp(&new.range.end.to_offset(snapshot))
895        })
896        .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
897}