buffer_diagnostics.rs

   1use crate::{
   2    DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
   3    diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
   4    toolbar_controls::DiagnosticsToolbarEditor,
   5};
   6use anyhow::Result;
   7use collections::HashMap;
   8use editor::{
   9    Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
  10    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
  11    multibuffer_context_lines,
  12};
  13use gpui::{
  14    AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
  15    InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
  16    Task, WeakEntity, Window, actions, div,
  17};
  18use language::{Buffer, DiagnosticEntry, DiagnosticEntryRef, Point};
  19use project::{
  20    DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
  21    project_settings::{DiagnosticSeverity, ProjectSettings},
  22};
  23use settings::Settings;
  24use std::{
  25    any::{Any, TypeId},
  26    cmp::{self, Ordering},
  27    sync::Arc,
  28};
  29use text::{Anchor, BufferSnapshot, OffsetRangeExt};
  30use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
  31use workspace::{
  32    ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
  33    item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
  34};
  35
  36actions!(
  37    diagnostics,
  38    [
  39        /// Opens the project diagnostics view for the currently focused file.
  40        DeployCurrentFile,
  41    ]
  42);
  43
  44/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
  45/// with diagnostics for a single buffer, as only the excerpts of the buffer
  46/// where diagnostics are available are displayed.
  47pub(crate) struct BufferDiagnosticsEditor {
  48    pub project: Entity<Project>,
  49    focus_handle: FocusHandle,
  50    editor: Entity<Editor>,
  51    /// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
  52    /// allow quick comparison of updated diagnostics, to confirm if anything
  53    /// has changed.
  54    pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
  55    /// The blocks used to display the diagnostics' content in the editor, next
  56    /// to the excerpts where the diagnostic originated.
  57    blocks: Vec<CustomBlockId>,
  58    /// Multibuffer to contain all excerpts that contain diagnostics, which are
  59    /// to be rendered in the editor.
  60    multibuffer: Entity<MultiBuffer>,
  61    /// The buffer for which the editor is displaying diagnostics and excerpts
  62    /// for.
  63    buffer: Option<Entity<Buffer>>,
  64    /// The path for which the editor is displaying diagnostics for.
  65    project_path: ProjectPath,
  66    /// Summary of the number of warnings and errors for the path. Used to
  67    /// display the number of warnings and errors in the tab's content.
  68    summary: DiagnosticSummary,
  69    /// Whether to include warnings in the list of diagnostics shown in the
  70    /// editor.
  71    pub(crate) include_warnings: bool,
  72    /// Keeps track of whether there's a background task already running to
  73    /// update the excerpts, in order to avoid firing multiple tasks for this purpose.
  74    pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
  75    /// The project's subscription, responsible for processing events related to
  76    /// diagnostics.
  77    _subscription: Subscription,
  78}
  79
  80impl BufferDiagnosticsEditor {
  81    /// Creates new instance of the `BufferDiagnosticsEditor` which can then be
  82    /// displayed by adding it to a pane.
  83    pub fn new(
  84        project_path: ProjectPath,
  85        project_handle: Entity<Project>,
  86        buffer: Option<Entity<Buffer>>,
  87        include_warnings: bool,
  88        window: &mut Window,
  89        cx: &mut Context<Self>,
  90    ) -> Self {
  91        // Subscribe to project events related to diagnostics so the
  92        // `BufferDiagnosticsEditor` can update its state accordingly.
  93        let project_event_subscription = cx.subscribe_in(
  94            &project_handle,
  95            window,
  96            |buffer_diagnostics_editor, _project, event, window, cx| match event {
  97                Event::DiskBasedDiagnosticsStarted { .. } => {
  98                    cx.notify();
  99                }
 100                Event::DiskBasedDiagnosticsFinished { .. } => {
 101                    buffer_diagnostics_editor.update_all_excerpts(window, cx);
 102                }
 103                Event::DiagnosticsUpdated {
 104                    paths,
 105                    language_server_id,
 106                } => {
 107                    // When diagnostics have been updated, the
 108                    // `BufferDiagnosticsEditor` should update its state only if
 109                    // one of the paths matches its `project_path`, otherwise
 110                    // the event should be ignored.
 111                    if paths.contains(&buffer_diagnostics_editor.project_path) {
 112                        buffer_diagnostics_editor.update_diagnostic_summary(cx);
 113
 114                        if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
 115                            log::debug!("diagnostics updated for server {language_server_id}. recording change");
 116                        } else {
 117                            log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
 118                            buffer_diagnostics_editor.update_all_excerpts(window, cx);
 119                        }
 120                    }
 121                }
 122                _ => {}
 123            },
 124        );
 125
 126        let focus_handle = cx.focus_handle();
 127
 128        cx.on_focus_in(
 129            &focus_handle,
 130            window,
 131            |buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
 132        )
 133        .detach();
 134
 135        cx.on_focus_out(
 136            &focus_handle,
 137            window,
 138            |buffer_diagnostics_editor, _event, window, cx| {
 139                buffer_diagnostics_editor.focus_out(window, cx)
 140            },
 141        )
 142        .detach();
 143
 144        let summary = project_handle
 145            .read(cx)
 146            .diagnostic_summary_for_path(&project_path, cx);
 147
 148        let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
 149        let max_severity = Self::max_diagnostics_severity(include_warnings);
 150        let editor = cx.new(|cx| {
 151            let mut editor = Editor::for_multibuffer(
 152                multibuffer.clone(),
 153                Some(project_handle.clone()),
 154                window,
 155                cx,
 156            );
 157            editor.set_vertical_scroll_margin(5, cx);
 158            editor.disable_inline_diagnostics();
 159            editor.set_max_diagnostics_severity(max_severity, cx);
 160            editor.set_all_diagnostics_active(cx);
 161            editor
 162        });
 163
 164        // Subscribe to events triggered by the editor in order to correctly
 165        // update the buffer's excerpts.
 166        cx.subscribe_in(
 167            &editor,
 168            window,
 169            |buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
 170                cx.emit(event.clone());
 171
 172                match event {
 173                    // If the user tries to focus on the editor but there's actually
 174                    // no excerpts for the buffer, focus back on the
 175                    // `BufferDiagnosticsEditor` instance.
 176                    EditorEvent::Focused => {
 177                        if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
 178                            window.focus(&buffer_diagnostics_editor.focus_handle);
 179                        }
 180                    }
 181                    EditorEvent::Blurred => {
 182                        buffer_diagnostics_editor.update_all_excerpts(window, cx)
 183                    }
 184                    _ => {}
 185                }
 186            },
 187        )
 188        .detach();
 189
 190        let diagnostics = vec![];
 191        let update_excerpts_task = None;
 192        let mut buffer_diagnostics_editor = Self {
 193            project: project_handle,
 194            focus_handle,
 195            editor,
 196            diagnostics,
 197            blocks: Default::default(),
 198            multibuffer,
 199            buffer,
 200            project_path,
 201            summary,
 202            include_warnings,
 203            update_excerpts_task,
 204            _subscription: project_event_subscription,
 205        };
 206
 207        buffer_diagnostics_editor.update_all_diagnostics(window, cx);
 208        buffer_diagnostics_editor
 209    }
 210
 211    fn deploy(
 212        workspace: &mut Workspace,
 213        _: &DeployCurrentFile,
 214        window: &mut Window,
 215        cx: &mut Context<Workspace>,
 216    ) {
 217        // Determine the currently opened path by finding the active editor and
 218        // finding the project path for the buffer.
 219        // If there's no active editor with a project path, avoiding deploying
 220        // the buffer diagnostics view.
 221        if let Some(editor) = workspace.active_item_as::<Editor>(cx)
 222            && let Some(project_path) = editor.project_path(cx)
 223        {
 224            // Check if there's already a `BufferDiagnosticsEditor` tab for this
 225            // same path, and if so, focus on that one instead of creating a new
 226            // one.
 227            let existing_editor = workspace
 228                .items_of_type::<BufferDiagnosticsEditor>(cx)
 229                .find(|editor| editor.read(cx).project_path == project_path);
 230
 231            if let Some(editor) = existing_editor {
 232                workspace.activate_item(&editor, true, true, window, cx);
 233            } else {
 234                let include_warnings = match cx.try_global::<IncludeWarnings>() {
 235                    Some(include_warnings) => include_warnings.0,
 236                    None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
 237                };
 238
 239                let item = cx.new(|cx| {
 240                    Self::new(
 241                        project_path,
 242                        workspace.project().clone(),
 243                        editor.read(cx).buffer().read(cx).as_singleton(),
 244                        include_warnings,
 245                        window,
 246                        cx,
 247                    )
 248                });
 249
 250                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
 251            }
 252        }
 253    }
 254
 255    pub fn register(
 256        workspace: &mut Workspace,
 257        _window: Option<&mut Window>,
 258        _: &mut Context<Workspace>,
 259    ) {
 260        workspace.register_action(Self::deploy);
 261    }
 262
 263    fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 264        self.update_all_excerpts(window, cx);
 265    }
 266
 267    fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
 268        let project = self.project.read(cx);
 269
 270        self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
 271    }
 272
 273    /// Enqueue an update to the excerpts and diagnostic blocks being shown in
 274    /// the editor.
 275    pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 276        // If there's already a task updating the excerpts, early return and let
 277        // the other task finish.
 278        if self.update_excerpts_task.is_some() {
 279            return;
 280        }
 281
 282        let buffer = self.buffer.clone();
 283
 284        self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
 285            cx.background_executor()
 286                .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
 287                .await;
 288
 289            if let Some(buffer) = buffer {
 290                editor
 291                    .update_in(cx, |editor, window, cx| {
 292                        editor.update_excerpts(buffer, window, cx)
 293                    })?
 294                    .await?;
 295            };
 296
 297            let _ = editor.update(cx, |editor, cx| {
 298                editor.update_excerpts_task = None;
 299                cx.notify();
 300            });
 301
 302            Ok(())
 303        }));
 304    }
 305
 306    /// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
 307    /// buffer.
 308    fn update_excerpts(
 309        &mut self,
 310        buffer: Entity<Buffer>,
 311        window: &mut Window,
 312        cx: &mut Context<Self>,
 313    ) -> Task<Result<()>> {
 314        let was_empty = self.multibuffer.read(cx).is_empty();
 315        let multibuffer_context = multibuffer_context_lines(cx);
 316        let buffer_snapshot = buffer.read(cx).snapshot();
 317        let buffer_snapshot_max = buffer_snapshot.max_point();
 318        let max_severity = Self::max_diagnostics_severity(self.include_warnings)
 319            .into_lsp()
 320            .unwrap_or(lsp::DiagnosticSeverity::WARNING);
 321
 322        cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
 323            // Fetch the diagnostics for the whole of the buffer
 324            // (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
 325            // if the diagnostics changed, if it didn't, early return as there's
 326            // nothing to update.
 327            let diagnostics = buffer_snapshot
 328                .diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
 329                .collect::<Vec<_>>();
 330
 331            let unchanged =
 332                buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
 333                    if buffer_diagnostics_editor
 334                        .diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
 335                    {
 336                        return true;
 337                    }
 338
 339                    buffer_diagnostics_editor.set_diagnostics(&diagnostics);
 340                    return false;
 341                })?;
 342
 343            if unchanged {
 344                return Ok(());
 345            }
 346
 347            // Mapping between the Group ID and a vector of DiagnosticEntry.
 348            let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
 349            for entry in diagnostics {
 350                grouped
 351                    .entry(entry.diagnostic.group_id)
 352                    .or_default()
 353                    .push(DiagnosticEntryRef {
 354                        range: entry.range.to_point(&buffer_snapshot),
 355                        diagnostic: entry.diagnostic,
 356                    })
 357            }
 358
 359            let mut blocks: Vec<DiagnosticBlock> = Vec::new();
 360            for (_, group) in grouped {
 361                // If the minimum severity of the group is higher than the
 362                // maximum severity, or it doesn't even have severity, skip this
 363                // group.
 364                if group
 365                    .iter()
 366                    .map(|d| d.diagnostic.severity)
 367                    .min()
 368                    .is_none_or(|severity| severity > max_severity)
 369                {
 370                    continue;
 371                }
 372
 373                let languages = buffer_diagnostics_editor
 374                    .read_with(cx, |b, cx| b.project.read(cx).languages().clone())
 375                    .ok();
 376
 377                let diagnostic_blocks = cx.update(|_window, cx| {
 378                    DiagnosticRenderer::diagnostic_blocks_for_group(
 379                        group,
 380                        buffer_snapshot.remote_id(),
 381                        Some(Arc::new(buffer_diagnostics_editor.clone())),
 382                        languages,
 383                        cx,
 384                    )
 385                })?;
 386
 387                // For each of the diagnostic blocks to be displayed in the
 388                // editor, figure out its index in the list of blocks.
 389                //
 390                // The following rules are used to determine the order:
 391                // 1. Blocks with a lower start position should come first.
 392                // 2. If two blocks have the same start position, the one with
 393                // the higher end position should come first.
 394                for diagnostic_block in diagnostic_blocks {
 395                    let index = blocks.partition_point(|probe| {
 396                        match probe
 397                            .initial_range
 398                            .start
 399                            .cmp(&diagnostic_block.initial_range.start)
 400                        {
 401                            Ordering::Less => true,
 402                            Ordering::Greater => false,
 403                            Ordering::Equal => {
 404                                probe.initial_range.end > diagnostic_block.initial_range.end
 405                            }
 406                        }
 407                    });
 408
 409                    blocks.insert(index, diagnostic_block);
 410                }
 411            }
 412
 413            // Build the excerpt ranges for this specific buffer's diagnostics,
 414            // so those excerpts can later be used to update the excerpts shown
 415            // in the editor.
 416            // This is done by iterating over the list of diagnostic blocks and
 417            // determine what range does the diagnostic block span.
 418            let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
 419
 420            for diagnostic_block in blocks.iter() {
 421                let excerpt_range = context_range_for_entry(
 422                    diagnostic_block.initial_range.clone(),
 423                    multibuffer_context,
 424                    buffer_snapshot.clone(),
 425                    &mut cx,
 426                )
 427                .await;
 428                let initial_range = buffer_snapshot
 429                    .anchor_after(diagnostic_block.initial_range.start)
 430                    ..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
 431
 432                let bin_search = |probe: &ExcerptRange<text::Anchor>| {
 433                    let context_start = || {
 434                        probe
 435                            .context
 436                            .start
 437                            .cmp(&excerpt_range.start, &buffer_snapshot)
 438                    };
 439                    let context_end =
 440                        || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
 441                    let primary_start = || {
 442                        probe
 443                            .primary
 444                            .start
 445                            .cmp(&initial_range.start, &buffer_snapshot)
 446                    };
 447                    let primary_end =
 448                        || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
 449                    context_start()
 450                        .then_with(context_end)
 451                        .then_with(primary_start)
 452                        .then_with(primary_end)
 453                        .then(cmp::Ordering::Greater)
 454                };
 455
 456                let index = excerpt_ranges
 457                    .binary_search_by(bin_search)
 458                    .unwrap_or_else(|i| i);
 459
 460                excerpt_ranges.insert(
 461                    index,
 462                    ExcerptRange {
 463                        context: excerpt_range,
 464                        primary: initial_range,
 465                    },
 466                )
 467            }
 468
 469            // Finally, update the editor's content with the new excerpt ranges
 470            // for this editor, as well as the diagnostic blocks.
 471            buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
 472                // Remove the list of `CustomBlockId` from the editor's display
 473                // map, ensuring that if any diagnostics have been solved, the
 474                // associated block stops being shown.
 475                let block_ids = buffer_diagnostics_editor.blocks.clone();
 476
 477                buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
 478                    editor.display_map.update(cx, |display_map, cx| {
 479                        display_map.remove_blocks(block_ids.into_iter().collect(), cx);
 480                    })
 481                });
 482
 483                let (anchor_ranges, _) =
 484                    buffer_diagnostics_editor
 485                        .multibuffer
 486                        .update(cx, |multibuffer, cx| {
 487                            let excerpt_ranges = excerpt_ranges
 488                                .into_iter()
 489                                .map(|range| ExcerptRange {
 490                                    context: range.context.to_point(&buffer_snapshot),
 491                                    primary: range.primary.to_point(&buffer_snapshot),
 492                                })
 493                                .collect();
 494                            multibuffer.set_excerpt_ranges_for_path(
 495                                PathKey::for_buffer(&buffer, cx),
 496                                buffer.clone(),
 497                                &buffer_snapshot,
 498                                excerpt_ranges,
 499                                cx,
 500                            )
 501                        });
 502
 503                if was_empty {
 504                    if let Some(anchor_range) = anchor_ranges.first() {
 505                        let range_to_select = anchor_range.start..anchor_range.start;
 506
 507                        buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
 508                            editor.change_selections(Default::default(), window, cx, |selection| {
 509                                selection.select_anchor_ranges([range_to_select])
 510                            })
 511                        });
 512
 513                        // If the `BufferDiagnosticsEditor` is currently
 514                        // focused, move focus to its editor.
 515                        if buffer_diagnostics_editor.focus_handle.is_focused(window) {
 516                            buffer_diagnostics_editor
 517                                .editor
 518                                .read(cx)
 519                                .focus_handle(cx)
 520                                .focus(window);
 521                        }
 522                    }
 523                }
 524
 525                // Cloning the blocks before moving ownership so these can later
 526                // be used to set the block contents for testing purposes.
 527                #[cfg(test)]
 528                let cloned_blocks = blocks.clone();
 529
 530                // Build new diagnostic blocks to be added to the editor's
 531                // display map for the new diagnostics. Update the `blocks`
 532                // property before finishing, to ensure the blocks are removed
 533                // on the next execution.
 534                let editor_blocks =
 535                    anchor_ranges
 536                        .into_iter()
 537                        .zip(blocks.into_iter())
 538                        .map(|(anchor, block)| {
 539                            let editor = buffer_diagnostics_editor.editor.downgrade();
 540
 541                            BlockProperties {
 542                                placement: BlockPlacement::Near(anchor.start),
 543                                height: Some(1),
 544                                style: BlockStyle::Flex,
 545                                render: Arc::new(move |block_context| {
 546                                    block.render_block(editor.clone(), block_context)
 547                                }),
 548                                priority: 1,
 549                            }
 550                        });
 551
 552                let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
 553                    editor.display_map.update(cx, |display_map, cx| {
 554                        display_map.insert_blocks(editor_blocks, cx)
 555                    })
 556                });
 557
 558                // In order to be able to verify which diagnostic blocks are
 559                // rendered in the editor, the `set_block_content_for_tests`
 560                // function must be used, so that the
 561                // `editor::test::editor_content_with_blocks` function can then
 562                // be called to fetch these blocks.
 563                #[cfg(test)]
 564                {
 565                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
 566                        let markdown = block.markdown.clone();
 567                        editor::test::set_block_content_for_tests(
 568                            &buffer_diagnostics_editor.editor,
 569                            *block_id,
 570                            cx,
 571                            move |cx| {
 572                                markdown::MarkdownElement::rendered_text(
 573                                    markdown.clone(),
 574                                    cx,
 575                                    editor::hover_popover::diagnostics_markdown_style,
 576                                )
 577                            },
 578                        );
 579                    }
 580                }
 581
 582                buffer_diagnostics_editor.blocks = block_ids;
 583                cx.notify()
 584            })
 585        })
 586    }
 587
 588    fn set_diagnostics(&mut self, diagnostics: &[DiagnosticEntryRef<'_, Anchor>]) {
 589        self.diagnostics = diagnostics
 590            .iter()
 591            .map(DiagnosticEntryRef::to_owned)
 592            .collect();
 593    }
 594
 595    fn diagnostics_are_unchanged(
 596        &self,
 597        diagnostics: &Vec<DiagnosticEntryRef<'_, Anchor>>,
 598        snapshot: &BufferSnapshot,
 599    ) -> bool {
 600        if self.diagnostics.len() != diagnostics.len() {
 601            return false;
 602        }
 603
 604        self.diagnostics
 605            .iter()
 606            .zip(diagnostics.iter())
 607            .all(|(existing, new)| {
 608                existing.diagnostic.message == new.diagnostic.message
 609                    && existing.diagnostic.severity == new.diagnostic.severity
 610                    && existing.diagnostic.is_primary == new.diagnostic.is_primary
 611                    && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
 612            })
 613    }
 614
 615    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 616        // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
 617        // not empty, focus on the editor instead, which will allow the user to
 618        // start interacting and editing the buffer's contents.
 619        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
 620            self.editor.focus_handle(cx).focus(window)
 621        }
 622    }
 623
 624    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 625        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
 626        {
 627            self.update_all_excerpts(window, cx);
 628        }
 629    }
 630
 631    pub fn toggle_warnings(
 632        &mut self,
 633        _: &ToggleWarnings,
 634        window: &mut Window,
 635        cx: &mut Context<Self>,
 636    ) {
 637        let include_warnings = !self.include_warnings;
 638        let max_severity = Self::max_diagnostics_severity(include_warnings);
 639
 640        self.editor.update(cx, |editor, cx| {
 641            editor.set_max_diagnostics_severity(max_severity, cx);
 642        });
 643
 644        self.include_warnings = include_warnings;
 645        self.diagnostics.clear();
 646        self.update_all_diagnostics(window, cx);
 647    }
 648
 649    fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
 650        match include_warnings {
 651            true => DiagnosticSeverity::Warning,
 652            false => DiagnosticSeverity::Error,
 653        }
 654    }
 655
 656    #[cfg(test)]
 657    pub fn editor(&self) -> &Entity<Editor> {
 658        &self.editor
 659    }
 660
 661    #[cfg(test)]
 662    pub fn summary(&self) -> &DiagnosticSummary {
 663        &self.summary
 664    }
 665}
 666
 667impl Focusable for BufferDiagnosticsEditor {
 668    fn focus_handle(&self, _: &App) -> FocusHandle {
 669        self.focus_handle.clone()
 670    }
 671}
 672
 673impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
 674
 675impl Item for BufferDiagnosticsEditor {
 676    type Event = EditorEvent;
 677
 678    fn act_as_type<'a>(
 679        &'a self,
 680        type_id: std::any::TypeId,
 681        self_handle: &'a Entity<Self>,
 682        _: &'a App,
 683    ) -> Option<gpui::AnyView> {
 684        if type_id == TypeId::of::<Self>() {
 685            Some(self_handle.to_any())
 686        } else if type_id == TypeId::of::<Editor>() {
 687            Some(self.editor.to_any())
 688        } else {
 689            None
 690        }
 691    }
 692
 693    fn added_to_workspace(
 694        &mut self,
 695        workspace: &mut Workspace,
 696        window: &mut Window,
 697        cx: &mut Context<Self>,
 698    ) {
 699        self.editor.update(cx, |editor, cx| {
 700            editor.added_to_workspace(workspace, window, cx)
 701        });
 702    }
 703
 704    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 705        ToolbarItemLocation::PrimaryLeft
 706    }
 707
 708    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
 709        self.editor.breadcrumbs(theme, cx)
 710    }
 711
 712    fn can_save(&self, _cx: &App) -> bool {
 713        true
 714    }
 715
 716    fn can_split(&self) -> bool {
 717        true
 718    }
 719
 720    fn clone_on_split(
 721        &self,
 722        _workspace_id: Option<workspace::WorkspaceId>,
 723        window: &mut Window,
 724        cx: &mut Context<Self>,
 725    ) -> Task<Option<Entity<Self>>>
 726    where
 727        Self: Sized,
 728    {
 729        Task::ready(Some(cx.new(|cx| {
 730            BufferDiagnosticsEditor::new(
 731                self.project_path.clone(),
 732                self.project.clone(),
 733                self.buffer.clone(),
 734                self.include_warnings,
 735                window,
 736                cx,
 737            )
 738        })))
 739    }
 740
 741    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 742        self.editor
 743            .update(cx, |editor, cx| editor.deactivated(window, cx));
 744    }
 745
 746    fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
 747        self.editor.for_each_project_item(cx, f);
 748    }
 749
 750    fn has_conflict(&self, cx: &App) -> bool {
 751        self.multibuffer.read(cx).has_conflict(cx)
 752    }
 753
 754    fn has_deleted_file(&self, cx: &App) -> bool {
 755        self.multibuffer.read(cx).has_deleted_file(cx)
 756    }
 757
 758    fn is_dirty(&self, cx: &App) -> bool {
 759        self.multibuffer.read(cx).is_dirty(cx)
 760    }
 761
 762    fn navigate(
 763        &mut self,
 764        data: Box<dyn Any>,
 765        window: &mut Window,
 766        cx: &mut Context<Self>,
 767    ) -> bool {
 768        self.editor
 769            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 770    }
 771
 772    fn reload(
 773        &mut self,
 774        project: Entity<Project>,
 775        window: &mut Window,
 776        cx: &mut Context<Self>,
 777    ) -> Task<Result<()>> {
 778        self.editor.reload(project, window, cx)
 779    }
 780
 781    fn save(
 782        &mut self,
 783        options: workspace::item::SaveOptions,
 784        project: Entity<Project>,
 785        window: &mut Window,
 786        cx: &mut Context<Self>,
 787    ) -> Task<Result<()>> {
 788        self.editor.save(options, project, window, cx)
 789    }
 790
 791    fn save_as(
 792        &mut self,
 793        _project: Entity<Project>,
 794        _path: ProjectPath,
 795        _window: &mut Window,
 796        _cx: &mut Context<Self>,
 797    ) -> Task<Result<()>> {
 798        unreachable!()
 799    }
 800
 801    fn set_nav_history(
 802        &mut self,
 803        nav_history: ItemNavHistory,
 804        _window: &mut Window,
 805        cx: &mut Context<Self>,
 806    ) {
 807        self.editor.update(cx, |editor, _| {
 808            editor.set_nav_history(Some(nav_history));
 809        })
 810    }
 811
 812    // Builds the content to be displayed in the tab.
 813    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 814        let path_style = self.project.read(cx).path_style(cx);
 815        let error_count = self.summary.error_count;
 816        let warning_count = self.summary.warning_count;
 817        let label = Label::new(
 818            self.project_path
 819                .path
 820                .file_name()
 821                .map(|s| s.to_string())
 822                .unwrap_or_else(|| self.project_path.path.display(path_style).to_string()),
 823        );
 824
 825        h_flex()
 826            .gap_1()
 827            .child(label)
 828            .when(error_count == 0 && warning_count == 0, |parent| {
 829                parent.child(
 830                    h_flex()
 831                        .gap_1()
 832                        .child(Icon::new(IconName::Check).color(Color::Success)),
 833                )
 834            })
 835            .when(error_count > 0, |parent| {
 836                parent.child(
 837                    h_flex()
 838                        .gap_1()
 839                        .child(Icon::new(IconName::XCircle).color(Color::Error))
 840                        .child(Label::new(error_count.to_string()).color(params.text_color())),
 841                )
 842            })
 843            .when(warning_count > 0, |parent| {
 844                parent.child(
 845                    h_flex()
 846                        .gap_1()
 847                        .child(Icon::new(IconName::Warning).color(Color::Warning))
 848                        .child(Label::new(warning_count.to_string()).color(params.text_color())),
 849                )
 850            })
 851            .into_any_element()
 852    }
 853
 854    fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
 855        "Buffer Diagnostics".into()
 856    }
 857
 858    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
 859        let path_style = self.project.read(cx).path_style(cx);
 860        Some(
 861            format!(
 862                "Buffer Diagnostics - {}",
 863                self.project_path.path.display(path_style)
 864            )
 865            .into(),
 866        )
 867    }
 868
 869    fn telemetry_event_text(&self) -> Option<&'static str> {
 870        Some("Buffer Diagnostics Opened")
 871    }
 872
 873    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 874        Editor::to_item_events(event, f)
 875    }
 876}
 877
 878impl Render for BufferDiagnosticsEditor {
 879    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 880        let path_style = self.project.read(cx).path_style(cx);
 881        let filename = self.project_path.path.display(path_style).to_string();
 882        let error_count = self.summary.error_count;
 883        let warning_count = match self.include_warnings {
 884            true => self.summary.warning_count,
 885            false => 0,
 886        };
 887
 888        let child = if error_count + warning_count == 0 {
 889            let label = match warning_count {
 890                0 => "No problems in",
 891                _ => "No errors in",
 892            };
 893
 894            v_flex()
 895                .key_context("EmptyPane")
 896                .size_full()
 897                .gap_1()
 898                .justify_center()
 899                .items_center()
 900                .text_center()
 901                .bg(cx.theme().colors().editor_background)
 902                .child(
 903                    div()
 904                        .h_flex()
 905                        .child(Label::new(label).color(Color::Muted))
 906                        .child(
 907                            Button::new("open-file", filename)
 908                                .style(ButtonStyle::Transparent)
 909                                .tooltip(Tooltip::text("Open File"))
 910                                .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
 911                                    if let Some(workspace) = window.root::<Workspace>().flatten() {
 912                                        workspace.update(cx, |workspace, cx| {
 913                                            workspace
 914                                                .open_path(
 915                                                    buffer_diagnostics.project_path.clone(),
 916                                                    None,
 917                                                    true,
 918                                                    window,
 919                                                    cx,
 920                                                )
 921                                                .detach_and_log_err(cx);
 922                                        })
 923                                    }
 924                                })),
 925                        ),
 926                )
 927                .when(self.summary.warning_count > 0, |div| {
 928                    let label = match self.summary.warning_count {
 929                        1 => "Show 1 warning".into(),
 930                        warning_count => format!("Show {} warnings", warning_count),
 931                    };
 932
 933                    div.child(
 934                        Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
 935                            |buffer_diagnostics_editor, _, window, cx| {
 936                                buffer_diagnostics_editor.toggle_warnings(
 937                                    &Default::default(),
 938                                    window,
 939                                    cx,
 940                                );
 941                                cx.notify();
 942                            },
 943                        )),
 944                    )
 945                })
 946        } else {
 947            div().size_full().child(self.editor.clone())
 948        };
 949
 950        div()
 951            .key_context("Diagnostics")
 952            .track_focus(&self.focus_handle(cx))
 953            .size_full()
 954            .child(child)
 955    }
 956}
 957
 958impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
 959    fn include_warnings(&self, cx: &App) -> bool {
 960        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
 961            buffer_diagnostics_editor.include_warnings
 962        })
 963        .unwrap_or(false)
 964    }
 965
 966    fn is_updating(&self, cx: &App) -> bool {
 967        self.read_with(cx, |buffer_diagnostics_editor, cx| {
 968            buffer_diagnostics_editor.update_excerpts_task.is_some()
 969                || buffer_diagnostics_editor
 970                    .project
 971                    .read(cx)
 972                    .language_servers_running_disk_based_diagnostics(cx)
 973                    .next()
 974                    .is_some()
 975        })
 976        .unwrap_or(false)
 977    }
 978
 979    fn stop_updating(&self, cx: &mut App) {
 980        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
 981            buffer_diagnostics_editor.update_excerpts_task = None;
 982            cx.notify();
 983        });
 984    }
 985
 986    fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
 987        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
 988            buffer_diagnostics_editor.update_all_excerpts(window, cx);
 989        });
 990    }
 991
 992    fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
 993        let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
 994            buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
 995        });
 996    }
 997
 998    fn get_diagnostics_for_buffer(
 999        &self,
1000        _buffer_id: text::BufferId,
1001        cx: &App,
1002    ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
1003        self.read_with(cx, |buffer_diagnostics_editor, _cx| {
1004            buffer_diagnostics_editor.diagnostics.clone()
1005        })
1006        .unwrap_or_default()
1007    }
1008}