diagnostics.rs

   1mod cargo;
   2pub mod items;
   3mod toolbar_controls;
   4
   5mod diagnostic_renderer;
   6
   7#[cfg(test)]
   8mod diagnostics_tests;
   9
  10use anyhow::Result;
  11use cargo::{
  12    FetchStatus, FetchUpdate, cargo_diagnostics_sources, fetch_worktree_diagnostics,
  13    is_outdated_cargo_fetch_diagnostic, map_rust_diagnostic_to_lsp, next_cargo_fetch_generation,
  14    url_from_abs_path,
  15};
  16use collections::{BTreeSet, HashMap, HashSet};
  17use diagnostic_renderer::DiagnosticBlock;
  18use editor::{
  19    DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
  20    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
  21    scroll::Autoscroll,
  22};
  23use gpui::{
  24    AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
  25    Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
  26    Subscription, Task, WeakEntity, Window, actions, div,
  27};
  28use language::{
  29    Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
  30};
  31use lsp::{DiagnosticSeverity, LanguageServerId};
  32use project::{
  33    DiagnosticSummary, Project, ProjectPath, Worktree,
  34    lsp_store::rust_analyzer_ext::{CARGO_DIAGNOSTICS_SOURCE_NAME, RUST_ANALYZER_NAME},
  35    project_settings::ProjectSettings,
  36};
  37use settings::Settings;
  38use std::{
  39    any::{Any, TypeId},
  40    cmp::{self, Ordering},
  41    ops::{Range, RangeInclusive},
  42    sync::Arc,
  43    time::Duration,
  44};
  45use text::{BufferId, OffsetRangeExt};
  46use theme::ActiveTheme;
  47pub use toolbar_controls::ToolbarControls;
  48use ui::{Icon, IconName, Label, h_flex, prelude::*};
  49use util::ResultExt;
  50use workspace::{
  51    ItemNavHistory, ToolbarItemLocation, Workspace,
  52    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
  53    searchable::SearchableItemHandle,
  54};
  55
  56actions!(
  57    diagnostics,
  58    [Deploy, ToggleWarnings, ToggleDiagnosticsRefresh]
  59);
  60
  61#[derive(Default)]
  62pub(crate) struct IncludeWarnings(bool);
  63impl Global for IncludeWarnings {}
  64
  65pub fn init(cx: &mut App) {
  66    editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
  67    cx.observe_new(ProjectDiagnosticsEditor::register).detach();
  68}
  69
  70pub(crate) struct ProjectDiagnosticsEditor {
  71    project: Entity<Project>,
  72    workspace: WeakEntity<Workspace>,
  73    focus_handle: FocusHandle,
  74    editor: Entity<Editor>,
  75    diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
  76    blocks: HashMap<BufferId, Vec<CustomBlockId>>,
  77    summary: DiagnosticSummary,
  78    multibuffer: Entity<MultiBuffer>,
  79    paths_to_update: BTreeSet<ProjectPath>,
  80    include_warnings: bool,
  81    update_excerpts_task: Option<Task<Result<()>>>,
  82    cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
  83    _subscription: Subscription,
  84}
  85
  86struct CargoDiagnosticsFetchState {
  87    task: Option<Task<()>>,
  88    rust_analyzer: Option<LanguageServerId>,
  89}
  90
  91impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
  92
  93const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
  94
  95impl Render for ProjectDiagnosticsEditor {
  96    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  97        let warning_count = if self.include_warnings {
  98            self.summary.warning_count
  99        } else {
 100            0
 101        };
 102
 103        let child = if warning_count + self.summary.error_count == 0 {
 104            let label = if self.summary.warning_count == 0 {
 105                SharedString::new_static("No problems in workspace")
 106            } else {
 107                SharedString::new_static("No errors in workspace")
 108            };
 109            v_flex()
 110                .key_context("EmptyPane")
 111                .size_full()
 112                .gap_1()
 113                .justify_center()
 114                .items_center()
 115                .text_center()
 116                .bg(cx.theme().colors().editor_background)
 117                .child(Label::new(label).color(Color::Muted))
 118                .when(self.summary.warning_count > 0, |this| {
 119                    let plural_suffix = if self.summary.warning_count > 1 {
 120                        "s"
 121                    } else {
 122                        ""
 123                    };
 124                    let label = format!(
 125                        "Show {} warning{}",
 126                        self.summary.warning_count, plural_suffix
 127                    );
 128                    this.child(
 129                        Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
 130                            |this, _, window, cx| {
 131                                this.toggle_warnings(&Default::default(), window, cx);
 132                                cx.notify();
 133                            },
 134                        )),
 135                    )
 136                })
 137        } else {
 138            div().size_full().child(self.editor.clone())
 139        };
 140
 141        div()
 142            .key_context("Diagnostics")
 143            .track_focus(&self.focus_handle(cx))
 144            .size_full()
 145            .on_action(cx.listener(Self::toggle_warnings))
 146            .on_action(cx.listener(Self::toggle_diagnostics_refresh))
 147            .child(child)
 148    }
 149}
 150
 151impl ProjectDiagnosticsEditor {
 152    fn register(
 153        workspace: &mut Workspace,
 154        _window: Option<&mut Window>,
 155        _: &mut Context<Workspace>,
 156    ) {
 157        workspace.register_action(Self::deploy);
 158    }
 159
 160    fn new(
 161        include_warnings: bool,
 162        project_handle: Entity<Project>,
 163        workspace: WeakEntity<Workspace>,
 164        window: &mut Window,
 165        cx: &mut Context<Self>,
 166    ) -> Self {
 167        let project_event_subscription =
 168            cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event {
 169                project::Event::DiskBasedDiagnosticsStarted { .. } => {
 170                    cx.notify();
 171                }
 172                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 173                    log::debug!("disk based diagnostics finished for server {language_server_id}");
 174                    this.update_stale_excerpts(window, cx);
 175                }
 176                project::Event::DiagnosticsUpdated {
 177                    language_server_id,
 178                    path,
 179                } => {
 180                    this.paths_to_update.insert(path.clone());
 181                    this.summary = project.read(cx).diagnostic_summary(false, cx);
 182                    cx.emit(EditorEvent::TitleChanged);
 183
 184                    if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
 185                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
 186                    } else {
 187                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
 188                        this.update_stale_excerpts(window, cx);
 189                    }
 190                }
 191                _ => {}
 192            });
 193
 194        let focus_handle = cx.focus_handle();
 195        cx.on_focus_in(&focus_handle, window, |this, window, cx| {
 196            this.focus_in(window, cx)
 197        })
 198        .detach();
 199        cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
 200            this.focus_out(window, cx)
 201        })
 202        .detach();
 203
 204        let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
 205        let editor = cx.new(|cx| {
 206            let mut editor =
 207                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
 208            editor.set_vertical_scroll_margin(5, cx);
 209            editor.disable_inline_diagnostics();
 210            editor.set_all_diagnostics_active(cx);
 211            editor
 212        });
 213        cx.subscribe_in(
 214            &editor,
 215            window,
 216            |this, _editor, event: &EditorEvent, window, cx| {
 217                cx.emit(event.clone());
 218                match event {
 219                    EditorEvent::Focused => {
 220                        if this.multibuffer.read(cx).is_empty() {
 221                            window.focus(&this.focus_handle);
 222                        }
 223                    }
 224                    EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
 225                    _ => {}
 226                }
 227            },
 228        )
 229        .detach();
 230        cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
 231            this.include_warnings = cx.global::<IncludeWarnings>().0;
 232            this.diagnostics.clear();
 233            this.update_all_diagnostics(window, cx);
 234        })
 235        .detach();
 236        cx.observe_release(&cx.entity(), |editor, _, cx| {
 237            editor.stop_cargo_diagnostics_fetch(cx);
 238        })
 239        .detach();
 240
 241        let project = project_handle.read(cx);
 242        let mut this = Self {
 243            project: project_handle.clone(),
 244            summary: project.diagnostic_summary(false, cx),
 245            diagnostics: Default::default(),
 246            blocks: Default::default(),
 247            include_warnings,
 248            workspace,
 249            multibuffer: excerpts,
 250            focus_handle,
 251            editor,
 252            paths_to_update: Default::default(),
 253            update_excerpts_task: None,
 254            cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
 255                task: None,
 256                rust_analyzer: None,
 257            },
 258            _subscription: project_event_subscription,
 259        };
 260        this.update_all_diagnostics(window, cx);
 261        this
 262    }
 263
 264    fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 265        if self.update_excerpts_task.is_some() {
 266            return;
 267        }
 268
 269        let project_handle = self.project.clone();
 270        self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
 271            cx.background_executor()
 272                .timer(DIAGNOSTICS_UPDATE_DELAY)
 273                .await;
 274            loop {
 275                let Some(path) = this.update(cx, |this, cx| {
 276                    let Some(path) = this.paths_to_update.pop_first() else {
 277                        this.update_excerpts_task = None;
 278                        cx.notify();
 279                        return None;
 280                    };
 281                    Some(path)
 282                })?
 283                else {
 284                    break;
 285                };
 286
 287                if let Some(buffer) = project_handle
 288                    .update(cx, |project, cx| project.open_buffer(path.clone(), cx))?
 289                    .await
 290                    .log_err()
 291                {
 292                    this.update_in(cx, |this, window, cx| {
 293                        this.update_excerpts(buffer, window, cx)
 294                    })?
 295                    .await?;
 296                }
 297            }
 298            Ok(())
 299        }));
 300    }
 301
 302    fn deploy(
 303        workspace: &mut Workspace,
 304        _: &Deploy,
 305        window: &mut Window,
 306        cx: &mut Context<Workspace>,
 307    ) {
 308        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 309            let is_active = workspace
 310                .active_item(cx)
 311                .is_some_and(|item| item.item_id() == existing.item_id());
 312            workspace.activate_item(&existing, true, !is_active, window, cx);
 313        } else {
 314            let workspace_handle = cx.entity().downgrade();
 315
 316            let include_warnings = match cx.try_global::<IncludeWarnings>() {
 317                Some(include_warnings) => include_warnings.0,
 318                None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
 319            };
 320
 321            let diagnostics = cx.new(|cx| {
 322                ProjectDiagnosticsEditor::new(
 323                    include_warnings,
 324                    workspace.project().clone(),
 325                    workspace_handle,
 326                    window,
 327                    cx,
 328                )
 329            });
 330            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, window, cx);
 331        }
 332    }
 333
 334    fn toggle_warnings(&mut self, _: &ToggleWarnings, _: &mut Window, cx: &mut Context<Self>) {
 335        cx.set_global(IncludeWarnings(!self.include_warnings));
 336    }
 337
 338    fn toggle_diagnostics_refresh(
 339        &mut self,
 340        _: &ToggleDiagnosticsRefresh,
 341        window: &mut Window,
 342        cx: &mut Context<Self>,
 343    ) {
 344        let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
 345            .diagnostics
 346            .fetch_cargo_diagnostics();
 347
 348        if fetch_cargo_diagnostics {
 349            if self.cargo_diagnostics_fetch.task.is_some() {
 350                self.stop_cargo_diagnostics_fetch(cx);
 351            } else {
 352                self.update_all_diagnostics(window, cx);
 353            }
 354        } else {
 355            if self.update_excerpts_task.is_some() {
 356                self.update_excerpts_task = None;
 357            } else {
 358                self.update_all_diagnostics(window, cx);
 359            }
 360        }
 361        cx.notify();
 362    }
 363
 364    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 365        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
 366            self.editor.focus_handle(cx).focus(window)
 367        }
 368    }
 369
 370    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 371        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
 372        {
 373            self.update_stale_excerpts(window, cx);
 374        }
 375    }
 376
 377    fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 378        let cargo_diagnostics_sources = cargo_diagnostics_sources(self, cx);
 379        if cargo_diagnostics_sources.is_empty() {
 380            self.update_all_excerpts(window, cx);
 381        } else {
 382            self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), window, cx);
 383        }
 384    }
 385
 386    fn fetch_cargo_diagnostics(
 387        &mut self,
 388        diagnostics_sources: Arc<Vec<Entity<Worktree>>>,
 389        window: &mut Window,
 390        cx: &mut Context<Self>,
 391    ) {
 392        self.cargo_diagnostics_fetch.task = Some(cx.spawn_in(window, async move |editor, cx| {
 393            let rust_analyzer_server = editor
 394                .update(cx, |editor, cx| {
 395                    editor
 396                        .project
 397                        .read(cx)
 398                        .language_server_with_name(RUST_ANALYZER_NAME, cx)
 399                })
 400                .ok();
 401            let rust_analyzer_server = match rust_analyzer_server {
 402                Some(rust_analyzer_server) => rust_analyzer_server.await,
 403                None => None,
 404            };
 405
 406            let mut worktree_diagnostics_tasks = Vec::new();
 407            let mut paths_with_reported_cargo_diagnostics = HashSet::default();
 408            if let Some(rust_analyzer_server) = rust_analyzer_server {
 409                let can_continue = editor
 410                    .update(cx, |editor, cx| {
 411                        editor.cargo_diagnostics_fetch.rust_analyzer = Some(rust_analyzer_server);
 412                        let status_inserted =
 413                            editor
 414                                .project
 415                                .read(cx)
 416                                .lsp_store()
 417                                .update(cx, |lsp_store, cx| {
 418                                    if let Some(rust_analyzer_status) = lsp_store
 419                                        .language_server_statuses
 420                                        .get_mut(&rust_analyzer_server)
 421                                    {
 422                                        rust_analyzer_status
 423                                            .progress_tokens
 424                                            .insert(fetch_cargo_diagnostics_token());
 425                                        paths_with_reported_cargo_diagnostics.extend(editor.diagnostics.iter().filter_map(|(buffer_id, diagnostics)| {
 426                                            if diagnostics.iter().any(|d| d.diagnostic.source.as_deref() == Some(CARGO_DIAGNOSTICS_SOURCE_NAME)) {
 427                                                Some(*buffer_id)
 428                                            } else {
 429                                                None
 430                                            }
 431                                        }).filter_map(|buffer_id| {
 432                                            let buffer = lsp_store.buffer_store().read(cx).get(buffer_id)?;
 433                                            let path = buffer.read(cx).file()?.as_local()?.abs_path(cx);
 434                                            Some(url_from_abs_path(&path))
 435                                        }));
 436                                        true
 437                                    } else {
 438                                        false
 439                                    }
 440                                });
 441                        if status_inserted {
 442                            editor.update_cargo_fetch_status(FetchStatus::Started, cx);
 443                            next_cargo_fetch_generation();
 444                            true
 445                        } else {
 446                            false
 447                        }
 448                    })
 449                    .unwrap_or(false);
 450
 451                if can_continue {
 452                    for worktree in diagnostics_sources.iter() {
 453                        if let Some(((_task, worktree_diagnostics), worktree_root)) = cx
 454                            .update(|_, cx| {
 455                                let worktree_root = worktree.read(cx).abs_path();
 456                                log::info!("Fetching cargo diagnostics for {worktree_root:?}");
 457                                fetch_worktree_diagnostics(&worktree_root, cx)
 458                                    .zip(Some(worktree_root))
 459                            })
 460                            .ok()
 461                            .flatten()
 462                        {
 463                            let editor = editor.clone();
 464                            worktree_diagnostics_tasks.push(cx.spawn(async move |cx| {
 465                                let _task = _task;
 466                                let mut file_diagnostics = HashMap::default();
 467                                let mut diagnostics_total = 0;
 468                                let mut updated_urls = HashSet::default();
 469                                while let Ok(fetch_update) = worktree_diagnostics.recv().await {
 470                                    match fetch_update {
 471                                        FetchUpdate::Diagnostic(diagnostic) => {
 472                                            for (url, diagnostic) in map_rust_diagnostic_to_lsp(
 473                                                &worktree_root,
 474                                                &diagnostic,
 475                                            ) {
 476                                                let file_diagnostics = file_diagnostics
 477                                                    .entry(url)
 478                                                    .or_insert_with(Vec::<lsp::Diagnostic>::new);
 479                                                let i = file_diagnostics
 480                                                    .binary_search_by(|probe| {
 481                                                        probe.range.start.cmp(&diagnostic.range.start)
 482                                                            .then(probe.range.end.cmp(&diagnostic.range.end))
 483                                                            .then(Ordering::Greater)
 484                                                    })
 485                                                    .unwrap_or_else(|i| i);
 486                                                file_diagnostics.insert(i, diagnostic);
 487                                            }
 488
 489                                            let file_changed = file_diagnostics.len() > 1;
 490                                            if file_changed {
 491                                                if editor
 492                                                    .update_in(cx, |editor, window, cx| {
 493                                                        editor
 494                                                            .project
 495                                                            .read(cx)
 496                                                            .lsp_store()
 497                                                            .update(cx, |lsp_store, cx| {
 498                                                                for (uri, mut diagnostics) in
 499                                                                    file_diagnostics.drain()
 500                                                                {
 501                                                                    diagnostics.dedup();
 502                                                                    diagnostics_total += diagnostics.len();
 503                                                                    updated_urls.insert(uri.clone());
 504
 505                                                                    lsp_store.merge_diagnostics(
 506                                                                    rust_analyzer_server,
 507                                                                    lsp::PublishDiagnosticsParams {
 508                                                                        uri,
 509                                                                        diagnostics,
 510                                                                        version: None,
 511                                                                    },
 512                                                                    &[],
 513                                                                    |diagnostic, _| {
 514                                                                        !is_outdated_cargo_fetch_diagnostic(diagnostic)
 515                                                                    },
 516                                                                    cx,
 517                                                                )?;
 518                                                                }
 519                                                                anyhow::Ok(())
 520                                                            })?;
 521                                                        editor.update_all_excerpts(window, cx);
 522                                                        anyhow::Ok(())
 523                                                    })
 524                                                    .ok()
 525                                                    .transpose()
 526                                                    .ok()
 527                                                    .flatten()
 528                                                    .is_none()
 529                                                {
 530                                                    break;
 531                                                }
 532                                            }
 533                                        }
 534                                        FetchUpdate::Progress(message) => {
 535                                            if editor
 536                                                .update(cx, |editor, cx| {
 537                                                    editor.update_cargo_fetch_status(
 538                                                        FetchStatus::Progress { message },
 539                                                        cx,
 540                                                    );
 541                                                })
 542                                                .is_err()
 543                                            {
 544                                                return updated_urls;
 545                                            }
 546                                        }
 547                                    }
 548                                }
 549
 550                                editor
 551                                    .update_in(cx, |editor, window, cx| {
 552                                        editor
 553                                            .project
 554                                            .read(cx)
 555                                            .lsp_store()
 556                                            .update(cx, |lsp_store, cx| {
 557                                                for (uri, mut diagnostics) in
 558                                                    file_diagnostics.drain()
 559                                                {
 560                                                    diagnostics.dedup();
 561                                                    diagnostics_total += diagnostics.len();
 562                                                    updated_urls.insert(uri.clone());
 563
 564                                                    lsp_store.merge_diagnostics(
 565                                                        rust_analyzer_server,
 566                                                        lsp::PublishDiagnosticsParams {
 567                                                            uri,
 568                                                            diagnostics,
 569                                                            version: None,
 570                                                        },
 571                                                        &[],
 572                                                        |diagnostic, _| {
 573                                                            !is_outdated_cargo_fetch_diagnostic(diagnostic)
 574                                                        },
 575                                                        cx,
 576                                                    )?;
 577                                                }
 578                                                anyhow::Ok(())
 579                                            })?;
 580                                        editor.update_all_excerpts(window, cx);
 581                                        anyhow::Ok(())
 582                                    })
 583                                    .ok();
 584                                log::info!("Fetched {diagnostics_total} cargo diagnostics for worktree {worktree_root:?}");
 585                                updated_urls
 586                            }));
 587                        }
 588                    }
 589                } else {
 590                    log::info!(
 591                        "No rust-analyzer language server found, skipping diagnostics fetch"
 592                    );
 593                }
 594            }
 595
 596
 597            let updated_urls = futures::future::join_all(worktree_diagnostics_tasks).await.into_iter().flatten().collect();
 598            if let Some(rust_analyzer_server) = rust_analyzer_server {
 599            editor
 600                .update_in(cx, |editor, window, cx| {
 601                    editor
 602                        .project
 603                        .read(cx)
 604                        .lsp_store()
 605                        .update(cx, |lsp_store, cx| {
 606                            for uri_to_cleanup in paths_with_reported_cargo_diagnostics.difference(&updated_urls).cloned() {
 607                                lsp_store.merge_diagnostics(
 608                                    rust_analyzer_server,
 609                                    lsp::PublishDiagnosticsParams {
 610                                        uri: uri_to_cleanup,
 611                                        diagnostics: Vec::new(),
 612                                        version: None,
 613                                    },
 614                                    &[],
 615                                    |diagnostic, _| {
 616                                        !is_outdated_cargo_fetch_diagnostic(diagnostic)
 617                                    },
 618                                    cx,
 619                                ).ok();
 620                            }
 621                        });
 622                    editor.update_all_excerpts(window, cx);
 623
 624                    editor.stop_cargo_diagnostics_fetch(cx);
 625                    cx.notify();
 626                })
 627                .ok();
 628            }
 629        }));
 630    }
 631
 632    fn update_cargo_fetch_status(&self, status: FetchStatus, cx: &mut App) {
 633        let Some(rust_analyzer) = self.cargo_diagnostics_fetch.rust_analyzer else {
 634            return;
 635        };
 636
 637        let work_done = match status {
 638            FetchStatus::Started => lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
 639                title: "cargo".to_string(),
 640                cancellable: None,
 641                message: Some("Fetching cargo diagnostics".to_string()),
 642                percentage: None,
 643            }),
 644            FetchStatus::Progress { message } => {
 645                lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
 646                    message: Some(message),
 647                    cancellable: None,
 648                    percentage: None,
 649                })
 650            }
 651            FetchStatus::Finished => {
 652                lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message: None })
 653            }
 654        };
 655        let progress = lsp::ProgressParams {
 656            token: lsp::NumberOrString::String(fetch_cargo_diagnostics_token()),
 657            value: lsp::ProgressParamsValue::WorkDone(work_done),
 658        };
 659
 660        self.project
 661            .read(cx)
 662            .lsp_store()
 663            .update(cx, |lsp_store, cx| {
 664                lsp_store.on_lsp_progress(progress, rust_analyzer, None, cx)
 665            });
 666    }
 667
 668    fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
 669        self.update_cargo_fetch_status(FetchStatus::Finished, cx);
 670        self.cargo_diagnostics_fetch.task = None;
 671        log::info!("Finished fetching cargo diagnostics");
 672    }
 673
 674    /// Enqueue an update of all excerpts. Updates all paths that either
 675    /// currently have diagnostics or are currently present in this view.
 676    fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 677        self.project.update(cx, |project, cx| {
 678            let mut paths = project
 679                .diagnostic_summaries(false, cx)
 680                .map(|(path, _, _)| path)
 681                .collect::<BTreeSet<_>>();
 682            self.multibuffer.update(cx, |multibuffer, cx| {
 683                for buffer in multibuffer.all_buffers() {
 684                    if let Some(file) = buffer.read(cx).file() {
 685                        paths.insert(ProjectPath {
 686                            path: file.path().clone(),
 687                            worktree_id: file.worktree_id(cx),
 688                        });
 689                    }
 690                }
 691            });
 692            self.paths_to_update = paths;
 693        });
 694        self.update_stale_excerpts(window, cx);
 695    }
 696
 697    fn diagnostics_are_unchanged(
 698        &self,
 699        existing: &Vec<DiagnosticEntry<text::Anchor>>,
 700        new: &Vec<DiagnosticEntry<text::Anchor>>,
 701        snapshot: &BufferSnapshot,
 702    ) -> bool {
 703        if existing.len() != new.len() {
 704            return false;
 705        }
 706        existing.iter().zip(new.iter()).all(|(existing, new)| {
 707            existing.diagnostic.message == new.diagnostic.message
 708                && existing.diagnostic.severity == new.diagnostic.severity
 709                && existing.diagnostic.is_primary == new.diagnostic.is_primary
 710                && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
 711        })
 712    }
 713
 714    fn update_excerpts(
 715        &mut self,
 716        buffer: Entity<Buffer>,
 717        window: &mut Window,
 718        cx: &mut Context<Self>,
 719    ) -> Task<Result<()>> {
 720        let was_empty = self.multibuffer.read(cx).is_empty();
 721        let buffer_snapshot = buffer.read(cx).snapshot();
 722        let buffer_id = buffer_snapshot.remote_id();
 723        let max_severity = if self.include_warnings {
 724            DiagnosticSeverity::WARNING
 725        } else {
 726            DiagnosticSeverity::ERROR
 727        };
 728
 729        cx.spawn_in(window, async move |this, mut cx| {
 730            let diagnostics = buffer_snapshot
 731                .diagnostics_in_range::<_, text::Anchor>(
 732                    Point::zero()..buffer_snapshot.max_point(),
 733                    false,
 734                )
 735                .collect::<Vec<_>>();
 736            let unchanged = this.update(cx, |this, _| {
 737                if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
 738                    this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
 739                }) {
 740                    return true;
 741                }
 742                this.diagnostics.insert(buffer_id, diagnostics.clone());
 743                return false;
 744            })?;
 745            if unchanged {
 746                return Ok(());
 747            }
 748
 749            let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
 750            for entry in diagnostics {
 751                grouped
 752                    .entry(entry.diagnostic.group_id)
 753                    .or_default()
 754                    .push(DiagnosticEntry {
 755                        range: entry.range.to_point(&buffer_snapshot),
 756                        diagnostic: entry.diagnostic,
 757                    })
 758            }
 759            let mut blocks: Vec<DiagnosticBlock> = Vec::new();
 760
 761            for (_, group) in grouped {
 762                let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
 763                if group_severity.is_none_or(|s| s > max_severity) {
 764                    continue;
 765                }
 766                let more = cx.update(|_, cx| {
 767                    crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
 768                        group,
 769                        buffer_snapshot.remote_id(),
 770                        Some(this.clone()),
 771                        cx,
 772                    )
 773                })?;
 774
 775                for item in more {
 776                    let i = blocks
 777                        .binary_search_by(|probe| {
 778                            probe
 779                                .initial_range
 780                                .start
 781                                .cmp(&item.initial_range.start)
 782                                .then(probe.initial_range.end.cmp(&item.initial_range.end))
 783                                .then(Ordering::Greater)
 784                        })
 785                        .unwrap_or_else(|i| i);
 786                    blocks.insert(i, item);
 787                }
 788            }
 789
 790            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
 791            for b in blocks.iter() {
 792                let excerpt_range = context_range_for_entry(
 793                    b.initial_range.clone(),
 794                    DEFAULT_MULTIBUFFER_CONTEXT,
 795                    buffer_snapshot.clone(),
 796                    &mut cx,
 797                )
 798                .await;
 799                let i = excerpt_ranges
 800                    .binary_search_by(|probe| {
 801                        probe
 802                            .context
 803                            .start
 804                            .cmp(&excerpt_range.start)
 805                            .then(probe.context.end.cmp(&excerpt_range.end))
 806                            .then(probe.primary.start.cmp(&b.initial_range.start))
 807                            .then(probe.primary.end.cmp(&b.initial_range.end))
 808                            .then(cmp::Ordering::Greater)
 809                    })
 810                    .unwrap_or_else(|i| i);
 811                excerpt_ranges.insert(
 812                    i,
 813                    ExcerptRange {
 814                        context: excerpt_range,
 815                        primary: b.initial_range.clone(),
 816                    },
 817                )
 818            }
 819
 820            this.update_in(cx, |this, window, cx| {
 821                if let Some(block_ids) = this.blocks.remove(&buffer_id) {
 822                    this.editor.update(cx, |editor, cx| {
 823                        editor.display_map.update(cx, |display_map, cx| {
 824                            display_map.remove_blocks(block_ids.into_iter().collect(), cx)
 825                        });
 826                    })
 827                }
 828                let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
 829                    multi_buffer.set_excerpt_ranges_for_path(
 830                        PathKey::for_buffer(&buffer, cx),
 831                        buffer.clone(),
 832                        &buffer_snapshot,
 833                        excerpt_ranges,
 834                        cx,
 835                    )
 836                });
 837                #[cfg(test)]
 838                let cloned_blocks = blocks.clone();
 839
 840                if was_empty {
 841                    if let Some(anchor_range) = anchor_ranges.first() {
 842                        let range_to_select = anchor_range.start..anchor_range.start;
 843                        this.editor.update(cx, |editor, cx| {
 844                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 845                                s.select_anchor_ranges([range_to_select]);
 846                            })
 847                        });
 848                        if this.focus_handle.is_focused(window) {
 849                            this.editor.read(cx).focus_handle(cx).focus(window);
 850                        }
 851                    }
 852                }
 853
 854                let editor_blocks =
 855                    anchor_ranges
 856                        .into_iter()
 857                        .zip(blocks.into_iter())
 858                        .map(|(anchor, block)| {
 859                            let editor = this.editor.downgrade();
 860                            BlockProperties {
 861                                placement: BlockPlacement::Near(anchor.start),
 862                                height: Some(1),
 863                                style: BlockStyle::Flex,
 864                                render: Arc::new(move |bcx| {
 865                                    block.render_block(editor.clone(), bcx)
 866                                }),
 867                                priority: 1,
 868                            }
 869                        });
 870                let block_ids = this.editor.update(cx, |editor, cx| {
 871                    editor.display_map.update(cx, |display_map, cx| {
 872                        display_map.insert_blocks(editor_blocks, cx)
 873                    })
 874                });
 875
 876                #[cfg(test)]
 877                {
 878                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
 879                        let markdown = block.markdown.clone();
 880                        editor::test::set_block_content_for_tests(
 881                            &this.editor,
 882                            *block_id,
 883                            cx,
 884                            move |cx| {
 885                                markdown::MarkdownElement::rendered_text(
 886                                    markdown.clone(),
 887                                    cx,
 888                                    editor::hover_popover::diagnostics_markdown_style,
 889                                )
 890                            },
 891                        );
 892                    }
 893                }
 894
 895                this.blocks.insert(buffer_id, block_ids);
 896                cx.notify()
 897            })
 898        })
 899    }
 900}
 901
 902impl Focusable for ProjectDiagnosticsEditor {
 903    fn focus_handle(&self, _: &App) -> FocusHandle {
 904        self.focus_handle.clone()
 905    }
 906}
 907
 908impl Item for ProjectDiagnosticsEditor {
 909    type Event = EditorEvent;
 910
 911    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 912        Editor::to_item_events(event, f)
 913    }
 914
 915    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 916        self.editor
 917            .update(cx, |editor, cx| editor.deactivated(window, cx));
 918    }
 919
 920    fn navigate(
 921        &mut self,
 922        data: Box<dyn Any>,
 923        window: &mut Window,
 924        cx: &mut Context<Self>,
 925    ) -> bool {
 926        self.editor
 927            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 928    }
 929
 930    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
 931        Some("Project Diagnostics".into())
 932    }
 933
 934    fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
 935        "Diagnostics".into()
 936    }
 937
 938    fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
 939        h_flex()
 940            .gap_1()
 941            .when(
 942                self.summary.error_count == 0 && self.summary.warning_count == 0,
 943                |then| {
 944                    then.child(
 945                        h_flex()
 946                            .gap_1()
 947                            .child(Icon::new(IconName::Check).color(Color::Success))
 948                            .child(Label::new("No problems").color(params.text_color())),
 949                    )
 950                },
 951            )
 952            .when(self.summary.error_count > 0, |then| {
 953                then.child(
 954                    h_flex()
 955                        .gap_1()
 956                        .child(Icon::new(IconName::XCircle).color(Color::Error))
 957                        .child(
 958                            Label::new(self.summary.error_count.to_string())
 959                                .color(params.text_color()),
 960                        ),
 961                )
 962            })
 963            .when(self.summary.warning_count > 0, |then| {
 964                then.child(
 965                    h_flex()
 966                        .gap_1()
 967                        .child(Icon::new(IconName::Warning).color(Color::Warning))
 968                        .child(
 969                            Label::new(self.summary.warning_count.to_string())
 970                                .color(params.text_color()),
 971                        ),
 972                )
 973            })
 974            .into_any_element()
 975    }
 976
 977    fn telemetry_event_text(&self) -> Option<&'static str> {
 978        Some("Project Diagnostics Opened")
 979    }
 980
 981    fn for_each_project_item(
 982        &self,
 983        cx: &App,
 984        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 985    ) {
 986        self.editor.for_each_project_item(cx, f)
 987    }
 988
 989    fn is_singleton(&self, _: &App) -> bool {
 990        false
 991    }
 992
 993    fn set_nav_history(
 994        &mut self,
 995        nav_history: ItemNavHistory,
 996        _: &mut Window,
 997        cx: &mut Context<Self>,
 998    ) {
 999        self.editor.update(cx, |editor, _| {
1000            editor.set_nav_history(Some(nav_history));
1001        });
1002    }
1003
1004    fn clone_on_split(
1005        &self,
1006        _workspace_id: Option<workspace::WorkspaceId>,
1007        window: &mut Window,
1008        cx: &mut Context<Self>,
1009    ) -> Option<Entity<Self>>
1010    where
1011        Self: Sized,
1012    {
1013        Some(cx.new(|cx| {
1014            ProjectDiagnosticsEditor::new(
1015                self.include_warnings,
1016                self.project.clone(),
1017                self.workspace.clone(),
1018                window,
1019                cx,
1020            )
1021        }))
1022    }
1023
1024    fn is_dirty(&self, cx: &App) -> bool {
1025        self.multibuffer.read(cx).is_dirty(cx)
1026    }
1027
1028    fn has_deleted_file(&self, cx: &App) -> bool {
1029        self.multibuffer.read(cx).has_deleted_file(cx)
1030    }
1031
1032    fn has_conflict(&self, cx: &App) -> bool {
1033        self.multibuffer.read(cx).has_conflict(cx)
1034    }
1035
1036    fn can_save(&self, _: &App) -> bool {
1037        true
1038    }
1039
1040    fn save(
1041        &mut self,
1042        format: bool,
1043        project: Entity<Project>,
1044        window: &mut Window,
1045        cx: &mut Context<Self>,
1046    ) -> Task<Result<()>> {
1047        self.editor.save(format, project, window, cx)
1048    }
1049
1050    fn save_as(
1051        &mut self,
1052        _: Entity<Project>,
1053        _: ProjectPath,
1054        _window: &mut Window,
1055        _: &mut Context<Self>,
1056    ) -> Task<Result<()>> {
1057        unreachable!()
1058    }
1059
1060    fn reload(
1061        &mut self,
1062        project: Entity<Project>,
1063        window: &mut Window,
1064        cx: &mut Context<Self>,
1065    ) -> Task<Result<()>> {
1066        self.editor.reload(project, window, cx)
1067    }
1068
1069    fn act_as_type<'a>(
1070        &'a self,
1071        type_id: TypeId,
1072        self_handle: &'a Entity<Self>,
1073        _: &'a App,
1074    ) -> Option<AnyView> {
1075        if type_id == TypeId::of::<Self>() {
1076            Some(self_handle.to_any())
1077        } else if type_id == TypeId::of::<Editor>() {
1078            Some(self.editor.to_any())
1079        } else {
1080            None
1081        }
1082    }
1083
1084    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1085        Some(Box::new(self.editor.clone()))
1086    }
1087
1088    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
1089        ToolbarItemLocation::PrimaryLeft
1090    }
1091
1092    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1093        self.editor.breadcrumbs(theme, cx)
1094    }
1095
1096    fn added_to_workspace(
1097        &mut self,
1098        workspace: &mut Workspace,
1099        window: &mut Window,
1100        cx: &mut Context<Self>,
1101    ) {
1102        self.editor.update(cx, |editor, cx| {
1103            editor.added_to_workspace(workspace, window, cx)
1104        });
1105    }
1106}
1107
1108const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
1109
1110async fn context_range_for_entry(
1111    range: Range<Point>,
1112    context: u32,
1113    snapshot: BufferSnapshot,
1114    cx: &mut AsyncApp,
1115) -> Range<Point> {
1116    if let Some(rows) = heuristic_syntactic_expand(
1117        range.clone(),
1118        DIAGNOSTIC_EXPANSION_ROW_LIMIT,
1119        snapshot.clone(),
1120        cx,
1121    )
1122    .await
1123    {
1124        return Range {
1125            start: Point::new(*rows.start(), 0),
1126            end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
1127        };
1128    }
1129    Range {
1130        start: Point::new(range.start.row.saturating_sub(context), 0),
1131        end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
1132    }
1133}
1134
1135/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
1136/// to the specified `max_row_count`.
1137///
1138/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
1139/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
1140async fn heuristic_syntactic_expand(
1141    input_range: Range<Point>,
1142    max_row_count: u32,
1143    snapshot: BufferSnapshot,
1144    cx: &mut AsyncApp,
1145) -> Option<RangeInclusive<BufferRow>> {
1146    let input_row_count = input_range.end.row - input_range.start.row;
1147    if input_row_count > max_row_count {
1148        return None;
1149    }
1150
1151    // If the outline node contains the diagnostic and is small enough, just use that.
1152    let outline_range = snapshot.outline_range_containing(input_range.clone());
1153    if let Some(outline_range) = outline_range.clone() {
1154        // Remove blank lines from start and end
1155        if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
1156            .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
1157        {
1158            if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
1159                .rev()
1160                .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
1161            {
1162                let row_count = end_row.saturating_sub(start_row);
1163                if row_count <= max_row_count {
1164                    return Some(RangeInclusive::new(
1165                        outline_range.start.row,
1166                        outline_range.end.row,
1167                    ));
1168                }
1169            }
1170        }
1171    }
1172
1173    let mut node = snapshot.syntax_ancestor(input_range.clone())?;
1174
1175    loop {
1176        let node_start = Point::from_ts_point(node.start_position());
1177        let node_end = Point::from_ts_point(node.end_position());
1178        let node_range = node_start..node_end;
1179        let row_count = node_end.row - node_start.row + 1;
1180        let mut ancestor_range = None;
1181        let reached_outline_node = cx.background_executor().scoped({
1182                 let node_range = node_range.clone();
1183                 let outline_range = outline_range.clone();
1184                 let ancestor_range =  &mut ancestor_range;
1185                |scope| {scope.spawn(async move {
1186                    // Stop if we've exceeded the row count or reached an outline node. Then, find the interval
1187                    // of node children which contains the query range. For example, this allows just returning
1188                    // the header of a declaration rather than the entire declaration.
1189                    if row_count > max_row_count || outline_range == Some(node_range.clone()) {
1190                        let mut cursor = node.walk();
1191                        let mut included_child_start = None;
1192                        let mut included_child_end = None;
1193                        let mut previous_end = node_start;
1194                        if cursor.goto_first_child() {
1195                            loop {
1196                                let child_node = cursor.node();
1197                                let child_range = previous_end..Point::from_ts_point(child_node.end_position());
1198                                if included_child_start.is_none() && child_range.contains(&input_range.start) {
1199                                    included_child_start = Some(child_range.start);
1200                                }
1201                                if child_range.contains(&input_range.end) {
1202                                    included_child_end = Some(child_range.end);
1203                                }
1204                                previous_end = child_range.end;
1205                                if !cursor.goto_next_sibling() {
1206                                    break;
1207                                }
1208                            }
1209                        }
1210                        let end = included_child_end.unwrap_or(node_range.end);
1211                        if let Some(start) = included_child_start {
1212                            let row_count = end.row - start.row;
1213                            if row_count < max_row_count {
1214                                *ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
1215                                return;
1216                            }
1217                        }
1218
1219                        log::info!(
1220                            "Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
1221                            node.grammar_name()
1222                        );
1223                        *ancestor_range = Some(None);
1224                    }
1225                })
1226            }});
1227        reached_outline_node.await;
1228        if let Some(node) = ancestor_range {
1229            return node;
1230        }
1231
1232        let node_name = node.grammar_name();
1233        let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
1234        if node_name.ends_with("block") {
1235            return Some(node_row_range);
1236        } else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
1237            // Expand to the nearest dedent or blank line for statements and declarations.
1238            let tab_size = cx
1239                .update(|cx| snapshot.settings_at(node_range.start, cx).tab_size.get())
1240                .ok()?;
1241            let indent_level = snapshot
1242                .line_indent_for_row(node_range.start.row)
1243                .len(tab_size);
1244            let rows_remaining = max_row_count.saturating_sub(row_count);
1245            let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
1246                ..node_range.start.row)
1247                .rev()
1248                .find(|row| {
1249                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
1250                })
1251            else {
1252                return Some(node_row_range);
1253            };
1254            let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
1255            let Some(end_row) = (node_range.end.row + 1
1256                ..cmp::min(
1257                    node_range.end.row + rows_remaining + 1,
1258                    snapshot.row_count(),
1259                ))
1260                .find(|row| {
1261                    is_line_blank_or_indented_less(indent_level, *row, tab_size, &snapshot.clone())
1262                })
1263            else {
1264                return Some(node_row_range);
1265            };
1266            return Some(RangeInclusive::new(start_row, end_row));
1267        }
1268
1269        // TODO: doing this instead of walking a cursor as that doesn't work - why?
1270        let Some(parent) = node.parent() else {
1271            log::info!(
1272                "Expanding to ancestor reached the top node, so using default context line count.",
1273            );
1274            return None;
1275        };
1276        node = parent;
1277    }
1278}
1279
1280fn is_line_blank_or_indented_less(
1281    indent_level: u32,
1282    row: u32,
1283    tab_size: u32,
1284    snapshot: &BufferSnapshot,
1285) -> bool {
1286    let line_indent = snapshot.line_indent_for_row(row);
1287    line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
1288}
1289
1290fn fetch_cargo_diagnostics_token() -> String {
1291    "fetch_cargo_diagnostics".to_string()
1292}