use concinnity_core::render::shader_programs::vocabulary::{
self, Entry, Kind, RECORD_STRUCT, VARYING_STRUCT,
};
use super::{Highlighter, LineState, Scan, Span, Token, push};
pub(crate) static HLSL: Hlsl = Hlsl {
entries: vocabulary::ENTRIES,
structs: &[RECORD_STRUCT, VARYING_STRUCT],
};
pub(crate) static SDF_HLSL: Hlsl = Hlsl {
entries: vocabulary::sdf::ENTRIES,
structs: &[],
};
#[derive(Debug)]
pub(crate) struct Hlsl {
entries: &'static [Entry],
structs: &'static [&'static str],
}
const IN_BLOCK_COMMENT: LineState = LineState(1);
const KEYWORDS: &[&str] = &[
"break",
"case",
"cbuffer",
"centroid",
"const",
"continue",
"default",
"discard",
"do",
"else",
"false",
"for",
"groupshared",
"if",
"in",
"inline",
"inout",
"linear",
"nointerpolation",
"noperspective",
"out",
"packoffset",
"precise",
"register",
"return",
"sample",
"static",
"struct",
"switch",
"tbuffer",
"true",
"typedef",
"uniform",
"void",
"while",
];
const SCALARS: &[&str] = &[
"bool",
"int",
"uint",
"dword",
"half",
"float",
"double",
"min16float",
"min10float",
"min16int",
"min12int",
"min16uint",
];
const OBJECT_TYPES: &[&str] = &[
"Buffer",
"ByteAddressBuffer",
"ConstantBuffer",
"RWBuffer",
"RWByteAddressBuffer",
"RWStructuredBuffer",
"RWTexture1D",
"RWTexture2D",
"RWTexture3D",
"SamplerComparisonState",
"SamplerState",
"StructuredBuffer",
"Texture1D",
"Texture2D",
"Texture2DArray",
"Texture3D",
"TextureCube",
"TextureCubeArray",
"matrix",
"vector",
];
fn is_type(word: &str) -> bool {
if OBJECT_TYPES.contains(&word) {
return true;
}
SCALARS.iter().any(|s| {
word.strip_prefix(s)
.is_some_and(|rest| match rest.as_bytes() {
[] => true,
[n] => (b'1'..=b'4').contains(n),
[r, b'x', c] => (b'1'..=b'4').contains(r) && (b'1'..=b'4').contains(c),
_ => false,
})
})
}
impl Hlsl {
fn is_engine_name(&self, word: &str) -> bool {
self.structs.contains(&word)
|| self.entries.iter().any(|e| match e.kind {
Kind::Helper | Kind::Hook | Kind::Type => e.name == word,
Kind::BlockField(b) => b.name() == word,
Kind::RecordField | Kind::Varying | Kind::ReturnField(_) => false,
})
}
fn is_engine_field(&self, owner: Option<&str>, field: &str) -> bool {
self.entries.iter().any(|e| {
e.name == field
&& match e.kind {
Kind::Helper | Kind::Hook | Kind::Type => false,
Kind::BlockField(b) => owner == Some(b.name()),
Kind::RecordField | Kind::Varying | Kind::ReturnField(_) => true,
}
})
}
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_ident(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
impl Highlighter for Hlsl {
fn line(&self, line: &str, state: LineState, out: &mut Vec<Span>) -> LineState {
let mut s = Scan::new(line);
if state == IN_BLOCK_COMMENT && !close_block_comment(&mut s, out) {
return IN_BLOCK_COMMENT;
}
let mut owner: Option<String> = None;
let mut after_dot = false;
while !s.done() {
let c = s.peek(0).unwrap_or(' ');
let start = s.at;
if s.starts_with("//") {
push(out, start, s.len(), Token::Comment);
break;
}
if s.starts_with("/*") {
s.at += 2;
if !close_block_comment_from(&mut s, start, out) {
return IN_BLOCK_COMMENT;
}
after_dot = false;
continue;
}
if c == '"' {
scan_string(&mut s);
push(out, start, s.at, Token::String);
} else if c == '#' && s.chars[..start].iter().all(|c| c.is_whitespace()) {
s.at += 1;
while s.peek(0).is_some_and(|c| c == ' ') {
s.at += 1;
}
while s.peek(0).is_some_and(is_ident) {
s.at += 1;
}
push(out, start, s.at, Token::Preprocessor);
} else if c.is_ascii_digit()
|| (c == '.' && s.peek(1).is_some_and(|d| d.is_ascii_digit()))
{
scan_number(&mut s, start);
push(out, start, s.at, Token::Number);
} else if is_ident_start(c) {
while s.peek(0).is_some_and(is_ident) {
s.at += 1;
}
let word: String = s.chars[start..s.at].iter().collect();
let token = if after_dot {
self.is_engine_field(owner.as_deref(), &word)
.then_some(Token::Engine)
} else if KEYWORDS.contains(&word.as_str()) {
Some(Token::Keyword)
} else if is_type(&word) {
Some(Token::Type)
} else if self.is_engine_name(&word) {
Some(Token::Engine)
} else {
None
};
if let Some(token) = token {
push(out, start, s.at, token);
}
owner = Some(word);
after_dot = false;
continue;
} else {
s.at += 1;
}
after_dot = c == '.';
if !after_dot && !c.is_whitespace() {
owner = None;
}
}
LineState::default()
}
}
fn close_block_comment(s: &mut Scan, out: &mut Vec<Span>) -> bool {
close_block_comment_from(s, 0, out)
}
fn close_block_comment_from(s: &mut Scan, start: usize, out: &mut Vec<Span>) -> bool {
match s.find("*/") {
Some(end) => {
s.at = end + 2;
push(out, start, s.at, Token::Comment);
true
}
None => {
s.at = s.len();
push(out, start, s.at, Token::Comment);
false
}
}
}
fn scan_string(s: &mut Scan) {
s.at += 1;
while let Some(c) = s.peek(0) {
s.at += 1;
match c {
'\\' => s.at += 1,
'"' => break,
_ => {}
}
}
s.at = s.at.min(s.len());
}
fn scan_number(s: &mut Scan, start: usize) {
let hex =
s.chars[start..].starts_with(&['0', 'x']) || s.chars[start..].starts_with(&['0', 'X']);
while let Some(c) = s.peek(0) {
let exponent_sign =
(c == '+' || c == '-') && !hex && matches!(s.chars[s.at - 1], 'e' | 'E');
if c.is_ascii_alphanumeric() || c == '.' || exponent_sign {
s.at += 1;
} else {
break;
}
}
}