diagnostic_renderer.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use editor::{
  4    Anchor, Editor, EditorSnapshot, ToOffset,
  5    display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
  6    hover_popover::diagnostics_markdown_style,
  7};
  8use gpui::{AppContext, Entity, Focusable, WeakEntity};
  9use language::{BufferId, Diagnostic, DiagnosticEntryRef, LanguageRegistry};
 10use lsp::DiagnosticSeverity;
 11use markdown::{Markdown, MarkdownElement};
 12use settings::Settings;
 13use text::{AnchorRangeExt, Point};
 14use theme::ThemeSettings;
 15use ui::{
 16    ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
 17    Window, div,
 18};
 19use util::maybe;
 20
 21use crate::toolbar_controls::DiagnosticsToolbarEditor;
 22
 23pub struct DiagnosticRenderer;
 24
 25impl DiagnosticRenderer {
 26    pub fn diagnostic_blocks_for_group(
 27        diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
 28        buffer_id: BufferId,
 29        diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
 30        language_registry: Option<Arc<LanguageRegistry>>,
 31        cx: &mut App,
 32    ) -> Vec<DiagnosticBlock> {
 33        let Some(primary_ix) = diagnostic_group
 34            .iter()
 35            .position(|d| d.diagnostic.is_primary)
 36        else {
 37            return Vec::new();
 38        };
 39        let primary = &diagnostic_group[primary_ix];
 40        let group_id = primary.diagnostic.group_id;
 41        let mut results = vec![];
 42        for entry in diagnostic_group.iter() {
 43            let mut markdown = Self::markdown(&entry.diagnostic);
 44            if entry.diagnostic.is_primary {
 45                let diagnostic = &primary.diagnostic;
 46                if diagnostic.source.is_some() || diagnostic.code.is_some() {
 47                    markdown.push_str(" (");
 48                }
 49                if let Some(source) = diagnostic.source.as_ref() {
 50                    markdown.push_str(&Markdown::escape(source));
 51                }
 52                if diagnostic.source.is_some() && diagnostic.code.is_some() {
 53                    markdown.push(' ');
 54                }
 55                if let Some(code) = diagnostic.code.as_ref() {
 56                    if let Some(description) = diagnostic.code_description.as_ref() {
 57                        markdown.push('[');
 58                        markdown.push_str(&Markdown::escape(&code.to_string()));
 59                        markdown.push_str("](");
 60                        markdown.push_str(&Markdown::escape(description.as_ref()));
 61                        markdown.push(')');
 62                    } else {
 63                        markdown.push_str(&Markdown::escape(&code.to_string()));
 64                    }
 65                }
 66                if diagnostic.source.is_some() || diagnostic.code.is_some() {
 67                    markdown.push(')');
 68                }
 69
 70                for (ix, entry) in diagnostic_group.iter().enumerate() {
 71                    if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
 72                        markdown.push_str("\n- hint: [");
 73                        markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
 74                        markdown.push_str(&format!(
 75                            "](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
 76                        ))
 77                    }
 78                }
 79
 80                results.push(DiagnosticBlock {
 81                    initial_range: primary.range.clone(),
 82                    severity: primary.diagnostic.severity,
 83                    diagnostics_editor: diagnostics_editor.clone(),
 84                    markdown: cx.new(|cx| {
 85                        Markdown::new(markdown.into(), language_registry.clone(), None, cx)
 86                    }),
 87                });
 88            } else {
 89                if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
 90                    markdown.push_str(&format!(
 91                        " ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
 92                    ));
 93                }
 94                results.push(DiagnosticBlock {
 95                    initial_range: entry.range.clone(),
 96                    severity: entry.diagnostic.severity,
 97                    diagnostics_editor: diagnostics_editor.clone(),
 98                    markdown: cx.new(|cx| {
 99                        Markdown::new(markdown.into(), language_registry.clone(), None, cx)
100                    }),
101                });
102            }
103        }
104
105        results
106    }
107
108    fn markdown(diagnostic: &Diagnostic) -> String {
109        let mut markdown = String::new();
110
111        if let Some(md) = &diagnostic.markdown {
112            markdown.push_str(md);
113        } else {
114            markdown.push_str(&Markdown::escape(&diagnostic.message));
115        };
116        markdown
117    }
118}
119
120impl editor::DiagnosticRenderer for DiagnosticRenderer {
121    fn render_group(
122        &self,
123        diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
124        buffer_id: BufferId,
125        snapshot: EditorSnapshot,
126        editor: WeakEntity<Editor>,
127        language_registry: Option<Arc<LanguageRegistry>>,
128        cx: &mut App,
129    ) -> Vec<BlockProperties<Anchor>> {
130        let blocks = Self::diagnostic_blocks_for_group(
131            diagnostic_group,
132            buffer_id,
133            None,
134            language_registry,
135            cx,
136        );
137
138        blocks
139            .into_iter()
140            .map(|block| {
141                let editor = editor.clone();
142                BlockProperties {
143                    placement: BlockPlacement::Near(
144                        snapshot
145                            .buffer_snapshot()
146                            .anchor_after(block.initial_range.start),
147                    ),
148                    height: Some(1),
149                    style: BlockStyle::Flex,
150                    render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
151                    priority: 1,
152                }
153            })
154            .collect()
155    }
156
157    fn render_hover(
158        &self,
159        diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
160        range: Range<Point>,
161        buffer_id: BufferId,
162        language_registry: Option<Arc<LanguageRegistry>>,
163        cx: &mut App,
164    ) -> Option<Entity<Markdown>> {
165        let blocks = Self::diagnostic_blocks_for_group(
166            diagnostic_group,
167            buffer_id,
168            None,
169            language_registry,
170            cx,
171        );
172        blocks
173            .into_iter()
174            .find_map(|block| (block.initial_range == range).then(|| block.markdown))
175    }
176
177    fn open_link(
178        &self,
179        editor: &mut Editor,
180        link: SharedString,
181        window: &mut Window,
182        cx: &mut Context<Editor>,
183    ) {
184        DiagnosticBlock::open_link(editor, &None, link, window, cx);
185    }
186}
187
188#[derive(Clone)]
189pub(crate) struct DiagnosticBlock {
190    pub(crate) initial_range: Range<Point>,
191    pub(crate) severity: DiagnosticSeverity,
192    pub(crate) markdown: Entity<Markdown>,
193    pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
194}
195
196impl DiagnosticBlock {
197    pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
198        let cx = &bcx.app;
199        let status_colors = cx.theme().status();
200
201        let max_width = bcx.em_width * 120.;
202
203        let (background_color, border_color) = match self.severity {
204            DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
205            DiagnosticSeverity::WARNING => {
206                (status_colors.warning_background, status_colors.warning)
207            }
208            DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
209            DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
210            _ => (status_colors.ignored_background, status_colors.ignored),
211        };
212        let settings = ThemeSettings::get_global(cx);
213        let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
214        let line_height = editor_line_height;
215        let diagnostics_editor = self.diagnostics_editor.clone();
216
217        div()
218            .border_l_2()
219            .px_2()
220            .line_height(line_height)
221            .bg(background_color)
222            .border_color(border_color)
223            .max_w(max_width)
224            .child(
225                MarkdownElement::new(
226                    self.markdown.clone(),
227                    diagnostics_markdown_style(bcx.window, cx),
228                )
229                .code_block_renderer(markdown::CodeBlockRenderer::Default {
230                    copy_button: false,
231                    copy_button_on_hover: false,
232                    border: false,
233                })
234                .on_url_click({
235                    move |link, window, cx| {
236                        editor
237                            .update(cx, |editor, cx| {
238                                Self::open_link(editor, &diagnostics_editor, link, window, cx)
239                            })
240                            .ok();
241                    }
242                }),
243            )
244            .into_any_element()
245    }
246
247    pub fn open_link(
248        editor: &mut Editor,
249        diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
250        link: SharedString,
251        window: &mut Window,
252        cx: &mut Context<Editor>,
253    ) {
254        let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
255            editor::hover_popover::open_markdown_url(link, window, cx);
256            return;
257        };
258        let Some((buffer_id, group_id, ix)) = maybe!({
259            let mut parts = diagnostic_link.split('-');
260            let buffer_id: u64 = parts.next()?.parse().ok()?;
261            let group_id: usize = parts.next()?.parse().ok()?;
262            let ix: usize = parts.next()?.parse().ok()?;
263            Some((BufferId::new(buffer_id).ok()?, group_id, ix))
264        }) else {
265            return;
266        };
267
268        if let Some(diagnostics_editor) = diagnostics_editor {
269            if let Some(diagnostic) = diagnostics_editor
270                .get_diagnostics_for_buffer(buffer_id, cx)
271                .into_iter()
272                .filter(|d| d.diagnostic.group_id == group_id)
273                .nth(ix)
274            {
275                let multibuffer = editor.buffer().read(cx);
276                let Some(snapshot) = multibuffer
277                    .buffer(buffer_id)
278                    .map(|entity| entity.read(cx).snapshot())
279                else {
280                    return;
281                };
282
283                for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
284                    if range.context.overlaps(&diagnostic.range, &snapshot) {
285                        Self::jump_to(
286                            editor,
287                            Anchor::range_in_buffer(excerpt_id, buffer_id, diagnostic.range),
288                            window,
289                            cx,
290                        );
291                        return;
292                    }
293                }
294            }
295        } else if let Some(diagnostic) = editor
296            .snapshot(window, cx)
297            .buffer_snapshot()
298            .diagnostic_group(buffer_id, group_id)
299            .nth(ix)
300        {
301            Self::jump_to(editor, diagnostic.range, window, cx)
302        };
303    }
304
305    fn jump_to<I: ToOffset>(
306        editor: &mut Editor,
307        range: Range<I>,
308        window: &mut Window,
309        cx: &mut Context<Editor>,
310    ) {
311        let snapshot = &editor.buffer().read(cx).snapshot(cx);
312        let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot);
313
314        editor.unfold_ranges(&[range.start..range.end], true, false, cx);
315        editor.change_selections(Default::default(), window, cx, |s| {
316            s.select_ranges([range.start..range.start]);
317        });
318        window.focus(&editor.focus_handle(cx));
319    }
320}