919 lines · 33.3 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 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 lsp::DiagnosticSeverity;use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};use settings::Settings;use std::{    any::{Any, TypeId},    cmp,    cmp::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]); 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<()>>>,    _subscription: Subscription,} 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))            .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_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| {            this.include_warnings = cx.global::<IncludeWarnings>().0;            this.update_all_excerpts(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,            _subscription: project_event_subscription,        };        this.update_all_excerpts(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, _| {                    let Some(path) = this.paths_to_update.pop_first() else {                        this.update_excerpts_task.take();                        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, window: &mut Window, cx: &mut Context<Self>) {        self.include_warnings = !self.include_warnings;        cx.set_global(IncludeWarnings(self.include_warnings));        self.update_all_excerpts(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);        }    }     /// 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,        buffer: Entity<Buffer>,        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 {            DiagnosticSeverity::WARNING        } else {            DiagnosticSeverity::ERROR        };         cx.spawn_in(window, async move |this, mut cx| {            let diagnostics = buffer_snapshot                .diagnostics_in_range::<_, text::Anchor>(                    Point::zero()..buffer_snapshot.max_point(),                    false,                )                .filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))                .collect::<Vec<_>>();            let unchanged = this.update(cx, |this, _| {                if this.diagnostics.get(&buffer_id).is_some_and(|existing| {                    this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)                }) {                    return true;                }                this.diagnostics.insert(buffer_id, diagnostics.clone());                return false;            })?;            if unchanged {                return Ok(());            }             let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();            for entry in diagnostics {                grouped                    .entry(entry.diagnostic.group_id)                    .or_default()                    .push(DiagnosticEntry {                        range: entry.range.to_point(&buffer_snapshot),                        diagnostic: entry.diagnostic,                    })            }            let mut blocks: Vec<DiagnosticBlock> = Vec::new();             for (_, group) in grouped {                let group_severity = group.iter().map(|d| d.diagnostic.severity).min();                if group_severity.is_none_or(|s| s > max_severity) {                    continue;                }                let more = cx.update(|_, cx| {                    crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(                        group,                        buffer_snapshot.remote_id(),                        Some(this.clone()),                        cx,                    )                })?;                 for item in more {                    let insert_pos = blocks                        .binary_search_by(|existing| {                            match existing.initial_range.start.cmp(&item.initial_range.start) {                                Ordering::Equal => item                                    .initial_range                                    .end                                    .cmp(&existing.initial_range.end)                                    .reverse(),                                other => other,                            }                        })                        .unwrap_or_else(|pos| pos);                     blocks.insert(insert_pos, item);                }            }             let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();            for b in blocks.iter() {                let excerpt_range = context_range_for_entry(                    b.initial_range.clone(),                    DEFAULT_MULTIBUFFER_CONTEXT,                    buffer_snapshot.clone(),                    &mut cx,                )                .await;                excerpt_ranges.push(ExcerptRange {                    context: excerpt_range,                    primary: b.initial_range.clone(),                })            }             this.update_in(cx, |this, window, cx| {                if let Some(block_ids) = this.blocks.remove(&buffer_id) {                    this.editor.update(cx, |editor, cx| {                        editor.display_map.update(cx, |display_map, cx| {                            display_map.remove_blocks(block_ids.into_iter().collect(), cx)                        });                    })                }                let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {                    multi_buffer.set_excerpt_ranges_for_path(                        PathKey::for_buffer(&buffer, cx),                        buffer.clone(),                        &buffer_snapshot,                        excerpt_ranges,                        cx,                    )                });                #[cfg(test)]                let cloned_blocks = blocks.clone();                 if was_empty {                    if let Some(anchor_range) = anchor_ranges.first() {                        let range_to_select = anchor_range.start..anchor_range.start;                        this.editor.update(cx, |editor, cx| {                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {                                s.select_anchor_ranges([range_to_select]);                            })                        })                    }                }                 let editor_blocks =                    anchor_ranges                        .into_iter()                        .zip(blocks.into_iter())                        .map(|(anchor, block)| {                            let editor = this.editor.downgrade();                            BlockProperties {                                placement: BlockPlacement::Near(anchor.start),                                height: Some(1),                                style: BlockStyle::Flex,                                render: Arc::new(move |bcx| {                                    block.render_block(editor.clone(), bcx)

Lines 1–500 of 919.