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_markdown_style,
  7    scroll::Autoscroll,
  8};
  9use gpui::{AppContext, Entity, Focusable, WeakEntity};
 10use language::{BufferId, 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, px,
 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 mut same_row = Vec::new();
 41        let mut close = Vec::new();
 42        let mut distant = Vec::new();
 43        let group_id = primary.diagnostic.group_id;
 44        for (ix, entry) in diagnostic_group.into_iter().enumerate() {
 45            if entry.diagnostic.is_primary {
 46                continue;
 47            }
 48            if entry.range.start.row == primary.range.start.row {
 49                same_row.push(entry)
 50            } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
 51                close.push(entry)
 52            } else {
 53                distant.push((ix, entry))
 54            }
 55        }
 56
 57        let mut markdown =
 58            Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
 59                format!("{}: {}", source, primary.diagnostic.message)
 60            } else {
 61                primary.diagnostic.message
 62            })
 63            .to_string();
 64        for entry in same_row {
 65            markdown.push_str("\n- hint: ");
 66            markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
 67        }
 68
 69        for (ix, entry) in &distant {
 70            markdown.push_str("\n- hint: [");
 71            markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
 72            markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
 73        }
 74
 75        let mut results = vec![DiagnosticBlock {
 76            initial_range: primary.range,
 77            severity: primary.diagnostic.severity,
 78            buffer_id,
 79            diagnostics_editor: diagnostics_editor.clone(),
 80            markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
 81        }];
 82
 83        for entry in close {
 84            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
 85                format!("{}: {}", source, entry.diagnostic.message)
 86            } else {
 87                entry.diagnostic.message
 88            };
 89            let markdown = Markdown::escape(&markdown).to_string();
 90
 91            results.push(DiagnosticBlock {
 92                initial_range: entry.range,
 93                severity: entry.diagnostic.severity,
 94                buffer_id,
 95                diagnostics_editor: diagnostics_editor.clone(),
 96                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
 97            });
 98        }
 99
