use super::semantics::{SemanticRole, role_color, semantic_role_for_token};
use crate::preview::appearance;
use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
use syntect::{
easy::ScopeRangeIterator,
parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
};
pub(super) fn render_syntect_code_preview<F>(
text: &str,
syntax_set: &SyntaxSet,
syntax: &SyntaxReference,
line_numbers: bool,
line_limit: usize,
canceled: &F,
) -> Result<Vec<Line<'static>>, ()>
where
F: Fn() -> bool,
{
let source_lines = crate::preview::collect_preview_lines_with_limit(
text,
crate::preview::clamp_code_preview_line_limit(line_limit),
);
let number_width = crate::preview::line_number_width(source_lines.len());
let code_palette = appearance::code_palette();
let mut parse_state = ParseState::new(syntax);
let mut scope_stack = ScopeStack::new();
let mut rendered = Vec::new();
for (index, line) in source_lines.iter().enumerate() {
if canceled() {
break;
}
let mut spans = Vec::new();
if line_numbers {
spans.push(crate::preview::line_number_span(index + 1, number_width));
} else {
spans.push(Span::styled(
"│ ",
Style::default().fg(code_palette.line_number),
));
}
let line_with_nl = format!("{line}\n");
let ops = parse_state
.parse_line(&line_with_nl, syntax_set)
.map_err(|_| ())?;
let mut pending_text = String::new();
let mut pending_style: Option<Style> = None;
for (range, op) in ScopeRangeIterator::new(&ops, &line_with_nl) {
scope_stack.apply(op).map_err(|_| ())?;
let token = line_with_nl[range].trim_end_matches('\n');
if token.is_empty() {
continue;
}
let role = semantic_role_for_token(token, scope_stack.as_slice());
let mut style = Style::default().fg(role_color(role, code_palette));
if role == SemanticRole::Invalid {
style = style.add_modifier(Modifier::UNDERLINED);
}
let expanded = crate::preview::expand_tabs(token);
match pending_style {
Some(s) if s == style => {
pending_text.push_str(&expanded);
}
Some(s) => {
spans.push(Span::styled(std::mem::take(&mut pending_text), s));
pending_text = expanded;
pending_style = Some(style);
}
None => {
pending_text = expanded;
pending_style = Some(style);
}
}
}
if let Some(s) = pending_style {
spans.push(Span::styled(pending_text, s));
}
rendered.push(Line::from(spans));
}
if rendered.is_empty() && !canceled() {
rendered.push(Line::from("File is empty"));
}
Ok(rendered)
}