1232 lines · 47.2 KB · Rust
RawDownload
pub mod items;mod toolbar_controls; #[cfg(test)]mod diagnostics_tests; use anyhow::Result;use collections::{BTreeSet, HashSet};use editor::{    diagnostic_block_renderer,    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},    highlight_diagnostic_message,    scroll::Autoscroll,    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,};use gpui::{    actions, div, svg, AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter,    FocusHandle, Focusable, Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement,    Render, SharedString, Styled, StyledText, Subscription, Task, WeakEntity, Window,};use language::{    Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,    Point, Selection, SelectionGoal, ToTreeSitterPoint,};use lsp::LanguageServerId;use project::{project_settings::ProjectSettings, DiagnosticSummary, Project, ProjectPath};use settings::Settings;use std::{    any::{Any, TypeId},    cmp,    cmp::Ordering,    mem,    ops::{Range, RangeInclusive},    sync::Arc,    time::Duration,};use theme::ActiveTheme;pub use toolbar_controls::ToolbarControls;use ui::{h_flex, prelude::*, Icon, IconName, Label};use util::ResultExt;use workspace::{    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},    searchable::SearchableItemHandle,    ItemNavHistory, ToolbarItemLocation, Workspace,}; actions!(diagnostics, [Deploy, ToggleWarnings]); struct IncludeWarnings(bool);impl Global for IncludeWarnings {} pub fn init(cx: &mut App) {    cx.observe_new(ProjectDiagnosticsEditor::register).detach();} struct ProjectDiagnosticsEditor {    project: Entity<Project>,    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    editor: Entity<Editor>,    summary: DiagnosticSummary,    excerpts: Entity<MultiBuffer>,    path_states: Vec<PathState>,    paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,    include_warnings: bool,    context: u32,    update_excerpts_task: Option<Task<Result<()>>>,    _subscription: Subscription,} struct PathState {    path: ProjectPath,    diagnostic_groups: Vec<DiagnosticGroupState>,} struct DiagnosticGroupState {    language_server_id: LanguageServerId,    primary_diagnostic: DiagnosticEntry<language::Anchor>,    primary_excerpt_ix: usize,    excerpts: Vec<ExcerptId>,    blocks: HashSet<CustomBlockId>,    block_count: usize,} impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DEBOUNCE: 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_with_context(        context: u32,        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(), Some(*language_server_id)));                    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()),                true,                window,                cx,            );            editor.set_vertical_scroll_margin(5, cx);            editor.disable_inline_diagnostics();            editor        });        cx.subscribe_in(            &editor,            window,            |this, _editor, event: &EditorEvent, window, cx| {                cx.emit(event.clone());                match event {                    EditorEvent::Focused => {                        if this.path_states.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(),            context,            summary: project.diagnostic_summary(false, cx),            include_warnings,            workspace,            excerpts,            focus_handle,            editor,            path_states: Default::default(),            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, |this, mut cx| async move {            cx.background_executor()                .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)                .await;            loop {                let Some((path, language_server_id)) = this.update(&mut cx, |this, _| {                    let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {                        this.update_excerpts_task.take();                        return None;                    };                    Some((path, language_server_id))                })?                else {                    break;                };                 if let Some(buffer) = project_handle                    .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?                    .await                    .log_err()                {                    this.update_in(&mut cx, |this, window, cx| {                        this.update_excerpts(path, language_server_id, buffer, window, cx)                    })?                    .await?;                }            }            Ok(())        }));    }     fn new(        project_handle: Entity<Project>,        include_warnings: bool,        workspace: WeakEntity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        Self::new_with_context(            editor::DEFAULT_MULTIBUFFER_CONTEXT,            include_warnings,            project_handle,            workspace,            window,            cx,        )    }     fn deploy(        workspace: &mut Workspace,        _: &Deploy,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {            workspace.activate_item(&existing, true, true, 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(                    workspace.project().clone(),                    include_warnings,                    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.path_states.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, None))                .collect::<BTreeSet<_>>();            paths.extend(                self.path_states                    .iter()                    .map(|state| (state.path.clone(), None)),            );            let paths_to_update = std::mem::take(&mut self.paths_to_update);            paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));            self.paths_to_update = paths;        });        self.update_stale_excerpts(window, cx);    }     fn update_excerpts(        &mut self,        path_to_update: ProjectPath,        server_to_update: Option<LanguageServerId>,        buffer: Entity<Buffer>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Task<Result<()>> {        let was_empty = self.path_states.is_empty();        let snapshot = buffer.read(cx).snapshot();        let path_ix = match self            .path_states            .binary_search_by_key(&&path_to_update, |e| &e.path)        {            Ok(ix) => ix,            Err(ix) => {                self.path_states.insert(                    ix,                    PathState {                        path: path_to_update.clone(),                        diagnostic_groups: Default::default(),                    },                );                ix            }        };        let mut prev_excerpt_id = if path_ix > 0 {            let prev_path_last_group = &self.path_states[path_ix - 1]                .diagnostic_groups                .last()                .unwrap();            *prev_path_last_group.excerpts.last().unwrap()        } else {            ExcerptId::min()        };         let mut new_group_ixs = Vec::new();        let mut blocks_to_add = Vec::new();        let mut blocks_to_remove = HashSet::default();        let mut first_excerpt_id = None;        let max_severity = if self.include_warnings {            DiagnosticSeverity::WARNING        } else {            DiagnosticSeverity::ERROR        };        let excerpts = self.excerpts.clone().downgrade();        let context = self.context;        let editor = self.editor.clone().downgrade();        cx.spawn_in(window, move |this, mut cx| async move {            let mut old_groups = this                .update(&mut cx, |this, _| {                    mem::take(&mut this.path_states[path_ix].diagnostic_groups)                })?                .into_iter()                .enumerate()                .peekable();            let mut new_groups = snapshot                .diagnostic_groups(server_to_update)                .into_iter()                .filter(|(_, group)| {                    group.entries[group.primary_ix].diagnostic.severity <= max_severity                })                .peekable();            loop {                let mut to_insert = None;                let mut to_remove = None;                let mut to_keep = None;                match (old_groups.peek(), new_groups.peek()) {                    (None, None) => break,                    (None, Some(_)) => to_insert = new_groups.next(),                    (Some((_, old_group)), None) => {                        if server_to_update.map_or(true, |id| id == old_group.language_server_id) {                            to_remove = old_groups.next();                        } else {                            to_keep = old_groups.next();                        }                    }                    (Some((_, old_group)), Some((new_language_server_id, new_group))) => {                        let old_primary = &old_group.primary_diagnostic;                        let new_primary = &new_group.entries[new_group.primary_ix];                        match compare_diagnostics(old_primary, new_primary, &snapshot)                            .then_with(|| old_group.language_server_id.cmp(new_language_server_id))                        {                            Ordering::Less => {                                if server_to_update                                    .map_or(true, |id| id == old_group.language_server_id)                                {                                    to_remove = old_groups.next();                                } else {                                    to_keep = old_groups.next();                                }                            }                            Ordering::Equal => {                                to_keep = old_groups.next();                                new_groups.next();                            }                            Ordering::Greater => to_insert = new_groups.next(),                        }                    }                }                 if let Some((language_server_id, group)) = to_insert {                    let mut group_state = DiagnosticGroupState {                        language_server_id,                        primary_diagnostic: group.entries[group.primary_ix].clone(),                        primary_excerpt_ix: 0,                        excerpts: Default::default(),                        blocks: Default::default(),                        block_count: 0,                    };                    let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;                    let mut is_first_excerpt_for_group = true;                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));                        let expanded_range = if let Some(entry) = &resolved_entry {                            Some(                                context_range_for_entry(                                    entry.range.clone(),                                    context,                                    snapshot.clone(),                                    (*cx).clone(),                                )                                .await,                            )                        } else {

Lines 1–500 of 1232.