use std::{ops::Range, sync::Arc}; use editor::{ Anchor, Editor, EditorSnapshot, ToOffset, display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle}, hover_markdown_style, scroll::Autoscroll,};use gpui::{AppContext, Entity, Focusable, WeakEntity};use language::{BufferId, DiagnosticEntry};use lsp::DiagnosticSeverity;use markdown::{Markdown, MarkdownElement};use settings::Settings;use text::{AnchorRangeExt, Point};use theme::ThemeSettings;use ui::{ ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, Window, div, px,};use util::maybe; use crate::ProjectDiagnosticsEditor; pub struct DiagnosticRenderer; impl DiagnosticRenderer { pub fn diagnostic_blocks_for_group( diagnostic_group: Vec<DiagnosticEntry<Point>>, buffer_id: BufferId, diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>, cx: &mut App, ) -> Vec<DiagnosticBlock> { let Some(primary_ix) = diagnostic_group .iter() .position(|d| d.diagnostic.is_primary) else { return Vec::new(); }; let primary = diagnostic_group[primary_ix].clone(); let mut same_row = Vec::new(); let mut close = Vec::new(); let mut distant = Vec::new(); let group_id = primary.diagnostic.group_id; for (ix, entry) in diagnostic_group.into_iter().enumerate() { if entry.diagnostic.is_primary { continue; } if entry.range.start.row == primary.range.start.row { same_row.push(entry) } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 { close.push(entry) } else { distant.push((ix, entry)) } } let mut markdown = Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() { format!("{}: {}", source, primary.diagnostic.message) } else { primary.diagnostic.message }) .to_string(); for entry in same_row { markdown.push_str("\n- hint: "); markdown.push_str(&Markdown::escape(&entry.diagnostic.message)) } for (ix, entry) in &distant { markdown.push_str("\n- hint: ["); markdown.push_str(&Markdown::escape(&entry.diagnostic.message)); markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",)) } let mut results = vec![DiagnosticBlock { initial_range: primary.range, severity: primary.diagnostic.severity, buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }]; for entry in close { let markdown = if let Some(source) = entry.diagnostic.source.as_ref() { format!("{}: {}", source, entry.diagnostic.message) } else { entry.diagnostic.message }; let markdown = Markdown::escape(&markdown).to_string(); results.push(DiagnosticBlock { initial_range: entry.range, severity: entry.diagnostic.severity, buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }); } for (_, entry) in distant { let markdown = if let Some(source) = entry.diagnostic.source.as_ref() { format!("{}: {}", source, entry.diagnostic.message) } else { entry.diagnostic.message }; let mut markdown = Markdown::escape(&markdown).to_string(); markdown.push_str(&format!( " ([back](file://#diagnostic-{group_id}-{primary_ix}))" )); // problem: group-id changes... // - only an issue in diagnostics because caching results.push(DiagnosticBlock { initial_range: entry.range, severity: entry.diagnostic.severity, buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }); } results }} impl editor::DiagnosticRenderer for DiagnosticRenderer { fn render_group( &self, diagnostic_group: Vec<DiagnosticEntry<Point>>, buffer_id: BufferId, snapshot: EditorSnapshot, editor: WeakEntity<Editor>, cx: &mut App, ) -> Vec<BlockProperties<Anchor>> { let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx); blocks .into_iter() .map(|block| { let editor = editor.clone(); BlockProperties { placement: BlockPlacement::Near( snapshot .buffer_snapshot .anchor_after(block.initial_range.start), ), height: Some(1), style: BlockStyle::Flex, render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)), priority: 1, } }) .collect() }} #[derive(Clone)]pub(crate) struct DiagnosticBlock { pub(crate) initial_range: Range<Point>, pub(crate) severity: DiagnosticSeverity, pub(crate) buffer_id: BufferId, pub(crate) markdown: Entity<Markdown>, pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,} impl DiagnosticBlock { pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement { let cx = &bcx.app; let status_colors = bcx.app.theme().status(); let max_width = px(600.); let (background_color, border_color) = match self.severity { DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error), DiagnosticSeverity::WARNING => { (status_colors.warning_background, status_colors.warning) } DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info), DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info), _ => (status_colors.ignored_background, status_colors.ignored), }; let settings = ThemeSettings::get_global(cx); let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round(); let line_height = editor_line_height; let buffer_id = self.buffer_id; let diagnostics_editor = self.diagnostics_editor.clone(); div() .border_l_2() .px_2() .line_height(line_height) .bg(background_color) .border_color(border_color) .max_w(max_width) .child( MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx)) .on_url_click({ move |link, window, cx| { Self::open_link( editor.clone(), &diagnostics_editor, link, window, buffer_id, cx, ) } }), ) .into_any_element() } pub fn open_link( editor: WeakEntity<Editor>, diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>, link: SharedString, window: &mut Window, buffer_id: BufferId, cx: &mut App, ) { editor .update(cx, |editor, cx| { let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else { editor::hover_popover::open_markdown_url(link, window, cx); return; }; let Some((group_id, ix)) = maybe!({ let (group_id, ix) = diagnostic_link.split_once('-')?; let group_id: usize = group_id.parse().ok()?; let ix: usize = ix.parse().ok()?; Some((group_id, ix)) }) else { return; }; if let Some(diagnostics_editor) = diagnostics_editor { if let Some(diagnostic) = diagnostics_editor .update(cx, |diagnostics, _| { diagnostics .diagnostics .get(&buffer_id) .cloned() .unwrap_or_default() .into_iter() .filter(|d| d.diagnostic.group_id == group_id) .nth(ix) }) .ok() .flatten() { let multibuffer = editor.buffer().read(cx); let Some(snapshot) = multibuffer .buffer(buffer_id) .map(|entity| entity.read(cx).snapshot()) else { return; }; for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) { if range.context.overlaps(&diagnostic.range, &snapshot) { Self::jump_to( editor, Anchor::range_in_buffer( excerpt_id, buffer_id, diagnostic.range, ), window, cx, ); return; } } } } else { if let Some(diagnostic) = editor .snapshot(window, cx) .buffer_snapshot .diagnostic_group(buffer_id, group_id) .nth(ix) { Self::jump_to(editor, diagnostic.range, window, cx) } }; }) .ok(); } fn jump_to<T: ToOffset>( editor: &mut Editor, range: Range<T>, window: &mut Window, cx: &mut Context<Editor>, ) { let snapshot = &editor.buffer().read(cx).snapshot(cx); let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); editor.unfold_ranges(&[range.start..range.end], true, false, cx); editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges([range.start..range.start]); }); window.focus(&editor.focus_handle(cx)); }}
use std::{ops::Range, sync::Arc};
use editor::{
Anchor, Editor, EditorSnapshot, ToOffset,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
hover_markdown_style,
scroll::Autoscroll,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, DiagnosticEntry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownElement};
use settings::Settings;
use text::{AnchorRangeExt, Point};
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
Window, div, px,
};
use util::maybe;
use crate::ProjectDiagnosticsEditor;
pub struct DiagnosticRenderer;
impl DiagnosticRenderer {
pub fn diagnostic_blocks_for_group(
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
.iter()
.position(|d| d.diagnostic.is_primary)
else {
return Vec::new();
};
let primary = diagnostic_group[primary_ix].clone();
let mut same_row = Vec::new();
let mut close = Vec::new();
let mut distant = Vec::new();
let group_id = primary.diagnostic.group_id;
for (ix, entry) in diagnostic_group.into_iter().enumerate() {
if entry.diagnostic.is_primary {
continue;
}
if entry.range.start.row == primary.range.start.row {
same_row.push(entry)
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
close.push(entry)
} else {
distant.push((ix, entry))
}
}
let mut markdown =
Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
format!("{}: {}", source, primary.diagnostic.message)
} else {
primary.diagnostic.message
})
.to_string();
for entry in same_row {
markdown.push_str("\n- hint: ");
markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
}
for (ix, entry) in &distant {
markdown.push_str("\n- hint: [");
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
}
let mut results = vec![DiagnosticBlock {
initial_range: primary.range,
severity: primary.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
}];
for entry in close {
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
format!("{}: {}", source, entry.diagnostic.message)
} else {
entry.diagnostic.message
};
let markdown = Markdown::escape(&markdown).to_string();
results.push(DiagnosticBlock {
initial_range: entry.range,
severity: entry.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
}
for (_, entry) in distant {
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
format!("{}: {}", source, entry.diagnostic.message)
} else {
entry.diagnostic.message
};
let mut markdown = Markdown::escape(&markdown).to_string();
markdown.push_str(&format!(
" ([back](file://#diagnostic-{group_id}-{primary_ix}))"
));
// problem: group-id changes...
// - only an issue in diagnostics because caching
results.push(DiagnosticBlock {
initial_range: entry.range,
severity: entry.diagnostic.severity,
buffer_id,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
}
results
}
}
impl editor::DiagnosticRenderer for DiagnosticRenderer {
fn render_group(
&self,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
snapshot: EditorSnapshot,
editor: WeakEntity<Editor>,
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
blocks
.into_iter()
.map(|block| {
let editor = editor.clone();
BlockProperties {
placement: BlockPlacement::Near(
snapshot
.buffer_snapshot
.anchor_after(block.initial_range.start),
),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
priority: 1,
}
})
.collect()
}
}
#[derive(Clone)]
pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) buffer_id: BufferId,
pub(crate) markdown: Entity<Markdown>,
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
}
impl DiagnosticBlock {
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
let cx = &bcx.app;
let status_colors = bcx.app.theme().status();
let max_width = px(600.);
let (background_color, border_color) = match self.severity {
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
DiagnosticSeverity::WARNING => {
(status_colors.warning_background, status_colors.warning)
}
DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
_ => (status_colors.ignored_background, status_colors.ignored),
};
let settings = ThemeSettings::get_global(cx);
let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
let line_height = editor_line_height;
let buffer_id = self.buffer_id;
let diagnostics_editor = self.diagnostics_editor.clone();
div()
.border_l_2()
.px_2()
.line_height(line_height)
.bg(background_color)
.border_color(border_color)
.max_w(max_width)
.child(
MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
.on_url_click({
move |link, window, cx| {
Self::open_link(
editor.clone(),
&diagnostics_editor,
link,
window,
buffer_id,
cx,
)
}
}),
)
.into_any_element()
}
pub fn open_link(
editor: WeakEntity<Editor>,
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
link: SharedString,
window: &mut Window,
buffer_id: BufferId,
cx: &mut App,
) {
editor
.update(cx, |editor, cx| {
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
editor::hover_popover::open_markdown_url(link, window, cx);
return;
};
let Some((group_id, ix)) = maybe!({
let (group_id, ix) = diagnostic_link.split_once('-')?;
let group_id: usize = group_id.parse().ok()?;
let ix: usize = ix.parse().ok()?;
Some((group_id, ix))
}) else {
return;
};
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
.update(cx, |diagnostics, _| {
diagnostics
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
})
.ok()
.flatten()
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
.buffer(buffer_id)
.map(|entity| entity.read(cx).snapshot())
else {
return;
};
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
if range.context.overlaps(&diagnostic.range, &snapshot) {
Self::jump_to(
editor,
Anchor::range_in_buffer(
excerpt_id,
buffer_id,
diagnostic.range,
),
window,
cx,
);
return;
}
}
}
} else {
if let Some(diagnostic) = editor
.snapshot(window, cx)
.buffer_snapshot
.diagnostic_group(buffer_id, group_id)
.nth(ix)
{
Self::jump_to(editor, diagnostic.range, window, cx)
}
};
})
.ok();
}
fn jump_to<T: ToOffset>(
editor: &mut Editor,
range: Range<T>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let snapshot = &editor.buffer().read(cx).snapshot(cx);
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_ranges([range.start..range.start]);
});
window.focus(&editor.focus_handle(cx));
}
}