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