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, px,
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 let max_width = px(600.);
170
171 let (background_color, border_color) = match self.severity {
172 DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
173 DiagnosticSeverity::WARNING => {
174 (status_colors.warning_background, status_colors.warning)
175 }
176 DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
177 DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
178 _ => (status_colors.ignored_background, status_colors.ignored),
179 };
180 let settings = ThemeSettings::get_global(cx);
181 let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
182 let line_height = editor_line_height;
183 let buffer_id = self.buffer_id;
184 let diagnostics_editor = self.diagnostics_editor.clone();
185
186 div()
187 .border_l_2()
188 .px_2()
189 .line_height(line_height)
190 .bg(background_color)
191 .border_color(border_color)
192 .max_w(max_width)
193 .child(
194 MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
195 .on_url_click({
196 move |link, window, cx| {
197 Self::open_link(
198 editor.clone(),
199 &diagnostics_editor,
200 link,
201 window,
202 buffer_id,
203 cx,
204 )
205 }
206 }),
207 )
208 .into_any_element()
209 }
210
211 pub fn open_link(
212 editor: WeakEntity<Editor>,
213 diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
214 link: SharedString,
215 window: &mut Window,
216 buffer_id: BufferId,
217 cx: &mut App,
218 ) {
219 editor
220 .update(cx, |editor, cx| {
221 let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
222 editor::hover_popover::open_markdown_url(link, window, cx);
223 return;
224 };
225 let Some((group_id, ix)) = maybe!({
226 let (group_id, ix) = diagnostic_link.split_once('-')?;
227 let group_id: usize = group_id.parse().ok()?;
228 let ix: usize = ix.parse().ok()?;
229 Some((group_id, ix))
230 }) else {
231 return;
232 };
233
234 if let Some(diagnostics_editor) = diagnostics_editor {
235 if let Some(diagnostic) = diagnostics_editor
236 .update(cx, |diagnostics, _| {
237 diagnostics
238 .diagnostics
239 .get(&buffer_id)
240 .cloned()
241 .unwrap_or_default()
242 .into_iter()
243 .filter(|d| d.diagnostic.group_id == group_id)
244 .nth(ix)
245 })
246 .ok()
247 .flatten()
248 {
249 let multibuffer = editor.buffer().read(cx);
250 let Some(snapshot) = multibuffer
251 .buffer(buffer_id)
252 .map(|entity| entity.read(cx).snapshot())
253 else {
254 return;
255 };
256
257 for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
258 if range.context.overlaps(&diagnostic.range, &snapshot) {
259 Self::jump_to(
260 editor,
261 Anchor::range_in_buffer(
262 excerpt_id,
263 buffer_id,
264 diagnostic.range,
265 ),
266 window,
267 cx,
268 );
269 return;
270 }
271 }
272 }
273 } else {
274 if let Some(diagnostic) = editor
275 .snapshot(window, cx)
276 .buffer_snapshot
277 .diagnostic_group(buffer_id, group_id)
278 .nth(ix)
279 {
280 Self::jump_to(editor, diagnostic.range, window, cx)
281 }
282 };
283 })
284 .ok();
285 }
286
287 fn jump_to<T: ToOffset>(
288 editor: &mut Editor,
289 range: Range<T>,
290 window: &mut Window,
291 cx: &mut Context<Editor>,
292 ) {
293 let snapshot = &editor.buffer().read(cx).snapshot(cx);
294 let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
295
296 editor.unfold_ranges(&[range.start..range.end], true, false, cx);
297 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
298 s.select_ranges([range.start..range.start]);
299 });
300 window.focus(&editor.focus_handle(cx));
301 }
302}