use tower_lsp::lsp_types::{
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokensResult,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum TokenType {
Namespace = 0,
Type = 1,
Class = 2,
Enum = 3,
Interface = 4,
Struct = 5,
TypeParameter = 6,
Parameter = 7,
Variable = 8,
Property = 9,
EnumMember = 10,
Event = 11,
Function = 12,
Method = 13,
Macro = 14,
Keyword = 15,
Modifier = 16,
Comment = 17,
String = 18,
Number = 19,
Regexp = 20,
Operator = 21,
Decorator = 22,
}
impl TokenType {
pub fn legend() -> Vec<SemanticTokenType> {
vec![
SemanticTokenType::NAMESPACE,
SemanticTokenType::TYPE,
SemanticTokenType::CLASS,
SemanticTokenType::ENUM,
SemanticTokenType::INTERFACE,
SemanticTokenType::STRUCT,
SemanticTokenType::TYPE_PARAMETER,
SemanticTokenType::PARAMETER,
SemanticTokenType::VARIABLE,
SemanticTokenType::PROPERTY,
SemanticTokenType::ENUM_MEMBER,
SemanticTokenType::EVENT,
SemanticTokenType::FUNCTION,
SemanticTokenType::METHOD,
SemanticTokenType::MACRO,
SemanticTokenType::KEYWORD,
SemanticTokenType::MODIFIER,
SemanticTokenType::COMMENT,
SemanticTokenType::STRING,
SemanticTokenType::NUMBER,
SemanticTokenType::REGEXP,
SemanticTokenType::OPERATOR,
SemanticTokenType::DECORATOR,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum TokenModifier {
Declaration = 0,
Definition = 1,
Readonly = 2,
Static = 3,
Deprecated = 4,
Abstract = 5,
Async = 6,
Modification = 7,
Documentation = 8,
DefaultLibrary = 9,
}
impl TokenModifier {
pub fn legend() -> Vec<SemanticTokenModifier> {
vec![
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::DEFINITION,
SemanticTokenModifier::READONLY,
SemanticTokenModifier::STATIC,
SemanticTokenModifier::DEPRECATED,
SemanticTokenModifier::ABSTRACT,
SemanticTokenModifier::ASYNC,
SemanticTokenModifier::MODIFICATION,
SemanticTokenModifier::DOCUMENTATION,
SemanticTokenModifier::DEFAULT_LIBRARY,
]
}
pub fn encode(modifiers: &[TokenModifier]) -> u32 {
modifiers
.iter()
.fold(0u32, |acc, m| acc | (1 << (*m as u32)))
}
}
#[derive(Debug, Clone)]
struct AbsoluteToken {
line: u32,
start: u32,
length: u32,
token_type: u32,
modifiers: u32,
}
pub struct SemanticTokensService;
impl SemanticTokensService {
pub fn get_tokens(
content: &str,
uri: &tower_lsp::lsp_types::Url,
) -> Option<SemanticTokensResult> {
if uri.path().ends_with(".art.vue") {
return Self::get_art_tokens(content);
}
let options = vize_atelier_sfc::SfcParseOptions {
filename: uri.path().to_string(),
..Default::default()
};
let descriptor = vize_atelier_sfc::parse_sfc(content, options).ok()?;
let mut tokens: Vec<AbsoluteToken> = Vec::new();
if let Some(ref template) = descriptor.template {
Self::collect_template_tokens(
&template.content,
template.loc.start_line as u32,
&mut tokens,
);
}
if let Some(ref script_setup) = descriptor.script_setup {
Self::collect_script_tokens(
&script_setup.content,
script_setup.loc.start_line as u32,
&mut tokens,
);
}
if let Some(ref script) = descriptor.script {
Self::collect_script_tokens(&script.content, script.loc.start_line as u32, &mut tokens);
}
for style in &descriptor.styles {
Self::collect_style_tokens(&style.content, style.loc.start_line as u32, &mut tokens);
}
tokens.sort_by(|a, b| a.line.cmp(&b.line).then(a.start.cmp(&b.start)));
let semantic_tokens = Self::encode_tokens(&tokens);
Some(SemanticTokensResult::Tokens(
tower_lsp::lsp_types::SemanticTokens {
result_id: None,
data: semantic_tokens,
},
))
}
fn collect_template_tokens(template: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
Self::collect_directive_tokens(template, base_line, tokens);
Self::collect_interpolation_tokens(template, base_line, tokens);
Self::collect_event_tokens(template, base_line, tokens);
Self::collect_bind_tokens(template, base_line, tokens);
}
fn collect_directive_tokens(template: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
let directives = [
"v-if",
"v-else-if",
"v-else",
"v-for",
"v-show",
"v-model",
"v-bind",
"v-on",
"v-slot",
"v-pre",
"v-once",
"v-memo",
"v-cloak",
];
for directive in directives {
let mut pos = 0;
while let Some(found) = template[pos..].find(directive) {
let abs_pos = pos + found;
let (line, col) = Self::offset_to_line_col(template, abs_pos);
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: directive.len() as u32,
token_type: TokenType::Keyword as u32,
modifiers: 0,
});
pos = abs_pos + directive.len();
}
}
}
fn collect_interpolation_tokens(
template: &str,
base_line: u32,
tokens: &mut Vec<AbsoluteToken>,
) {
let mut pos = 0;
while let Some(start) = template[pos..].find("{{") {
let abs_start = pos + start;
if let Some(end) = template[abs_start..].find("}}") {
let expr_start = abs_start + 2;
let expr_end = abs_start + end;
let expr = &template[expr_start..expr_end];
for (ident, offset) in Self::extract_identifiers(expr) {
let abs_offset = expr_start + offset;
let (line, col) = Self::offset_to_line_col(template, abs_offset);
let token_type = if Self::looks_like_function_call(expr, offset) {
TokenType::Function
} else {
TokenType::Variable
};
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: ident.len() as u32,
token_type: token_type as u32,
modifiers: 0,
});
}
pos = abs_start + end + 2;
} else {
break;
}
}
}
fn collect_event_tokens(template: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
let mut pos = 0;
while let Some(start) = template[pos..].find('@') {
let abs_start = pos + start;
let remaining = &template[abs_start + 1..];
let event_end = remaining
.find(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != ':' && c != '.')
.unwrap_or(remaining.len());
if event_end > 0 {
let (line, col) = Self::offset_to_line_col(template, abs_start);
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: (event_end + 1) as u32, token_type: TokenType::Event as u32,
modifiers: 0,
});
}
pos = abs_start + 1;
}
}
fn collect_bind_tokens(template: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
let mut pos = 0;
while let Some(start) = template[pos..].find(':') {
let abs_start = pos + start;
if abs_start + 1 < template.len() && template.as_bytes()[abs_start + 1] == b':' {
pos = abs_start + 2;
continue;
}
if abs_start > 0 {
let before = template.as_bytes()[abs_start - 1];
if before == b' ' || before == b'\n' || before == b'\t' {
let remaining = &template[abs_start + 1..];
let prop_end = remaining
.find(|c: char| !c.is_ascii_alphanumeric() && c != '-')
.unwrap_or(remaining.len());
if prop_end > 0 {
let (line, col) = Self::offset_to_line_col(template, abs_start);
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: (prop_end + 1) as u32, token_type: TokenType::Property as u32,
modifiers: 0,
});
}
}
}
pos = abs_start + 1;
}
}
fn collect_script_tokens(script: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
let vue_functions = [
"ref",
"reactive",
"computed",
"watch",
"watchEffect",
"onMounted",
"onUnmounted",
"onBeforeMount",
"onBeforeUnmount",
"onUpdated",
"onBeforeUpdate",
"provide",
"inject",
"defineProps",
"defineEmits",
"defineExpose",
"withDefaults",
];
for func in vue_functions {
let pattern = format!("{}(", func);
let mut pos = 0;
while let Some(found) = script[pos..].find(&pattern) {
let abs_pos = pos + found;
let is_start =
abs_pos == 0 || !Self::is_ident_char(script.as_bytes()[abs_pos - 1] as char);
if is_start {
let (line, col) = Self::offset_to_line_col(script, abs_pos);
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: func.len() as u32,
token_type: TokenType::Function as u32,
modifiers: TokenModifier::encode(&[TokenModifier::DefaultLibrary]),
});
}
pos = abs_pos + func.len();
}
}
}
fn collect_style_tokens(style: &str, base_line: u32, tokens: &mut Vec<AbsoluteToken>) {
let pattern = "v-bind(";
let mut pos = 0;
while let Some(start) = style[pos..].find(pattern) {
let abs_start = pos + start;
let (line, col) = Self::offset_to_line_col(style, abs_start);
tokens.push(AbsoluteToken {
line: base_line + line - 1,
start: col,
length: 6, token_type: TokenType::Function as u32,
modifiers: 0,
});
if let Some(end) = style[abs_start + pattern.len()..].find(')') {
let var_start = abs_start + pattern.len();
let var = style[var_start..var_start + end].trim();
let var = var.trim_matches(|c| c == '"' || c == '\'');
if !var.is_empty() {
let (var_line, var_col) = Self::offset_to_line_col(style, var_start);
tokens.push(AbsoluteToken {
line: base_line + var_line - 1,
start: var_col,
length: var.len() as u32,
token_type: TokenType::Variable as u32,
modifiers: 0,
});
}
pos = var_start + end + 1;
} else {
break;
}
}
}
fn extract_identifiers(expr: &str) -> Vec<(&str, usize)> {
let mut identifiers = Vec::new();
let bytes = expr.as_bytes();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && !Self::is_ident_start(bytes[i] as char) {
i += 1;
}
if i >= bytes.len() {
break;
}
let start = i;
while i < bytes.len() && Self::is_ident_char(bytes[i] as char) {
i += 1;
}
if start < i {
let ident = &expr[start..i];
if !Self::is_keyword_or_literal(ident) {
identifiers.push((ident, start));
}
}
}
identifiers
}
fn looks_like_function_call(expr: &str, offset: usize) -> bool {
let bytes = expr.as_bytes();
let mut i = offset;
while i < bytes.len() && Self::is_ident_char(bytes[i] as char) {
i += 1;
}
while i < bytes.len() && (bytes[i] as char).is_whitespace() {
i += 1;
}
i < bytes.len() && bytes[i] == b'('
}
fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
let mut line = 1u32;
let mut col = 0u32;
let mut current = 0;
for ch in source.chars() {
if current >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
current += ch.len_utf8();
}
(line, col)
}
fn encode_tokens(tokens: &[AbsoluteToken]) -> Vec<SemanticToken> {
let mut result = Vec::with_capacity(tokens.len());
let mut prev_line = 0u32;
let mut prev_start = 0u32;
for token in tokens {
let delta_line = token.line - prev_line;
let delta_start = if delta_line == 0 {
token.start - prev_start
} else {
token.start
};
result.push(SemanticToken {
delta_line,
delta_start,
length: token.length,
token_type: token.token_type,
token_modifiers_bitset: token.modifiers,
});
prev_line = token.line;
prev_start = token.start;
}
result
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '$'
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '$'
}
fn is_keyword_or_literal(s: &str) -> bool {
matches!(
s,
"true"
| "false"
| "null"
| "undefined"
| "this"
| "if"
| "else"
| "for"
| "while"
| "do"
| "const"
| "let"
| "var"
| "function"
| "class"
| "return"
| "break"
| "continue"
| "new"
| "typeof"
| "in"
| "of"
| "instanceof"
| "async"
| "await"
)
}
fn get_art_tokens(content: &str) -> Option<SemanticTokensResult> {
let mut tokens: Vec<AbsoluteToken> = Vec::new();
Self::collect_art_block_tokens(content, &mut tokens);
Self::collect_variant_block_tokens(content, &mut tokens);
Self::collect_art_attribute_tokens(content, &mut tokens);
Self::collect_art_script_tokens(content, &mut tokens);
tokens.sort_by(|a, b| a.line.cmp(&b.line).then(a.start.cmp(&b.start)));
let semantic_tokens = Self::encode_tokens(&tokens);
Some(SemanticTokensResult::Tokens(
tower_lsp::lsp_types::SemanticTokens {
result_id: None,
data: semantic_tokens,
},
))
}
fn collect_art_block_tokens(content: &str, tokens: &mut Vec<AbsoluteToken>) {
let mut pos = 0;
while let Some(start) = content[pos..].find("<art") {
let abs_start = pos + start;
let next_char_pos = abs_start + 4;
if next_char_pos < content.len() {
let next_char = content.as_bytes()[next_char_pos];
if next_char == b' '
|| next_char == b'\n'
|| next_char == b'\t'
|| next_char == b'>'
{
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 4, token_type: TokenType::Keyword as u32,
modifiers: TokenModifier::encode(&[TokenModifier::Declaration]),
});
}
}
pos = abs_start + 4;
}
pos = 0;
while let Some(start) = content[pos..].find("</art>") {
let abs_start = pos + start;
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 6, token_type: TokenType::Keyword as u32,
modifiers: 0,
});
pos = abs_start + 6;
}
}
fn collect_variant_block_tokens(content: &str, tokens: &mut Vec<AbsoluteToken>) {
let mut pos = 0;
while let Some(start) = content[pos..].find("<variant") {
let abs_start = pos + start;
let next_char_pos = abs_start + 8;
if next_char_pos < content.len() {
let next_char = content.as_bytes()[next_char_pos];
if next_char == b' '
|| next_char == b'\n'
|| next_char == b'\t'
|| next_char == b'>'
{
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 8, token_type: TokenType::Class as u32,
modifiers: TokenModifier::encode(&[TokenModifier::Declaration]),
});
}
}
pos = abs_start + 8;
}
pos = 0;
while let Some(start) = content[pos..].find("</variant>") {
let abs_start = pos + start;
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 10, token_type: TokenType::Class as u32,
modifiers: 0,
});
pos = abs_start + 10;
}
}
fn collect_art_attribute_tokens(content: &str, tokens: &mut Vec<AbsoluteToken>) {
let art_attrs = [
"title",
"description",
"component",
"category",
"tags",
"status",
"order",
];
let variant_attrs = ["name", "default", "args", "viewport", "skip-vrt"];
for attr in art_attrs.iter().chain(variant_attrs.iter()) {
let pattern_eq = format!("{}=", attr);
let mut pos = 0;
while let Some(start) = content[pos..].find(&pattern_eq) {
let abs_start = pos + start;
if abs_start > 0 {
let before = content.as_bytes()[abs_start - 1];
if before == b' ' || before == b'\n' || before == b'\t' {
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: attr.len() as u32,
token_type: TokenType::Property as u32,
modifiers: 0,
});
let value_start = abs_start + attr.len() + 1; if value_start < content.len() {
let quote_char = content.as_bytes()[value_start];
if quote_char == b'"' || quote_char == b'\'' {
if let Some(end) =
content[value_start + 1..].find(quote_char as char)
{
let (val_line, val_col) =
Self::offset_to_line_col(content, value_start);
tokens.push(AbsoluteToken {
line: val_line,
start: val_col,
length: (end + 2) as u32, token_type: TokenType::String as u32,
modifiers: 0,
});
}
}
}
}
}
pos = abs_start + attr.len();
}
}
let mut pos = 0;
while let Some(start) = content[pos..].find(" default") {
let abs_start = pos + start + 1; let after_pos = abs_start + 7;
if after_pos < content.len() {
let after = content.as_bytes()[after_pos];
if after == b' '
|| after == b'>'
|| after == b'\n'
|| after == b'\t'
|| after == b'/'
{
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 7, token_type: TokenType::Modifier as u32,
modifiers: 0,
});
}
}
pos = abs_start + 7;
}
}
fn collect_art_script_tokens(content: &str, tokens: &mut Vec<AbsoluteToken>) {
if let Some(script_start) = content.find("<script") {
if let Some(script_end) = content[script_start..].find("</script>") {
let script_content_start = content[script_start..]
.find('>')
.map(|p| script_start + p + 1)
.unwrap_or(script_start);
let script_content_end = script_start + script_end;
if script_content_start < script_content_end {
let script_content = &content[script_content_start..script_content_end];
let base_offset = script_content_start;
let mut pos = 0;
while let Some(start) = script_content[pos..].find("import ") {
let abs_start = base_offset + pos + start;
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 6, token_type: TokenType::Keyword as u32,
modifiers: 0,
});
pos += start + 6;
}
pos = 0;
while let Some(start) = script_content[pos..].find(" from ") {
let abs_start = base_offset + pos + start + 1; let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: 4, token_type: TokenType::Keyword as u32,
modifiers: 0,
});
pos += start + 5;
}
pos = 0;
while pos < script_content.len() {
let remaining = &script_content[pos..];
let quote_pos = remaining.find(['"', '\'']);
if let Some(start) = quote_pos {
let quote_char = remaining.as_bytes()[start];
let after_quote = &remaining[start + 1..];
if let Some(end) = after_quote.find(quote_char as char) {
let abs_start = base_offset + pos + start;
let (line, col) = Self::offset_to_line_col(content, abs_start);
tokens.push(AbsoluteToken {
line,
start: col,
length: (end + 2) as u32, token_type: TokenType::String as u32,
modifiers: 0,
});
pos += start + end + 2;
} else {
pos += start + 1;
}
} else {
break;
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_identifiers() {
let expr = "count + message.length";
let idents = SemanticTokensService::extract_identifiers(expr);
assert_eq!(idents.len(), 3);
assert_eq!(idents[0].0, "count");
assert_eq!(idents[1].0, "message");
assert_eq!(idents[2].0, "length");
}
#[test]
fn test_looks_like_function_call() {
let expr = "handleClick()";
assert!(SemanticTokensService::looks_like_function_call(expr, 0));
let expr = "count + 1";
assert!(!SemanticTokensService::looks_like_function_call(expr, 0));
}
#[test]
fn test_offset_to_line_col() {
let source = "abc\ndef\nghi";
assert_eq!(SemanticTokensService::offset_to_line_col(source, 0), (1, 0));
assert_eq!(SemanticTokensService::offset_to_line_col(source, 4), (2, 0));
assert_eq!(SemanticTokensService::offset_to_line_col(source, 8), (3, 0));
}
#[test]
fn test_token_modifier_encode() {
let modifiers = vec![TokenModifier::Declaration, TokenModifier::Readonly];
let encoded = TokenModifier::encode(&modifiers);
assert_eq!(encoded, 0b101); }
#[test]
fn test_art_tokens_basic() {
let content = r#"<art title="Button" component="./Button.vue">
<variant name="Primary" default>
<Button>Click</Button>
</variant>
</art>
<script setup>
import Button from './Button.vue'
</script>"#;
let result = SemanticTokensService::get_art_tokens(content);
assert!(result.is_some());
if let Some(SemanticTokensResult::Tokens(tokens)) = result {
assert!(!tokens.data.is_empty());
}
}
#[test]
fn test_art_block_tokens() {
let content = "<art title=\"Test\">\n</art>";
let mut tokens = Vec::new();
SemanticTokensService::collect_art_block_tokens(content, &mut tokens);
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].length, 4); assert_eq!(tokens[1].length, 6); }
#[test]
fn test_variant_block_tokens() {
let content = "<variant name=\"Primary\">\n</variant>";
let mut tokens = Vec::new();
SemanticTokensService::collect_variant_block_tokens(content, &mut tokens);
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].length, 8); assert_eq!(tokens[1].length, 10); }
#[test]
fn test_art_attribute_tokens() {
let content = r#"<art title="Button" component="./Button.vue">"#;
let mut tokens = Vec::new();
SemanticTokensService::collect_art_attribute_tokens(content, &mut tokens);
assert!(tokens.len() >= 4);
}
#[test]
fn test_art_script_tokens() {
let content = r#"<script setup>
import Button from './Button.vue'
</script>"#;
let mut tokens = Vec::new();
SemanticTokensService::collect_art_script_tokens(content, &mut tokens);
assert!(tokens.len() >= 3);
}
}