diagnostics.rs

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