use crate::theme::Palette;
use ratatui::{
Frame,
layout::{Constraint, Layout},
style::Style,
text::{Line, Span, Text},
widgets::Paragraph,
};
#[allow(clippy::too_many_arguments)]
pub fn render_text_with_gutter(
f: &mut Frame,
rect: ratatui::layout::Rect,
text: Text<'static>,
first_line_number: u32,
total_doc_lines: u32,
p: &Palette,
scroll_skip: u16,
physical_to_logical: Option<&[u32]>,
) {
let num_digits = if total_doc_lines == 0 {
4
} else {
(total_doc_lines.ilog10() + 1).max(4)
};
let gutter_width = num_digits + 3;
let chunks = Layout::horizontal([
Constraint::Length(crate::cast::u16_from_u32(gutter_width)),
Constraint::Min(0),
])
.split(rect);
let gutter_style = Style::new().fg(p.gutter);
let gutter_lines = build_gutter_lines(
text.lines.len(),
first_line_number,
num_digits as usize,
physical_to_logical,
gutter_style,
);
let mut gutter_para = Paragraph::new(Text::from(gutter_lines));
if scroll_skip > 0 {
gutter_para = gutter_para.scroll((scroll_skip, 0));
}
f.render_widget(gutter_para, chunks[0]);
let mut content_para = Paragraph::new(text);
if scroll_skip > 0 {
content_para = content_para.scroll((scroll_skip, 0));
}
f.render_widget(content_para, chunks[1]);
}
fn build_gutter_lines(
row_count: usize,
first_line_number: u32,
num_digits: usize,
physical_to_logical: Option<&[u32]>,
style: Style,
) -> Vec<Line<'static>> {
let blank_span = Span::styled(format!("{:>num_digits$} | ", "",), style);
let mut out: Vec<Line<'static>> = Vec::with_capacity(row_count);
match physical_to_logical {
Some(p2l) => {
let mut source_number = first_line_number;
let mut prev_logical: Option<u32> = None;
for i in 0..row_count {
let current = p2l.get(i).copied();
let is_new = match (prev_logical, current) {
(None, _) | (Some(_), None) => true,
(Some(p), Some(c)) => c != p,
};
if is_new {
out.push(Line::from(Span::styled(
format!("{source_number:>num_digits$} | "),
style,
)));
source_number = source_number.saturating_add(1);
} else {
out.push(Line::from(blank_span.clone()));
}
prev_logical = current;
}
}
None => {
for i in 0..row_count {
out.push(Line::from(Span::styled(
format!(
"{:>num_digits$} | ",
first_line_number + crate::cast::u32_sat(i)
),
style,
)));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::build_gutter_lines;
use ratatui::style::Style;
fn render(
row_count: usize,
first_line: u32,
digits: usize,
p2l: Option<&[u32]>,
) -> Vec<String> {
build_gutter_lines(row_count, first_line, digits, p2l, Style::default())
.into_iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.trim_end_matches(" | ")
.trim()
.to_string()
})
.collect()
}
#[test]
fn text_wrapped_logical_line_numbers_once() {
let p2l = vec![0u32, 0, 0];
let out = render(3, 1, 4, Some(&p2l));
assert_eq!(out, vec!["1", "", ""]);
}
#[test]
fn text_mixed_heights_advance_per_logical_line() {
let p2l = vec![0u32, 0, 1, 2, 2, 2];
let out = render(6, 1, 4, Some(&p2l));
assert_eq!(out, vec!["1", "", "2", "3", "", ""]);
}
#[test]
fn text_starts_from_offset() {
let p2l = vec![0u32, 1];
let out = render(2, 105, 4, Some(&p2l));
assert_eq!(out, vec!["105", "106"]);
}
#[test]
fn table_rows_numbered_sequentially() {
let out = render(4, 10, 4, None);
assert_eq!(out, vec!["10", "11", "12", "13"]);
}
#[test]
fn blank_padding_matches_digit_width() {
let p2l = vec![0u32, 0];
let lines = build_gutter_lines(2, 1, 4, Some(&p2l), Style::default());
let blank: String = lines[1].spans.iter().map(|s| s.content.as_ref()).collect();
assert_eq!(blank, " | ", "4 spaces + ` | ` = 7 cells");
}
}