1098 lines · 39.5 KB · Rust
RawDownload
pub mod items;mod toolbar_controls; mod diagnostic_renderer; #[cfg(test)]mod diagnostics_tests; use anyhow::Result;use collections::{BTreeSet, HashMap};use diagnostic_renderer::DiagnosticBlock;use editor::{    DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},    scroll::Autoscroll,};use futures::future::join_all;use gpui::{    AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,    Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,    Subscription, Task, WeakEntity, Window, actions, div,};use language::{    Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,};use project::{    DiagnosticSummary, Project, ProjectPath,    lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},    project_settings::{DiagnosticSeverity, ProjectSettings},};use settings::Settings;use std::{    any::{Any, TypeId},    cmp::{self, Ordering},    ops::{Range, RangeInclusive},    sync::Arc,    time::Duration,};use text::{BufferId, OffsetRangeExt};use theme::ActiveTheme;pub use toolbar_controls::ToolbarControls;use ui::{Icon, IconName, Label, h_flex, prelude::*};use util::ResultExt;use workspace::{    ItemNavHistory, ToolbarItemLocation, Workspace,    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},    searchable::SearchableItemHandle,}; actions!(    diagnostics,    [Deploy, ToggleWarnings, 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();} pub(crate) struct ProjectDiagnosticsEditor {    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<()>>>,    cargo_diagnostics_fetch: CargoDiagnosticsFetchState,    _subscription: Subscription,} struct CargoDiagnosticsFetchState {    fetch_task: Option<Task<()>>,    cancel_task: Option<Task<()>>,    diagnostic_sources: Arc<Vec<ProjectPath>>,} impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); 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 {            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)    }} impl ProjectDiagnosticsEditor {    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.update_stale_excerpts(window, cx);                }                project::Event::DiagnosticsUpdated {                    language_server_id,                    path,                } => {                    this.paths_to_update.insert(path.clone());                    this.summary = project.read(cx).diagnostic_summary(false, cx);                    cx.emit(EditorEvent::TitleChanged);                     if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");                    } else {                        log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");                        this.update_stale_excerpts(window, cx);                    }                }                _ => {}            });         let focus_handle = cx.focus_handle();        cx.on_focus_in(&focus_handle, window, |this, window, cx| {            this.focus_in(window, cx)        })        .detach();        cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {            this.focus_out(window, cx)        })        .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);                        }                    }                    EditorEvent::Blurred => this.update_stale_excerpts(window, cx),                    _ => {}                }            },        )        .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.diagnostics.clear();            this.update_all_diagnostics(false, window, cx);        })        .detach();        cx.observe_release(&cx.entity(), |editor, _, cx| {            editor.stop_cargo_diagnostics_fetch(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,            cargo_diagnostics_fetch: CargoDiagnosticsFetchState {                fetch_task: None,                cancel_task: None,                diagnostic_sources: Arc::new(Vec::new()),            },            _subscription: project_event_subscription,        };        this.update_all_diagnostics(true, window, cx);        this    }     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_DELAY)                .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| {                        this.update_excerpts(buffer, 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>,    ) {        let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)            .diagnostics            .fetch_cargo_diagnostics();         if fetch_cargo_diagnostics {            if self.cargo_diagnostics_fetch.fetch_task.is_some() {                self.stop_cargo_diagnostics_fetch(cx);            } else {                self.update_all_diagnostics(false, window, cx);            }        } else {            if self.update_excerpts_task.is_some() {                self.update_excerpts_task = None;            } else {                self.update_all_diagnostics(false, 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)        }    }     fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {        if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)        {            self.update_stale_excerpts(window, cx);        }    }     fn update_all_diagnostics(        &mut self,        first_launch: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);        if cargo_diagnostics_sources.is_empty() {            self.update_all_excerpts(window, cx);        } else if first_launch && !self.summary.is_empty() {            self.update_all_excerpts(window, cx);        } else {            self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);        }    }     fn fetch_cargo_diagnostics(        &mut self,        diagnostics_sources: Arc<Vec<ProjectPath>>,        cx: &mut Context<Self>,    ) {        let project = self.project.clone();        self.cargo_diagnostics_fetch.cancel_task = None;        self.cargo_diagnostics_fetch.fetch_task = None;        self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();        if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {            return;        }         self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {            let mut fetch_tasks = Vec::new();            for buffer_path in diagnostics_sources.iter().cloned() {                if cx                    .update(|cx| {                        fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));                    })                    .is_err()                {                    break;                }            }             let _ = join_all(fetch_tasks).await;            editor                .update(cx, |editor, _| {                    editor.cargo_diagnostics_fetch.fetch_task = None;                })                .ok();        }));    }     fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {        self.cargo_diagnostics_fetch.fetch_task = None;        let mut cancel_gasks = Vec::new();        for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)            .iter()            .cloned()        {            cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));        }         self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {            let _ = join_all(cancel_gasks).await;            log::info!("Finished fetching cargo diagnostics");        }));    }     /// Enqueue an update of all excerpts. Updates all paths that either    /// currently have diagnostics or are currently present in this view.    fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.project.update(cx, |project, cx| {            let mut paths = project                .diagnostic_summaries(false, cx)                .map(|(path, _, _)| path)                .collect::<BTreeSet<_>>();            self.multibuffer.update(cx, |multibuffer, cx| {                for buffer in multibuffer.all_buffers() {                    if let Some(file) = buffer.read(cx).file() {                        paths.insert(ProjectPath {                            path: file.path().clone(),                            worktree_id: file.worktree_id(cx),                        });                    }                }            });            self.paths_to_update = paths;        });        self.update_stale_excerpts(window, cx);    }     fn diagnostics_are_unchanged(        &self,        existing: &Vec<DiagnosticEntry<text::Anchor>>,        new: &Vec<DiagnosticEntry<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,

Lines 1–500 of 1098.