use ratatui_core::layout::Rect;
use ratatui_core::style::{Modifier, Style};
use ratatui_core::text::{Line, Span};
use crate::components::text::line_width;
use crate::geometry::Size;
use crate::highlight::CodeHighlighter;
use crate::style::Theme;
use crate::surface::Surface;
use crate::view::{RenderCtx, View};
const RAIL: &str = "▏ ";
pub(crate) fn code_block_lines(
lang: &str,
body: &[&str],
theme: &Theme,
highlighter: CodeHighlighter,
show_label: bool,
gutter: Option<usize>,
) -> Vec<Line<'static>> {
let code = &theme.code;
let rail_style = Style::default().fg(code.label).bg(code.background);
let plain = Style::default().fg(code.text).bg(code.background);
let gutter_style = Style::default().fg(code.label).bg(code.background);
let gutter_width = gutter.map(|start| {
let last = start + body.len().saturating_sub(1);
let digits = last.to_string().len();
digits + 2
});
let mut out = Vec::new();
let label = lang.trim();
if show_label && !label.is_empty() {
let mut spans = Vec::new();
if let Some(w) = gutter_width {
spans.push(Span::styled(" ".repeat(w), gutter_style));
}
spans.push(Span::styled(
format!("{RAIL}{label}"),
Style::default()
.fg(code.label)
.add_modifier(Modifier::ITALIC),
));
out.push(Line::from(spans));
}
let highlighted = highlighter.highlight(label, body, theme);
for (row, source) in body.iter().enumerate() {
let mut spans = Vec::new();
if let (Some(start), Some(w)) = (gutter, gutter_width) {
spans.push(Span::styled(
format!(" {:>width$} ", start + row, width = w - 2),
gutter_style,
));
}
spans.push(Span::styled(RAIL.to_string(), rail_style));
match highlighted.as_ref().and_then(|lines| lines.get(row)) {
Some(cells) if !cells.is_empty() => {
for cell in cells {
spans.push(Span::styled(
cell.content.to_string(),
cell.style.bg(code.background),
));
}
}
_ => spans.push(Span::styled((*source).to_string(), plain)),
}
out.push(Line::from(spans));
}
out
}
pub struct CodeBlock<'a> {
lang: String,
body: Vec<String>,
highlighter: CodeHighlighter<'a>,
show_label: bool,
start_line: Option<usize>,
}
impl<'a> CodeBlock<'a> {
pub fn new(lang: impl Into<String>, source: impl AsRef<str>) -> Self {
Self {
lang: lang.into(),
body: source.as_ref().split('\n').map(str::to_owned).collect(),
highlighter: CodeHighlighter::Plain,
show_label: true,
start_line: None,
}
}
pub fn highlighter(mut self, highlighter: &'a dyn crate::highlight::Highlighter) -> Self {
self.highlighter = CodeHighlighter::With(highlighter);
self
}
pub fn label(mut self, show: bool) -> Self {
self.show_label = show;
self
}
pub fn line_numbers(mut self, show: bool) -> Self {
self.start_line = show.then(|| self.start_line.unwrap_or(1));
self
}
pub fn start_line(mut self, first: usize) -> Self {
self.start_line = Some(first);
self
}
fn lines(&self, theme: &Theme) -> Vec<Line<'static>> {
let body: Vec<&str> = self.body.iter().map(String::as_str).collect();
code_block_lines(
&self.lang,
&body,
theme,
self.highlighter,
self.show_label,
self.start_line,
)
}
}
impl View for CodeBlock<'_> {
fn measure(&self, available: Size) -> Size {
let theme = Theme::default();
let lines = self.lines(&theme);
let width = lines
.iter()
.map(|l| line_width(l))
.max()
.unwrap_or(0)
.min(available.width);
Size::new(width, lines.len() as u16)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
let lines = self.lines(ctx.theme);
for (row, line) in lines.iter().enumerate() {
let y = area.y.saturating_add(row as u16);
if y >= area.bottom() {
break;
}
let mut x = area.x;
for span in &line.spans {
if x >= area.right() {
break;
}
x = surface.set_string(x, y, span.content.as_ref(), span.style);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Theme;
use crate::test_support::row;
#[test]
fn line_numbers_gutter_counts_and_aligns() {
let theme = Theme::default();
let src = (1..=9)
.map(|n| format!("line{n}"))
.collect::<Vec<_>>()
.join("\n");
let block = CodeBlock::new("", &src).label(false).line_numbers(true);
let buf = crate::testing::render(&block, 30, 9, &theme);
assert!(
row(&buf, 0).starts_with(" 1 "),
"first row: {:?}",
row(&buf, 0)
);
assert!(
row(&buf, 8).starts_with(" 9 "),
"last row: {:?}",
row(&buf, 8)
);
assert!(row(&buf, 0).contains('▏'));
}
#[test]
fn start_line_offsets_and_widens_gutter() {
let theme = Theme::default();
let block = CodeBlock::new("", "a\nb").label(false).start_line(99);
let buf = crate::testing::render(&block, 30, 2, &theme);
assert!(
row(&buf, 0).starts_with(" 99 "),
"row0: {:?}",
row(&buf, 0)
);
assert!(
row(&buf, 1).starts_with(" 100 "),
"row1: {:?}",
row(&buf, 1)
);
}
#[test]
fn no_gutter_by_default() {
let theme = Theme::default();
let block = CodeBlock::new("", "x").label(false);
let buf = crate::testing::render(&block, 20, 1, &theme);
assert!(row(&buf, 0).starts_with('▏'), "row0: {:?}", row(&buf, 0));
}
}