grouped_diagnostics.rs

   1use anyhow::Result;
   2use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
   3use editor::{
   4    diagnostic_block_renderer,
   5    display_map::{
   6        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
   7        RenderBlock,
   8    },
   9    scroll::Autoscroll,
  10    Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint,
  11};
  12use futures::{
  13    channel::mpsc::{self, UnboundedSender},
  14    StreamExt as _,
  15};
  16use gpui::{
  17    actions, div, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
  18    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString,
  19    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
  20};
  21use language::{
  22    Buffer, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, ToOffset,
  23    ToPoint as _,
  24};
  25use lsp::LanguageServerId;
  26use multi_buffer::{build_excerpt_ranges, ExpandExcerptDirection, MultiBufferRow};
  27use project::{DiagnosticSummary, Project, ProjectPath};
  28use settings::Settings;
  29use std::{
  30    any::{Any, TypeId},
  31    cmp::Ordering,
  32    ops::Range,
  33    sync::{
  34        atomic::{self, AtomicBool},
  35        Arc,
  36    },
  37};
  38use theme::ActiveTheme;
  39use ui::{h_flex, prelude::*, Icon, IconName, Label};
  40use util::{debug_panic, ResultExt};
  41use workspace::{
  42    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
  43    ItemNavHistory, ToolbarItemLocation, Workspace,
  44};
  45
  46use crate::project_diagnostics_settings::ProjectDiagnosticsSettings;
  47actions!(grouped_diagnostics, [Deploy, ToggleWarnings]);
  48
  49pub fn init(cx: &mut AppContext) {
  50    cx.observe_new_views(GroupedDiagnosticsEditor::register)
  51        .detach();
  52}
  53
  54pub struct GroupedDiagnosticsEditor {
  55    pub project: Model<Project>,
  56    workspace: WeakView<Workspace>,
  57    focus_handle: FocusHandle,
  58    editor: View<Editor>,
  59    summary: DiagnosticSummary,
  60    excerpts: Model<MultiBuffer>,
  61    path_states: Vec<PathState>,
  62    pub paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
  63    pub include_warnings: bool,
  64    context: u32,
  65    pub update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
  66    _update_excerpts_task: Task<Result<()>>,
  67    _subscription: Subscription,
  68}
  69
  70struct PathState {
  71    path: ProjectPath,
  72    first_excerpt_id: Option<ExcerptId>,
  73    last_excerpt_id: Option<ExcerptId>,
  74    diagnostics: Vec<(DiagnosticData, CustomBlockId)>,
  75}
  76
  77#[derive(Debug, Clone)]
  78struct DiagnosticData {
  79    language_server_id: LanguageServerId,
  80    is_primary: bool,
  81    entry: DiagnosticEntry<language::Anchor>,
  82}
  83
  84impl DiagnosticData {
  85    fn diagnostic_entries_equal(&self, other: &DiagnosticData) -> bool {
  86        self.language_server_id == other.language_server_id
  87            && self.is_primary == other.is_primary
  88            && self.entry.range == other.entry.range
  89            && equal_without_group_ids(&self.entry.diagnostic, &other.entry.diagnostic)
  90    }
  91}
  92
  93// `group_id` can differ between LSP server diagnostics output,
  94// hence ignore it when checking diagnostics for updates.
  95fn equal_without_group_ids(a: &language::Diagnostic, b: &language::Diagnostic) -> bool {
  96    a.source == b.source
  97        && a.code == b.code
  98        && a.severity == b.severity
  99        && a.message == b.message
 100        && a.is_primary == b.is_primary
 101        && a.is_disk_based == b.is_disk_based
 102        && a.is_unnecessary == b.is_unnecessary
 103}
 104
 105impl EventEmitter<EditorEvent> for GroupedDiagnosticsEditor {}
 106
 107impl Render for GroupedDiagnosticsEditor {
 108    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 109        let child = if self.path_states.is_empty() {
 110            div()
 111                .bg(cx.theme().colors().editor_background)
 112                .flex()
 113                .items_center()
 114                .justify_center()
 115                .size_full()
 116                .child(Label::new("No problems in workspace"))
 117        } else {
 118            div().size_full().child(self.editor.clone())
 119        };
 120
 121        div()
 122            .track_focus(&self.focus_handle)
 123            .when(self.path_states.is_empty(), |el| {
 124                el.key_context("EmptyPane")
 125            })
 126            .size_full()
 127            .on_action(cx.listener(Self::toggle_warnings))
 128            .child(child)
 129    }
 130}
 131
 132impl GroupedDiagnosticsEditor {
 133    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 134        workspace.register_action(Self::deploy);
 135    }
 136
 137    fn new_with_context(
 138        context: u32,
 139        project_handle: Model<Project>,
 140        workspace: WeakView<Workspace>,
 141        cx: &mut ViewContext<Self>,
 142    ) -> Self {
 143        let project_event_subscription =
 144            cx.subscribe(&project_handle, |this, project, event, cx| match event {
 145                project::Event::DiskBasedDiagnosticsStarted { .. } => {
 146                    cx.notify();
 147                }
 148                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 149                    log::debug!("disk based diagnostics finished for server {language_server_id}");
 150                    this.enqueue_update_stale_excerpts(Some(*language_server_id));
 151                }
 152                project::Event::DiagnosticsUpdated {
 153                    language_server_id,
 154                    path,
 155                } => {
 156                    this.paths_to_update
 157                        .insert((path.clone(), *language_server_id));
 158                    this.summary = project.read(cx).diagnostic_summary(false, cx);
 159                    cx.emit(EditorEvent::TitleChanged);
 160
 161                    if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
 162                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
 163                    } else {
 164                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
 165                        this.enqueue_update_stale_excerpts(Some(*language_server_id));
 166                    }
 167                }
 168                _ => {}
 169            });
 170
 171        let focus_handle = cx.focus_handle();
 172        cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
 173            .detach();
 174        cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
 175            .detach();
 176
 177        let excerpts = cx.new_model(|cx| {
 178            MultiBuffer::new(
 179                project_handle.read(cx).replica_id(),
 180                project_handle.read(cx).capability(),
 181            )
 182        });
 183        let editor = cx.new_view(|cx| {
 184            let mut editor =
 185                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
 186            editor.set_vertical_scroll_margin(5, cx);
 187            editor
 188        });
 189        cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
 190            cx.emit(event.clone());
 191            match event {
 192                EditorEvent::Focused => {
 193                    if this.path_states.is_empty() {
 194                        cx.focus(&this.focus_handle);
 195                    }
 196                }
 197                EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
 198                _ => {}
 199            }
 200        })
 201        .detach();
 202
 203        let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
 204
 205        let project = project_handle.read(cx);
 206        let mut this = Self {
 207            project: project_handle.clone(),
 208            context,
 209            summary: project.diagnostic_summary(false, cx),
 210            workspace,
 211            excerpts,
 212            focus_handle,
 213            editor,
 214            path_states: Vec::new(),
 215            paths_to_update: BTreeSet::new(),
 216            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
 217            update_paths_tx: update_excerpts_tx,
 218            _update_excerpts_task: cx.spawn(move |this, mut cx| async move {
 219                while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
 220                    if let Some(buffer) = project_handle
 221                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
 222                        .await
 223                        .log_err()
 224                    {
 225                        this.update(&mut cx, |this, cx| {
 226                            this.update_excerpts(path, language_server_id, buffer, cx);
 227                        })?;
 228                    }
 229                }
 230                anyhow::Ok(())
 231            }),
 232            _subscription: project_event_subscription,
 233        };
 234        this.enqueue_update_all_excerpts(cx);
 235        this
 236    }
 237
 238    fn new(
 239        project_handle: Model<Project>,
 240        workspace: WeakView<Workspace>,
 241        cx: &mut ViewContext<Self>,
 242    ) -> Self {
 243        Self::new_with_context(
 244            editor::DEFAULT_MULTIBUFFER_CONTEXT,
 245            project_handle,
 246            workspace,
 247            cx,
 248        )
 249    }
 250
 251    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 252        if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(cx) {
 253            workspace.activate_item(&existing, true, true, cx);
 254        } else {
 255            let workspace_handle = cx.view().downgrade();
 256            let diagnostics = cx.new_view(|cx| {
 257                GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 258            });
 259            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
 260        }
 261    }
 262
 263    pub fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
 264        self.include_warnings = !self.include_warnings;
 265        self.enqueue_update_all_excerpts(cx);
 266        cx.notify();
 267    }
 268
 269    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 270        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
 271            self.editor.focus_handle(cx).focus(cx)
 272        }
 273    }
 274
 275    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
 276        if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
 277            self.enqueue_update_stale_excerpts(None);
 278        }
 279    }
 280
 281    /// Enqueue an update of all excerpts. Updates all paths that either
 282    /// currently have diagnostics or are currently present in this view.
 283    fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
 284        self.project.update(cx, |project, cx| {
 285            let mut paths = project
 286                .diagnostic_summaries(false, cx)
 287                .map(|(path, _, _)| path)
 288                .collect::<BTreeSet<_>>();
 289            paths.extend(self.path_states.iter().map(|state| state.path.clone()));
 290            for path in paths {
 291                self.update_paths_tx.unbounded_send((path, None)).unwrap();
 292            }
 293        });
 294    }
 295
 296    /// Enqueue an update of the excerpts for any path whose diagnostics are known
 297    /// to have changed. If a language server id is passed, then only the excerpts for
 298    /// that language server's diagnostics will be updated. Otherwise, all stale excerpts
 299    /// will be refreshed.
 300    pub fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
 301        for (path, server_id) in &self.paths_to_update {
 302            if language_server_id.map_or(true, |id| id == *server_id) {
 303                self.update_paths_tx
 304                    .unbounded_send((path.clone(), Some(*server_id)))
 305                    .unwrap();
 306            }
 307        }
 308    }
 309
 310    fn update_excerpts(
 311        &mut self,
 312        path_to_update: ProjectPath,
 313        server_to_update: Option<LanguageServerId>,
 314        buffer: Model<Buffer>,
 315        cx: &mut ViewContext<Self>,
 316    ) {
 317        self.paths_to_update.retain(|(path, server_id)| {
 318            *path != path_to_update
 319                || server_to_update.map_or(false, |to_update| *server_id != to_update)
 320        });
 321
 322        // TODO change selections as in the old panel, to the next primary diagnostics
 323        // TODO make [shift-]f8 to work, jump to the next block group
 324        let _was_empty = self.path_states.is_empty();
 325        let path_ix = match self.path_states.binary_search_by(|probe| {
 326            project::compare_paths((&probe.path.path, true), (&path_to_update.path, true))
 327        }) {
 328            Ok(ix) => ix,
 329            Err(ix) => {
 330                self.path_states.insert(
 331                    ix,
 332                    PathState {
 333                        path: path_to_update.clone(),
 334                        diagnostics: Vec::new(),
 335                        last_excerpt_id: None,
 336                        first_excerpt_id: None,
 337                    },
 338                );
 339                ix
 340            }
 341        };
 342
 343        let max_severity = if self.include_warnings {
 344            DiagnosticSeverity::WARNING
 345        } else {
 346            DiagnosticSeverity::ERROR
 347        };
 348
 349        let excerpt_borders = self.excerpt_borders_for_path(path_ix);
 350        let path_state = &mut self.path_states[path_ix];
 351        let buffer_snapshot = buffer.read(cx).snapshot();
 352
 353        let mut path_update = PathUpdate::new(
 354            excerpt_borders,
 355            &buffer_snapshot,
 356            server_to_update,
 357            max_severity,
 358            path_state,
 359        );
 360        path_update.prepare_excerpt_data(
 361            self.context,
 362            self.excerpts.read(cx).snapshot(cx),
 363            buffer.read(cx).snapshot(),
 364            path_state.diagnostics.iter(),
 365        );
 366        self.excerpts.update(cx, |multi_buffer, cx| {
 367            path_update.apply_excerpt_changes(
 368                path_state,
 369                self.context,
 370                buffer_snapshot,
 371                multi_buffer,
 372                buffer,
 373                cx,
 374            );
 375        });
 376
 377        let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx);
 378        let blocks_to_insert =
 379            path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot);
 380
 381        let new_block_ids = self.editor.update(cx, |editor, cx| {
 382            editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx);
 383            editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx)
 384        });
 385        path_state.diagnostics = path_update.new_blocks(new_block_ids);
 386
 387        if self.path_states.is_empty() {
 388            if self.editor.focus_handle(cx).is_focused(cx) {
 389                cx.focus(&self.focus_handle);
 390            }
 391        } else if self.focus_handle.is_focused(cx) {
 392            let focus_handle = self.editor.focus_handle(cx);
 393            cx.focus(&focus_handle);
 394        }
 395
 396        #[cfg(test)]
 397        self.check_invariants(cx);
 398
 399        cx.notify();
 400    }
 401
 402    fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option<ExcerptId>, Option<ExcerptId>) {
 403        let previous_path_state_ix =
 404            Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix);
 405        let next_path_state_ix = path_ix + 1;
 406        let start = previous_path_state_ix.and_then(|i| {
 407            self.path_states[..=i]
 408                .iter()
 409                .rev()
 410                .find_map(|state| state.last_excerpt_id)
 411        });
 412        let end = self.path_states[next_path_state_ix..]
 413            .iter()
 414            .find_map(|state| state.first_excerpt_id);
 415        (start, end)
 416    }
 417
 418    #[cfg(test)]
 419    fn check_invariants(&self, cx: &mut ViewContext<Self>) {
 420        let mut excerpts = Vec::new();
 421        for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
 422            if let Some(file) = buffer.file() {
 423                excerpts.push((id, file.path().clone()));
 424            }
 425        }
 426
 427        let mut prev_path = None;
 428        for (_, path) in &excerpts {
 429            if let Some(prev_path) = prev_path {
 430                if path < prev_path {
 431                    panic!("excerpts are not sorted by path {:?}", excerpts);
 432                }
 433            }
 434            prev_path = Some(path);
 435        }
 436    }
 437}
 438
 439impl FocusableView for GroupedDiagnosticsEditor {
 440    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
 441        self.focus_handle.clone()
 442    }
 443}
 444
 445impl Item for GroupedDiagnosticsEditor {
 446    type Event = EditorEvent;
 447
 448    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 449        Editor::to_item_events(event, f)
 450    }
 451
 452    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 453        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 454    }
 455
 456    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 457        self.editor
 458            .update(cx, |editor, cx| editor.navigate(data, cx))
 459    }
 460
 461    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
 462        Some("Project Diagnostics".into())
 463    }
 464
 465    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
 466        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
 467            Label::new("No problems")
 468                .color(params.text_color())
 469                .into_any_element()
 470        } else {
 471            h_flex()
 472                .gap_1()
 473                .when(self.summary.error_count > 0, |then| {
 474                    then.child(
 475                        h_flex()
 476                            .gap_1()
 477                            .child(Icon::new(IconName::XCircle).color(Color::Error))
 478                            .child(
 479                                Label::new(self.summary.error_count.to_string())
 480                                    .color(params.text_color()),
 481                            ),
 482                    )
 483                })
 484                .when(self.summary.warning_count > 0, |then| {
 485                    then.child(
 486                        h_flex()
 487                            .gap_1()
 488                            .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
 489                            .child(
 490                                Label::new(self.summary.warning_count.to_string())
 491                                    .color(params.text_color()),
 492                            ),
 493                    )
 494                })
 495                .into_any_element()
 496        }
 497    }
 498
 499    fn telemetry_event_text(&self) -> Option<&'static str> {
 500        Some("project diagnostics")
 501    }
 502
 503    fn for_each_project_item(
 504        &self,
 505        cx: &AppContext,
 506        f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
 507    ) {
 508        self.editor.for_each_project_item(cx, f)
 509    }
 510
 511    fn is_singleton(&self, _: &AppContext) -> bool {
 512        false
 513    }
 514
 515    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 516        self.editor.update(cx, |editor, _| {
 517            editor.set_nav_history(Some(nav_history));
 518        });
 519    }
 520
 521    fn clone_on_split(
 522        &self,
 523        _workspace_id: Option<workspace::WorkspaceId>,
 524        cx: &mut ViewContext<Self>,
 525    ) -> Option<View<Self>>
 526    where
 527        Self: Sized,
 528    {
 529        Some(cx.new_view(|cx| {
 530            GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
 531        }))
 532    }
 533
 534    fn is_dirty(&self, cx: &AppContext) -> bool {
 535        self.excerpts.read(cx).is_dirty(cx)
 536    }
 537
 538    fn has_conflict(&self, cx: &AppContext) -> bool {
 539        self.excerpts.read(cx).has_conflict(cx)
 540    }
 541
 542    fn can_save(&self, _: &AppContext) -> bool {
 543        true
 544    }
 545
 546    fn save(
 547        &mut self,
 548        format: bool,
 549        project: Model<Project>,
 550        cx: &mut ViewContext<Self>,
 551    ) -> Task<Result<()>> {
 552        self.editor.save(format, project, cx)
 553    }
 554
 555    fn save_as(
 556        &mut self,
 557        _: Model<Project>,
 558        _: ProjectPath,
 559        _: &mut ViewContext<Self>,
 560    ) -> Task<Result<()>> {
 561        unreachable!()
 562    }
 563
 564    fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 565        self.editor.reload(project, cx)
 566    }
 567
 568    fn act_as_type<'a>(
 569        &'a self,
 570        type_id: TypeId,
 571        self_handle: &'a View<Self>,
 572        _: &'a AppContext,
 573    ) -> Option<AnyView> {
 574        if type_id == TypeId::of::<Self>() {
 575            Some(self_handle.to_any())
 576        } else if type_id == TypeId::of::<Editor>() {
 577            Some(self.editor.to_any())
 578        } else {
 579            None
 580        }
 581    }
 582
 583    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 584        ToolbarItemLocation::PrimaryLeft
 585    }
 586
 587    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 588        self.editor.breadcrumbs(theme, cx)
 589    }
 590
 591    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 592        self.editor
 593            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 594    }
 595}
 596
 597fn compare_data_locations(
 598    old: &DiagnosticData,
 599    new: &DiagnosticData,
 600    snapshot: &BufferSnapshot,
 601) -> Ordering {
 602    compare_diagnostics(&old.entry, &new.entry, snapshot)
 603        .then_with(|| old.language_server_id.cmp(&new.language_server_id))
 604}
 605
 606fn compare_diagnostics(
 607    old: &DiagnosticEntry<language::Anchor>,
 608    new: &DiagnosticEntry<language::Anchor>,
 609    snapshot: &BufferSnapshot,
 610) -> Ordering {
 611    compare_diagnostic_ranges(&old.range, &new.range, snapshot)
 612        .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
 613}
 614
 615fn compare_diagnostic_ranges(
 616    old: &Range<language::Anchor>,
 617    new: &Range<language::Anchor>,
 618    snapshot: &BufferSnapshot,
 619) -> Ordering {
 620    // The diagnostics may point to a previously open Buffer for this file.
 621    if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) {
 622        return Ordering::Greater;
 623    }
 624
 625    old.start
 626        .to_offset(snapshot)
 627        .cmp(&new.start.to_offset(snapshot))
 628        .then_with(|| {
 629            old.end
 630                .to_offset(snapshot)
 631                .cmp(&new.end.to_offset(snapshot))
 632        })
 633}
 634
 635// TODO wrong? What to do here instead?
 636fn compare_diagnostic_range_edges(
 637    old: &Range<language::Anchor>,
 638    new: &Range<language::Anchor>,
 639    snapshot: &BufferSnapshot,
 640) -> (Ordering, Ordering) {
 641    // The diagnostics may point to a previously open Buffer for this file.
 642    let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) {
 643        (false, false) => old.start.offset.cmp(&new.start.offset),
 644        (false, true) => Ordering::Greater,
 645        (true, false) => Ordering::Less,
 646        (true, true) => old.start.cmp(&new.start, snapshot),
 647    };
 648
 649    let end_cmp = old
 650        .end
 651        .to_offset(snapshot)
 652        .cmp(&new.end.to_offset(snapshot));
 653    (start_cmp, end_cmp)
 654}
 655
 656#[derive(Debug)]
 657struct PathUpdate {
 658    path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
 659    latest_excerpt_id: ExcerptId,
 660    new_diagnostics: Vec<(DiagnosticData, Option<CustomBlockId>)>,
 661    diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
 662    blocks_to_remove: HashSet<CustomBlockId>,
 663    unchanged_blocks: HashMap<usize, CustomBlockId>,
 664    excerpts_with_new_diagnostics: HashSet<ExcerptId>,
 665    excerpts_to_remove: Vec<ExcerptId>,
 666    excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
 667    excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
 668    first_excerpt_id: Option<ExcerptId>,
 669    last_excerpt_id: Option<ExcerptId>,
 670}
 671
 672impl PathUpdate {
 673    fn new(
 674        path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
 675        buffer_snapshot: &BufferSnapshot,
 676        server_to_update: Option<LanguageServerId>,
 677        max_severity: DiagnosticSeverity,
 678        path_state: &PathState,
 679    ) -> Self {
 680        let mut blocks_to_remove = HashSet::default();
 681        let mut removed_groups = HashSet::default();
 682        let mut new_diagnostics = path_state
 683            .diagnostics
 684            .iter()
 685            .filter(|(diagnostic_data, _)| {
 686                server_to_update.map_or(true, |server_id| {
 687                    diagnostic_data.language_server_id != server_id
 688                })
 689            })
 690            .filter(|(diagnostic_data, block_id)| {
 691                let diagnostic = &diagnostic_data.entry.diagnostic;
 692                let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity;
 693                if !retain {
 694                    removed_groups.insert(diagnostic.group_id);
 695                    blocks_to_remove.insert(*block_id);
 696                }
 697                retain
 698            })
 699            .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id)))
 700            .collect::<Vec<_>>();
 701        new_diagnostics.retain(|(diagnostic_data, block_id)| {
 702            let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id);
 703            if !retain {
 704                if let Some(block_id) = block_id {
 705                    blocks_to_remove.insert(*block_id);
 706                }
 707            }
 708            retain
 709        });
 710        for (server_id, group) in buffer_snapshot
 711            .diagnostic_groups(server_to_update)
 712            .into_iter()
 713            .filter(|(_, group)| {
 714                group.entries[group.primary_ix].diagnostic.severity <= max_severity
 715            })
 716        {
 717            for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() {
 718                let new_data = DiagnosticData {
 719                    language_server_id: server_id,
 720                    is_primary: diagnostic_index == group.primary_ix,
 721                    entry: diagnostic.clone(),
 722                };
 723                let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| {
 724                    compare_data_locations(&probe.0, &new_data, &buffer_snapshot)
 725                });
 726                new_diagnostics.insert(i, (new_data, None));
 727            }
 728        }
 729
 730        let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min());
 731        Self {
 732            latest_excerpt_id,
 733            path_excerpts_borders,
 734            new_diagnostics,
 735            blocks_to_remove,
 736            diagnostics_by_row_label: BTreeMap::new(),
 737            excerpts_to_remove: Vec::new(),
 738            excerpts_with_new_diagnostics: HashSet::default(),
 739            unchanged_blocks: HashMap::default(),
 740            excerpts_to_add: HashMap::default(),
 741            excerpt_expands: HashMap::default(),
 742            first_excerpt_id: None,
 743            last_excerpt_id: None,
 744        }
 745    }
 746
 747    fn prepare_excerpt_data<'a>(
 748        &'a mut self,
 749        context: u32,
 750        multi_buffer_snapshot: MultiBufferSnapshot,
 751        buffer_snapshot: BufferSnapshot,
 752        current_diagnostics: impl Iterator<Item = &'a (DiagnosticData, CustomBlockId)> + 'a,
 753    ) {
 754        let mut current_diagnostics = current_diagnostics.fuse().peekable();
 755        let mut excerpts_to_expand =
 756            HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::default();
 757        let mut current_excerpts = path_state_excerpts(
 758            self.path_excerpts_borders.0,
 759            self.path_excerpts_borders.1,
 760            &multi_buffer_snapshot,
 761        )
 762        .fuse()
 763        .peekable();
 764
 765        for (diagnostic_index, (new_diagnostic, existing_block)) in
 766            self.new_diagnostics.iter().enumerate()
 767        {
 768            if let Some(existing_block) = existing_block {
 769                self.unchanged_blocks
 770                    .insert(diagnostic_index, *existing_block);
 771            }
 772
 773            loop {
 774                match current_excerpts.peek() {
 775                    None => {
 776                        let excerpt_ranges = self
 777                            .excerpts_to_add
 778                            .entry(self.latest_excerpt_id)
 779                            .or_default();
 780                        let new_range = new_diagnostic.entry.range.clone();
 781                        let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| {
 782                            compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot)
 783                        });
 784                        excerpt_ranges.insert(i, new_range);
 785                        break;
 786                    }
 787                    Some((current_excerpt_id, _, current_excerpt_range)) => {
 788                        match compare_diagnostic_range_edges(
 789                            &current_excerpt_range.context,
 790                            &new_diagnostic.entry.range,
 791                            &buffer_snapshot,
 792                        ) {
 793                            /*
 794                                  new_s new_e
 795                            ----[---->><<----]--
 796                             cur_s         cur_e
 797                            */
 798                            (
 799                                Ordering::Less | Ordering::Equal,
 800                                Ordering::Greater | Ordering::Equal,
 801                            ) => {
 802                                self.excerpts_with_new_diagnostics
 803                                    .insert(*current_excerpt_id);
 804                                if self.first_excerpt_id.is_none() {
 805                                    self.first_excerpt_id = Some(*current_excerpt_id);
 806                                }
 807                                self.last_excerpt_id = Some(*current_excerpt_id);
 808                                break;
 809                            }
 810                            /*
 811                                  cur_s cur_e
 812                            ---->>>>>[--]<<<<<--
 813                             new_s         new_e
 814                            */
 815                            (
 816                                Ordering::Greater | Ordering::Equal,
 817                                Ordering::Less | Ordering::Equal,
 818                            ) => {
 819                                let expand_up = current_excerpt_range
 820                                    .context
 821                                    .start
 822                                    .to_point(&buffer_snapshot)
 823                                    .row
 824                                    .saturating_sub(
 825                                        new_diagnostic
 826                                            .entry
 827                                            .range
 828                                            .start
 829                                            .to_point(&buffer_snapshot)
 830                                            .row,
 831                                    );
 832                                let expand_down = new_diagnostic
 833                                    .entry
 834                                    .range
 835                                    .end
 836                                    .to_point(&buffer_snapshot)
 837                                    .row
 838                                    .saturating_sub(
 839                                        current_excerpt_range
 840                                            .context
 841                                            .end
 842                                            .to_point(&buffer_snapshot)
 843                                            .row,
 844                                    );
 845                                let expand_value = excerpts_to_expand
 846                                    .entry(*current_excerpt_id)
 847                                    .or_default()
 848                                    .entry(ExpandExcerptDirection::UpAndDown)
 849                                    .or_default();
 850                                *expand_value = (*expand_value).max(expand_up).max(expand_down);
 851                                self.excerpts_with_new_diagnostics
 852                                    .insert(*current_excerpt_id);
 853                                if self.first_excerpt_id.is_none() {
 854                                    self.first_excerpt_id = Some(*current_excerpt_id);
 855                                }
 856                                self.last_excerpt_id = Some(*current_excerpt_id);
 857                                break;
 858                            }
 859                            /*
 860                                    new_s   new_e
 861                                     >       <
 862                            ----[---->>>]<<<<<--
 863                             cur_s    cur_e
 864
 865                            or
 866                                      new_s new_e
 867                                        >    <
 868                            ----[----]-->>><<<--
 869                             cur_s cur_e
 870                            */
 871                            (Ordering::Less, Ordering::Less) => {
 872                                if current_excerpt_range
 873                                    .context
 874                                    .end
 875                                    .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot)
 876                                    .is_ge()
 877                                {
 878                                    let expand_down = new_diagnostic
 879                                        .entry
 880                                        .range
 881                                        .end
 882                                        .to_point(&buffer_snapshot)
 883                                        .row
 884                                        .saturating_sub(
 885                                            current_excerpt_range
 886                                                .context
 887                                                .end
 888                                                .to_point(&buffer_snapshot)
 889                                                .row,
 890                                        );
 891                                    let expand_value = excerpts_to_expand
 892                                        .entry(*current_excerpt_id)
 893                                        .or_default()
 894                                        .entry(ExpandExcerptDirection::Down)
 895                                        .or_default();
 896                                    *expand_value = (*expand_value).max(expand_down);
 897                                    self.excerpts_with_new_diagnostics
 898                                        .insert(*current_excerpt_id);
 899                                    if self.first_excerpt_id.is_none() {
 900                                        self.first_excerpt_id = Some(*current_excerpt_id);
 901                                    }
 902                                    self.last_excerpt_id = Some(*current_excerpt_id);
 903                                    break;
 904                                } else if !self
 905                                    .excerpts_with_new_diagnostics
 906                                    .contains(current_excerpt_id)
 907                                {
 908                                    self.excerpts_to_remove.push(*current_excerpt_id);
 909                                }
 910                            }
 911                            /*
 912                                  cur_s      cur_e
 913                            ---->>>>>[<<<<----]--
 914                                >        <
 915                               new_s    new_e
 916
 917                            or
 918                                      cur_s cur_e
 919                            ---->>><<<--[----]--
 920                                >    <
 921                               new_s new_e
 922                            */
 923                            (Ordering::Greater, Ordering::Greater) => {
 924                                if current_excerpt_range
 925                                    .context
 926                                    .start
 927                                    .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot)
 928                                    .is_le()
 929                                {
 930                                    let expand_up = current_excerpt_range
 931                                        .context
 932                                        .start
 933                                        .to_point(&buffer_snapshot)
 934                                        .row
 935                                        .saturating_sub(
 936                                            new_diagnostic
 937                                                .entry
 938                                                .range
 939                                                .start
 940                                                .to_point(&buffer_snapshot)
 941                                                .row,
 942                                        );
 943                                    let expand_value = excerpts_to_expand
 944                                        .entry(*current_excerpt_id)
 945                                        .or_default()
 946                                        .entry(ExpandExcerptDirection::Up)
 947                                        .or_default();
 948                                    *expand_value = (*expand_value).max(expand_up);
 949                                    self.excerpts_with_new_diagnostics
 950                                        .insert(*current_excerpt_id);
 951                                    if self.first_excerpt_id.is_none() {
 952                                        self.first_excerpt_id = Some(*current_excerpt_id);
 953                                    }
 954                                    self.last_excerpt_id = Some(*current_excerpt_id);
 955                                    break;
 956                                } else {
 957                                    let excerpt_ranges = self
 958                                        .excerpts_to_add
 959                                        .entry(self.latest_excerpt_id)
 960                                        .or_default();
 961                                    let new_range = new_diagnostic.entry.range.clone();
 962                                    let (Ok(i) | Err(i)) =
 963                                        excerpt_ranges.binary_search_by(|probe| {
 964                                            compare_diagnostic_ranges(
 965                                                probe,
 966                                                &new_range,
 967                                                &buffer_snapshot,
 968                                            )
 969                                        });
 970                                    excerpt_ranges.insert(i, new_range);
 971                                    break;
 972                                }
 973                            }
 974                        }
 975                        if let Some((next_id, ..)) = current_excerpts.next() {
 976                            self.latest_excerpt_id = next_id;
 977                        }
 978                    }
 979                }
 980            }
 981
 982            loop {
 983                match current_diagnostics.peek() {
 984                    None => break,
 985                    Some((current_diagnostic, current_block)) => {
 986                        match compare_data_locations(
 987                            current_diagnostic,
 988                            new_diagnostic,
 989                            &buffer_snapshot,
 990                        ) {
 991                            Ordering::Less => {
 992                                self.blocks_to_remove.insert(*current_block);
 993                            }
 994                            Ordering::Equal => {
 995                                if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) {
 996                                    self.unchanged_blocks
 997                                        .insert(diagnostic_index, *current_block);
 998                                } else {
 999                                    self.blocks_to_remove.insert(*current_block);
1000                                }
1001                                let _ = current_diagnostics.next();
1002                                break;
1003                            }
1004                            Ordering::Greater => break,
1005                        }
1006                        let _ = current_diagnostics.next();
1007                    }
1008                }
1009            }
1010        }
1011
1012        self.excerpts_to_remove.retain(|excerpt_id| {
1013            !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1014                && !excerpts_to_expand.contains_key(excerpt_id)
1015        });
1016        self.excerpts_to_remove.extend(
1017            current_excerpts
1018                .filter(|(excerpt_id, ..)| {
1019                    !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1020                        && !excerpts_to_expand.contains_key(excerpt_id)
1021                })
1022                .map(|(excerpt_id, ..)| excerpt_id),
1023        );
1024        let mut excerpt_expands = HashMap::default();
1025        for (excerpt_id, directions) in excerpts_to_expand {
1026            let excerpt_expand = if directions.len() > 1 {
1027                Some((
1028                    ExpandExcerptDirection::UpAndDown,
1029                    directions
1030                        .values()
1031                        .max()
1032                        .copied()
1033                        .unwrap_or_default()
1034                        .max(context),
1035                ))
1036            } else {
1037                directions
1038                    .into_iter()
1039                    .next()
1040                    .map(|(direction, expand)| (direction, expand.max(context)))
1041            };
1042            if let Some(expand) = excerpt_expand {
1043                excerpt_expands
1044                    .entry(expand)
1045                    .or_insert_with(|| Vec::new())
1046                    .push(excerpt_id);
1047            }
1048        }
1049        self.blocks_to_remove
1050            .extend(current_diagnostics.map(|(_, block_id)| block_id));
1051    }
1052
1053    fn apply_excerpt_changes(
1054        &mut self,
1055        path_state: &mut PathState,
1056        context: u32,
1057        buffer_snapshot: BufferSnapshot,
1058        multi_buffer: &mut MultiBuffer,
1059        buffer: Model<Buffer>,
1060        cx: &mut gpui::ModelContext<MultiBuffer>,
1061    ) {
1062        let max_point = buffer_snapshot.max_point();
1063        for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) {
1064            let ranges = ranges
1065                .into_iter()
1066                .map(|range| {
1067                    let mut extended_point_range = range.to_point(&buffer_snapshot);
1068                    extended_point_range.start.row =
1069                        extended_point_range.start.row.saturating_sub(context);
1070                    extended_point_range.start.column = 0;
1071                    extended_point_range.end.row =
1072                        (extended_point_range.end.row + context).min(max_point.row);
1073                    extended_point_range.end.column = u32::MAX;
1074                    let extended_start =
1075                        buffer_snapshot.clip_point(extended_point_range.start, Bias::Left);
1076                    let extended_end =
1077                        buffer_snapshot.clip_point(extended_point_range.end, Bias::Right);
1078                    extended_start..extended_end
1079                })
1080                .collect::<Vec<_>>();
1081            let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context);
1082            let excerpts = multi_buffer.insert_excerpts_after(
1083                after_excerpt_id,
1084                buffer.clone(),
1085                joined_ranges,
1086                cx,
1087            );
1088            if self.first_excerpt_id.is_none() {
1089                self.first_excerpt_id = excerpts.first().copied();
1090            }
1091            self.last_excerpt_id = excerpts.last().copied();
1092        }
1093        for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) {
1094            multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
1095        }
1096        multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx);
1097        path_state.first_excerpt_id = self.first_excerpt_id;
1098        path_state.last_excerpt_id = self.last_excerpt_id;
1099    }
1100
1101    fn prepare_blocks_to_insert(
1102        &mut self,
1103        editor: View<Editor>,
1104        multi_buffer_snapshot: MultiBufferSnapshot,
1105    ) -> Vec<BlockProperties<editor::Anchor>> {
1106        let mut updated_excerpts = path_state_excerpts(
1107            self.path_excerpts_borders.0,
1108            self.path_excerpts_borders.1,
1109            &multi_buffer_snapshot,
1110        )
1111        .fuse()
1112        .peekable();
1113        let mut used_labels = BTreeMap::new();
1114        self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold(
1115            BTreeMap::new(),
1116            |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| {
1117                let new_diagnostic = &diagnostic.entry;
1118                let block_position = new_diagnostic.range.start;
1119                let excerpt_id = loop {
1120                    match updated_excerpts.peek() {
1121                        None => break None,
1122                        Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => {
1123                            let excerpt_range = &excerpt_range.context;
1124                            match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot)
1125                            {
1126                                Ordering::Less => break None,
1127                                Ordering::Equal | Ordering::Greater => match block_position
1128                                    .cmp(&excerpt_range.end, excerpt_buffer_snapshot)
1129                                {
1130                                    Ordering::Equal | Ordering::Less => break Some(*excerpt_id),
1131                                    Ordering::Greater => {
1132                                        let _ = updated_excerpts.next();
1133                                    }
1134                                },
1135                            }
1136                        }
1137                    }
1138                };
1139
1140                let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| {
1141                    multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position)
1142                }) else {
1143                    return diagnostics_by_row_label;
1144                };
1145
1146                let multi_buffer_row = MultiBufferRow(
1147                    position_in_multi_buffer
1148                        .to_point(&multi_buffer_snapshot)
1149                        .row,
1150                );
1151
1152                let grouped_diagnostics = &mut diagnostics_by_row_label
1153                    .entry(multi_buffer_row)
1154                    .or_insert_with(|| (position_in_multi_buffer, Vec::new()))
1155                    .1;
1156                let new_label = used_labels
1157                    .entry(multi_buffer_row)
1158                    .or_insert_with(|| HashSet::default())
1159                    .insert((
1160                        new_diagnostic.diagnostic.source.as_deref(),
1161                        new_diagnostic.diagnostic.message.as_str(),
1162                    ));
1163
1164                if !new_label || !grouped_diagnostics.is_empty() {
1165                    if let Some(existing_block) = existing_block {
1166                        self.blocks_to_remove.insert(*existing_block);
1167                    }
1168                    if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) {
1169                        self.blocks_to_remove.insert(block_id);
1170                    }
1171                }
1172                if new_label {
1173                    let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| {
1174                        let a = &self.new_diagnostics[probe].0.entry.diagnostic;
1175                        let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic;
1176                        a.group_id
1177                            .cmp(&b.group_id)
1178                            .then_with(|| a.is_primary.cmp(&b.is_primary).reverse())
1179                            .then_with(|| a.severity.cmp(&b.severity))
1180                    });
1181                    grouped_diagnostics.insert(i, diagnostic_index);
1182                }
1183
1184                diagnostics_by_row_label
1185            },
1186        );
1187
1188        self.diagnostics_by_row_label
1189            .values()
1190            .filter_map(|(earliest_in_row_position, diagnostics_at_line)| {
1191                let earliest_in_row_position = *earliest_in_row_position;
1192                match diagnostics_at_line.len() {
1193                    0 => None,
1194                    len => {
1195                        if len == 1 {
1196                            let i = diagnostics_at_line.first().copied()?;
1197                            if self.unchanged_blocks.contains_key(&i) {
1198                                return None;
1199                            }
1200                        }
1201                        let lines_in_first_message = diagnostic_text_lines(
1202                            &self
1203                                .new_diagnostics
1204                                .get(diagnostics_at_line.first().copied()?)?
1205                                .0
1206                                .entry
1207                                .diagnostic,
1208                        );
1209                        let folded_block_height = lines_in_first_message.clamp(1, 2);
1210                        let diagnostics_to_render = Arc::new(
1211                            diagnostics_at_line
1212                                .iter()
1213                                .filter_map(|&index| self.new_diagnostics.get(index))
1214                                .map(|(diagnostic_data, _)| {
1215                                    diagnostic_data.entry.diagnostic.clone()
1216                                })
1217                                .collect::<Vec<_>>(),
1218                        );
1219                        Some(BlockProperties {
1220                            position: earliest_in_row_position,
1221                            height: folded_block_height,
1222                            style: BlockStyle::Sticky,
1223                            render: render_same_line_diagnostics(
1224                                Arc::new(AtomicBool::new(false)),
1225                                diagnostics_to_render,
1226                                editor.clone(),
1227                                folded_block_height,
1228                            ),
1229                            disposition: BlockDisposition::Above,
1230                        })
1231                    }
1232                }
1233            })
1234            .collect()
1235    }
1236
1237    fn new_blocks(
1238        mut self,
1239        new_block_ids: Vec<CustomBlockId>,
1240    ) -> Vec<(DiagnosticData, CustomBlockId)> {
1241        let mut new_block_ids = new_block_ids.into_iter().fuse();
1242        for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label {
1243            let mut created_block_id = None;
1244            match grouped_diagnostics.len() {
1245                0 => {
1246                    debug_panic!("Unexpected empty diagnostics group");
1247                    continue;
1248                }
1249                1 => {
1250                    let index = grouped_diagnostics[0];
1251                    if let Some(&block_id) = self.unchanged_blocks.get(&index) {
1252                        self.new_diagnostics[index].1 = Some(block_id);
1253                    } else {
1254                        let Some(block_id) =
1255                            created_block_id.get_or_insert_with(|| new_block_ids.next())
1256                        else {
1257                            debug_panic!("Expected a new block for each new diagnostic");
1258                            continue;
1259                        };
1260                        self.new_diagnostics[index].1 = Some(*block_id);
1261                    }
1262                }
1263                _ => {
1264                    let Some(block_id) =
1265                        created_block_id.get_or_insert_with(|| new_block_ids.next())
1266                    else {
1267                        debug_panic!("Expected a new block for each new diagnostic group");
1268                        continue;
1269                    };
1270                    for i in grouped_diagnostics {
1271                        self.new_diagnostics[i].1 = Some(*block_id);
1272                    }
1273                }
1274            }
1275        }
1276
1277        self.new_diagnostics
1278            .into_iter()
1279            .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?)))
1280            .collect()
1281    }
1282}
1283
1284fn render_same_line_diagnostics(
1285    expanded: Arc<AtomicBool>,
1286    diagnostics: Arc<Vec<language::Diagnostic>>,
1287    editor_handle: View<Editor>,
1288    folded_block_height: u8,
1289) -> RenderBlock {
1290    Box::new(move |cx: &mut BlockContext| {
1291        let block_id = match cx.block_id {
1292            BlockId::Custom(block_id) => block_id,
1293            _ => {
1294                debug_panic!("Expected a block id for the diagnostics block");
1295                return div().into_any_element();
1296            }
1297        };
1298        let Some(first_diagnostic) = diagnostics.first() else {
1299            debug_panic!("Expected at least one diagnostic");
1300            return div().into_any_element();
1301        };
1302        let button_expanded = expanded.clone();
1303        let expanded = expanded.load(atomic::Ordering::Acquire);
1304        let expand_label = if expanded { '-' } else { '+' };
1305        let first_diagnostics_height = diagnostic_text_lines(first_diagnostic);
1306        let extra_diagnostics = diagnostics.len() - 1;
1307        let toggle_expand_label =
1308            if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 {
1309                None
1310            } else if extra_diagnostics > 0 {
1311                Some(format!("{expand_label}{extra_diagnostics}"))
1312            } else {
1313                Some(expand_label.to_string())
1314            };
1315
1316        let expanded_block_height = diagnostics
1317            .iter()
1318            .map(|diagnostic| diagnostic_text_lines(diagnostic))
1319            .sum::<u8>();
1320        let editor_handle = editor_handle.clone();
1321        let parent = h_flex()
1322            .items_start()
1323            .child(v_flex().size_full().when_some_else(
1324                toggle_expand_label,
1325                |parent, label| {
1326                    parent.child(Button::new(cx.block_id, label).on_click({
1327                        let diagnostics = Arc::clone(&diagnostics);
1328                        move |_, cx| {
1329                            let new_expanded = !expanded;
1330                            button_expanded.store(new_expanded, atomic::Ordering::Release);
1331                            let new_size = if new_expanded {
1332                                expanded_block_height
1333                            } else {
1334                                folded_block_height
1335                            };
1336                            editor_handle.update(cx, |editor, cx| {
1337                                editor.replace_blocks(
1338                                    HashMap::from_iter(Some((
1339                                        block_id,
1340                                        (
1341                                            Some(new_size),
1342                                            render_same_line_diagnostics(
1343                                                Arc::clone(&button_expanded),
1344                                                Arc::clone(&diagnostics),
1345                                                editor_handle.clone(),
1346                                                folded_block_height,
1347                                            ),
1348                                        ),
1349                                    ))),
1350                                    None,
1351                                    cx,
1352                                )
1353                            });
1354                        }
1355                    }))
1356                },
1357                |parent| {
1358                    parent.child(
1359                        h_flex()
1360                            .size(IconSize::default().rems())
1361                            .invisible()
1362                            .flex_none(),
1363                    )
1364                },
1365            ));
1366        let max_message_rows = if expanded {
1367            None
1368        } else {
1369            Some(folded_block_height)
1370        };
1371        let mut renderer =
1372            diagnostic_block_renderer(first_diagnostic.clone(), max_message_rows, false, true);
1373        let mut diagnostics_element = v_flex();
1374        diagnostics_element = diagnostics_element.child(renderer(cx));
1375        if expanded {
1376            for diagnostic in diagnostics.iter().skip(1) {
1377                let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
1378                diagnostics_element = diagnostics_element.child(renderer(cx));
1379            }
1380        }
1381        parent.child(diagnostics_element).into_any_element()
1382    })
1383}
1384
1385fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 {
1386    diagnostic.message.matches('\n').count() as u8 + 1
1387}
1388
1389fn path_state_excerpts(
1390    after_excerpt_id: Option<ExcerptId>,
1391    before_excerpt_id: Option<ExcerptId>,
1392    multi_buffer_snapshot: &editor::MultiBufferSnapshot,
1393) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
1394    multi_buffer_snapshot
1395        .excerpts()
1396        .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id {
1397            Some(after_excerpt_id) => after_excerpt_id != excerpt_id,
1398            None => false,
1399        })
1400        .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id))
1401        .take_while(move |&(excerpt_id, ..)| match before_excerpt_id {
1402            Some(before_excerpt_id) => before_excerpt_id != excerpt_id,
1403            None => true,
1404        })
1405}