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, RenderBlock,
   7        TransformBlockId,
   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, Pane, 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
  54struct GroupedDiagnosticsEditor {
  55    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    paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
  63    include_warnings: bool,
  64    context: u32,
  65    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, BlockId)>,
  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, 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, cx);
 260        }
 261    }
 262
 263    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    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 kb change selections as in the old panel, to the next primary diagnostics
 323        // TODO kb 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        // TODO kb when warnings are turned off, there's a lot of refresh for many paths happening, why?
 344        let max_severity = if self.include_warnings {
 345            DiagnosticSeverity::WARNING
 346        } else {
 347            DiagnosticSeverity::ERROR
 348        };
 349
 350        let excerpt_borders = self.excerpt_borders_for_path(path_ix);
 351        let path_state = &mut self.path_states[path_ix];
 352        let buffer_snapshot = buffer.read(cx).snapshot();
 353
 354        let mut path_update = PathUpdate::new(
 355            excerpt_borders,
 356            &buffer_snapshot,
 357            server_to_update,
 358            max_severity,
 359            path_state,
 360        );
 361        path_update.prepare_excerpt_data(
 362            self.context,
 363            self.excerpts.read(cx).snapshot(cx),
 364            buffer.read(cx).snapshot(),
 365            path_state.diagnostics.iter(),
 366        );
 367        self.excerpts.update(cx, |multi_buffer, cx| {
 368            path_update.apply_excerpt_changes(
 369                path_state,
 370                self.context,
 371                buffer_snapshot,
 372                multi_buffer,
 373                buffer,
 374                cx,
 375            );
 376        });
 377
 378        let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx);
 379        let blocks_to_insert =
 380            path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot);
 381
 382        let new_block_ids = self.editor.update(cx, |editor, cx| {
 383            editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx);
 384            editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx)
 385        });
 386        path_state.diagnostics = path_update.new_blocks(new_block_ids);
 387
 388        if self.path_states.is_empty() {
 389            if self.editor.focus_handle(cx).is_focused(cx) {
 390                cx.focus(&self.focus_handle);
 391            }
 392        } else if self.focus_handle.is_focused(cx) {
 393            let focus_handle = self.editor.focus_handle(cx);
 394            cx.focus(&focus_handle);
 395        }
 396
 397        #[cfg(test)]
 398        self.check_invariants(cx);
 399
 400        cx.notify();
 401    }
 402
 403    fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option<ExcerptId>, Option<ExcerptId>) {
 404        let previous_path_state_ix =
 405            Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix);
 406        let next_path_state_ix = path_ix + 1;
 407        let start = previous_path_state_ix.and_then(|i| {
 408            self.path_states[..=i]
 409                .iter()
 410                .rev()
 411                .find_map(|state| state.last_excerpt_id)
 412        });
 413        let end = self.path_states[next_path_state_ix..]
 414            .iter()
 415            .find_map(|state| state.first_excerpt_id);
 416        (start, end)
 417    }
 418
 419    #[cfg(test)]
 420    fn check_invariants(&self, cx: &mut ViewContext<Self>) {
 421        let mut excerpts = Vec::new();
 422        for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
 423            if let Some(file) = buffer.file() {
 424                excerpts.push((id, file.path().clone()));
 425            }
 426        }
 427
 428        let mut prev_path = None;
 429        for (_, path) in &excerpts {
 430            if let Some(prev_path) = prev_path {
 431                if path < prev_path {
 432                    panic!("excerpts are not sorted by path {:?}", excerpts);
 433                }
 434            }
 435            prev_path = Some(path);
 436        }
 437    }
 438}
 439
 440impl FocusableView for GroupedDiagnosticsEditor {
 441    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
 442        self.focus_handle.clone()
 443    }
 444}
 445
 446impl Item for GroupedDiagnosticsEditor {
 447    type Event = EditorEvent;
 448
 449    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 450        Editor::to_item_events(event, f)
 451    }
 452
 453    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 454        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 455    }
 456
 457    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 458        self.editor
 459            .update(cx, |editor, cx| editor.navigate(data, cx))
 460    }
 461
 462    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
 463        Some("Project Diagnostics".into())
 464    }
 465
 466    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
 467        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
 468            Label::new("No problems")
 469                .color(if params.selected {
 470                    Color::Default
 471                } else {
 472                    Color::Muted
 473                })
 474                .into_any_element()
 475        } else {
 476            h_flex()
 477                .gap_1()
 478                .when(self.summary.error_count > 0, |then| {
 479                    then.child(
 480                        h_flex()
 481                            .gap_1()
 482                            .child(Icon::new(IconName::XCircle).color(Color::Error))
 483                            .child(Label::new(self.summary.error_count.to_string()).color(
 484                                if params.selected {
 485                                    Color::Default
 486                                } else {
 487                                    Color::Muted
 488                                },
 489                            )),
 490                    )
 491                })
 492                .when(self.summary.warning_count > 0, |then| {
 493                    then.child(
 494                        h_flex()
 495                            .gap_1()
 496                            .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
 497                            .child(Label::new(self.summary.warning_count.to_string()).color(
 498                                if params.selected {
 499                                    Color::Default
 500                                } else {
 501                                    Color::Muted
 502                                },
 503                            )),
 504                    )
 505                })
 506                .into_any_element()
 507        }
 508    }
 509
 510    fn telemetry_event_text(&self) -> Option<&'static str> {
 511        Some("project diagnostics")
 512    }
 513
 514    fn for_each_project_item(
 515        &self,
 516        cx: &AppContext,
 517        f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
 518    ) {
 519        self.editor.for_each_project_item(cx, f)
 520    }
 521
 522    fn is_singleton(&self, _: &AppContext) -> bool {
 523        false
 524    }
 525
 526    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 527        self.editor.update(cx, |editor, _| {
 528            editor.set_nav_history(Some(nav_history));
 529        });
 530    }
 531
 532    fn clone_on_split(
 533        &self,
 534        _workspace_id: Option<workspace::WorkspaceId>,
 535        cx: &mut ViewContext<Self>,
 536    ) -> Option<View<Self>>
 537    where
 538        Self: Sized,
 539    {
 540        Some(cx.new_view(|cx| {
 541            GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
 542        }))
 543    }
 544
 545    fn is_dirty(&self, cx: &AppContext) -> bool {
 546        self.excerpts.read(cx).is_dirty(cx)
 547    }
 548
 549    fn has_conflict(&self, cx: &AppContext) -> bool {
 550        self.excerpts.read(cx).has_conflict(cx)
 551    }
 552
 553    fn can_save(&self, _: &AppContext) -> bool {
 554        true
 555    }
 556
 557    fn save(
 558        &mut self,
 559        format: bool,
 560        project: Model<Project>,
 561        cx: &mut ViewContext<Self>,
 562    ) -> Task<Result<()>> {
 563        self.editor.save(format, project, cx)
 564    }
 565
 566    fn save_as(
 567        &mut self,
 568        _: Model<Project>,
 569        _: ProjectPath,
 570        _: &mut ViewContext<Self>,
 571    ) -> Task<Result<()>> {
 572        unreachable!()
 573    }
 574
 575    fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 576        self.editor.reload(project, cx)
 577    }
 578
 579    fn act_as_type<'a>(
 580        &'a self,
 581        type_id: TypeId,
 582        self_handle: &'a View<Self>,
 583        _: &'a AppContext,
 584    ) -> Option<AnyView> {
 585        if type_id == TypeId::of::<Self>() {
 586            Some(self_handle.to_any())
 587        } else if type_id == TypeId::of::<Editor>() {
 588            Some(self.editor.to_any())
 589        } else {
 590            None
 591        }
 592    }
 593
 594    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 595        ToolbarItemLocation::PrimaryLeft
 596    }
 597
 598    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 599        self.editor.breadcrumbs(theme, cx)
 600    }
 601
 602    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 603        self.editor
 604            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 605    }
 606
 607    fn serialized_item_kind() -> Option<&'static str> {
 608        Some("diagnostics")
 609    }
 610
 611    fn deserialize(
 612        project: Model<Project>,
 613        workspace: WeakView<Workspace>,
 614        _workspace_id: workspace::WorkspaceId,
 615        _item_id: workspace::ItemId,
 616        cx: &mut ViewContext<Pane>,
 617    ) -> Task<Result<View<Self>>> {
 618        Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
 619    }
 620}
 621
 622fn compare_data_locations(
 623    old: &DiagnosticData,
 624    new: &DiagnosticData,
 625    snapshot: &BufferSnapshot,
 626) -> Ordering {
 627    compare_diagnostics(&old.entry, &new.entry, snapshot)
 628        .then_with(|| old.language_server_id.cmp(&new.language_server_id))
 629}
 630
 631fn compare_diagnostics(
 632    old: &DiagnosticEntry<language::Anchor>,
 633    new: &DiagnosticEntry<language::Anchor>,
 634    snapshot: &BufferSnapshot,
 635) -> Ordering {
 636    compare_diagnostic_ranges(&old.range, &new.range, snapshot)
 637        .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
 638}
 639
 640fn compare_diagnostic_ranges(
 641    old: &Range<language::Anchor>,
 642    new: &Range<language::Anchor>,
 643    snapshot: &BufferSnapshot,
 644) -> Ordering {
 645    // The diagnostics may point to a previously open Buffer for this file.
 646    if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) {
 647        return Ordering::Greater;
 648    }
 649
 650    old.start
 651        .to_offset(snapshot)
 652        .cmp(&new.start.to_offset(snapshot))
 653        .then_with(|| {
 654            old.end
 655                .to_offset(snapshot)
 656                .cmp(&new.end.to_offset(snapshot))
 657        })
 658}
 659
 660// TODO kb wrong? What to do here instead?
 661fn compare_diagnostic_range_edges(
 662    old: &Range<language::Anchor>,
 663    new: &Range<language::Anchor>,
 664    snapshot: &BufferSnapshot,
 665) -> (Ordering, Ordering) {
 666    // The diagnostics may point to a previously open Buffer for this file.
 667    let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) {
 668        (false, false) => old.start.offset.cmp(&new.start.offset),
 669        (false, true) => Ordering::Greater,
 670        (true, false) => Ordering::Less,
 671        (true, true) => old.start.cmp(&new.start, snapshot),
 672    };
 673
 674    let end_cmp = old
 675        .end
 676        .to_offset(snapshot)
 677        .cmp(&new.end.to_offset(snapshot));
 678    (start_cmp, end_cmp)
 679}
 680
 681#[derive(Debug)]
 682struct PathUpdate {
 683    path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
 684    latest_excerpt_id: ExcerptId,
 685    new_diagnostics: Vec<(DiagnosticData, Option<BlockId>)>,
 686    diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
 687    blocks_to_remove: HashSet<BlockId>,
 688    unchanged_blocks: HashMap<usize, BlockId>,
 689    excerpts_with_new_diagnostics: HashSet<ExcerptId>,
 690    excerpts_to_remove: Vec<ExcerptId>,
 691    excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
 692    excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
 693    first_excerpt_id: Option<ExcerptId>,
 694    last_excerpt_id: Option<ExcerptId>,
 695}
 696
 697impl PathUpdate {
 698    fn new(
 699        path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
 700        buffer_snapshot: &BufferSnapshot,
 701        server_to_update: Option<LanguageServerId>,
 702        max_severity: DiagnosticSeverity,
 703        path_state: &PathState,
 704    ) -> Self {
 705        let mut blocks_to_remove = HashSet::default();
 706        let mut removed_groups = HashSet::default();
 707        let mut new_diagnostics = path_state
 708            .diagnostics
 709            .iter()
 710            .filter(|(diagnostic_data, _)| {
 711                server_to_update.map_or(true, |server_id| {
 712                    diagnostic_data.language_server_id != server_id
 713                })
 714            })
 715            .filter(|(diagnostic_data, block_id)| {
 716                let diagnostic = &diagnostic_data.entry.diagnostic;
 717                let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity;
 718                if !retain {
 719                    removed_groups.insert(diagnostic.group_id);
 720                    blocks_to_remove.insert(*block_id);
 721                }
 722                retain
 723            })
 724            .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id)))
 725            .collect::<Vec<_>>();
 726        new_diagnostics.retain(|(diagnostic_data, block_id)| {
 727            let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id);
 728            if !retain {
 729                if let Some(block_id) = block_id {
 730                    blocks_to_remove.insert(*block_id);
 731                }
 732            }
 733            retain
 734        });
 735        for (server_id, group) in buffer_snapshot
 736            .diagnostic_groups(server_to_update)
 737            .into_iter()
 738            .filter(|(_, group)| {
 739                group.entries[group.primary_ix].diagnostic.severity <= max_severity
 740            })
 741        {
 742            for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() {
 743                let new_data = DiagnosticData {
 744                    language_server_id: server_id,
 745                    is_primary: diagnostic_index == group.primary_ix,
 746                    entry: diagnostic.clone(),
 747                };
 748                let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| {
 749                    compare_data_locations(&probe.0, &new_data, &buffer_snapshot)
 750                });
 751                new_diagnostics.insert(i, (new_data, None));
 752            }
 753        }
 754
 755        let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min());
 756        Self {
 757            latest_excerpt_id,
 758            path_excerpts_borders,
 759            new_diagnostics,
 760            blocks_to_remove,
 761            diagnostics_by_row_label: BTreeMap::new(),
 762            excerpts_to_remove: Vec::new(),
 763            excerpts_with_new_diagnostics: HashSet::default(),
 764            unchanged_blocks: HashMap::default(),
 765            excerpts_to_add: HashMap::default(),
 766            excerpt_expands: HashMap::default(),
 767            first_excerpt_id: None,
 768            last_excerpt_id: None,
 769        }
 770    }
 771
 772    fn prepare_excerpt_data<'a>(
 773        &'a mut self,
 774        context: u32,
 775        multi_buffer_snapshot: MultiBufferSnapshot,
 776        buffer_snapshot: BufferSnapshot,
 777        current_diagnostics: impl Iterator<Item = &'a (DiagnosticData, BlockId)> + 'a,
 778    ) {
 779        let mut current_diagnostics = current_diagnostics.fuse().peekable();
 780        let mut excerpts_to_expand =
 781            HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::default();
 782        let mut current_excerpts = path_state_excerpts(
 783            self.path_excerpts_borders.0,
 784            self.path_excerpts_borders.1,
 785            &multi_buffer_snapshot,
 786        )
 787        .fuse()
 788        .peekable();
 789
 790        for (diagnostic_index, (new_diagnostic, existing_block)) in
 791            self.new_diagnostics.iter().enumerate()
 792        {
 793            if let Some(existing_block) = existing_block {
 794                self.unchanged_blocks
 795                    .insert(diagnostic_index, *existing_block);
 796            }
 797
 798            loop {
 799                match current_excerpts.peek() {
 800                    None => {
 801                        let excerpt_ranges = self
 802                            .excerpts_to_add
 803                            .entry(self.latest_excerpt_id)
 804                            .or_default();
 805                        let new_range = new_diagnostic.entry.range.clone();
 806                        let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| {
 807                            compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot)
 808                        });
 809                        excerpt_ranges.insert(i, new_range);
 810                        break;
 811                    }
 812                    Some((current_excerpt_id, _, current_excerpt_range)) => {
 813                        match compare_diagnostic_range_edges(
 814                            &current_excerpt_range.context,
 815                            &new_diagnostic.entry.range,
 816                            &buffer_snapshot,
 817                        ) {
 818                            /*
 819                                  new_s new_e
 820                            ----[---->><<----]--
 821                             cur_s         cur_e
 822                            */
 823                            (
 824                                Ordering::Less | Ordering::Equal,
 825                                Ordering::Greater | Ordering::Equal,
 826                            ) => {
 827                                self.excerpts_with_new_diagnostics
 828                                    .insert(*current_excerpt_id);
 829                                if self.first_excerpt_id.is_none() {
 830                                    self.first_excerpt_id = Some(*current_excerpt_id);
 831                                }
 832                                self.last_excerpt_id = Some(*current_excerpt_id);
 833                                break;
 834                            }
 835                            /*
 836                                  cur_s cur_e
 837                            ---->>>>>[--]<<<<<--
 838                             new_s         new_e
 839                            */
 840                            (
 841                                Ordering::Greater | Ordering::Equal,
 842                                Ordering::Less | Ordering::Equal,
 843                            ) => {
 844                                let expand_up = current_excerpt_range
 845                                    .context
 846                                    .start
 847                                    .to_point(&buffer_snapshot)
 848                                    .row
 849                                    .saturating_sub(
 850                                        new_diagnostic
 851                                            .entry
 852                                            .range
 853                                            .start
 854                                            .to_point(&buffer_snapshot)
 855                                            .row,
 856                                    );
 857                                let expand_down = new_diagnostic
 858                                    .entry
 859                                    .range
 860                                    .end
 861                                    .to_point(&buffer_snapshot)
 862                                    .row
 863                                    .saturating_sub(
 864                                        current_excerpt_range
 865                                            .context
 866                                            .end
 867                                            .to_point(&buffer_snapshot)
 868                                            .row,
 869                                    );
 870                                let expand_value = excerpts_to_expand
 871                                    .entry(*current_excerpt_id)
 872                                    .or_default()
 873                                    .entry(ExpandExcerptDirection::UpAndDown)
 874                                    .or_default();
 875                                *expand_value = (*expand_value).max(expand_up).max(expand_down);
 876                                self.excerpts_with_new_diagnostics
 877                                    .insert(*current_excerpt_id);
 878                                if self.first_excerpt_id.is_none() {
 879                                    self.first_excerpt_id = Some(*current_excerpt_id);
 880                                }
 881                                self.last_excerpt_id = Some(*current_excerpt_id);
 882                                break;
 883                            }
 884                            /*
 885                                    new_s   new_e
 886                                     >       <
 887                            ----[---->>>]<<<<<--
 888                             cur_s    cur_e
 889
 890                            or
 891                                      new_s new_e
 892                                        >    <
 893                            ----[----]-->>><<<--
 894                             cur_s cur_e
 895                            */
 896                            (Ordering::Less, Ordering::Less) => {
 897                                if current_excerpt_range
 898                                    .context
 899                                    .end
 900                                    .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot)
 901                                    .is_ge()
 902                                {
 903                                    let expand_down = new_diagnostic
 904                                        .entry
 905                                        .range
 906                                        .end
 907                                        .to_point(&buffer_snapshot)
 908                                        .row
 909                                        .saturating_sub(
 910                                            current_excerpt_range
 911                                                .context
 912                                                .end
 913                                                .to_point(&buffer_snapshot)
 914                                                .row,
 915                                        );
 916                                    let expand_value = excerpts_to_expand
 917                                        .entry(*current_excerpt_id)
 918                                        .or_default()
 919                                        .entry(ExpandExcerptDirection::Down)
 920                                        .or_default();
 921                                    *expand_value = (*expand_value).max(expand_down);
 922                                    self.excerpts_with_new_diagnostics
 923                                        .insert(*current_excerpt_id);
 924                                    if self.first_excerpt_id.is_none() {
 925                                        self.first_excerpt_id = Some(*current_excerpt_id);
 926                                    }
 927                                    self.last_excerpt_id = Some(*current_excerpt_id);
 928                                    break;
 929                                } else if !self
 930                                    .excerpts_with_new_diagnostics
 931                                    .contains(current_excerpt_id)
 932                                {
 933                                    self.excerpts_to_remove.push(*current_excerpt_id);
 934                                }
 935                            }
 936                            /*
 937                                  cur_s      cur_e
 938                            ---->>>>>[<<<<----]--
 939                                >        <
 940                               new_s    new_e
 941
 942                            or
 943                                      cur_s cur_e
 944                            ---->>><<<--[----]--
 945                                >    <
 946                               new_s new_e
 947                            */
 948                            (Ordering::Greater, Ordering::Greater) => {
 949                                if current_excerpt_range
 950                                    .context
 951                                    .start
 952                                    .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot)
 953                                    .is_le()
 954                                {
 955                                    let expand_up = current_excerpt_range
 956                                        .context
 957                                        .start
 958                                        .to_point(&buffer_snapshot)
 959                                        .row
 960                                        .saturating_sub(
 961                                            new_diagnostic
 962                                                .entry
 963                                                .range
 964                                                .start
 965                                                .to_point(&buffer_snapshot)
 966                                                .row,
 967                                        );
 968                                    let expand_value = excerpts_to_expand
 969                                        .entry(*current_excerpt_id)
 970                                        .or_default()
 971                                        .entry(ExpandExcerptDirection::Up)
 972                                        .or_default();
 973                                    *expand_value = (*expand_value).max(expand_up);
 974                                    self.excerpts_with_new_diagnostics
 975                                        .insert(*current_excerpt_id);
 976                                    if self.first_excerpt_id.is_none() {
 977                                        self.first_excerpt_id = Some(*current_excerpt_id);
 978                                    }
 979                                    self.last_excerpt_id = Some(*current_excerpt_id);
 980                                    break;
 981                                } else {
 982                                    let excerpt_ranges = self
 983                                        .excerpts_to_add
 984                                        .entry(self.latest_excerpt_id)
 985                                        .or_default();
 986                                    let new_range = new_diagnostic.entry.range.clone();
 987                                    let (Ok(i) | Err(i)) =
 988                                        excerpt_ranges.binary_search_by(|probe| {
 989                                            compare_diagnostic_ranges(
 990                                                probe,
 991                                                &new_range,
 992                                                &buffer_snapshot,
 993                                            )
 994                                        });
 995                                    excerpt_ranges.insert(i, new_range);
 996                                    break;
 997                                }
 998                            }
 999                        }
1000                        if let Some((next_id, ..)) = current_excerpts.next() {
1001                            self.latest_excerpt_id = next_id;
1002                        }
1003                    }
1004                }
1005            }
1006
1007            loop {
1008                match current_diagnostics.peek() {
1009                    None => break,
1010                    Some((current_diagnostic, current_block)) => {
1011                        match compare_data_locations(
1012                            current_diagnostic,
1013                            new_diagnostic,
1014                            &buffer_snapshot,
1015                        ) {
1016                            Ordering::Less => {
1017                                self.blocks_to_remove.insert(*current_block);
1018                            }
1019                            Ordering::Equal => {
1020                                if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) {
1021                                    self.unchanged_blocks
1022                                        .insert(diagnostic_index, *current_block);
1023                                } else {
1024                                    self.blocks_to_remove.insert(*current_block);
1025                                }
1026                                let _ = current_diagnostics.next();
1027                                break;
1028                            }
1029                            Ordering::Greater => break,
1030                        }
1031                        let _ = current_diagnostics.next();
1032                    }
1033                }
1034            }
1035        }
1036
1037        self.excerpts_to_remove.retain(|excerpt_id| {
1038            !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1039                && !excerpts_to_expand.contains_key(excerpt_id)
1040        });
1041        self.excerpts_to_remove.extend(
1042            current_excerpts
1043                .filter(|(excerpt_id, ..)| {
1044                    !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1045                        && !excerpts_to_expand.contains_key(excerpt_id)
1046                })
1047                .map(|(excerpt_id, ..)| excerpt_id),
1048        );
1049        let mut excerpt_expands = HashMap::default();
1050        for (excerpt_id, directions) in excerpts_to_expand {
1051            let excerpt_expand = if directions.len() > 1 {
1052                Some((
1053                    ExpandExcerptDirection::UpAndDown,
1054                    directions
1055                        .values()
1056                        .max()
1057                        .copied()
1058                        .unwrap_or_default()
1059                        .max(context),
1060                ))
1061            } else {
1062                directions
1063                    .into_iter()
1064                    .next()
1065                    .map(|(direction, expand)| (direction, expand.max(context)))
1066            };
1067            if let Some(expand) = excerpt_expand {
1068                excerpt_expands
1069                    .entry(expand)
1070                    .or_insert_with(|| Vec::new())
1071                    .push(excerpt_id);
1072            }
1073        }
1074        self.blocks_to_remove
1075            .extend(current_diagnostics.map(|(_, block_id)| block_id));
1076    }
1077
1078    fn apply_excerpt_changes(
1079        &mut self,
1080        path_state: &mut PathState,
1081        context: u32,
1082        buffer_snapshot: BufferSnapshot,
1083        multi_buffer: &mut MultiBuffer,
1084        buffer: Model<Buffer>,
1085        cx: &mut gpui::ModelContext<MultiBuffer>,
1086    ) {
1087        let max_point = buffer_snapshot.max_point();
1088        for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) {
1089            let ranges = ranges
1090                .into_iter()
1091                .map(|range| {
1092                    let mut extended_point_range = range.to_point(&buffer_snapshot);
1093                    extended_point_range.start.row =
1094                        extended_point_range.start.row.saturating_sub(context);
1095                    extended_point_range.start.column = 0;
1096                    extended_point_range.end.row =
1097                        (extended_point_range.end.row + context).min(max_point.row);
1098                    extended_point_range.end.column = u32::MAX;
1099                    let extended_start =
1100                        buffer_snapshot.clip_point(extended_point_range.start, Bias::Left);
1101                    let extended_end =
1102                        buffer_snapshot.clip_point(extended_point_range.end, Bias::Right);
1103                    extended_start..extended_end
1104                })
1105                .collect::<Vec<_>>();
1106            let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context);
1107            let excerpts = multi_buffer.insert_excerpts_after(
1108                after_excerpt_id,
1109                buffer.clone(),
1110                joined_ranges,
1111                cx,
1112            );
1113            if self.first_excerpt_id.is_none() {
1114                self.first_excerpt_id = excerpts.first().copied();
1115            }
1116            self.last_excerpt_id = excerpts.last().copied();
1117        }
1118        for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) {
1119            multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
1120        }
1121        multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx);
1122        path_state.first_excerpt_id = self.first_excerpt_id;
1123        path_state.last_excerpt_id = self.last_excerpt_id;
1124    }
1125
1126    fn prepare_blocks_to_insert(
1127        &mut self,
1128        editor: View<Editor>,
1129        multi_buffer_snapshot: MultiBufferSnapshot,
1130    ) -> Vec<BlockProperties<editor::Anchor>> {
1131        let mut updated_excerpts = path_state_excerpts(
1132            self.path_excerpts_borders.0,
1133            self.path_excerpts_borders.1,
1134            &multi_buffer_snapshot,
1135        )
1136        .fuse()
1137        .peekable();
1138        let mut used_labels = BTreeMap::new();
1139        self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold(
1140            BTreeMap::new(),
1141            |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| {
1142                let new_diagnostic = &diagnostic.entry;
1143                let block_position = new_diagnostic.range.start;
1144                let excerpt_id = loop {
1145                    match updated_excerpts.peek() {
1146                        None => break None,
1147                        Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => {
1148                            let excerpt_range = &excerpt_range.context;
1149                            match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot)
1150                            {
1151                                Ordering::Less => break None,
1152                                Ordering::Equal | Ordering::Greater => match block_position
1153                                    .cmp(&excerpt_range.end, excerpt_buffer_snapshot)
1154                                {
1155                                    Ordering::Equal | Ordering::Less => break Some(*excerpt_id),
1156                                    Ordering::Greater => {
1157                                        let _ = updated_excerpts.next();
1158                                    }
1159                                },
1160                            }
1161                        }
1162                    }
1163                };
1164
1165                let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| {
1166                    multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position)
1167                }) else {
1168                    return diagnostics_by_row_label;
1169                };
1170
1171                let multi_buffer_row = MultiBufferRow(
1172                    position_in_multi_buffer
1173                        .to_point(&multi_buffer_snapshot)
1174                        .row,
1175                );
1176
1177                let grouped_diagnostics = &mut diagnostics_by_row_label
1178                    .entry(multi_buffer_row)
1179                    .or_insert_with(|| (position_in_multi_buffer, Vec::new()))
1180                    .1;
1181                let new_label = used_labels
1182                    .entry(multi_buffer_row)
1183                    .or_insert_with(|| HashSet::default())
1184                    .insert((
1185                        new_diagnostic.diagnostic.source.as_deref(),
1186                        new_diagnostic.diagnostic.message.as_str(),
1187                    ));
1188
1189                if !new_label || !grouped_diagnostics.is_empty() {
1190                    if let Some(existing_block) = existing_block {
1191                        self.blocks_to_remove.insert(*existing_block);
1192                    }
1193                    if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) {
1194                        self.blocks_to_remove.insert(block_id);
1195                    }
1196                }
1197                if new_label {
1198                    let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| {
1199                        let a = &self.new_diagnostics[probe].0.entry.diagnostic;
1200                        let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic;
1201                        a.group_id
1202                            .cmp(&b.group_id)
1203                            .then_with(|| a.is_primary.cmp(&b.is_primary).reverse())
1204                            .then_with(|| a.severity.cmp(&b.severity))
1205                    });
1206                    grouped_diagnostics.insert(i, diagnostic_index);
1207                }
1208
1209                diagnostics_by_row_label
1210            },
1211        );
1212
1213        self.diagnostics_by_row_label
1214            .values()
1215            .filter_map(|(earliest_in_row_position, diagnostics_at_line)| {
1216                let earliest_in_row_position = *earliest_in_row_position;
1217                match diagnostics_at_line.len() {
1218                    0 => None,
1219                    len => {
1220                        if len == 1 {
1221                            let i = diagnostics_at_line.first().copied()?;
1222                            if self.unchanged_blocks.contains_key(&i) {
1223                                return None;
1224                            }
1225                        }
1226                        let lines_in_first_message = diagnostic_text_lines(
1227                            &self
1228                                .new_diagnostics
1229                                .get(diagnostics_at_line.first().copied()?)?
1230                                .0
1231                                .entry
1232                                .diagnostic,
1233                        );
1234                        let folded_block_height = lines_in_first_message.clamp(1, 2);
1235                        let diagnostics_to_render = Arc::new(
1236                            diagnostics_at_line
1237                                .iter()
1238                                .filter_map(|&index| self.new_diagnostics.get(index))
1239                                .map(|(diagnostic_data, _)| {
1240                                    diagnostic_data.entry.diagnostic.clone()
1241                                })
1242                                .collect::<Vec<_>>(),
1243                        );
1244                        Some(BlockProperties {
1245                            position: earliest_in_row_position,
1246                            height: folded_block_height,
1247                            style: BlockStyle::Sticky,
1248                            render: render_same_line_diagnostics(
1249                                Arc::new(AtomicBool::new(false)),
1250                                diagnostics_to_render,
1251                                editor.clone(),
1252                                folded_block_height,
1253                            ),
1254                            disposition: BlockDisposition::Above,
1255                        })
1256                    }
1257                }
1258            })
1259            .collect()
1260    }
1261
1262    fn new_blocks(mut self, new_block_ids: Vec<BlockId>) -> Vec<(DiagnosticData, BlockId)> {
1263        let mut new_block_ids = new_block_ids.into_iter().fuse();
1264        for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label {
1265            let mut created_block_id = None;
1266            match grouped_diagnostics.len() {
1267                0 => {
1268                    debug_panic!("Unexpected empty diagnostics group");
1269                    continue;
1270                }
1271                1 => {
1272                    let index = grouped_diagnostics[0];
1273                    if let Some(&block_id) = self.unchanged_blocks.get(&index) {
1274                        self.new_diagnostics[index].1 = Some(block_id);
1275                    } else {
1276                        let Some(block_id) =
1277                            created_block_id.get_or_insert_with(|| new_block_ids.next())
1278                        else {
1279                            debug_panic!("Expected a new block for each new diagnostic");
1280                            continue;
1281                        };
1282                        self.new_diagnostics[index].1 = Some(*block_id);
1283                    }
1284                }
1285                _ => {
1286                    let Some(block_id) =
1287                        created_block_id.get_or_insert_with(|| new_block_ids.next())
1288                    else {
1289                        debug_panic!("Expected a new block for each new diagnostic group");
1290                        continue;
1291                    };
1292                    for i in grouped_diagnostics {
1293                        self.new_diagnostics[i].1 = Some(*block_id);
1294                    }
1295                }
1296            }
1297        }
1298
1299        self.new_diagnostics
1300            .into_iter()
1301            .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?)))
1302            .collect()
1303    }
1304}
1305
1306fn render_same_line_diagnostics(
1307    expanded: Arc<AtomicBool>,
1308    diagnostics: Arc<Vec<language::Diagnostic>>,
1309    editor_handle: View<Editor>,
1310    folded_block_height: u8,
1311) -> RenderBlock {
1312    Box::new(move |cx: &mut BlockContext| {
1313        let block_id = match cx.transform_block_id {
1314            TransformBlockId::Block(block_id) => block_id,
1315            _ => {
1316                debug_panic!("Expected a block id for the diagnostics block");
1317                return div().into_any_element();
1318            }
1319        };
1320        let Some(first_diagnostic) = diagnostics.first() else {
1321            debug_panic!("Expected at least one diagnostic");
1322            return div().into_any_element();
1323        };
1324        let button_expanded = expanded.clone();
1325        let expanded = expanded.load(atomic::Ordering::Acquire);
1326        let expand_label = if expanded { '-' } else { '+' };
1327        let first_diagnostics_height = diagnostic_text_lines(first_diagnostic);
1328        let extra_diagnostics = diagnostics.len() - 1;
1329        let toggle_expand_label =
1330            if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 {
1331                None
1332            } else if extra_diagnostics > 0 {
1333                Some(format!("{expand_label}{extra_diagnostics}"))
1334            } else {
1335                Some(expand_label.to_string())
1336            };
1337
1338        let expanded_block_height = diagnostics
1339            .iter()
1340            .map(|diagnostic| diagnostic_text_lines(diagnostic))
1341            .sum::<u8>();
1342        let editor_handle = editor_handle.clone();
1343        let mut parent = v_flex();
1344        let mut diagnostics_iter = diagnostics.iter().fuse();
1345        if let Some(first_diagnostic) = diagnostics_iter.next() {
1346            let mut renderer = diagnostic_block_renderer(
1347                first_diagnostic.clone(),
1348                Some(folded_block_height),
1349                false,
1350                true,
1351            );
1352            parent = parent.child(
1353                h_flex()
1354                    .when_some(toggle_expand_label, |parent, label| {
1355                        parent.child(Button::new(cx.transform_block_id, label).on_click({
1356                            let diagnostics = Arc::clone(&diagnostics);
1357                            move |_, cx| {
1358                                let new_expanded = !expanded;
1359                                button_expanded.store(new_expanded, atomic::Ordering::Release);
1360                                let new_size = if new_expanded {
1361                                    expanded_block_height
1362                                } else {
1363                                    folded_block_height
1364                                };
1365                                editor_handle.update(cx, |editor, cx| {
1366                                    editor.replace_blocks(
1367                                        HashMap::from_iter(Some((
1368                                            block_id,
1369                                            (
1370                                                Some(new_size),
1371                                                render_same_line_diagnostics(
1372                                                    Arc::clone(&button_expanded),
1373                                                    Arc::clone(&diagnostics),
1374                                                    editor_handle.clone(),
1375                                                    folded_block_height,
1376                                                ),
1377                                            ),
1378                                        ))),
1379                                        None,
1380                                        cx,
1381                                    )
1382                                });
1383                            }
1384                        }))
1385                    })
1386                    .child(renderer(cx)),
1387            );
1388        }
1389        if expanded {
1390            for diagnostic in diagnostics_iter {
1391                let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
1392                parent = parent.child(renderer(cx));
1393            }
1394        }
1395        parent.into_any_element()
1396    })
1397}
1398
1399fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 {
1400    diagnostic.message.matches('\n').count() as u8 + 1
1401}
1402
1403fn path_state_excerpts(
1404    after_excerpt_id: Option<ExcerptId>,
1405    before_excerpt_id: Option<ExcerptId>,
1406    multi_buffer_snapshot: &editor::MultiBufferSnapshot,
1407) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
1408    multi_buffer_snapshot
1409        .excerpts()
1410        .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id {
1411            Some(after_excerpt_id) => after_excerpt_id != excerpt_id,
1412            None => false,
1413        })
1414        .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id))
1415        .take_while(move |&(excerpt_id, ..)| match before_excerpt_id {
1416            Some(before_excerpt_id) => before_excerpt_id != excerpt_id,
1417            None => true,
1418        })
1419}