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