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,
 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
170        let max_width = bcx.em_width * 100.;
171
172        let (background_color, border_color) = match self.severity {
173            DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
174            DiagnosticSeverity::WARNING => {
175                (status_colors.warning_background, status_colors.warning)
176            }
177            DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
178            DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
179            _ => (status_colors.ignored_background, status_colors.ignored),
180        };
181        let settings = ThemeSettings::get_global(cx);
182        let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
183        let line_height = editor_line_height;
184        let buffer_id = self.buffer_id;
185        let diagnostics_editor = self.diagnostics_editor.clone();
186
187        div()
188            .border_l_2()
189            .px_2()
190            .line_height(line_height)
191            .bg(background_color)
192            .border_color(border_color)
193            .max_w(max_width)
194            .child(
195                MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
196                    .on_url_click({
197                        move |link, window, cx| {
198                            Self::open_link(
199                                editor.clone(),
200                                &diagnostics_editor,
201                                link,
202                                window,
203                                buffer_id,
204                                cx,
205                            )
206                        }
207                    }),
208            )
209            .into_any_element()
210    }
211
212    pub fn open_link(
213        editor: WeakEntity<Editor>,
214        diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
215        link: SharedString,
216        window: &mut Window,
217        buffer_id: BufferId,
218        cx: &mut App,
219    ) {
220        editor
221            .update(cx, |editor, cx| {
222                let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
223                    editor::hover_popover::open_markdown_url(link, window, cx);
224                    return;
225                };
226                let Some((group_id, ix)) = maybe!({
227                    let (group_id, ix) = diagnostic_link.split_once('-')?;
228                    let group_id: usize = group_id.parse().ok()?;
229                    let ix: usize = ix.parse().ok()?;
230                    Some((group_id, ix))
231                }) else {
232                    return;
233                };
234
235                if let Some(diagnostics_editor) = diagnostics_editor {
236                    if let Some(diagnostic) = diagnostics_editor
237                        .update(cx, |diagnostics, _| {
238                            diagnostics
239                                .diagnostics
240                                .get(&buffer_id)
241                                .cloned()
242                                .unwrap_or_default()
243                                .into_iter()
244                                .filter(|d| d.diagnostic.group_id == group_id)
245                                .nth(ix)
246                        })
247                        .ok()
248                        .flatten()
249                    {
250                        let multibuffer = editor.buffer().read(cx);
251                        let Some(snapshot) = multibuffer
252                            .buffer(buffer_id)
253                            .map(|entity| entity.read(cx).snapshot())
254                        else {
255                            return;
256                        };
257
258                        for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
259                            if range.context.overlaps(&diagnostic.range, &snapshot) {
260                                Self::jump_to(
261                                    editor,
262                                    Anchor::range_in_buffer(
263                                        excerpt_id,
264                                        buffer_id,
265                                        diagnostic.range,
266                                    ),
267                                    window,
268                                    cx,
269                                );
270                                return;
271                            }
272                        }
273                    }
274                } else {
275                    if let Some(diagnostic) = editor
276                        .snapshot(window, cx)
277                        .buffer_snapshot
278                        .diagnostic_group(buffer_id, group_id)
279                        .nth(ix)
280                    {
281                        Self::jump_to(editor, diagnostic.range, window, cx)
282                    }
283                };
284            })
285            .ok();
286    }
287
288    fn jump_to<T: ToOffset>(
289        editor: &mut Editor,
290        range: Range<T>,
291        window: &mut Window,
292        cx: &mut Context<Editor>,
293    ) {
294        let snapshot = &editor.buffer().read(cx).snapshot(cx);
295        let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
296
297        editor.unfold_ranges(&[range.start..range.end], true, false, cx);
298        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
299            s.select_ranges([range.start..range.start]);
300        });
301        window.focus(&editor.focus_handle(cx));
302    }
303}