920 lines · 34.5 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::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},    highlight_diagnostic_message,    scroll::Autoscroll,    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,};use gpui::{    actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,    FocusableView, Global, 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,    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},    ItemNavHistory, ToolbarItemLocation, Workspace,}; actions!(diagnostics, [Deploy, ToggleWarnings]); struct IncludeWarnings(bool);impl Global for IncludeWarnings {} 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, 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, 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(cx))            .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,        include_warnings: bool,        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.update_stale_excerpts(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(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.update_stale_excerpts(cx);                    }                }                _ => {}            });         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).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.update_stale_excerpts(cx),                _ => {}            }        })        .detach();        cx.observe_global::<IncludeWarnings>(|this, cx| {            this.include_warnings = cx.global::<IncludeWarnings>().0;            this.update_all_excerpts(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(cx);        this    }     fn update_stale_excerpts(&mut self, cx: &mut ViewContext<Self>) {        if self.update_excerpts_task.is_some() {            return;        }        let project_handle = self.project.clone();        self.update_excerpts_task = Some(cx.spawn(|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(&mut cx, |this, cx| {                        this.update_excerpts(path, language_server_id, buffer, cx);                    })?;                }            }            Ok(())        }));    }     fn new(        project_handle: Model<Project>,        include_warnings: bool,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<Self>,    ) -> Self {        Self::new_with_context(            editor::DEFAULT_MULTIBUFFER_CONTEXT,            include_warnings,            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 include_warnings = match cx.try_global::<IncludeWarnings>() {                Some(include_warnings) => include_warnings.0,                None => ProjectDiagnosticsSettings::get_global(cx).include_warnings,            };             let diagnostics = cx.new_view(|cx| {                ProjectDiagnosticsEditor::new(                    workspace.project().clone(),                    include_warnings,                    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;        cx.set_global(IncludeWarnings(self.include_warnings));        self.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.update_stale_excerpts(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, cx: &mut ViewContext<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(cx);    }     fn update_excerpts(        &mut self,        path_to_update: ProjectPath,        server_to_update: Option<LanguageServerId>,        buffer: Model<Buffer>,        cx: &mut ViewContext<Self>,    ) {        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(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 {                                    placement: BlockPlacement::Above(header_position),                                    height: 2,                                    style: BlockStyle::Sticky,                                    render: diagnostic_header_renderer(primary),                                    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 {                                        placement: BlockPlacement::Below((                                            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,                                        ),                                        priority: 0,                                    });                                }                            }

Lines 1–500 of 920.