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}