use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::layout::PaneId;
use crate::lsp::Severity;
use crate::lsp::diagnostics_pane::DiagItem;
use crate::pane::Pane;
use crate::ui::theme::{self, Theme};
pub fn draw(
frame: &mut Frame,
app: &mut App,
pane_id: PaneId,
area: Rect,
_focused: bool,
) -> Option<(u16, u16)> {
if area.width == 0 || area.height == 0 {
return None;
}
let t = theme::cur();
frame.render_widget(
Paragraph::new("").style(Style::default().bg(t.bg_dark)),
area,
);
if !app.right_panel_panes.contains(&pane_id) {
app.rects.editor_panes.push((area, pane_id));
}
let want_sb = area.width >= 8;
let sb_w = if want_sb { 1 } else { 0 };
let body_area = Rect::new(area.x, area.y, area.width - sb_w, area.height);
let sb_area = Rect::new(area.x + area.width - sb_w, area.y, sb_w, area.height);
let area = body_area;
let Some(Pane::Diagnostics(d)) = app.panes.get_mut(pane_id) else {
return None;
};
d.clamp();
let (errors, warnings) = d.counts();
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(vec![
Span::styled(" ", Style::default().bg(t.bg_dark)),
Span::styled(
format!("{errors} error{}", if errors == 1 { "" } else { "s" }),
Style::default()
.fg(if errors > 0 { t.red } else { t.comment })
.bg(t.bg_dark)
.add_modifier(Modifier::BOLD),
),
Span::styled(" · ", Style::default().fg(t.comment).bg(t.bg_dark)),
Span::styled(
format!("{warnings} warning{}", if warnings == 1 { "" } else { "s" }),
Style::default()
.fg(if warnings > 0 { t.yellow } else { t.comment })
.bg(t.bg_dark)
.add_modifier(Modifier::BOLD),
),
]));
let help_text: &str = if area.width >= 52 {
" ⏎ jump r refresh s severity-filter esc back"
} else if area.width >= 30 {
" ⏎ jump · r refresh · s filter"
} else {
" ⏎/r/s"
};
lines.push(Line::from(Span::styled(
help_text,
Style::default().fg(t.comment).bg(t.bg_dark),
)));
let vis = d.visible_indices();
if vis.is_empty() {
lines.push(Line::from(Span::styled(
" ",
Style::default().bg(t.bg_dark),
)));
let msg = if d.items.is_empty() {
" ✓ no diagnostics in open files"
} else {
" (filtered out — `s` cycles severity)"
};
lines.push(Line::from(Span::styled(
msg,
Style::default().fg(t.green).bg(t.bg_dark),
)));
frame.render_widget(
Paragraph::new(lines).style(Style::default().bg(t.bg_dark)),
area,
);
return None;
}
let total = d.items.len();
let shown = vis.len();
let chip = format!(
" filter: {} ({shown}/{total}) · `s` cycles severity",
d.severity_label()
);
lines.push(Line::from(Span::styled(
chip,
Style::default().fg(t.comment).bg(t.bg_dark),
)));
let body_start_offset = lines.len();
let mut selected_row = lines.len();
let mut row_indices: Vec<usize> = Vec::with_capacity(vis.len());
for (visible_idx, &items_idx) in vis.iter().enumerate() {
let it = &d.items[items_idx];
let sel = visible_idx == d.selected;
if sel {
selected_row = lines.len();
}
row_indices.push(lines.len());
lines.push(item_line(&t, it, sel));
}
let h = area.height as usize;
if selected_row < d.scroll {
d.scroll = selected_row;
} else if selected_row >= d.scroll + h {
d.scroll = selected_row + 1 - h;
}
let max_scroll = lines.len().saturating_sub(h.min(lines.len()));
d.scroll = d.scroll.min(max_scroll);
for (idx, line_y) in row_indices.iter().enumerate() {
if *line_y < d.scroll || *line_y >= d.scroll + h {
continue;
}
let visible_y = line_y - d.scroll;
let screen_y = area.y.saturating_add(visible_y as u16);
if screen_y < area.y.saturating_add(area.height) {
app.rects.list_rows.push((
ratatui::layout::Rect {
x: area.x,
y: screen_y,
width: area.width,
height: 1,
},
pane_id,
idx,
));
}
}
let _ = body_start_offset;
let total_lines = lines.len();
let scroll = d.scroll;
let view: Vec<Line> = lines.into_iter().skip(scroll).take(h).collect();
frame.render_widget(
Paragraph::new(view).style(Style::default().bg(t.bg_dark)),
area,
);
if sb_w > 0 {
crate::ui::scrollbar::paint_simple_scrollbar(frame, sb_area, &t, total_lines, h, scroll);
app.rects.scrollbars.push(crate::app::ScrollbarHit {
area: sb_area,
pane_id,
total: total_lines,
viewport: h,
kind: crate::app::ScrollbarKind::Diagnostics,
});
}
None
}
fn sev_glyph(s: Severity) -> &'static str {
match s {
Severity::Error => "✗",
Severity::Warning => "⚠",
Severity::Info => "ℹ",
Severity::Hint => "·",
}
}
fn item_line(t: &Theme, it: &DiagItem, selected: bool) -> Line<'static> {
let bg = if selected { t.bg2 } else { t.bg_dark };
let sev_color = match it.severity {
Severity::Error => t.red,
Severity::Warning => t.yellow,
Severity::Info => t.blue,
Severity::Hint => t.comment,
};
let loc = format!("{}:{}:{}", it.rel, it.line + 1, it.col + 1);
let mut spans = vec![
Span::styled(
if selected { " ▶ " } else { " " },
Style::default().fg(t.yellow).bg(bg),
),
Span::styled(
format!("{} ", sev_glyph(it.severity)),
Style::default()
.fg(sev_color)
.bg(bg)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{loc} "), Style::default().fg(t.comment).bg(bg)),
Span::styled(it.message.clone(), Style::default().fg(t.fg).bg(bg)),
];
if let Some(src) = &it.source {
spans.push(Span::styled(
format!(" ({src})"),
Style::default().fg(t.comment).bg(bg),
));
}
Line::from(spans)
}