1use ratatui_core::layout::Rect;
15use ratatui_core::style::{Modifier, Style};
16use ratatui_core::text::{Line, Span};
17
18use crate::components::text::line_width;
19use crate::geometry::Size;
20use crate::highlight::CodeHighlighter;
21use crate::style::Theme;
22use crate::surface::Surface;
23use crate::view::{RenderCtx, View};
24use crate::width::str_cols;
25
26const RAIL: &str = "▏ ";
28
29pub(crate) struct CodeRow {
31 pub(crate) line: Line<'static>,
32 pub(crate) width: u16,
33}
34
35pub(crate) fn code_block_lines(
48 lang: &str,
49 body: &[&str],
50 theme: &Theme,
51 highlighter: CodeHighlighter,
52 show_label: bool,
53 gutter: Option<usize>,
54) -> Vec<Line<'static>> {
55 code_block_rows(lang, body, theme, highlighter, show_label, gutter)
56 .into_iter()
57 .map(|row| row.line)
58 .collect()
59}
60
61pub(crate) fn code_block_rows(
63 lang: &str,
64 body: &[&str],
65 theme: &Theme,
66 highlighter: CodeHighlighter,
67 show_label: bool,
68 gutter: Option<usize>,
69) -> Vec<CodeRow> {
70 let code = &theme.code;
71 let rail_style = Style::default().fg(code.label).bg(code.background);
72 let plain = Style::default().fg(code.text).bg(code.background);
73 let gutter_style = Style::default().fg(code.label).bg(code.background);
74
75 let gutter_width = gutter.map(|start| {
78 let last = start + body.len().saturating_sub(1);
79 let digits = last.to_string().len();
80 digits + 2
81 });
82
83 let mut out = Vec::new();
84
85 let label = lang.trim();
86 if show_label && !label.is_empty() {
87 let mut spans = Vec::new();
88 if let Some(w) = gutter_width {
89 spans.push(Span::styled(" ".repeat(w), gutter_style));
90 }
91 spans.push(Span::styled(
92 format!("{RAIL}{label}"),
93 Style::default()
94 .fg(code.label)
95 .bg(code.background)
96 .add_modifier(Modifier::ITALIC),
97 ));
98 out.push(CodeRow {
99 line: Line::from(spans),
100 width: u16::try_from(gutter_width.unwrap_or(0))
101 .unwrap_or(u16::MAX)
102 .saturating_add(str_cols(RAIL))
103 .saturating_add(str_cols(label)),
104 });
105 }
106
107 let highlighted = highlighter.highlight(label, body, theme);
109
110 for (row, source) in body.iter().enumerate() {
111 let mut spans = Vec::new();
112 if let (Some(start), Some(w)) = (gutter, gutter_width) {
113 spans.push(Span::styled(
115 format!(" {:>width$} ", start + row, width = w - 2),
116 gutter_style,
117 ));
118 }
119 spans.push(Span::styled(RAIL.to_string(), rail_style));
120 match highlighted.as_ref().and_then(|lines| lines.get(row)) {
121 Some(cells) if !cells.is_empty() => {
122 for cell in cells {
125 spans.push(Span::styled(
126 cell.content.to_string(),
127 cell.style.bg(code.background),
128 ));
129 }
130 }
131 _ => spans.push(Span::styled((*source).to_string(), plain)),
132 }
133 out.push(CodeRow {
134 line: Line::from(spans),
135 width: u16::try_from(gutter_width.unwrap_or(0))
136 .unwrap_or(u16::MAX)
137 .saturating_add(str_cols(RAIL))
138 .saturating_add(str_cols(source)),
139 });
140 }
141
142 out
143}
144
145pub struct CodeBlock<'a> {
175 lang: String,
176 body: Vec<String>,
177 highlighter: CodeHighlighter<'a>,
178 show_label: bool,
179 start_line: Option<usize>,
180}
181
182impl<'a> CodeBlock<'a> {
183 pub fn new(lang: impl Into<String>, source: impl AsRef<str>) -> Self {
186 Self {
187 lang: lang.into(),
188 body: source.as_ref().split('\n').map(str::to_owned).collect(),
189 highlighter: CodeHighlighter::Plain,
190 show_label: true,
191 start_line: None,
192 }
193 }
194
195 pub fn highlighter(mut self, highlighter: &'a dyn crate::highlight::Highlighter) -> Self {
197 self.highlighter = CodeHighlighter::With(highlighter);
198 self
199 }
200
201 pub fn label(mut self, show: bool) -> Self {
203 self.show_label = show;
204 self
205 }
206
207 pub fn line_numbers(mut self, show: bool) -> Self {
210 self.start_line = show.then(|| self.start_line.unwrap_or(1));
211 self
212 }
213
214 pub fn start_line(mut self, first: usize) -> Self {
217 self.start_line = Some(first);
218 self
219 }
220
221 fn lines(&self, theme: &Theme) -> Vec<Line<'static>> {
222 let body: Vec<&str> = self.body.iter().map(String::as_str).collect();
223 code_block_lines(
224 &self.lang,
225 &body,
226 theme,
227 self.highlighter,
228 self.show_label,
229 self.start_line,
230 )
231 }
232}
233
234impl View for CodeBlock<'_> {
235 fn measure(&self, available: Size, _ctx: &RenderCtx) -> Size {
236 let theme = Theme::default();
237 let lines = self.lines(&theme);
238 let width = lines
239 .iter()
240 .map(|l| line_width(l))
241 .max()
242 .unwrap_or(0)
243 .min(available.width);
244 Size::new(width, lines.len() as u16)
245 }
246
247 fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
248 let lines = self.lines(ctx.theme);
249 let background = Style::default().bg(ctx.theme.code.background);
250 for (row, line) in lines.iter().enumerate() {
251 let y = area.y.saturating_add(row as u16);
252 if y >= area.bottom() {
253 break;
254 }
255 for x in area.x..area.right() {
258 surface.set(x, y, ' ', background);
259 }
260 let mut x = area.x;
261 for span in &line.spans {
262 if x >= area.right() {
263 break;
264 }
265 x = surface.set_string(x, y, span.content.as_ref(), span.style);
266 }
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::style::Theme;
275 use crate::tests::support::row;
276
277 #[test]
278 fn line_numbers_gutter_counts_and_aligns() {
279 let theme = Theme::default();
280 let src = (1..=9)
282 .map(|n| format!("line{n}"))
283 .collect::<Vec<_>>()
284 .join("\n");
285 let block = CodeBlock::new("", &src).label(false).line_numbers(true);
286 let buf = crate::testing::render(&block, 30, 9, &theme);
287 assert!(
288 row(&buf, 0).starts_with(" 1 "),
289 "first row: {:?}",
290 row(&buf, 0)
291 );
292 assert!(
293 row(&buf, 8).starts_with(" 9 "),
294 "last row: {:?}",
295 row(&buf, 8)
296 );
297 assert!(row(&buf, 0).contains('▏'));
299 }
300
301 #[test]
302 fn start_line_offsets_and_widens_gutter() {
303 let theme = Theme::default();
304 let block = CodeBlock::new("", "a\nb").label(false).start_line(99);
307 let buf = crate::testing::render(&block, 30, 2, &theme);
308 assert!(
309 row(&buf, 0).starts_with(" 99 "),
310 "row0: {:?}",
311 row(&buf, 0)
312 );
313 assert!(
314 row(&buf, 1).starts_with(" 100 "),
315 "row1: {:?}",
316 row(&buf, 1)
317 );
318 }
319
320 #[test]
321 fn no_gutter_by_default() {
322 let theme = Theme::default();
323 let block = CodeBlock::new("", "x").label(false);
324 let buf = crate::testing::render(&block, 20, 1, &theme);
325 assert!(row(&buf, 0).starts_with('▏'), "row0: {:?}", row(&buf, 0));
327 }
328
329 #[test]
330 fn background_fills_the_assigned_width() {
331 let theme = crate::tests::support::rainbow_theme();
332 let block = CodeBlock::new("text", "x");
333 let buf = crate::testing::render(&block, 12, 2, &theme);
334
335 for y in 0..2 {
336 for x in 0..12 {
337 assert_eq!(buf[(x, y)].bg, theme.code.background, "cell ({x}, {y})");
338 }
339 }
340 }
341}