Skip to repository content333 lines · 12.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:34:20.640Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
diagnostic_renderer.rs
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, DiagnosticEntryRef, LanguageRegistry};
10use lsp::DiagnosticSeverity;
11use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
12use settings::Settings;
13use text::Point;
14use theme_settings::ThemeSettings;
15use ui::{CopyButton, prelude::*};
16use util::maybe;
17
18use crate::toolbar_controls::DiagnosticsToolbarEditor;
19
20pub struct DiagnosticRenderer;
21
22impl DiagnosticRenderer {
23 pub fn diagnostic_blocks_for_group(
24 diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
25 buffer_id: BufferId,
26 diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
27 language_registry: Option<Arc<LanguageRegistry>>,
28 cx: &mut App,
29 ) -> Vec<DiagnosticBlock> {
30 let Some(primary_ix) = diagnostic_group
31 .iter()
32 .position(|d| d.diagnostic.is_primary)
33 else {
34 return Vec::new();
35 };
36 let primary = &diagnostic_group[primary_ix];
37 let group_id = primary.diagnostic.group_id;
38 let mut results = vec![];
39 for entry in diagnostic_group.iter() {
40 let mut markdown = Self::markdown(&entry.diagnostic);
41 if entry.diagnostic.is_primary {
42 let diagnostic = &primary.diagnostic;
43 append_source_and_code(&mut markdown, diagnostic);
44
45 for (ix, entry) in diagnostic_group.iter().enumerate() {
46 if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
47 markdown.push_str("\n- hint: [");
48 markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
49 markdown.push_str(&format!(
50 "](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
51 ))
52 }
53 }
54
55 results.push(DiagnosticBlock {
56 initial_range: primary.range.clone(),
57 severity: primary.diagnostic.severity,
58 diagnostics_editor: diagnostics_editor.clone(),
59 copy_message: primary.diagnostic.message.clone().into(),
60 markdown: cx.new(|cx| {
61 Markdown::new(markdown.into(), language_registry.clone(), None, cx)
62 }),
63 });
64 } else {
65 append_source_and_code(&mut markdown, entry.diagnostic);
66
67 if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
68 markdown.push_str(&format!(
69 " ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
70 ));
71 }
72 results.push(DiagnosticBlock {
73 initial_range: entry.range.clone(),
74 severity: entry.diagnostic.severity,
75 diagnostics_editor: diagnostics_editor.clone(),
76 copy_message: entry.diagnostic.message.clone().into(),
77 markdown: cx.new(|cx| {
78 Markdown::new(markdown.into(), language_registry.clone(), None, cx)
79 }),
80 });
81 }
82 }
83
84 results
85 }
86
87 fn markdown(diagnostic: &Diagnostic) -> String {
88 let mut markdown = String::new();
89
90 if let Some(md) = &diagnostic.markdown {
91 markdown.push_str(md);
92 } else {
93 markdown.push_str(&Markdown::escape(&diagnostic.message));
94 };
95 markdown
96 }
97}
98
99fn append_source_and_code(markdown: &mut String, diagnostic: &Diagnostic) {
100 if diagnostic.source.is_none() && diagnostic.code.is_none() {
101 return;
102 }
103 markdown.push_str(" (");
104 if let Some(source) = diagnostic.source.as_ref() {
105 markdown.push_str(&Markdown::escape(source));
106 }
107 if diagnostic.source.is_some() && diagnostic.code.is_some() {
108 markdown.push(' ');
109 }
110 if let Some(code) = diagnostic.code.as_ref() {
111 if let Some(description) = diagnostic.code_description.as_ref() {
112 markdown.push('[');
113 markdown.push_str(&Markdown::escape(&code.to_string()));
114 markdown.push_str("](");
115 markdown.push_str(&Markdown::escape(description.as_ref()));
116 markdown.push(')');
117 } else {
118 markdown.push_str(&Markdown::escape(&code.to_string()));
119 }
120 }
121 markdown.push(')');
122}
123
124impl editor::DiagnosticRenderer for DiagnosticRenderer {
125 fn render_group(
126 &self,
127 diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
128 buffer_id: BufferId,
129 snapshot: EditorSnapshot,
130 editor: WeakEntity<Editor>,
131 language_registry: Option<Arc<LanguageRegistry>>,
132 cx: &mut App,
133 ) -> Vec<BlockProperties<Anchor>> {
134 let blocks = Self::diagnostic_blocks_for_group(
135 diagnostic_group,
136 buffer_id,
137 None,
138 language_registry,
139 cx,
140 );
141
142 blocks
143 .into_iter()
144 .map(|block| {
145 let editor = editor.clone();
146 BlockProperties {
147 placement: BlockPlacement::Near(
148 snapshot
149 .buffer_snapshot()
150 .anchor_after(block.initial_range.start),
151 ),
152 height: Some(1),
153 style: BlockStyle::Flex,
154 render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
155 priority: 1,
156 }
157 })
158 .collect()
159 }
160
161 fn render_hover(
162 &self,
163 diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
164 range: Range<Point>,
165 buffer_id: BufferId,
166 language_registry: Option<Arc<LanguageRegistry>>,
167 cx: &mut App,
168 ) -> Option<Entity<Markdown>> {
169 let blocks = Self::diagnostic_blocks_for_group(
170 diagnostic_group,
171 buffer_id,
172 None,
173 language_registry,
174 cx,
175 );
176 blocks
177 .into_iter()
178 .find_map(|block| (block.initial_range == range).then(|| block.markdown))
179 }
180
181 fn open_link(
182 &self,
183 editor: &mut Editor,
184 link: SharedString,
185 window: &mut Window,
186 cx: &mut Context<Editor>,
187 ) {
188 DiagnosticBlock::open_link(editor, &None, link, window, cx);
189 }
190}
191
192#[derive(Clone)]
193pub(crate) struct DiagnosticBlock {
194 pub(crate) initial_range: Range<Point>,
195 pub(crate) severity: DiagnosticSeverity,
196 pub(crate) markdown: Entity<Markdown>,
197 pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
198 pub(crate) copy_message: SharedString,
199}
200
201impl DiagnosticBlock {
202 pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
203 let cx = &bcx.app;
204 let status_colors = cx.theme().status();
205
206 let max_width = bcx.em_width * 120.;
207
208 let (background_color, border_color) = match self.severity {
209 DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
210 DiagnosticSeverity::WARNING => {
211 (status_colors.warning_background, status_colors.warning)
212 }
213 DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
214 DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.hint),
215 _ => (status_colors.ignored_background, status_colors.ignored),
216 };
217 let settings = ThemeSettings::get_global(cx);
218 let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
219 let line_height = editor_line_height;
220 let diagnostics_editor = self.diagnostics_editor.clone();
221
222 let copy_button_id = format!(
223 "copy-diagnostic-{}-{}-{}-{}",
224 self.initial_range.start.row,
225 self.initial_range.start.column,
226 self.initial_range.end.row,
227 self.initial_range.end.column
228 );
229
230 h_flex()
231 .max_w(max_width)
232 .pl_1p5()
233 .pr_0p5()
234 .items_start()
235 .gap_1()
236 .border_l_2()
237 .line_height(line_height)
238 .bg(background_color)
239 .border_color(border_color)
240 .child(
241 div().flex_1().min_w_0().child(
242 MarkdownElement::new(
243 self.markdown.clone(),
244 diagnostics_markdown_style(bcx.window, cx),
245 )
246 .code_block_renderer(markdown::CodeBlockRenderer::Default {
247 copy_button_visibility: CopyButtonVisibility::Hidden,
248 wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
249 border: false,
250 })
251 .on_url_click({
252 move |link, window, cx| {
253 editor
254 .update(cx, |editor, cx| {
255 Self::open_link(editor, &diagnostics_editor, link, window, cx)
256 })
257 .ok();
258 }
259 }),
260 ),
261 )
262 .child(
263 CopyButton::new(copy_button_id, self.copy_message.clone())
264 .tooltip_label("Copy Diagnostic"),
265 )
266 .into_any_element()
267 }
268
269 pub fn open_link(
270 editor: &mut Editor,
271 diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
272 link: SharedString,
273 window: &mut Window,
274 cx: &mut Context<Editor>,
275 ) {
276 let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
277 let workspace = editor.workspace();
278 editor::hover_popover::open_markdown_url(workspace, link, window, cx);
279 return;
280 };
281 let Some((buffer_id, group_id, ix)) = maybe!({
282 let mut parts = diagnostic_link.split('-');
283 let buffer_id: u64 = parts.next()?.parse().ok()?;
284 let group_id: usize = parts.next()?.parse().ok()?;
285 let ix: usize = parts.next()?.parse().ok()?;
286 Some((BufferId::new(buffer_id).ok()?, group_id, ix))
287 }) else {
288 return;
289 };
290
291 if let Some(diagnostics_editor) = diagnostics_editor {
292 if let Some(diagnostic) = diagnostics_editor
293 .get_diagnostics_for_buffer(buffer_id, cx)
294 .into_iter()
295 .filter(|d| d.diagnostic.group_id == group_id)
296 .nth(ix)
297 {
298 let multibuffer = editor.buffer().read(cx);
299 if let Some(anchor_range) = multibuffer
300 .snapshot(cx)
301 .buffer_anchor_range_to_anchor_range(diagnostic.range)
302 {
303 Self::jump_to(editor, anchor_range, window, cx);
304 return;
305 }
306 }
307 } else if let Some(diagnostic) = editor
308 .snapshot(window, cx)
309 .buffer_snapshot()
310 .diagnostic_group(buffer_id, group_id)
311 .nth(ix)
312 {
313 Self::jump_to(editor, diagnostic.range, window, cx)
314 };
315 }
316
317 fn jump_to<I: ToOffset>(
318 editor: &mut Editor,
319 range: Range<I>,
320 window: &mut Window,
321 cx: &mut Context<Editor>,
322 ) {
323 let snapshot = &editor.buffer().read(cx).snapshot(cx);
324 let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot);
325
326 editor.unfold_ranges(&[range.start..range.end], true, false, cx);
327 editor.change_selections(Default::default(), window, cx, |s| {
328 s.select_ranges([range.start..range.start]);
329 });
330 window.focus(&editor.focus_handle(cx), cx);
331 }
332}
333