871 lines · 33.2 KB · Rust
RawDownload
pub mod items;mod project_diagnostics_settings;mod toolbar_controls; #[cfg(test)]mod diagnostics_tests; use anyhow::Result;use collections::{BTreeSet, HashSet};use editor::{    diagnostic_block_renderer,    display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},    highlight_diagnostic_message,    scroll::Autoscroll,    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,};use futures::{    channel::mpsc::{self, UnboundedSender},    StreamExt as _,};use gpui::{    actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,    FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,    SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,    WeakView, WindowContext,};use language::{    Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,};use lsp::LanguageServerId;use project::{DiagnosticSummary, Project, ProjectPath};use project_diagnostics_settings::ProjectDiagnosticsSettings;use settings::Settings;use std::{    any::{Any, TypeId},    cmp::Ordering,    mem,    ops::Range,};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},    ItemNavHistory, ToolbarItemLocation, Workspace,}; actions!(diagnostics, [Deploy, ToggleWarnings]); pub fn init(cx: &mut AppContext) {    ProjectDiagnosticsSettings::register(cx);    cx.observe_new_views(ProjectDiagnosticsEditor::register)        .detach();} struct ProjectDiagnosticsEditor {    project: Model<Project>,    workspace: WeakView<Workspace>,    focus_handle: FocusHandle,    editor: View<Editor>,    summary: DiagnosticSummary,    excerpts: Model<MultiBuffer>,    path_states: Vec<PathState>,    paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,    include_warnings: bool,    context: u32,    update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,    _update_excerpts_task: 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 {} impl Render for ProjectDiagnosticsEditor {    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {        let child = if self.path_states.is_empty() {            div()                .bg(cx.theme().colors().editor_background)                .flex()                .items_center()                .justify_center()                .size_full()                .child(Label::new("No problems in workspace"))        } else {            div().size_full().child(self.editor.clone())        };         div()            .track_focus(&self.focus_handle)            .when(self.path_states.is_empty(), |el| {                el.key_context("EmptyPane")            })            .size_full()            .on_action(cx.listener(Self::toggle_warnings))            .child(child)    }} impl ProjectDiagnosticsEditor {    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {        workspace.register_action(Self::deploy);    }     fn new_with_context(        context: u32,        project_handle: Model<Project>,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<Self>,    ) -> Self {        let project_event_subscription =            cx.subscribe(&project_handle, |this, project, event, 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.enqueue_update_stale_excerpts(Some(*language_server_id));                }                project::Event::DiagnosticsUpdated {                    language_server_id,                    path,                } => {                    this.paths_to_update                        .insert((path.clone(), *language_server_id));                    this.summary = project.read(cx).diagnostic_summary(false, cx);                    cx.emit(EditorEvent::TitleChanged);                     if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(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.enqueue_update_stale_excerpts(Some(*language_server_id));                    }                }                _ => {}            });         let focus_handle = cx.focus_handle();        cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))            .detach();        cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))            .detach();         let excerpts = cx.new_model(|cx| {            MultiBuffer::new(                project_handle.read(cx).replica_id(),                project_handle.read(cx).capability(),            )        });        let editor = cx.new_view(|cx| {            let mut editor =                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);            editor.set_vertical_scroll_margin(5, cx);            editor        });        cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {            cx.emit(event.clone());            match event {                EditorEvent::Focused => {                    if this.path_states.is_empty() {                        cx.focus(&this.focus_handle);                    }                }                EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),                _ => {}            }        })        .detach();         let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();         let project = project_handle.read(cx);        let mut this = Self {            project: project_handle.clone(),            context,            summary: project.diagnostic_summary(false, cx),            workspace,            excerpts,            focus_handle,            editor,            path_states: Default::default(),            paths_to_update: Default::default(),            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,            update_paths_tx: update_excerpts_tx,            _update_excerpts_task: cx.spawn(move |this, mut cx| async move {                while let Some((path, language_server_id)) = update_excerpts_rx.next().await {                    if let Some(buffer) = project_handle                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?                        .await                        .log_err()                    {                        this.update(&mut cx, |this, cx| {                            this.update_excerpts(path, language_server_id, buffer, cx);                        })?;                    }                }                anyhow::Ok(())            }),            _subscription: project_event_subscription,        };        this.enqueue_update_all_excerpts(cx);        this    }     fn new(        project_handle: Model<Project>,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<Self>,    ) -> Self {        Self::new_with_context(            editor::DEFAULT_MULTIBUFFER_CONTEXT,            project_handle,            workspace,            cx,        )    }     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {            workspace.activate_item(&existing, true, true, cx);        } else {            let workspace_handle = cx.view().downgrade();            let diagnostics = cx.new_view(|cx| {                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)            });            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);        }    }     fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {        self.include_warnings = !self.include_warnings;        self.enqueue_update_all_excerpts(cx);        cx.notify();    }     fn focus_in(&mut self, cx: &mut ViewContext<Self>) {        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {            self.editor.focus_handle(cx).focus(cx)        }    }     fn focus_out(&mut self, cx: &mut ViewContext<Self>) {        if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {            self.enqueue_update_stale_excerpts(None);        }    }     /// Enqueue an update of all excerpts. Updates all paths that either    /// currently have diagnostics or are currently present in this view.    fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {        self.project.update(cx, |project, cx| {            let mut paths = project                .diagnostic_summaries(false, cx)                .map(|(path, _, _)| path)                .collect::<BTreeSet<_>>();            paths.extend(self.path_states.iter().map(|state| state.path.clone()));            for path in paths {                self.update_paths_tx.unbounded_send((path, None)).unwrap();            }        });    }     /// Enqueue an update of the excerpts for any path whose diagnostics are known    /// to have changed. If a language server id is passed, then only the excerpts for    /// that language server's diagnostics will be updated. Otherwise, all stale excerpts    /// will be refreshed.    fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {        for (path, server_id) in &self.paths_to_update {            if language_server_id.map_or(true, |id| id == *server_id) {                self.update_paths_tx                    .unbounded_send((path.clone(), Some(*server_id)))                    .unwrap();            }        }    }     fn update_excerpts(        &mut self,        path_to_update: ProjectPath,        server_to_update: Option<LanguageServerId>,        buffer: Model<Buffer>,        cx: &mut ViewContext<Self>,    ) {        self.paths_to_update.retain(|(path, server_id)| {            *path != path_to_update                || server_to_update.map_or(false, |to_update| *server_id != to_update)        });         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 path_state = &mut self.path_states[path_ix];        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_snapshot = self.excerpts.update(cx, |excerpts, cx| {            let mut old_groups = mem::take(&mut path_state.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>, 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));                        if let Some((range, start_ix)) = &mut pending_range {                            if let Some(entry) = resolved_entry.as_ref() {                                if entry.range.start.row <= range.end.row + 1 + self.context * 2 {                                    range.end = range.end.max(entry.range.end);                                    continue;                                }                            }                             let excerpt_start =                                Point::new(range.start.row.saturating_sub(self.context), 0);                            let excerpt_end = snapshot.clip_point(                                Point::new(range.end.row + self.context, u32::MAX),                                Bias::Left,                            );                             let excerpt_id = excerpts                                .insert_excerpts_after(                                    prev_excerpt_id,                                    buffer.clone(),                                    [ExcerptRange {                                        context: excerpt_start..excerpt_end,                                        primary: Some(range.clone()),                                    }],                                    cx,                                )                                .pop()                                .unwrap();                             prev_excerpt_id = excerpt_id;                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);                            group_state.excerpts.push(excerpt_id);                            let header_position = (excerpt_id, language::Anchor::MIN);                             if is_first_excerpt_for_group {                                is_first_excerpt_for_group = false;                                let mut primary =                                    group.entries[group.primary_ix].diagnostic.clone();                                primary.message =                                    primary.message.split('\n').next().unwrap().to_string();                                group_state.block_count += 1;                                blocks_to_add.push(BlockProperties {                                    position: header_position,                                    height: 2,                                    style: BlockStyle::Sticky,                                    render: diagnostic_header_renderer(primary),                                    disposition: BlockDisposition::Above,                                    priority: 0,                                });                            }                             for entry in &group.entries[*start_ix..ix] {                                let mut diagnostic = entry.diagnostic.clone();                                if diagnostic.is_primary {                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;                                    diagnostic.message =                                        entry.diagnostic.message.split('\n').skip(1).collect();                                }                                 if !diagnostic.message.is_empty() {                                    group_state.block_count += 1;                                    blocks_to_add.push(BlockProperties {                                        position: (excerpt_id, entry.range.start),                                        height: diagnostic.message.matches('\n').count() as u32 + 1,                                        style: BlockStyle::Fixed,                                        render: diagnostic_block_renderer(                                            diagnostic, None, true, true,                                        ),                                        disposition: BlockDisposition::Below,                                        priority: 0,                                    });                                }                            }                             pending_range.take();                        }                         if let Some(entry) = resolved_entry {                            pending_range = Some((entry.range.clone(), ix));                        }                    }                     new_group_ixs.push(path_state.diagnostic_groups.len());                    path_state.diagnostic_groups.push(group_state);                } else if let Some((_, group_state)) = to_remove {                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx);                    blocks_to_remove.extend(group_state.blocks.iter().copied());                } else if let Some((_, group_state)) = to_keep {                    prev_excerpt_id = *group_state.excerpts.last().unwrap();                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id);                    path_state.diagnostic_groups.push(group_state);                }            }             excerpts.snapshot(cx)        });

Lines 1–500 of 871.