100        for (_, entry) in distant {
101            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
102                format!("{}: {}", source, entry.diagnostic.message)
103            } else {
104                entry.diagnostic.message
105            };
106            let mut markdown = Markdown::escape(&markdown).to_string();
107            markdown.push_str(&format!(
108                " ([back](file://#diagnostic-{group_id}-{primary_ix}))"
109            ));
110            // problem: group-id changes...
111            //  - only an issue in diagnostics because caching
112
113            results.push(DiagnosticBlock {
114                initial_range: entry.range,
115                severity: entry.diagnostic.severity,
116                buffer_id,
117                diagnostics_editor: diagnostics_editor.clone(),
118                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
119            });
120        }
121
122        results
123    }
124}
125
126impl editor::DiagnosticRenderer for DiagnosticRenderer {
127    fn render_group(
128        &self,
129        diagnostic_group: Vec<DiagnosticEntry<Point>>,
130        buffer_id: BufferId,
131        snapshot: EditorSnapshot,
132        editor: WeakEntity<Editor>,
133        cx: &mut App,
134    ) -> Vec<BlockProperties<Anchor>> {
135        let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
136        blocks
137            .into_iter()
138            .map(|block| {
139                let editor = editor.clone();
140                BlockProperties {
141                    placement: BlockPlacement::Near(
142                        snapshot
143                            .buffer_snapshot
144                            .anchor_after(block.initial_range.start),
145                    ),
146                    height: Some(1),
147                    style: BlockStyle::Flex,
148                    render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
149                    priority: 1,
150                }
151            })
152            .collect()
153    }
154}
155
156#[derive(Clone)]
157pub(crate) struct DiagnosticBlock {
158    pub(crate) initial_range: Range<Point>,
159    pub(crate) severity: DiagnosticSeverity,
160    pub(crate) buffer_id: BufferId,
161    pub(crate) markdown: Entity<Markdown>,
162    pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
163}
164
165impl DiagnosticBlock {
166    pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
167        let cx = &bcx.app;
168        let status_colors = bcx.app.theme().status();
169        let max_width = px(600.);
170
171        let (background_color, border_color) = match self.severity {
172            DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
173            DiagnosticSeverity::WARNING => {
174                (status_colors.warning_background, status_colors.warning)
175            }
176            DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
177            DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
178            _ => (status_colors.ignored_background, status_colors.ignored),
179        };
180        let settings = ThemeSettings::get_global(cx);
181        let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
182        let line_height = editor_line_height;
183        let buffer_id = self.buffer_id;
184        let diagnostics_editor = self.diagnostics_editor.clone();
185
186        div()
187            .border_l_2()
188            .px_2()
189            .line_height(line_height)
190            .bg(background_color)
191            .border_color(border_color)
192            .max_w(max_width)
193            .child(
194                MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
195                    .on_url_click({
196                        move |link, window, cx| {
197                            Self::open_link(
198                                editor.clone(),
199                                &diagnostics_editor,
200                                link,
201                                window,
202                                buffer_id,
203                                cx,
204                            )
205                        }
206                    }),
207            )
208            .into_any_element()
209    }
210
211    pub fn open_link(
212        editor: WeakEntity<Editor>,
213        diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
214        link: SharedString,
215        window: &mut Window,
216        buffer_id: BufferId,
217        cx: &mut App,
218    ) {
219        editor
220            .update(cx, |editor, cx| {
221                let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
222                    editor::hover_popover::open_markdown_url(link, window, cx);
223                    return;
224                };
225                let Some((group_id, ix)) = maybe!({
226                    let (group_id, ix) = diagnostic_link.split_once('-')?;
227                    let group_id: usize = group_id.parse().ok()?;
228                    let ix: usize = ix.parse().ok()?;
229                    Some((group_id, ix))
230                }) else {
231                    return;
232                };
233
234                if let Some(diagnostics_editor) = diagnostics_editor {
235                    if let Some(diagnostic) = diagnostics_editor
236                        .update(cx, |diagnostics, _| {
237                            diagnostics
238                                .diagnostics
239                                .get(&buffer_id)
240                                .cloned()
241                                .unwrap_or_default()
242                                .into_iter()
243                                .filter(|d| d.diagnostic.group_id == group_id)
244                                .nth(ix)
245                        })
246                        .ok()
247                        .flatten()
248                    {
249                        let multibuffer = editor.buffer().read(cx);
250                        let Some(snapshot) = multibuffer
251                            .buffer(buffer_id)
252                            .map(|entity| entity.read(cx).snapshot())
253                        else {
254                            return;
255                        };
256
257                        for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
258                            if range.context.overlaps(&diagnostic.range, &snapshot) {
259                                Self::jump_to(
260                                    editor,
261                                    Anchor::range_in_buffer(
262                                        excerpt_id,
263                                        buffer_id,
264                                        diagnostic.range,
265                                    ),
266                                    window,
267                                    cx,
268                                );
269                                return;
270                            }
271                        }
272                    }
273                } else {
274                    if let Some(diagnostic) = editor
275                        .snapshot(window, cx)
276                        .buffer_snapshot
277                        .diagnostic_group(buffer_id, group_id)
278                        .nth(ix)
279                    {
280                        Self::jump_to(editor, diagnostic.range, window, cx)
281                    }
282                };
283            })
284            .ok();
285    }
286
287    fn jump_to<T: ToOffset>(
288        editor: &mut Editor,
289        range: Range<T>,
290        window: &mut Window,
291        cx: &mut Context<Editor>,
292    ) {
293        let snapshot = &editor.buffer().read(cx).snapshot(cx);
294        let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
295
296        editor.unfold_ranges(&[range.start..range.end], true, false, cx);
297        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
298            s.select_ranges([range.start..range.start]);
299        });
300        window.focus(&editor.focus_handle(cx));
301    }
302}