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