use std::sync::OnceLock;
use damascene_core::tokens;
use damascene_core::tree::*;
use damascene_core::widgets::text::text;
use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxReference, SyntaxSet};
fn syntax_set() -> &'static SyntaxSet {
static SET: OnceLock<SyntaxSet> = OnceLock::new();
SET.get_or_init(two_face::syntax::extra_newlines)
}
pub(crate) fn find_syntax(label: &str) -> Option<&'static SyntaxReference> {
let label = label.trim();
if label.is_empty() {
return None;
}
let set = syntax_set();
set.find_syntax_by_token(label)
.or_else(|| set.find_syntax_by_extension(label))
.or_else(|| set.find_syntax_by_name(label))
}
pub(crate) fn highlight_to_runs(source: &str, syntax: &SyntaxReference) -> Vec<El> {
let set = syntax_set();
let mut state = ParseState::new(syntax);
let mut stack = ScopeStack::new();
let mut runs: Vec<El> = Vec::new();
let mut lines = source.split('\n').peekable();
while let Some(line) = lines.next() {
let with_newline = format!("{line}\n");
let ops = state.parse_line(&with_newline, set).unwrap_or_default();
let mut last = 0usize;
for (offset, op) in ops {
if offset > last && offset <= line.len() {
let chunk = &line[last..offset];
push_chunk(&mut runs, chunk, &stack);
}
stack.apply(&op).ok();
last = offset;
}
if last < line.len() {
push_chunk(&mut runs, &line[last..], &stack);
}
if lines.peek().is_some() {
runs.push(hard_break());
}
}
runs
}
fn push_chunk(out: &mut Vec<El>, chunk: &str, stack: &ScopeStack) {
if chunk.is_empty() {
return;
}
let color = scope_to_damascene_color(stack);
out.push(text(chunk).mono().color(color));
}
fn scope_to_damascene_color(stack: &ScopeStack) -> Color {
for scope in stack.as_slice().iter().rev() {
if let Some(color) = scope_color(*scope) {
return color;
}
}
tokens::FOREGROUND
}
fn scope_color(scope: Scope) -> Option<Color> {
let name = scope.build_string();
if name.starts_with("comment") {
return Some(tokens::MUTED_FOREGROUND);
}
if name.starts_with("string") {
return Some(tokens::SUCCESS);
}
if name.starts_with("constant.numeric") || name.starts_with("constant.language") {
return Some(tokens::INFO);
}
if name.starts_with("keyword") || name.starts_with("storage") {
return Some(tokens::INFO);
}
if name.starts_with("entity.name.function")
|| name.starts_with("support.function")
|| name.starts_with("meta.function-call")
{
return Some(tokens::WARNING);
}
if name.starts_with("entity.name.type")
|| name.starts_with("support.type")
|| name.starts_with("support.class")
{
return Some(tokens::WARNING);
}
if name.starts_with("variable.parameter") {
return Some(tokens::ACCENT_FOREGROUND);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_language_returns_none() {
assert!(find_syntax("zalgo-text").is_none());
assert!(find_syntax("").is_none());
}
#[test]
fn known_languages_resolve() {
assert!(find_syntax("rust").is_some());
assert!(find_syntax("rs").is_some());
assert!(find_syntax("python").is_some());
assert!(find_syntax("toml").is_some());
}
#[test]
fn rust_keyword_maps_to_keyword_color() {
let syntax = find_syntax("rust").expect("rust syntax bundled");
let runs = highlight_to_runs("fn main() {}\n", syntax);
let colors: Vec<_> = runs
.iter()
.filter(|r| r.kind == Kind::Text)
.filter_map(|r| r.text_color)
.collect();
assert!(
colors.contains(&tokens::INFO),
"expected `fn` keyword to map to INFO color, got colors: {colors:?}"
);
assert!(
runs.iter()
.filter(|r| r.kind == Kind::Text)
.all(|r| r.font_mono),
"every highlighted token should be font_mono = true"
);
}
#[test]
fn newlines_become_hard_breaks() {
let syntax = find_syntax("rust").unwrap();
let runs = highlight_to_runs("let a = 1;\nlet b = 2;\n", syntax);
let breaks = runs.iter().filter(|r| r.kind == Kind::HardBreak).count();
assert!(breaks >= 1, "expected at least one hard break for \\n");
}
#[test]
fn comment_runs_use_muted_foreground() {
let syntax = find_syntax("rust").unwrap();
let runs = highlight_to_runs("// hello\nlet x = 1;\n", syntax);
let colors: Vec<_> = runs
.iter()
.filter(|r| r.kind == Kind::Text)
.filter_map(|r| r.text_color)
.collect();
assert!(
colors.contains(&tokens::MUTED_FOREGROUND),
"expected comment to map to MUTED_FOREGROUND, got: {colors:?}"
);
}
}