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