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