Files
kjol/tools/tsgo/internal/ls/semantictokens.go
2026-07-09 16:50:43 -04:00

577 lines
17 KiB
Go

package ls
import (
"context"
"fmt"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tspath"
)
// tokenTypes defines the order of token types for encoding
var tokenTypes = []lsproto.SemanticTokenType{
lsproto.SemanticTokenTypeNamespace,
lsproto.SemanticTokenTypeClass,
lsproto.SemanticTokenTypeEnum,
lsproto.SemanticTokenTypeInterface,
lsproto.SemanticTokenTypeStruct,
lsproto.SemanticTokenTypeTypeParameter,
lsproto.SemanticTokenTypeType,
lsproto.SemanticTokenTypeParameter,
lsproto.SemanticTokenTypeVariable,
lsproto.SemanticTokenTypeProperty,
lsproto.SemanticTokenTypeEnumMember,
lsproto.SemanticTokenTypeDecorator,
lsproto.SemanticTokenTypeEvent,
lsproto.SemanticTokenTypeFunction,
lsproto.SemanticTokenTypeMethod,
lsproto.SemanticTokenTypeMacro,
lsproto.SemanticTokenTypeLabel,
lsproto.SemanticTokenTypeComment,
lsproto.SemanticTokenTypeString,
lsproto.SemanticTokenTypeKeyword,
lsproto.SemanticTokenTypeNumber,
lsproto.SemanticTokenTypeRegexp,
lsproto.SemanticTokenTypeOperator,
}
// tokenModifiers defines the order of token modifiers for encoding
var tokenModifiers = []lsproto.SemanticTokenModifier{
lsproto.SemanticTokenModifierDeclaration,
lsproto.SemanticTokenModifierDefinition,
lsproto.SemanticTokenModifierReadonly,
lsproto.SemanticTokenModifierStatic,
lsproto.SemanticTokenModifierDeprecated,
lsproto.SemanticTokenModifierAbstract,
lsproto.SemanticTokenModifierAsync,
lsproto.SemanticTokenModifierModification,
lsproto.SemanticTokenModifierDocumentation,
lsproto.SemanticTokenModifierDefaultLibrary,
"local",
}
type tokenType int
const (
tokenTypeNamespace tokenType = iota
tokenTypeClass
tokenTypeEnum
tokenTypeInterface
tokenTypeStruct
tokenTypeTypeParameter
tokenTypeType
tokenTypeParameter
tokenTypeVariable
tokenTypeProperty
tokenTypeEnumMember
tokenTypeDecorator
tokenTypeEvent
tokenTypeFunction
tokenTypeMethod // Previously called "member" in TypeScript
tokenTypeMacro
tokenTypeLabel
tokenTypeComment
tokenTypeString
tokenTypeKeyword
tokenTypeNumber
tokenTypeRegexp
tokenTypeOperator
)
type tokenModifier int
const (
tokenModifierDeclaration tokenModifier = 1 << iota
tokenModifierDefinition
tokenModifierReadonly
tokenModifierStatic
tokenModifierDeprecated
tokenModifierAbstract
tokenModifierAsync
tokenModifierModification
tokenModifierDocumentation
tokenModifierDefaultLibrary
tokenModifierLocal
)
// SemanticTokensLegend returns the legend describing the token types and modifiers.
// It filters the legend to only include types and modifiers that the client supports,
// as indicated by clientCapabilities.
func SemanticTokensLegend(clientCapabilities lsproto.ResolvedSemanticTokensClientCapabilities) *lsproto.SemanticTokensLegend {
types := make([]string, 0, len(tokenTypes))
for _, t := range tokenTypes {
if slices.Contains(clientCapabilities.TokenTypes, string(t)) {
types = append(types, string(t))
}
}
modifiers := make([]string, 0, len(tokenModifiers))
for _, m := range tokenModifiers {
if slices.Contains(clientCapabilities.TokenModifiers, string(m)) {
modifiers = append(modifiers, string(m))
}
}
return &lsproto.SemanticTokensLegend{
TokenTypes: types,
TokenModifiers: modifiers,
}
}
func (l *LanguageService) ProvideSemanticTokens(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.SemanticTokensResponse, error) {
program, file := l.getProgramAndFile(documentURI)
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
tokens := l.collectSemanticTokens(ctx, c, file, program)
if len(tokens) == 0 {
return lsproto.SemanticTokensOrNull{}, nil
}
// Convert to LSP format (relative encoding)
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
return lsproto.SemanticTokensOrNull{
SemanticTokens: &lsproto.SemanticTokens{
Data: encoded,
},
}, nil
}
func (l *LanguageService) ProvideSemanticTokensRange(ctx context.Context, documentURI lsproto.DocumentUri, rng lsproto.Range) (lsproto.SemanticTokensRangeResponse, error) {
program, file := l.getProgramAndFile(documentURI)
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
start := int(l.converters.LineAndCharacterToPosition(file, rng.Start))
end := int(l.converters.LineAndCharacterToPosition(file, rng.End))
tokens := l.collectSemanticTokensInRange(ctx, c, file, program, start, end)
if len(tokens) == 0 {
return lsproto.SemanticTokensOrNull{}, nil
}
// Convert to LSP format (relative encoding)
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
return lsproto.SemanticTokensOrNull{
SemanticTokens: &lsproto.SemanticTokens{
Data: encoded,
},
}, nil
}
type semanticToken struct {
node *ast.Node
tokenType tokenType
tokenModifier tokenModifier
}
func (l *LanguageService) collectSemanticTokens(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program) []semanticToken {
return l.collectSemanticTokensInRange(ctx, c, file, program, file.Pos(), file.End())
}
func (l *LanguageService) collectSemanticTokensInRange(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program, spanStart, spanEnd int) []semanticToken {
tokens := []semanticToken{}
inJSXElement := false
var visit func(*ast.Node) bool
visit = func(node *ast.Node) bool {
// Check for cancellation
if ctx.Err() != nil {
return false
}
if node == nil {
return false
}
if node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
nodeEnd := node.End()
if node.Pos() >= spanEnd || nodeEnd <= spanStart {
return false
}
prevInJSXElement := inJSXElement
if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) {
inJSXElement = true
} else if ast.IsJsxExpression(node) {
inJSXElement = false
}
if ast.IsIdentifier(node) && node.Text() != "" && !inJSXElement && !isInImportClause(node) && !isInfinityOrNaNString(node.Text()) {
symbol := c.GetSymbolAtLocation(node)
if symbol != nil {
// Resolve aliases
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
symbol = c.GetAliasedSymbol(symbol)
}
tokenType, ok := classifySymbol(symbol, getMeaningFromLocation(node))
if ok {
tokenModifier := tokenModifier(0)
// Check if this is a declaration
parent := node.Parent
if parent != nil {
parentIsDeclaration := ast.IsBindingElement(parent) || tokenFromDeclarationMapping(parent.Kind) == tokenType
if parentIsDeclaration && parent.Name() == node {
tokenModifier |= tokenModifierDeclaration
}
}
// Property declaration in constructor: reclassify parameters as properties in property access context
if tokenType == tokenTypeParameter && ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
tokenType = tokenTypeProperty
}
// Type-based reclassification
tokenType = reclassifyByType(c, node, tokenType)
// Get the value declaration to check modifiers
if decl := symbol.ValueDeclaration; decl != nil {
modifiers := ast.GetCombinedModifierFlags(decl)
nodeFlags := ast.GetCombinedNodeFlags(decl)
if modifiers&ast.ModifierFlagsStatic != 0 {
tokenModifier |= tokenModifierStatic
}
if modifiers&ast.ModifierFlagsAsync != 0 {
tokenModifier |= tokenModifierAsync
}
if tokenType != tokenTypeClass && tokenType != tokenTypeInterface {
if (modifiers&ast.ModifierFlagsReadonly != 0) || (nodeFlags&ast.NodeFlagsConst != 0) || (symbol.Flags&ast.SymbolFlagsEnumMember != 0) {
tokenModifier |= tokenModifierReadonly
}
}
if (tokenType == tokenTypeVariable || tokenType == tokenTypeFunction) && isLocalDeclaration(decl, file) {
tokenModifier |= tokenModifierLocal
}
declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
tokenModifier |= tokenModifierDefaultLibrary
}
} else if symbol.Declarations != nil {
for _, decl := range symbol.Declarations {
declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
tokenModifier |= tokenModifierDefaultLibrary
break
}
}
}
tokens = append(tokens, semanticToken{
node: node,
tokenType: tokenType,
tokenModifier: tokenModifier,
})
}
}
}
node.ForEachChild(visit)
inJSXElement = prevInJSXElement
return false
}
visit(file.AsNode())
// Check for cancellation after collection
if ctx.Err() != nil {
return nil
}
return tokens
}
func classifySymbol(symbol *ast.Symbol, meaning ast.SemanticMeaning) (tokenType, bool) {
flags := symbol.Flags
if flags&ast.SymbolFlagsClass != 0 {
return tokenTypeClass, true
}
if flags&ast.SymbolFlagsEnum != 0 {
return tokenTypeEnum, true
}
if flags&ast.SymbolFlagsTypeAlias != 0 {
return tokenTypeType, true
}
if flags&ast.SymbolFlagsInterface != 0 {
if meaning&ast.SemanticMeaningType != 0 {
return tokenTypeInterface, true
}
}
if flags&ast.SymbolFlagsTypeParameter != 0 {
return tokenTypeTypeParameter, true
}
// Check the value declaration
decl := symbol.ValueDeclaration
if decl == nil && len(symbol.Declarations) > 0 {
decl = symbol.Declarations[0]
}
if decl != nil {
if ast.IsBindingElement(decl) {
decl = getDeclarationForBindingElement(decl)
}
if tokenType := tokenFromDeclarationMapping(decl.Kind); tokenType >= 0 {
return tokenType, true
}
}
return 0, false
}
func tokenFromDeclarationMapping(kind ast.Kind) tokenType {
switch kind {
case ast.KindVariableDeclaration:
return tokenTypeVariable
case ast.KindParameter:
return tokenTypeParameter
case ast.KindPropertyDeclaration:
return tokenTypeProperty
case ast.KindModuleDeclaration:
return tokenTypeNamespace
case ast.KindEnumDeclaration:
return tokenTypeEnum
case ast.KindEnumMember:
return tokenTypeEnumMember
case ast.KindClassDeclaration, ast.KindClassExpression:
return tokenTypeClass
case ast.KindMethodDeclaration:
return tokenTypeMethod
case ast.KindFunctionDeclaration, ast.KindFunctionExpression:
return tokenTypeFunction
case ast.KindMethodSignature:
return tokenTypeMethod
case ast.KindGetAccessor, ast.KindSetAccessor:
return tokenTypeProperty
case ast.KindPropertySignature:
return tokenTypeProperty
case ast.KindInterfaceDeclaration:
return tokenTypeInterface
case ast.KindTypeAliasDeclaration:
return tokenTypeType
case ast.KindTypeParameter:
return tokenTypeTypeParameter
case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment:
return tokenTypeProperty
default:
return -1
}
}
func reclassifyByType(c *checker.Checker, node *ast.Node, tt tokenType) tokenType {
// Type-based reclassification for variables, properties, and parameters
if tt == tokenTypeVariable || tt == tokenTypeProperty || tt == tokenTypeParameter {
typ := c.GetTypeAtLocation(node)
if typ != nil {
test := func(condition func(*checker.Type) bool) bool {
if condition(typ) {
return true
}
if typ.Flags()&checker.TypeFlagsUnion != 0 {
if slices.ContainsFunc(typ.AsUnionType().Types(), condition) {
return true
}
}
return false
}
// Check for constructor signatures (class-like)
if tt != tokenTypeParameter && test(func(t *checker.Type) bool {
return len(c.GetSignaturesOfType(t, checker.SignatureKindConstruct)) > 0
}) {
return tokenTypeClass
}
// Check for call signatures (function-like)
// Must have call signatures AND (no properties OR be used in call context)
hasCallSignatures := test(func(t *checker.Type) bool {
return len(c.GetSignaturesOfType(t, checker.SignatureKindCall)) > 0
})
if hasCallSignatures {
hasNoProperties := !test(func(t *checker.Type) bool {
objType := t.AsObjectType()
return objType != nil && len(objType.Properties()) > 0
})
if hasNoProperties || isExpressionInCallExpression(node) {
if tt == tokenTypeProperty {
return tokenTypeMethod
}
return tokenTypeFunction
}
}
}
}
return tt
}
func isLocalDeclaration(decl *ast.Node, sourceFile *ast.SourceFile) bool {
if ast.IsBindingElement(decl) {
decl = getDeclarationForBindingElement(decl)
}
if ast.IsVariableDeclaration(decl) {
parent := decl.Parent
// Check if this is a catch clause parameter
if parent != nil && ast.IsCatchClause(parent) {
return ast.GetSourceFileOfNode(decl) == sourceFile
}
if parent != nil && ast.IsVariableDeclarationList(parent) {
grandparent := parent.Parent
if grandparent != nil {
greatGrandparent := grandparent.Parent
return (!ast.IsSourceFile(greatGrandparent) || ast.IsCatchClause(grandparent)) &&
ast.GetSourceFileOfNode(decl) == sourceFile
}
}
} else if ast.IsFunctionDeclaration(decl) {
parent := decl.Parent
return parent != nil && !ast.IsSourceFile(parent) && ast.GetSourceFileOfNode(decl) == sourceFile
}
return false
}
func getDeclarationForBindingElement(element *ast.Node) *ast.Node {
for {
parent := element.Parent
if parent != nil && ast.IsBindingPattern(parent) {
grandparent := parent.Parent
if grandparent != nil && ast.IsBindingElement(grandparent) {
element = grandparent
continue
}
return parent.Parent
}
return element
}
}
func isInImportClause(node *ast.Node) bool {
parent := node.Parent
return parent != nil && (ast.IsImportClause(parent) || ast.IsImportSpecifier(parent) || ast.IsNamespaceImport(parent))
}
func isExpressionInCallExpression(node *ast.Node) bool {
for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
node = node.Parent
}
parent := node.Parent
return parent != nil && ast.IsCallExpression(parent) && parent.Expression() == node
}
func isInfinityOrNaNString(text string) bool {
return text == "Infinity" || text == "NaN"
}
// encodeSemanticTokens encodes tokens into the LSP format using relative positioning.
// It filters tokens based on client capabilities, only including types and modifiers that the client supports.
func encodeSemanticTokens(ctx context.Context, tokens []semanticToken, file *ast.SourceFile, converters *lsconv.Converters) []uint32 {
// Build mapping from server token types/modifiers to client indices
typeMapping := make(map[tokenType]uint32)
modifierMapping := make(map[lsproto.SemanticTokenModifier]uint32)
clientCapabilities := lsproto.GetClientCapabilities(ctx).TextDocument.SemanticTokens
// Map server token types to client-supported indices
clientIdx := uint32(0)
for i, serverType := range tokenTypes {
if slices.Contains(clientCapabilities.TokenTypes, string(serverType)) {
typeMapping[tokenType(i)] = clientIdx
clientIdx++
}
}
// Map server token modifiers to client-supported bit positions
clientBit := uint32(0)
for _, serverModifier := range tokenModifiers {
if slices.Contains(clientCapabilities.TokenModifiers, string(serverModifier)) {
modifierMapping[serverModifier] = clientBit
clientBit++
}
}
// Each token encodes 5 uint32 values: deltaLine, deltaChar, length, tokenType, tokenModifiers
encoded := make([]uint32, 0, len(tokens)*5)
prevLine := uint32(0)
prevChar := uint32(0)
for _, token := range tokens {
// Skip tokens with types not supported by the client
clientTypeIdx, typeSupported := typeMapping[token.tokenType]
if !typeSupported {
continue
}
// Map modifiers to client-supported bit mask
clientModifierMask := uint32(0)
for i, serverModifier := range tokenModifiers {
if token.tokenModifier&(1<<i) != 0 {
if clientBit, ok := modifierMapping[serverModifier]; ok {
clientModifierMask |= 1 << clientBit
}
}
}
// Use GetTokenPosOfNode to skip trivia (comments, whitespace) before the identifier
tokenStart := scanner.GetTokenPosOfNode(token.node, file, false)
tokenEnd := token.node.End()
// Convert both start and end positions to LSP coordinates, then compute length
startPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenStart))
endPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenEnd))
// Length is the character difference when on the same line
var tokenLength uint32
if startPos.Line == endPos.Line {
tokenLength = endPos.Character - startPos.Character
} else {
panic(fmt.Sprintf("semantic tokens: token spans multiple lines: start=(%d,%d) end=(%d,%d) for token at offset %d",
startPos.Line, startPos.Character, endPos.Line, endPos.Character, tokenStart))
}
line := startPos.Line
char := startPos.Character
// Verify that positions are strictly increasing (visitor walks in order)
if len(encoded) > 0 && (line < prevLine || (line == prevLine && char <= prevChar)) {
panic(fmt.Sprintf("semantic tokens: positions must be strictly increasing: prev=(%d,%d) current=(%d,%d) for token at offset %d",
prevLine, prevChar, line, char, tokenStart))
}
// Encode as: [deltaLine, deltaChar, length, tokenType, tokenModifiers]
deltaLine := line - prevLine
var deltaChar uint32
if deltaLine == 0 {
deltaChar = char - prevChar
} else {
deltaChar = char
}
encoded = append(
encoded,
deltaLine,
deltaChar,
tokenLength,
clientTypeIdx,
clientModifierMask,
)
prevLine = line
prevChar = char
}
return encoded
}