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