1141 lines · 41.3 KB · Rust
RawDownload
pub mod items;mod toolbar_controls; mod buffer_diagnostics;mod diagnostic_renderer; #[cfg(test)]mod diagnostics_tests; use anyhow::Result;use buffer_diagnostics::BufferDiagnosticsEditor;use collections::{BTreeSet, HashMap, HashSet};use diagnostic_renderer::DiagnosticBlock;use editor::{    Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},    multibuffer_context_lines,};use gpui::{    AnyElement, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,    Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString,    Styled, Subscription, Task, WeakEntity, Window, actions, div,};use itertools::Itertools as _;use language::{    Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, DiagnosticEntryRef, Point,    ToTreeSitterPoint,};use project::{    DiagnosticSummary, Project, ProjectPath,    project_settings::{DiagnosticSeverity, ProjectSettings},};use settings::Settings;use std::{    any::{Any, TypeId},    cmp,    ops::{Range, RangeInclusive},    sync::Arc,    time::Duration,};use text::{BufferId, OffsetRangeExt};use theme::ActiveTheme;use toolbar_controls::DiagnosticsToolbarEditor;pub use toolbar_controls::ToolbarControls;use ui::{Icon, IconName, Label, h_flex, prelude::*};use util::ResultExt;use workspace::{    ItemNavHistory, Workspace,    item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},    searchable::SearchableItemHandle,}; actions!(    diagnostics,    [        /// Opens the project diagnostics view.        Deploy,        /// Toggles the display of warning-level diagnostics.        ToggleWarnings,        /// Toggles automatic refresh of diagnostics.        ToggleDiagnosticsRefresh    ]); #[derive(Default)]pub(crate) struct IncludeWarnings(bool);impl Global for IncludeWarnings {} pub fn init(cx: &mut App) {    editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);    cx.observe_new(ProjectDiagnosticsEditor::register).detach();    cx.observe_new(BufferDiagnosticsEditor::register).detach();} pub(crate) struct ProjectDiagnosticsEditor {    pub project: Entity<Project>,    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    editor: Entity<Editor>,    diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,    blocks: HashMap<BufferId, Vec<CustomBlockId>>,    summary: DiagnosticSummary,    multibuffer: Entity<MultiBuffer>,    paths_to_update: BTreeSet<ProjectPath>,    include_warnings: bool,    update_excerpts_task: Option<Task<Result<()>>>,    diagnostic_summary_update: Task<()>,    _subscription: Subscription,} impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);const DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE: Duration = Duration::from_millis(30); impl Render for ProjectDiagnosticsEditor {    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let warning_count = if self.include_warnings {            self.summary.warning_count        } else {            0        };         let child =            if warning_count + self.summary.error_count == 0 && self.editor.read(cx).is_empty(cx) {                let label = if self.summary.warning_count == 0 {                    SharedString::new_static("No problems in workspace")                } else {                    SharedString::new_static("No errors in workspace")                };                v_flex()                    .key_context("EmptyPane")                    .size_full()                    .gap_1()                    .justify_center()                    .items_center()                    .text_center()                    .bg(cx.theme().colors().editor_background)                    .child(Label::new(label).color(Color::Muted))                    .when(self.summary.warning_count > 0, |this| {                        let plural_suffix = if self.summary.warning_count > 1 {                            "s"                        } else {                            ""                        };                        let label = format!(                            "Show {} warning{}",                            self.summary.warning_count, plural_suffix                        );                        this.child(                            Button::new("diagnostics-show-warning-label", label).on_click(                                cx.listener(|this, _, window, cx| {                                    this.toggle_warnings(&Default::default(), window, cx);                                    cx.notify();                                }),                            ),                        )                    })            } else {                div().size_full().child(self.editor.clone())            };         div()            .key_context("Diagnostics")            .track_focus(&self.focus_handle(cx))            .size_full()            .on_action(cx.listener(Self::toggle_warnings))            .on_action(cx.listener(Self::toggle_diagnostics_refresh))            .child(child)    }} #[derive(PartialEq, Eq, Copy, Clone, Debug)]enum RetainExcerpts {    All,    Dirty,} impl ProjectDiagnosticsEditor {    pub fn register(        workspace: &mut Workspace,        _window: Option<&mut Window>,        _: &mut Context<Workspace>,    ) {        workspace.register_action(Self::deploy);    }     fn new(        include_warnings: bool,        project_handle: Entity<Project>,        workspace: WeakEntity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let project_event_subscription = cx.subscribe_in(            &project_handle,            window,            |this, _project, event, window, cx| match event {                project::Event::DiskBasedDiagnosticsStarted { .. } => {                    cx.notify();                }                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {                    log::debug!("disk based diagnostics finished for server {language_server_id}");                    this.close_diagnosticless_buffers(                        cx,                        this.editor.focus_handle(cx).contains_focused(window, cx)                            || this.focus_handle.contains_focused(window, cx),                    );                }                project::Event::DiagnosticsUpdated {                    language_server_id,                    paths,                } => {                    this.paths_to_update.extend(paths.clone());                    this.diagnostic_summary_update = cx.spawn(async move |this, cx| {                        cx.background_executor()                            .timer(DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE)                            .await;                        this.update(cx, |this, cx| {                            this.update_diagnostic_summary(cx);                        })                        .log_err();                    });                     log::debug!(                        "diagnostics updated for server {language_server_id}, \                        paths {paths:?}. updating excerpts"                    );                    this.update_stale_excerpts(window, cx);                }                _ => {}            },        );         let focus_handle = cx.focus_handle();        cx.on_focus_in(&focus_handle, window, Self::focus_in)            .detach();        cx.on_focus_out(&focus_handle, window, Self::focus_out)            .detach();         let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));        let editor = cx.new(|cx| {            let mut editor =                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);            editor.set_vertical_scroll_margin(5, cx);            editor.disable_inline_diagnostics();            editor.set_max_diagnostics_severity(                if include_warnings {                    DiagnosticSeverity::Warning                } else {                    DiagnosticSeverity::Error                },                cx,            );            editor.set_all_diagnostics_active(cx);            editor        });        cx.subscribe_in(            &editor,            window,            |this, _editor, event: &EditorEvent, window, cx| {                cx.emit(event.clone());                match event {                    EditorEvent::Focused => {                        if this.multibuffer.read(cx).is_empty() {                            window.focus(&this.focus_handle, cx);                        }                    }                    EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),                    EditorEvent::Saved => this.close_diagnosticless_buffers(cx, true),                    EditorEvent::SelectionsChanged { .. } => {                        this.close_diagnosticless_buffers(cx, true)                    }                    _ => {}                }            },        )        .detach();        cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {            let include_warnings = cx.global::<IncludeWarnings>().0;            this.include_warnings = include_warnings;            this.editor.update(cx, |editor, cx| {                editor.set_max_diagnostics_severity(                    if include_warnings {                        DiagnosticSeverity::Warning                    } else {                        DiagnosticSeverity::Error                    },                    cx,                )            });            this.refresh(window, cx);        })        .detach();         let project = project_handle.read(cx);        let mut this = Self {            project: project_handle.clone(),            summary: project.diagnostic_summary(false, cx),            diagnostics: Default::default(),            blocks: Default::default(),            include_warnings,            workspace,            multibuffer: excerpts,            focus_handle,            editor,            paths_to_update: Default::default(),            update_excerpts_task: None,            diagnostic_summary_update: Task::ready(()),            _subscription: project_event_subscription,        };        this.refresh(window, cx);        this    }     /// Closes all excerpts of buffers that:    ///  - have no diagnostics anymore    ///  - are saved (not dirty)    ///  - and, if `retain_selections` is true, do not have selections within them    fn close_diagnosticless_buffers(&mut self, cx: &mut Context<Self>, retain_selections: bool) {        let snapshot = self            .editor            .update(cx, |editor, cx| editor.display_snapshot(cx));        let buffer = self.multibuffer.read(cx);        let buffer_ids = buffer.all_buffer_ids();        let selected_buffers = self.editor.update(cx, |editor, _| {            editor                .selections                .all_anchors(&snapshot)                .iter()                .filter_map(|anchor| anchor.start.text_anchor.buffer_id)                .collect::<HashSet<_>>()        });        for buffer_id in buffer_ids {            if retain_selections && selected_buffers.contains(&buffer_id) {                continue;            }            let has_no_blocks = self                .blocks                .get(&buffer_id)                .is_none_or(|blocks| blocks.is_empty());            if !has_no_blocks {                continue;            }            let is_dirty = self                .multibuffer                .read(cx)                .buffer(buffer_id)                .is_none_or(|buffer| buffer.read(cx).is_dirty());            if is_dirty {                continue;            }            self.multibuffer.update(cx, |b, cx| {                b.remove_excerpts_for_buffer(buffer_id, cx);            });        }    }     fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {        if self.update_excerpts_task.is_some() {            return;        }         let project_handle = self.project.clone();        self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {            cx.background_executor()                .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)                .await;            loop {                let Some(path) = this.update(cx, |this, cx| {                    let Some(path) = this.paths_to_update.pop_first() else {                        this.update_excerpts_task = None;                        cx.notify();                        return None;                    };                    Some(path)                })?                else {                    break;                };                 if let Some(buffer) = project_handle                    .update(cx, |project, cx| project.open_buffer(path.clone(), cx))?                    .await                    .log_err()                {                    this.update_in(cx, |this, window, cx| {                        let focused = this.editor.focus_handle(cx).contains_focused(window, cx)                            || this.focus_handle.contains_focused(window, cx);                        let retain_excerpts = if focused {                            RetainExcerpts::All                        } else {                            RetainExcerpts::Dirty                        };                        this.update_excerpts(buffer, retain_excerpts, window, cx)                    })?                    .await?;                }            }            Ok(())        }));    }     fn deploy(        workspace: &mut Workspace,        _: &Deploy,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {            let is_active = workspace                .active_item(cx)                .is_some_and(|item| item.item_id() == existing.item_id());             workspace.activate_item(&existing, true, !is_active, window, cx);        } else {            let workspace_handle = cx.entity().downgrade();             let include_warnings = match cx.try_global::<IncludeWarnings>() {                Some(include_warnings) => include_warnings.0,                None => ProjectSettings::get_global(cx).diagnostics.include_warnings,            };             let diagnostics = cx.new(|cx| {                ProjectDiagnosticsEditor::new(                    include_warnings,                    workspace.project().clone(),                    workspace_handle,                    window,                    cx,                )            });            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, window, cx);        }    }     fn toggle_warnings(&mut self, _: &ToggleWarnings, _: &mut Window, cx: &mut Context<Self>) {        cx.set_global(IncludeWarnings(!self.include_warnings));    }     fn toggle_diagnostics_refresh(        &mut self,        _: &ToggleDiagnosticsRefresh,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if self.update_excerpts_task.is_some() {            self.update_excerpts_task = None;        } else {            self.refresh(window, cx);        }        cx.notify();    }     fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {            self.editor.focus_handle(cx).focus(window, cx)        }    }     fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)        {            self.close_diagnosticless_buffers(cx, false);        }    }     /// Clears all diagnostics in this view, and refetches them from the project.    fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.diagnostics.clear();        self.editor.update(cx, |editor, cx| {            for (_, block_ids) in self.blocks.drain() {                editor.display_map.update(cx, |display_map, cx| {                    display_map.remove_blocks(block_ids.into_iter().collect(), cx)                });            }        });        self.close_diagnosticless_buffers(cx, false);        self.project.update(cx, |project, cx| {            self.paths_to_update = project                .diagnostic_summaries(false, cx)                .map(|(project_path, _, _)| project_path)                .collect::<BTreeSet<_>>();        });         self.update_stale_excerpts(window, cx);    }     fn diagnostics_are_unchanged(        &self,        existing: &[DiagnosticEntry<text::Anchor>],        new: &[DiagnosticEntryRef<'_, text::Anchor>],        snapshot: &BufferSnapshot,    ) -> bool {        if existing.len() != new.len() {            return false;        }        existing.iter().zip(new.iter()).all(|(existing, new)| {            existing.diagnostic.message == new.diagnostic.message                && existing.diagnostic.severity == new.diagnostic.severity                && existing.diagnostic.is_primary == new.diagnostic.is_primary                && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)        })    }     fn update_excerpts(        &mut self,        buffer: Entity<Buffer>,        retain_excerpts: RetainExcerpts,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Task<Result<()>> {        let was_empty = self.multibuffer.read(cx).is_empty();        let buffer_snapshot = buffer.read(cx).snapshot();        let buffer_id = buffer_snapshot.remote_id();         let max_severity = if self.include_warnings {            lsp::DiagnosticSeverity::WARNING        } else {            lsp::DiagnosticSeverity::ERROR

Lines 1–500 of 1141.