use crate::base::FileId;
use crate::hir::{SymbolIndex, SymbolKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
Namespace,
Type,
Variable,
Property,
Keyword,
Comment,
}
impl TokenType {
pub fn to_lsp_index(self) -> u32 {
match self {
TokenType::Namespace => 0,
TokenType::Type => 1,
TokenType::Variable => 2,
TokenType::Property => 3,
TokenType::Keyword => 4,
TokenType::Comment => 5,
}
}
}
impl From<SymbolKind> for TokenType {
fn from(kind: SymbolKind) -> Self {
match kind {
SymbolKind::Package => TokenType::Namespace,
SymbolKind::PartDef
| SymbolKind::ItemDef
| SymbolKind::ActionDef
| SymbolKind::PortDef
| SymbolKind::AttributeDef
| SymbolKind::ConnectionDef
| SymbolKind::InterfaceDef
| SymbolKind::AllocationDef
| SymbolKind::RequirementDef
| SymbolKind::ConstraintDef
| SymbolKind::StateDef
| SymbolKind::CalculationDef
| SymbolKind::UseCaseDef
| SymbolKind::AnalysisCaseDef
| SymbolKind::ConcernDef
| SymbolKind::ViewDef
| SymbolKind::ViewpointDef
| SymbolKind::RenderingDef
| SymbolKind::EnumerationDef
| SymbolKind::MetaclassDef
| SymbolKind::InteractionDef => TokenType::Type,
SymbolKind::PartUsage
| SymbolKind::ItemUsage
| SymbolKind::ActionUsage
| SymbolKind::PortUsage
| SymbolKind::AttributeUsage
| SymbolKind::ConnectionUsage
| SymbolKind::InterfaceUsage
| SymbolKind::AllocationUsage
| SymbolKind::RequirementUsage
| SymbolKind::ConstraintUsage
| SymbolKind::StateUsage
| SymbolKind::CalculationUsage
| SymbolKind::ReferenceUsage
| SymbolKind::OccurrenceUsage
| SymbolKind::FlowUsage
| SymbolKind::ViewUsage
| SymbolKind::ViewpointUsage
| SymbolKind::RenderingUsage => TokenType::Property,
SymbolKind::Alias => TokenType::Variable,
SymbolKind::Import => TokenType::Namespace,
SymbolKind::Comment => TokenType::Comment,
SymbolKind::Dependency => TokenType::Variable,
SymbolKind::ExposeRelationship => TokenType::Variable,
SymbolKind::Other => TokenType::Variable,
}
}
}
#[derive(Debug, Clone)]
pub struct SemanticToken {
pub line: u32,
pub col: u32,
pub length: u32,
pub token_type: TokenType,
}
pub fn semantic_tokens(index: &SymbolIndex, file: FileId) -> Vec<SemanticToken> {
let mut tokens = Vec::new();
for symbol in index.symbols_in_file(file) {
let length = if symbol.end_col > symbol.start_col {
symbol.end_col - symbol.start_col
} else {
symbol.name.len() as u32
};
let is_valid_span =
symbol.start_col > 0 || (symbol.start_col == 0 && symbol.end_col == length);
if is_valid_span {
tokens.push(SemanticToken {
line: symbol.start_line,
col: symbol.start_col,
length,
token_type: TokenType::from(symbol.kind),
});
}
for type_ref_kind in &symbol.type_refs {
for type_ref in type_ref_kind.as_refs() {
let ref_length = (type_ref.end_col - type_ref.start_col).max(1);
let is_valid_ref = type_ref.start_col > 0
|| (type_ref.start_col == 0 && type_ref.end_col == ref_length);
if is_valid_ref {
tokens.push(SemanticToken {
line: type_ref.start_line,
col: type_ref.start_col,
length: ref_length,
token_type: TokenType::Type,
});
}
}
}
}
tokens.sort_by_key(|t| (t.line, t.col));
tokens
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::FileId;
use crate::hir::{SymbolIndex, extract_symbols_unified};
use crate::syntax::parser::parse_content;
fn build_index_from_source(source: &str) -> SymbolIndex {
let syntax = parse_content(source, std::path::Path::new("test.sysml")).unwrap();
let symbols = extract_symbols_unified(FileId(1), &syntax);
let mut index = SymbolIndex::new();
index.add_file(FileId(1), symbols);
index
}
#[test]
fn test_semantic_tokens_package_positions() {
let source = r#"package VehicleIndividuals {
package IndividualDefinitions {
}
}"#;
let index = build_index_from_source(source);
let tokens = semantic_tokens(&index, FileId(1));
println!("All tokens (sorted by position):");
for tok in &tokens {
println!(
" line={} col={} len={} type={:?}",
tok.line, tok.col, tok.length, tok.token_type
);
}
let zero_pos_tokens: Vec<_> = tokens
.iter()
.filter(|t| t.line == 0 && t.col == 0)
.collect();
if !zero_pos_tokens.is_empty() {
println!("WARNING: Found tokens at (0,0):");
for tok in &zero_pos_tokens {
println!(" len={} type={:?}", tok.length, tok.token_type);
}
}
assert_eq!(tokens.len(), 2, "Should have 2 package tokens");
let tok1 = &tokens[0];
assert_eq!(tok1.line, 0);
assert_eq!(tok1.col, 8, "VehicleIndividuals should start at col 8");
assert_eq!(tok1.length, 18, "VehicleIndividuals has 18 chars");
assert_eq!(tok1.token_type, TokenType::Namespace);
let tok2 = &tokens[1];
assert_eq!(tok2.line, 1);
assert_eq!(tok2.col, 9, "IndividualDefinitions should start at col 9");
assert_eq!(tok2.length, 21, "IndividualDefinitions has 21 chars");
assert_eq!(tok2.token_type, TokenType::Namespace);
}
#[test]
fn test_semantic_tokens_stdlib_requirements() {
let source = r#"standard library package Requirements {
private import Base::Anything;
private abstract constraint def RequirementConstraintCheck {
}
}"#;
let index = build_index_from_source(source);
let tokens = semantic_tokens(&index, FileId(1));
println!("Requirements.sysml tokens:");
for tok in &tokens {
println!(
" line={} col={} len={} type={:?}",
tok.line, tok.col, tok.length, tok.token_type
);
}
let zero_tokens: Vec<_> = tokens
.iter()
.filter(|t| t.line == 0 && t.col == 0)
.collect();
assert!(
zero_tokens.is_empty(),
"Should not have tokens at (0,0), found: {:?}",
zero_tokens
);
let pkg_token = tokens.iter().find(|t| t.token_type == TokenType::Namespace);
assert!(
pkg_token.is_some(),
"Should have a Namespace token for Requirements"
);
let pkg_token = pkg_token.unwrap();
println!(
"Package token: line={} col={} len={}",
pkg_token.line, pkg_token.col, pkg_token.length
);
assert_eq!(pkg_token.col, 25, "Requirements should start at col 25");
assert_eq!(pkg_token.length, 12, "Requirements has 12 chars");
}
}