302 lines · 11.2 KB · Rust
RawDownload
use std::{ops::Range, sync::Arc}; use editor::{    Anchor, Editor, EditorSnapshot, ToOffset,    display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},    hover_markdown_style,    scroll::Autoscroll,};use gpui::{AppContext, Entity, Focusable, WeakEntity};use language::{BufferId, DiagnosticEntry};use lsp::DiagnosticSeverity;use markdown::{Markdown, MarkdownElement};use settings::Settings;use text::{AnchorRangeExt, Point};use theme::ThemeSettings;use ui::{    ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,    Window, div, px,};use util::maybe; use crate::ProjectDiagnosticsEditor; pub struct DiagnosticRenderer; impl DiagnosticRenderer {    pub fn diagnostic_blocks_for_group(        diagnostic_group: Vec<DiagnosticEntry<Point>>,        buffer_id: BufferId,        diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,        cx: &mut App,    ) -> Vec<DiagnosticBlock> {        let Some(primary_ix) = diagnostic_group            .iter()            .position(|d| d.diagnostic.is_primary)        else {            return Vec::new();        };        let primary = diagnostic_group[primary_ix].clone();        let mut same_row = Vec::new();        let mut close = Vec::new();        let mut distant = Vec::new();        let group_id = primary.diagnostic.group_id;        for (ix, entry) in diagnostic_group.into_iter().enumerate() {            if entry.diagnostic.is_primary {                continue;            }            if entry.range.start.row == primary.range.start.row {                same_row.push(entry)            } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {                close.push(entry)            } else {                distant.push((ix, entry))            }        }         let mut markdown =            Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {                format!("{}: {}", source, primary.diagnostic.message)            } else {                primary.diagnostic.message            })            .to_string();        for entry in same_row {            markdown.push_str("\n- hint: ");            markdown.push_str(&Markdown::escape(&entry.diagnostic.message))        }         for (ix, entry) in &distant {            markdown.push_str("\n- hint: [");            markdown.push_str(&Markdown::escape(&entry.diagnostic.message));            markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))        }         let mut results = vec![DiagnosticBlock {            initial_range: primary.range,            severity: primary.diagnostic.severity,            buffer_id,            diagnostics_editor: diagnostics_editor.clone(),            markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),        }];         for entry in close {            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {                format!("{}: {}", source, entry.diagnostic.message)            } else {                entry.diagnostic.message            };            let markdown = Markdown::escape(&markdown).to_string();             results.push(DiagnosticBlock {                initial_range: entry.range,                severity: entry.diagnostic.severity,                buffer_id,                diagnostics_editor: diagnostics_editor.clone(),                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),            });        }         for (_, entry) in distant {            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {                format!("{}: {}", source, entry.diagnostic.message)            } else {                entry.diagnostic.message            };            let mut markdown = Markdown::escape(&markdown).to_string();            markdown.push_str(&format!(                " ([back](file://#diagnostic-{group_id}-{primary_ix}))"            ));            // problem: group-id changes...            //  - only an issue in diagnostics because caching             results.push(DiagnosticBlock {                initial_range: entry.range,                severity: entry.diagnostic.severity,                buffer_id,                diagnostics_editor: diagnostics_editor.clone(),                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),            });        }         results    }} impl editor::DiagnosticRenderer for DiagnosticRenderer {    fn render_group(        &self,        diagnostic_group: Vec<DiagnosticEntry<Point>>,        buffer_id: BufferId,        snapshot: EditorSnapshot,        editor: WeakEntity<Editor>,        cx: &mut App,    ) -> Vec<BlockProperties<Anchor>> {        let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);        blocks            .into_iter()            .map(|block| {                let editor = editor.clone();                BlockProperties {                    placement: BlockPlacement::Near(                        snapshot                            .buffer_snapshot                            .anchor_after(block.initial_range.start),                    ),                    height: Some(1),                    style: BlockStyle::Flex,                    render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),                    priority: 1,                }            })            .collect()    }} #[derive(Clone)]pub(crate) struct DiagnosticBlock {    pub(crate) initial_range: Range<Point>,    pub(crate) severity: DiagnosticSeverity,    pub(crate) buffer_id: BufferId,    pub(crate) markdown: Entity<Markdown>,    pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,} impl DiagnosticBlock {    pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {        let cx = &bcx.app;        let status_colors = bcx.app.theme().status();        let max_width = px(600.);         let (background_color, border_color) = match self.severity {            DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),            DiagnosticSeverity::WARNING => {                (status_colors.warning_background, status_colors.warning)            }            DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),            DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),            _ => (status_colors.ignored_background, status_colors.ignored),        };        let settings = ThemeSettings::get_global(cx);        let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();        let line_height = editor_line_height;        let buffer_id = self.buffer_id;        let diagnostics_editor = self.diagnostics_editor.clone();         div()            .border_l_2()            .px_2()            .line_height(line_height)            .bg(background_color)            .border_color(border_color)            .max_w(max_width)            .child(                MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))                    .on_url_click({                        move |link, window, cx| {                            Self::open_link(                                editor.clone(),                                &diagnostics_editor,                                link,                                window,                                buffer_id,                                cx,                            )                        }                    }),            )            .into_any_element()    }     pub fn open_link(        editor: WeakEntity<Editor>,        diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,        link: SharedString,        window: &mut Window,        buffer_id: BufferId,        cx: &mut App,    ) {        editor            .update(cx, |editor, cx| {                let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {                    editor::hover_popover::open_markdown_url(link, window, cx);                    return;                };                let Some((group_id, ix)) = maybe!({                    let (group_id, ix) = diagnostic_link.split_once('-')?;                    let group_id: usize = group_id.parse().ok()?;                    let ix: usize = ix.parse().ok()?;                    Some((group_id, ix))                }) else {                    return;                };                 if let Some(diagnostics_editor) = diagnostics_editor {                    if let Some(diagnostic) = diagnostics_editor                        .update(cx, |diagnostics, _| {                            diagnostics                                .diagnostics                                .get(&buffer_id)                                .cloned()                                .unwrap_or_default()                                .into_iter()                                .filter(|d| d.diagnostic.group_id == group_id)                                .nth(ix)                        })                        .ok()                        .flatten()                    {                        let multibuffer = editor.buffer().read(cx);                        let Some(snapshot) = multibuffer                            .buffer(buffer_id)                            .map(|entity| entity.read(cx).snapshot())                        else {                            return;                        };                         for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {                            if range.context.overlaps(&diagnostic.range, &snapshot) {                                Self::jump_to(                                    editor,                                    Anchor::range_in_buffer(                                        excerpt_id,                                        buffer_id,                                        diagnostic.range,                                    ),                                    window,                                    cx,                                );                                return;                            }                        }                    }                } else {                    if let Some(diagnostic) = editor                        .snapshot(window, cx)                        .buffer_snapshot                        .diagnostic_group(buffer_id, group_id)                        .nth(ix)                    {                        Self::jump_to(editor, diagnostic.range, window, cx)                    }                };            })            .ok();    }     fn jump_to<T: ToOffset>(        editor: &mut Editor,        range: Range<T>,        window: &mut Window,        cx: &mut Context<Editor>,    ) {        let snapshot = &editor.buffer().read(cx).snapshot(cx);        let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);         editor.unfold_ranges(&[range.start..range.end], true, false, cx);        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {            s.select_ranges([range.start..range.start]);        });        window.focus(&editor.focus_handle(cx));    }}