diagnostics.rs

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