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