Files
storybook/src/lsp/semantic_tokens.rs
Sienna Meridian Satterwhite 6e11126267 docs: create SBIR v0.3.0 specification
Updated binary format spec for v0.3.0 type system changes:
- Section 3 (Types): concept, sub_concept, concept_comparison encoding
- Section 4 (Characters): Value/Expression discriminant renames
- Section 5 (Templates): species_base field for inheritance
- Section 12 (Life Arcs): required_fields with type annotations
- Section 13 (Enums): note on sub_concept enum distinction
- Changelog and version history updated

Also fixed clippy/import issues in LSP semantic tokens module.
2026-02-14 14:36:34 +00:00

614 lines
22 KiB
Rust

//! Semantic tokens for enhanced syntax highlighting
//!
//! Provides detailed token type information beyond what a basic grammar can
//! provide, allowing the editor to highlight different kinds of identifiers,
//! keywords, and values with appropriate semantic meaning.
use tower_lsp::lsp_types::{
SemanticToken,
SemanticTokenType,
SemanticTokens,
SemanticTokensResult,
};
use super::document::Document;
use crate::syntax::{
ast::{
Declaration,
Field,
SubConceptKind,
Value,
},
lexer::{
Lexer,
Token,
},
};
/// Standard semantic token types supported by LSP
pub const LEGEND_TYPES: &[SemanticTokenType] = &[
SemanticTokenType::NAMESPACE, // use paths
SemanticTokenType::TYPE, // template names, species names, enum names
SemanticTokenType::CLASS, // character declarations
SemanticTokenType::ENUM, // enum declarations
SemanticTokenType::INTERFACE, // template declarations
SemanticTokenType::STRUCT, // institution, location declarations
SemanticTokenType::PARAMETER, // action parameters
SemanticTokenType::VARIABLE, // character names in schedules
SemanticTokenType::PROPERTY, // field names
SemanticTokenType::ENUM_MEMBER, // enum variant names
SemanticTokenType::FUNCTION, // behavior names
SemanticTokenType::METHOD, // relationship names
SemanticTokenType::KEYWORD, // keywords like "from", "include", "strict"
SemanticTokenType::STRING, // string literals
SemanticTokenType::NUMBER, // numeric literals
SemanticTokenType::OPERATOR, // operators like "..", "->", etc.
];
/// Semantic token modifiers (currently unused but available for future use)
pub const LEGEND_MODIFIERS: &[&str] = &[
"declaration",
"definition",
"readonly",
"static",
"deprecated",
"abstract",
"async",
"modification",
"documentation",
"defaultLibrary",
];
/// Helper to find identifier positions within a span using the lexer
fn find_identifiers_in_span(
text: &str,
span_start: usize,
span_end: usize,
target_names: &[String],
) -> Vec<(usize, String)> {
let span_text = &text[span_start..span_end];
let lexer = Lexer::new(span_text);
let tokens: Vec<_> = lexer.collect();
let mut results = Vec::new();
for (offset, token, _end) in tokens {
if let Token::Ident(name) = token {
if target_names.contains(&name) {
results.push((span_start + offset, name));
}
}
}
results
}
/// Recursively highlight behavior tree nodes
fn highlight_behavior_node(
builder: &mut SemanticTokensBuilder,
node: &crate::syntax::ast::BehaviorNode,
) {
use crate::syntax::ast::BehaviorNode;
match node {
| BehaviorNode::Selector { children, .. } | BehaviorNode::Sequence { children, .. } => {
for child in children {
highlight_behavior_node(builder, child);
}
},
| BehaviorNode::Action(_action_name, params) => {
// Action names don't have spans, so we'd need to search for them
// For now, just highlight the parameters
for param in params {
highlight_field(builder, param);
}
},
| BehaviorNode::Decorator { child, .. } => {
highlight_behavior_node(builder, child);
},
| BehaviorNode::SubTree(_path) => {
// SubTree references another behavior by path
// Would need position tracking to highlight
},
| BehaviorNode::Condition(_expr) => {
// Conditions contain expressions which could be highlighted
// Would need expression traversal
},
}
}
/// Generate semantic tokens for a document
pub fn get_semantic_tokens(doc: &Document) -> Option<SemanticTokensResult> {
let ast = doc.ast.as_ref()?;
let mut builder = SemanticTokensBuilder::new(&doc.text);
let mut positions = doc.positions.clone();
// Process all top-level declarations
for decl in &ast.declarations {
match decl {
| Declaration::Use(use_decl) => {
// Highlight use paths as namespaces
let path_positions = find_identifiers_in_span(
&doc.text,
use_decl.span.start,
use_decl.span.end,
&use_decl.path,
);
for (offset, segment) in path_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
segment.len(),
token_type_index(SemanticTokenType::NAMESPACE),
0,
);
}
},
| Declaration::Character(character) => {
// Highlight character name as CLASS
builder.add_token(
character.span.start_line,
character.span.start_col,
character.name.len(),
token_type_index(SemanticTokenType::CLASS),
0,
);
// Highlight species as TYPE
if let Some(ref species) = character.species {
let species_positions = find_identifiers_in_span(
&doc.text,
character.span.start,
character.span.end,
std::slice::from_ref(species),
);
for (offset, species_name) in species_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
species_name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
}
}
// Highlight template references
if let Some(ref templates) = character.template {
let template_positions = find_identifiers_in_span(
&doc.text,
character.span.start,
character.span.end,
templates,
);
for (offset, template_name) in template_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
template_name.len(),
token_type_index(SemanticTokenType::INTERFACE),
0,
);
}
}
// Highlight fields
for field in &character.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::Template(template) => {
// Highlight template name as INTERFACE
builder.add_token(
template.span.start_line,
template.span.start_col,
template.name.len(),
token_type_index(SemanticTokenType::INTERFACE),
0,
);
// Find and highlight includes using the lexer
let include_positions = find_identifiers_in_span(
&doc.text,
template.span.start,
template.span.end,
&template.includes,
);
for (offset, include_name) in include_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
include_name.len(),
token_type_index(SemanticTokenType::INTERFACE),
0,
);
}
// Highlight fields
for field in &template.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::Species(species) => {
// Highlight species name as TYPE
builder.add_token(
species.span.start_line,
species.span.start_col,
species.name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
// Highlight fields
for field in &species.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::Institution(institution) => {
// Highlight institution name as STRUCT
builder.add_token(
institution.span.start_line,
institution.span.start_col,
institution.name.len(),
token_type_index(SemanticTokenType::STRUCT),
0,
);
// Highlight fields
for field in &institution.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::Location(location) => {
// Highlight location name as STRUCT
builder.add_token(
location.span.start_line,
location.span.start_col,
location.name.len(),
token_type_index(SemanticTokenType::STRUCT),
0,
);
// Highlight fields
for field in &location.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::Behavior(behavior) => {
// Highlight behavior name as FUNCTION
builder.add_token(
behavior.span.start_line,
behavior.span.start_col,
behavior.name.len(),
token_type_index(SemanticTokenType::FUNCTION),
0,
);
// TODO: Traverse behavior tree to highlight conditions and actions
// Would need recursive function to walk BehaviorNode tree
highlight_behavior_node(&mut builder, &behavior.root);
},
| Declaration::Relationship(relationship) => {
// Highlight relationship name as METHOD
builder.add_token(
relationship.span.start_line,
relationship.span.start_col,
relationship.name.len(),
token_type_index(SemanticTokenType::METHOD),
0,
);
// Highlight participants
for participant in &relationship.participants {
// For qualified paths like "Alice.parent", we want to highlight each segment
// The participant has its own span, so we can search within it
let participant_names = participant.name.clone();
let name_positions = find_identifiers_in_span(
&doc.text,
participant.span.start,
participant.span.end,
&participant_names,
);
for (offset, name) in name_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
name.len(),
token_type_index(SemanticTokenType::VARIABLE),
0,
);
}
}
// Highlight fields
for field in &relationship.fields {
highlight_field(&mut builder, field);
}
},
| Declaration::LifeArc(life_arc) => {
// Highlight life_arc name as TYPE
builder.add_token(
life_arc.span.start_line,
life_arc.span.start_col,
life_arc.name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
// Highlight states and transitions
for state in &life_arc.states {
// State name as ENUM_MEMBER
builder.add_token(
state.span.start_line,
state.span.start_col,
state.name.len(),
token_type_index(SemanticTokenType::ENUM_MEMBER),
0,
);
// State fields
if let Some(ref fields) = state.on_enter {
for field in fields {
highlight_field(&mut builder, field);
}
}
}
},
| Declaration::Schedule(schedule) => {
// Highlight schedule name as TYPE
builder.add_token(
schedule.span.start_line,
schedule.span.start_col,
schedule.name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
// Highlight block fields
for block in &schedule.blocks {
for field in &block.fields {
highlight_field(&mut builder, field);
}
}
},
| Declaration::Concept(concept) => {
// Highlight concept name as TYPE
builder.add_token(
concept.span.start_line,
concept.span.start_col,
concept.name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
},
| Declaration::SubConcept(sub_concept) => {
// Highlight parent concept as TYPE
let parent_positions = find_identifiers_in_span(
&doc.text,
sub_concept.span.start,
sub_concept.span.end,
std::slice::from_ref(&sub_concept.parent_concept),
);
if let Some((offset, parent_name)) = parent_positions.into_iter().next() {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
parent_name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
}
// Highlight sub_concept name as ENUM
let name_positions = find_identifiers_in_span(
&doc.text,
sub_concept.span.start,
sub_concept.span.end,
std::slice::from_ref(&sub_concept.name),
);
if let Some((offset, name)) = name_positions.into_iter().next() {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
name.len(),
token_type_index(SemanticTokenType::ENUM),
0,
);
}
// Highlight enum variants as ENUM_MEMBER, or record fields
match &sub_concept.kind {
| SubConceptKind::Enum { variants } => {
let variant_positions = find_identifiers_in_span(
&doc.text,
sub_concept.span.start,
sub_concept.span.end,
variants,
);
for (offset, variant_name) in variant_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
variant_name.len(),
token_type_index(SemanticTokenType::ENUM_MEMBER),
0,
);
}
},
| SubConceptKind::Record { fields } => {
for field in fields {
highlight_field(&mut builder, field);
}
},
}
},
| Declaration::ConceptComparison(comparison) => {
// Highlight comparison name as TYPE
builder.add_token(
comparison.span.start_line,
comparison.span.start_col,
comparison.name.len(),
token_type_index(SemanticTokenType::TYPE),
0,
);
// Highlight variant names as ENUM_MEMBER
for variant in &comparison.variants {
let variant_positions = find_identifiers_in_span(
&doc.text,
variant.span.start,
variant.span.end,
std::slice::from_ref(&variant.name),
);
if let Some((offset, name)) = variant_positions.into_iter().next() {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
name.len(),
token_type_index(SemanticTokenType::ENUM_MEMBER),
0,
);
}
}
},
}
}
Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: builder.build(),
}))
}
/// Helper to highlight a field
fn highlight_field(builder: &mut SemanticTokensBuilder, field: &Field) {
// Highlight field name as PROPERTY
builder.add_token(
field.span.start_line,
field.span.start_col,
field.name.len(),
token_type_index(SemanticTokenType::PROPERTY),
0,
);
// Highlight field value
highlight_value(builder, &field.value);
}
/// Helper to highlight a value
fn highlight_value(builder: &mut SemanticTokensBuilder, value: &Value) {
match value {
| Value::Text(_s) => {
// Text literals are already highlighted by the grammar
// but we could add semantic highlighting here if needed
},
| Value::Number(_) | Value::Decimal(_) => {
// Number literals are already highlighted by the grammar
},
| Value::Boolean(_) => {
// Boolean literals are already highlighted by the grammar
},
| Value::Identifier(_path) => {
// Identifiers could be highlighted as TYPE references
// but we'd need precise position tracking
},
| Value::List(items) => {
for item in items {
highlight_value(builder, item);
}
},
| Value::Object(fields) => {
for field in fields {
highlight_field(builder, field);
}
},
| Value::Range(start, end) => {
highlight_value(builder, start);
highlight_value(builder, end);
},
| Value::ProseBlock(_) => {
// Prose blocks are already highlighted by the grammar
},
| Value::Override(_) => {
// Override values need their own handling
},
| Value::Time(_) | Value::Duration(_) => {
// Time/duration literals are already highlighted by the grammar
},
| Value::Any => {
// Any keyword is already highlighted by the grammar
},
}
}
/// Get the index of a semantic token type in the legend
fn token_type_index(token_type: SemanticTokenType) -> u32 {
LEGEND_TYPES
.iter()
.position(|t| t == &token_type)
.unwrap_or(0) as u32
}
/// Builder for semantic tokens with proper delta encoding
struct SemanticTokensBuilder {
tokens: Vec<(usize, usize, usize, u32, u32)>, // (line, col, length, type, modifiers)
}
impl SemanticTokensBuilder {
fn new(_text: &str) -> Self {
Self { tokens: Vec::new() }
}
fn add_token(
&mut self,
line: usize,
col: usize,
length: usize,
token_type: u32,
modifiers: u32,
) {
self.tokens.push((line, col, length, token_type, modifiers));
}
fn build(mut self) -> Vec<SemanticToken> {
// Sort tokens by position (line, then column)
self.tokens
.sort_by_key(|(line, col, _, _, _)| (*line, *col));
// Convert to delta-encoded format required by LSP
let mut result = Vec::new();
let mut prev_line = 0;
let mut prev_col = 0;
for (line, col, length, token_type, modifiers) in self.tokens {
let delta_line = line - prev_line;
let delta_start = if delta_line == 0 { col - prev_col } else { col };
result.push(SemanticToken {
delta_line: delta_line as u32,
delta_start: delta_start as u32,
length: length as u32,
token_type,
token_modifiers_bitset: modifiers,
});
prev_line = line;
prev_col = col;
}
result
}
}