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