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