From 1a35e6207d97c82b62f5bcfe4e5e8f0e30e10f25 Mon Sep 17 00:00:00 2001 From: karitham Date: Fri, 07 Aug 2026 20:16:36 +0000 Subject: [PATCH] lsp: thrift-named semantic token types for unions and exceptions --- lsp/semantic/semantic.go | 18 +++++++++++++++--- lsp/semantic/semantic_test.go | 37 ++++++++++++++++++++++++++++++++++++- lsp/symbols/document.go | 4 ++-- lsp/symbols/workspace_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 4 file(s) changed, 97 insertion(s)(+), 6 deletion(s)(-) diff --git a/lsp/semantic/semantic.go b/lsp/semantic/semantic.go --- a/lsp/semantic/semantic.go +++ b/lsp/semantic/semantic.go @@ -14,10 +14,12 @@ // legend is the ordered list of token types; indexes into it are the // encoded token types. The server advertises it in the registration -// options, so it must stay in sync with the constants below. +// options, so it must stay in sync with the constants below. The types +// follow thrift's own naming; "union" and "exception" are server-defined +// additions to the standard semantic token types. var legend = []string{ "keyword", "string", "number", "comment", - "type", "struct", "enum", "interface", + "type", "struct", "union", "exception", "enum", "interface", "property", "function", "enumMember", "variable", } @@ -28,6 +30,8 @@ tokComment tokType tokStruct + tokUnion + tokException tokEnum tokInterface tokProperty @@ -126,7 +130,15 @@ for _, n := range doc.Nodes { switch v := n.(type) { case *syntax.Struct: - names[v.Name.TokStart()] = tokStruct + switch v.Kind { + case syntax.UnionDecl: + names[v.Name.TokStart()] = tokUnion + case syntax.ExceptionDecl: + names[v.Name.TokStart()] = tokException + default: + names[v.Name.TokStart()] = tokStruct + } + for _, f := range v.Fields { names[f.Name.TokStart()] = tokProperty } diff --git a/lsp/semantic/semantic_test.go b/lsp/semantic/semantic_test.go --- a/lsp/semantic/semantic_test.go +++ b/lsp/semantic/semantic_test.go @@ -1,6 +1,7 @@ package semantic import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -167,9 +168,43 @@ func TestSemanticTokensLegend(t *testing.T) { assert.Equal(t, []string{ "keyword", "string", "number", "comment", - "type", "struct", "enum", "interface", + "type", "struct", "union", "exception", "enum", "interface", "property", "function", "enumMember", "variable", }, Legend()) } var _ = protocol.SemanticTokens{} + +// TestSemanticTokensUnionException pins distinct token types for union and +// exception definitions. +func TestSemanticTokensUnionException(t *testing.T) { + src := `union MobileArmor { + 1: string loadout, +} + +exception BayFull { + 1: string message, +}` + + got := semanticTokens(t, src) + + var union, exception decodedToken + found := 0 + + for _, tok := range got { + switch tok.typ { + case tokUnion: + union = tok + found++ + case tokException: + exception = tok + found++ + } + } + + require.Equal(t, 2, found) + + lines := strings.Split(src, "\n") + assert.Equal(t, "MobileArmor", lines[union.line][union.char:union.char+union.length]) + assert.Equal(t, "BayFull", lines[exception.line][exception.char:exception.char+exception.length]) +} diff --git a/lsp/symbols/document.go b/lsp/symbols/document.go --- a/lsp/symbols/document.go +++ b/lsp/symbols/document.go @@ -68,9 +68,9 @@ case syntax.StructDecl: return structSymbol(doc, v, "Struct", protocol.SymbolKindStruct) case syntax.UnionDecl: - return structSymbol(doc, v, "Union", protocol.SymbolKindStruct) + return structSymbol(doc, v, "Union", protocol.SymbolKindInterface) case syntax.ExceptionDecl: - return structSymbol(doc, v, "Exception", protocol.SymbolKindStruct) + return structSymbol(doc, v, "Exception", protocol.SymbolKindClass) } case *syntax.Enum: return enumSymbol(doc, v) diff --git a/lsp/symbols/workspace_test.go b/lsp/symbols/workspace_test.go --- a/lsp/symbols/workspace_test.go +++ b/lsp/symbols/workspace_test.go @@ -309,3 +309,47 @@ }) } } + +// TestWorkspaceSymbolsKinds pins the distinct symbol kinds: unions and +// exceptions surface differently from plain structs. +func TestWorkspaceSymbolsKinds(t *testing.T) { + dir := writeTree(t, map[string]string{ + "shapes.thrift": `struct Gundam { + 1: required string Name, +} + +union MobileArmor { + 1: string loadout, +} + +exception BayFull { + 1: string message, +}`, + }) + + session := cache.NewSession(cache.New(nil)) + openTree(t, session, dir, nil) + + syms := WorkspaceSymbols(t.Context(), session, "", 0) + byName := make(map[string]protocol.SymbolInformation, len(syms)) + for _, s := range syms { + byName[s.Name] = s + } + + tests := []struct { + name string + kind protocol.SymbolKind + }{ + {"Gundam", protocol.SymbolKindStruct}, + {"MobileArmor", protocol.SymbolKindInterface}, + {"BayFull", protocol.SymbolKindClass}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sym, ok := byName[tt.name] + require.True(t, ok, "symbol %q missing", tt.name) + assert.Equal(t, tt.kind, sym.Kind) + }) + } +} -- tangled.sh