use crate::tree::*;
use query_flow::{Db, QueryError, query};
use super::assets::TextFile;
use super::parse::ParseCst;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SemanticTokenType {
Keyword = 0,
Number = 1,
String = 2,
Comment = 3,
Operator = 4,
Property = 5,
Punctuation = 6,
Macro = 7,
Decorator = 8,
SectionMarker = 9,
ExtensionMarker = 10,
ExtensionIdent = 11,
}
impl SemanticTokenType {
pub fn all() -> &'static [SemanticTokenType] {
&[
SemanticTokenType::Keyword,
SemanticTokenType::Number,
SemanticTokenType::String,
SemanticTokenType::Comment,
SemanticTokenType::Operator,
SemanticTokenType::Property,
SemanticTokenType::Punctuation,
SemanticTokenType::Macro,
SemanticTokenType::Decorator,
SemanticTokenType::SectionMarker,
SemanticTokenType::ExtensionMarker,
SemanticTokenType::ExtensionIdent,
]
}
pub fn index(self) -> u32 {
self as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SemanticTokenModifier {
Declaration = 0,
Definition = 1,
SectionHeader = 2,
}
impl SemanticTokenModifier {
pub fn all() -> &'static [SemanticTokenModifier] {
&[
SemanticTokenModifier::Declaration,
SemanticTokenModifier::Definition,
SemanticTokenModifier::SectionHeader,
]
}
pub fn bitmask(self) -> u32 {
1 << (self as u32)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticToken {
pub start: u32,
pub length: u32,
pub token_type: SemanticTokenType,
pub modifiers: u32,
}
impl SemanticToken {
pub fn new(start: u32, length: u32, token_type: SemanticTokenType) -> Self {
Self {
start,
length,
token_type,
modifiers: 0,
}
}
pub fn with_modifiers(
start: u32,
length: u32,
token_type: SemanticTokenType,
modifiers: u32,
) -> Self {
Self {
start,
length,
token_type,
modifiers,
}
}
}
pub fn semantic_tokens(input: &str, cst: &Cst) -> Vec<SemanticToken> {
let mut visitor = SemanticTokenVisitor::new(input);
let _ = cst.visit_from_root(&mut visitor);
visitor.into_tokens()
}
#[query(debug = "{Self}({file})")]
pub fn get_semantic_tokens(db: &impl Db, file: TextFile) -> Result<Vec<SemanticToken>, QueryError> {
let parsed_cst = db.query(ParseCst::new(file.clone()))?;
let source = db.asset(file.clone())?;
Ok(semantic_tokens(source.get(), &parsed_cst.cst))
}
struct SemanticTokenVisitor<'a> {
input: &'a str,
tokens: Vec<SemanticToken>,
in_key_context: bool,
in_section_header: bool,
in_extension_namespace: bool,
}
impl<'a> SemanticTokenVisitor<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
tokens: Vec::new(),
in_key_context: false,
in_section_header: false,
in_extension_namespace: false,
}
}
fn into_tokens(mut self) -> Vec<SemanticToken> {
self.tokens.sort_by_key(|t| t.start);
self.tokens
}
fn emit_token_with_modifiers(
&mut self,
span: InputSpan,
token_type: SemanticTokenType,
modifiers: u32,
) {
self.tokens.push(SemanticToken::with_modifiers(
span.start,
span.end - span.start,
token_type,
modifiers,
));
}
fn emit_code_block_start(&mut self, span: InputSpan, backtick_count: u32) {
let text = span.as_str(self.input);
self.tokens.push(SemanticToken::new(
span.start,
backtick_count,
SemanticTokenType::Macro,
));
let after_backticks = &text[backtick_count as usize..];
let tag_len = after_backticks
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.count();
if tag_len > 0 {
self.tokens.push(SemanticToken::new(
span.start + backtick_count,
tag_len as u32,
SemanticTokenType::Decorator,
));
}
}
fn emit_inline_code_1(&mut self, span: InputSpan) {
let text = span.as_str(self.input);
if let Some(backtick_pos) = text.find('`') {
let tag_len = backtick_pos;
if tag_len > 0 {
self.tokens.push(SemanticToken::new(
span.start,
tag_len as u32,
SemanticTokenType::Decorator,
));
}
self.tokens.push(SemanticToken::new(
span.start + tag_len as u32,
(span.end - span.start) - tag_len as u32,
SemanticTokenType::Macro,
));
}
}
fn current_modifiers(&self, is_ident: bool) -> u32 {
let mut modifiers = 0;
if self.in_key_context && is_ident {
modifiers |= SemanticTokenModifier::Declaration.bitmask();
}
if self.in_section_header && is_ident {
modifiers |= SemanticTokenModifier::Definition.bitmask();
}
if self.in_section_header {
modifiers |= SemanticTokenModifier::SectionHeader.bitmask();
}
modifiers
}
fn terminal_to_token_type(&self, kind: TerminalKind) -> Option<SemanticTokenType> {
match kind {
TerminalKind::Hole => Some(SemanticTokenType::Keyword),
TerminalKind::True | TerminalKind::False | TerminalKind::Null => {
if self.in_key_context {
Some(SemanticTokenType::Property)
} else {
Some(SemanticTokenType::Keyword)
}
}
TerminalKind::Integer | TerminalKind::Float | TerminalKind::Inf | TerminalKind::NaN => {
Some(SemanticTokenType::Number)
}
TerminalKind::Str | TerminalKind::Text | TerminalKind::LitStr => {
Some(SemanticTokenType::String)
}
TerminalKind::LineComment | TerminalKind::BlockComment => {
Some(SemanticTokenType::Comment)
}
TerminalKind::At => Some(SemanticTokenType::SectionMarker),
TerminalKind::Dollar => Some(SemanticTokenType::ExtensionMarker),
TerminalKind::Bind
| TerminalKind::MapBind
| TerminalKind::Dot
| TerminalKind::Comma
| TerminalKind::Esc
| TerminalKind::Circumflex => Some(SemanticTokenType::Operator),
TerminalKind::LBrace
| TerminalKind::RBrace
| TerminalKind::LBracket
| TerminalKind::RBracket
| TerminalKind::LParen
| TerminalKind::RParen
| TerminalKind::TextStart
| TerminalKind::Hash => Some(SemanticTokenType::Punctuation),
TerminalKind::Ident => {
if self.in_extension_namespace {
Some(SemanticTokenType::ExtensionIdent)
} else if self.in_key_context {
Some(SemanticTokenType::Property)
} else {
None
}
}
TerminalKind::CodeBlockEnd3
| TerminalKind::CodeBlockEnd4
| TerminalKind::CodeBlockEnd5
| TerminalKind::CodeBlockEnd6
| TerminalKind::BacktickDelim
| TerminalKind::Backtick2
| TerminalKind::Backtick3
| TerminalKind::Backtick4
| TerminalKind::Backtick5
| TerminalKind::DelimCodeEnd1
| TerminalKind::DelimCodeEnd2
| TerminalKind::DelimCodeEnd3 => Some(SemanticTokenType::Macro),
TerminalKind::NoSQuote
| TerminalKind::SQuote
| TerminalKind::LitStr1End
| TerminalKind::LitStr2End
| TerminalKind::LitStr3End => Some(SemanticTokenType::String),
TerminalKind::Whitespace
| TerminalKind::NewLine
| TerminalKind::GrammarNewline
| TerminalKind::Ws => None,
TerminalKind::NoBacktick => None,
TerminalKind::InlineCode1
| TerminalKind::CodeBlockStart3
| TerminalKind::CodeBlockStart4
| TerminalKind::CodeBlockStart5
| TerminalKind::CodeBlockStart6
| TerminalKind::LitStr1Start
| TerminalKind::LitStr2Start
| TerminalKind::LitStr3Start
| TerminalKind::DelimCodeStart1
| TerminalKind::DelimCodeStart2
| TerminalKind::DelimCodeStart3
| TerminalKind::NewlineBind
| TerminalKind::NewlineTextStart => None,
}
}
}
impl<F: CstFacade> CstVisitor<F> for SemanticTokenVisitor<'_> {
type Error = std::convert::Infallible;
fn then_construct_error(
&mut self,
node_data: Option<CstNode>,
parent: CstNodeId,
kind: NodeKind,
_error: CstConstructError,
tree: &F,
) -> Result<(), Self::Error> {
self.recover_error(node_data, parent, kind, tree)
}
fn visit_terminal(
&mut self,
_id: CstNodeId,
kind: TerminalKind,
data: TerminalData,
_tree: &F,
) -> Result<(), Self::Error> {
let TerminalData::Input(span) = data else {
return Ok(());
};
if kind == TerminalKind::NewlineBind || kind == TerminalKind::NewlineTextStart {
let token_type = if kind == TerminalKind::NewlineBind {
SemanticTokenType::Operator
} else {
SemanticTokenType::Punctuation
};
let last_char = InputSpan {
start: span.end - 1,
end: span.end,
};
self.emit_token_with_modifiers(last_char, token_type, 0);
return Ok(());
}
let Some(token_type) = self.terminal_to_token_type(kind) else {
return Ok(());
};
let is_ident = matches!(
kind,
TerminalKind::Ident | TerminalKind::True | TerminalKind::False | TerminalKind::Null
);
let modifiers = self.current_modifiers(is_ident);
self.emit_token_with_modifiers(span, token_type, modifiers);
Ok(())
}
fn visit_section(
&mut self,
_handle: SectionHandle,
view: SectionView,
tree: &F,
) -> Result<(), Self::Error> {
self.in_section_header = true;
self.visit_at_handle(view.at, tree)?;
self.visit_keys_handle(view.keys, tree)?;
self.in_section_header = false;
self.visit_section_body_handle(view.section_body, tree)?;
Ok(())
}
fn visit_keys(
&mut self,
handle: KeysHandle,
view: KeysView,
tree: &F,
) -> Result<(), Self::Error> {
let prev = self.in_key_context;
self.in_key_context = true;
self.visit_keys_super(handle, view, tree)?;
self.in_key_context = prev;
Ok(())
}
fn visit_extension_name_space(
&mut self,
handle: ExtensionNameSpaceHandle,
view: ExtensionNameSpaceView,
tree: &F,
) -> Result<(), Self::Error> {
let prev = self.in_extension_namespace;
self.in_extension_namespace = true;
self.visit_extension_name_space_super(handle, view, tree)?;
self.in_extension_namespace = prev;
Ok(())
}
fn visit_value(
&mut self,
handle: ValueHandle,
view: ValueView,
tree: &F,
) -> Result<(), Self::Error> {
let prev = self.in_key_context;
self.in_key_context = false;
self.visit_value_super(handle, view, tree)?;
self.in_key_context = prev;
Ok(())
}
fn visit_inline_code_1(
&mut self,
_handle: InlineCode1Handle,
view: InlineCode1View,
tree: &F,
) -> Result<(), Self::Error> {
if let Ok(TerminalData::Input(span)) = view.inline_code_1.get_data(tree) {
self.emit_inline_code_1(span);
}
Ok(())
}
fn visit_code_block_3(
&mut self,
handle: CodeBlock3Handle,
view: CodeBlock3View,
tree: &F,
) -> Result<(), Self::Error> {
if let Ok(start_view) = view.code_block_start_3.get_view(tree)
&& let Ok(TerminalData::Input(span)) = start_view.code_block_start_3.get_data(tree)
{
self.emit_code_block_start(span, 3);
}
self.visit_code_block_3_list_handle(view.code_block_3_list, tree)?;
self.visit_code_block_end_3_handle(view.code_block_end_3, tree)?;
let _ = handle;
Ok(())
}
fn visit_code_block_4(
&mut self,
handle: CodeBlock4Handle,
view: CodeBlock4View,
tree: &F,
) -> Result<(), Self::Error> {
if let Ok(start_view) = view.code_block_start_4.get_view(tree)
&& let Ok(TerminalData::Input(span)) = start_view.code_block_start_4.get_data(tree)
{
self.emit_code_block_start(span, 4);
}
self.visit_code_block_4_list_handle(view.code_block_4_list, tree)?;
self.visit_code_block_end_4_handle(view.code_block_end_4, tree)?;
let _ = handle;
Ok(())
}
fn visit_code_block_5(
&mut self,
handle: CodeBlock5Handle,
view: CodeBlock5View,
tree: &F,
) -> Result<(), Self::Error> {
if let Ok(start_view) = view.code_block_start_5.get_view(tree)
&& let Ok(TerminalData::Input(span)) = start_view.code_block_start_5.get_data(tree)
{
self.emit_code_block_start(span, 5);
}
self.visit_code_block_5_list_handle(view.code_block_5_list, tree)?;
self.visit_code_block_end_5_handle(view.code_block_end_5, tree)?;
let _ = handle;
Ok(())
}
fn visit_code_block_6(
&mut self,
handle: CodeBlock6Handle,
view: CodeBlock6View,
tree: &F,
) -> Result<(), Self::Error> {
if let Ok(start_view) = view.code_block_start_6.get_view(tree)
&& let Ok(TerminalData::Input(span)) = start_view.code_block_start_6.get_data(tree)
{
self.emit_code_block_start(span, 6);
}
self.visit_code_block_6_list_handle(view.code_block_6_list, tree)?;
self.visit_code_block_end_6_handle(view.code_block_end_6, tree)?;
let _ = handle;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_and_get_tokens(input: &str) -> Vec<SemanticToken> {
let cst = crate::parol::parse(input).expect("Failed to parse");
semantic_tokens(input, &cst)
}
#[test]
fn test_simple_binding() {
let input = "key = 42";
let tokens = parse_and_get_tokens(input);
assert_eq!(tokens.len(), 3);
assert_eq!(tokens[0].token_type, SemanticTokenType::Property);
assert_eq!(
&input[tokens[0].start as usize..(tokens[0].start + tokens[0].length) as usize],
"key"
);
assert_eq!(tokens[1].token_type, SemanticTokenType::Operator);
assert_eq!(
&input[tokens[1].start as usize..(tokens[1].start + tokens[1].length) as usize],
"="
);
assert_eq!(tokens[2].token_type, SemanticTokenType::Number);
assert_eq!(
&input[tokens[2].start as usize..(tokens[2].start + tokens[2].length) as usize],
"42"
);
}
#[test]
fn test_keywords() {
let input = "a = true\nb = false\nc = null";
let tokens = parse_and_get_tokens(input);
let keywords: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Keyword)
.collect();
assert_eq!(keywords.len(), 3);
}
#[test]
fn test_string_literal() {
let input = r#"name = "hello""#;
let tokens = parse_and_get_tokens(input);
let strings: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::String)
.collect();
assert_eq!(strings.len(), 1);
assert_eq!(
&input[strings[0].start as usize..(strings[0].start + strings[0].length) as usize],
"\"hello\""
);
}
#[test]
fn test_section_header() {
let input = "@section.name\nkey = 1";
let tokens = parse_and_get_tokens(input);
let at_token = tokens.iter().find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "@"
});
assert!(at_token.is_some());
assert_eq!(
at_token.unwrap().token_type,
SemanticTokenType::SectionMarker
);
}
#[test]
fn test_section_header_modifier() {
let input = "@section.name\nkey = 1";
let tokens = parse_and_get_tokens(input);
let section_header_mask = SemanticTokenModifier::SectionHeader.bitmask();
let at_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "@"
})
.unwrap();
assert!(
at_token.modifiers & section_header_mask != 0,
"@ should have SectionHeader modifier"
);
let section_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "section"
})
.unwrap();
assert!(
section_token.modifiers & section_header_mask != 0,
"section should have SectionHeader modifier"
);
let dot_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "."
})
.unwrap();
assert!(
dot_token.modifiers & section_header_mask != 0,
". should have SectionHeader modifier"
);
let name_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "name"
})
.unwrap();
assert!(
name_token.modifiers & section_header_mask != 0,
"name should have SectionHeader modifier"
);
let key_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "key"
})
.unwrap();
assert!(
key_token.modifiers & section_header_mask == 0,
"key should NOT have SectionHeader modifier"
);
}
#[test]
fn test_extension_namespace() {
let input = "$variant = \"some-value\"";
let tokens = parse_and_get_tokens(input);
let dollar_token = tokens.iter().find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "$"
});
assert!(dollar_token.is_some());
assert_eq!(
dollar_token.unwrap().token_type,
SemanticTokenType::ExtensionMarker
);
let variant_token = tokens.iter().find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "variant"
});
assert!(variant_token.is_some());
assert_eq!(
variant_token.unwrap().token_type,
SemanticTokenType::ExtensionIdent
);
}
#[test]
fn test_code_block_with_language() {
let input = "code = ```rust\nfn main() {}\n```";
let tokens = parse_and_get_tokens(input);
let decorators: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Decorator)
.collect();
assert_eq!(decorators.len(), 1);
assert_eq!(
&input[decorators[0].start as usize
..(decorators[0].start + decorators[0].length) as usize],
"rust"
);
}
#[test]
fn test_inline_code() {
let input = "code = rust`let x = 1;`";
let tokens = parse_and_get_tokens(input);
let decorators: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Decorator)
.collect();
assert_eq!(decorators.len(), 1);
assert_eq!(
&input[decorators[0].start as usize
..(decorators[0].start + decorators[0].length) as usize],
"rust"
);
let macros: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Macro)
.collect();
assert!(!macros.is_empty());
}
#[test]
fn test_comment() {
let input = "// this is a comment\nkey = 1";
let tokens = parse_and_get_tokens(input);
let comments: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Comment)
.collect();
assert_eq!(comments.len(), 1);
}
#[test]
fn test_punctuation() {
let input = "arr = [1, 2, 3]";
let tokens = parse_and_get_tokens(input);
let punctuation: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == SemanticTokenType::Punctuation)
.collect();
assert!(punctuation.len() >= 2);
}
#[test]
fn test_declaration_modifier() {
let input = "myKey = 42";
let tokens = parse_and_get_tokens(input);
let key_token = tokens
.iter()
.find(|t| t.token_type == SemanticTokenType::Property)
.unwrap();
assert!(key_token.modifiers & SemanticTokenModifier::Declaration.bitmask() != 0);
}
#[test]
fn test_section_header_keyident_true_false_null() {
let input = "@true.false.null\nkey = 1";
let tokens = parse_and_get_tokens(input);
let true_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "true"
})
.unwrap();
assert_eq!(
true_token.token_type,
SemanticTokenType::Property,
"true in section header should be Property, not Keyword"
);
let false_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "false"
})
.unwrap();
assert_eq!(
false_token.token_type,
SemanticTokenType::Property,
"false in section header should be Property, not Keyword"
);
let null_token = tokens
.iter()
.find(|t| {
let text = &input[t.start as usize..(t.start + t.length) as usize];
text == "null"
})
.unwrap();
assert_eq!(
null_token.token_type,
SemanticTokenType::Property,
"null in section header should be Property, not Keyword"
);
let section_header_mask = SemanticTokenModifier::SectionHeader.bitmask();
assert!(
true_token.modifiers & section_header_mask != 0,
"true should have SectionHeader modifier"
);
assert!(
false_token.modifiers & section_header_mask != 0,
"false should have SectionHeader modifier"
);
assert!(
null_token.modifiers & section_header_mask != 0,
"null should have SectionHeader modifier"
);
}
}