use std::str::FromStr;
use std::sync::mpsc::Sender;
use std::sync::OnceLock;
use ratatui::style::{Color, Modifier, Style};
use syntect::easy::HighlightLines;
use syntect::highlighting::{
Color as SynColor, FontStyle, ScopeSelectors, StyleModifier, Theme, ThemeItem,
};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use crate::tuning::Palette;
const BUNDLED_GRAMMARS: &[&str] = &[include_str!("syntaxes/typescript.sublime-syntax")];
fn syntax_set() -> &'static SyntaxSet {
static SET: OnceLock<SyntaxSet> = OnceLock::new();
SET.get_or_init(|| {
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
for src in BUNDLED_GRAMMARS {
if let Ok(def) = syntect::parsing::SyntaxDefinition::load_from_str(src, true, None) {
builder.add(def);
}
}
if let Some(dir) = crate::sys::paths::home_dir() {
let _ = builder.add_from_folder(dir.join(".mars").join("syntaxes"), true);
}
builder.build()
})
}
fn syn(c: Color) -> SynColor {
let [r, g, b] = crate::themes::rgb_of(c);
SynColor { r, g, b, a: 0xff }
}
fn theme_for(p: &Palette) -> Theme {
let item = |scope: &str, color: Color| ThemeItem {
scope: ScopeSelectors::from_str(scope).unwrap_or_default(),
style: StyleModifier { foreground: Some(syn(color)), background: None, font_style: None },
};
let mut theme = Theme::default();
theme.settings.foreground = Some(syn(p.text));
theme.scopes = vec![
item("comment", p.text_faint),
item("string, constant.character, constant.other.symbol", p.success),
item("constant.numeric, constant.language, constant.character.escape", p.warning),
item("keyword, storage, storage.type, storage.modifier", p.accent),
item("entity.name.type, support.type, support.class, entity.other.inherited-class", p.info),
item("entity.name.function, support.function, meta.function-call entity", p.accent_bright),
item("variable, variable.parameter, punctuation, meta.brace", p.text_dim),
];
theme
}
fn ratatui_style(st: &syntect::highlighting::Style) -> Style {
let mut style = Style::default().fg(Color::Rgb(st.foreground.r, st.foreground.g, st.foreground.b));
if st.font_style.contains(FontStyle::BOLD) {
style = style.add_modifier(Modifier::BOLD);
}
if st.font_style.contains(FontStyle::ITALIC) {
style = style.add_modifier(Modifier::ITALIC);
}
style
}
fn per_char_styles(ranges: Vec<(syntect::highlighting::Style, &str)>) -> Vec<Style> {
let mut out = Vec::new();
for (st, text) in ranges {
let style = ratatui_style(&st);
for _ in text.trim_end_matches(['\n', '\r']).chars() {
out.push(style);
}
}
out
}
pub fn highlight_stream(job: crate::app::SyntaxJob, tx: Sender<crate::app::SyntaxEvent>) {
use crate::app::SyntaxEvent;
const CHUNK: usize = 512;
if job.code.lines().count() > 50_000 {
let _ = tx.send(SyntaxEvent::Chunk {
buf_id: job.buf_id, rev: job.rev, palette_id: job.palette_id,
start_line: 0, styles: Vec::new(), complete: true,
});
return;
}
std::thread::spawn(move || {
let ss = syntax_set();
let syntax = ss
.find_syntax_by_extension(&job.ext)
.or_else(|| ss.find_syntax_by_first_line(job.code.lines().next().unwrap_or("")));
let Some(syntax) = syntax else {
let _ = tx.send(SyntaxEvent::Chunk {
buf_id: job.buf_id, rev: job.rev, palette_id: job.palette_id,
start_line: 0, styles: Vec::new(), complete: true,
});
return;
};
let theme = theme_for(&job.palette);
let mut hl = HighlightLines::new(syntax, &theme);
let vp = job.viewport_bottom.max(1);
let mut batch: Vec<Vec<Style>> = Vec::new();
let mut batch_start = 0usize;
for line in LinesWithEndings::from(&job.code) {
let styles = match hl.highlight_line(line, ss) {
Ok(ranges) => per_char_styles(ranges),
Err(_) => break, };
batch.push(styles);
let covered = batch_start + batch.len();
let publish = (batch_start < vp && covered >= vp) || batch.len() >= CHUNK;
if publish {
let n = batch.len();
let styles = std::mem::take(&mut batch);
if tx.send(SyntaxEvent::Chunk {
buf_id: job.buf_id, rev: job.rev, palette_id: job.palette_id,
start_line: batch_start, styles, complete: false,
}).is_err() {
return; }
batch_start += n;
}
}
let _ = tx.send(SyntaxEvent::Chunk {
buf_id: job.buf_id, rev: job.rev, palette_id: job.palette_id,
start_line: batch_start, styles: batch, complete: true,
});
});
}
pub fn highlight(code: &str, ext: &str, palette: &Palette) -> Option<Vec<Vec<(Style, String)>>> {
let ss = syntax_set();
let syntax = ss
.find_syntax_by_extension(ext)
.or_else(|| ss.find_syntax_by_first_line(code.lines().next().unwrap_or("")))?;
let theme = theme_for(palette);
let mut hl = HighlightLines::new(syntax, &theme);
let mut out = Vec::new();
for line in LinesWithEndings::from(code) {
let ranges = hl.highlight_line(line, ss).ok()?;
let spans = ranges
.into_iter()
.map(|(st, text)| {
let mut style = Style::default()
.fg(Color::Rgb(st.foreground.r, st.foreground.g, st.foreground.b));
if st.font_style.contains(FontStyle::BOLD) {
style = style.add_modifier(Modifier::BOLD);
}
if st.font_style.contains(FontStyle::ITALIC) {
style = style.add_modifier(Modifier::ITALIC);
}
(style, text.trim_end_matches(['\n', '\r']).to_string())
})
.collect();
out.push(spans);
}
Some(out)
}