Skip to content

Commit 4feb261

Browse files
authored
fix: correct bug with broken semantic tokens request (#20)
1 parent f378d97 commit 4feb261

2 files changed

Lines changed: 135 additions & 10 deletions

File tree

lsp/capabilities.go

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,61 @@ type FoldingRangeClientCapabilities struct {
264264

265265
// SemanticTokensClientCapabilities declares which semantic token types, modifiers, and request styles (full, delta, range) the editor supports.
266266
type SemanticTokensClientCapabilities struct {
267-
DynamicRegistration *bool `json:"dynamicRegistration,omitempty"`
268-
Requests struct {
269-
Range *bool `json:"range,omitempty"`
270-
Full *SemanticTokensFull `json:"full,omitempty"`
271-
} `json:"requests"`
272-
TokenTypes []string `json:"tokenTypes"`
273-
TokenModifiers []string `json:"tokenModifiers"`
274-
Formats []TokenFormat `json:"formats"`
275-
OverlappingTokenSupport *bool `json:"overlappingTokenSupport,omitempty"`
276-
MultilineTokenSupport *bool `json:"multilineTokenSupport,omitempty"`
267+
DynamicRegistration *bool `json:"dynamicRegistration,omitempty"`
268+
Requests SemanticTokensRequestsCapabilities `json:"requests"`
269+
TokenTypes []string `json:"tokenTypes"`
270+
TokenModifiers []string `json:"tokenModifiers"`
271+
Formats []TokenFormat `json:"formats"`
272+
OverlappingTokenSupport *bool `json:"overlappingTokenSupport,omitempty"`
273+
MultilineTokenSupport *bool `json:"multilineTokenSupport,omitempty"`
274+
}
275+
276+
// SemanticTokensRequestsCapabilities describes the semantic token request styles the client supports.
277+
// Per the LSP spec, "range" is `boolean | {}` and "full" is `boolean | { delta?: boolean }`;
278+
// UnmarshalJSON accepts either form.
279+
type SemanticTokensRequestsCapabilities struct {
280+
Range *bool `json:"range,omitempty"`
281+
Full *SemanticTokensFull `json:"full,omitempty"`
282+
}
283+
284+
// UnmarshalJSON accepts both the boolean and object forms for "range" and "full" per the LSP spec.
285+
func (s *SemanticTokensRequestsCapabilities) UnmarshalJSON(data []byte) error {
286+
var raw struct {
287+
Range json.RawMessage `json:"range"`
288+
Full json.RawMessage `json:"full"`
289+
}
290+
if err := json.Unmarshal(data, &raw); err != nil {
291+
return err
292+
}
293+
294+
if len(raw.Range) > 0 && string(raw.Range) != "null" {
295+
var b bool
296+
if err := json.Unmarshal(raw.Range, &b); err == nil {
297+
s.Range = &b
298+
} else {
299+
// Empty object form ({}) — presence indicates support.
300+
t := true
301+
s.Range = &t
302+
}
303+
}
304+
305+
if len(raw.Full) > 0 && string(raw.Full) != "null" {
306+
var b bool
307+
if err := json.Unmarshal(raw.Full, &b); err == nil {
308+
if b {
309+
s.Full = &SemanticTokensFull{}
310+
}
311+
// false → leave Full nil to signal unsupported.
312+
} else {
313+
var f SemanticTokensFull
314+
if err := json.Unmarshal(raw.Full, &f); err != nil {
315+
return err
316+
}
317+
s.Full = &f
318+
}
319+
}
320+
321+
return nil
277322
}
278323

279324
// WindowClientCapabilities declares editor support for window features like work-done progress, show-message requests, and show-document.

lsp/lsp_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,86 @@ func TestServerCapabilitiesMarshal(t *testing.T) {
157157
}
158158
}
159159

160+
func TestSemanticTokensRequestsUnmarshal(t *testing.T) {
161+
tests := []struct {
162+
name string
163+
input string
164+
check func(t *testing.T, r SemanticTokensRequestsCapabilities)
165+
wantErr bool
166+
}{
167+
{
168+
name: "full as true",
169+
input: `{"requests":{"full":true}}`,
170+
check: func(t *testing.T, r SemanticTokensRequestsCapabilities) {
171+
if r.Full == nil {
172+
t.Fatal("full should be non-nil for boolean true")
173+
}
174+
if r.Full.Delta != nil {
175+
t.Errorf("delta should be nil, got %v", *r.Full.Delta)
176+
}
177+
},
178+
},
179+
{
180+
name: "full as false",
181+
input: `{"requests":{"full":false}}`,
182+
check: func(t *testing.T, r SemanticTokensRequestsCapabilities) {
183+
if r.Full != nil {
184+
t.Errorf("full should be nil for boolean false, got %+v", r.Full)
185+
}
186+
},
187+
},
188+
{
189+
name: "full as object with delta",
190+
input: `{"requests":{"full":{"delta":true}}}`,
191+
check: func(t *testing.T, r SemanticTokensRequestsCapabilities) {
192+
if r.Full == nil || r.Full.Delta == nil || !*r.Full.Delta {
193+
t.Fatalf("delta not preserved, got %+v", r.Full)
194+
}
195+
},
196+
},
197+
{
198+
name: "range as boolean",
199+
input: `{"requests":{"range":true}}`,
200+
check: func(t *testing.T, r SemanticTokensRequestsCapabilities) {
201+
if r.Range == nil || !*r.Range {
202+
t.Fatalf("range not preserved, got %+v", r.Range)
203+
}
204+
},
205+
},
206+
{
207+
name: "range as empty object",
208+
input: `{"requests":{"range":{}}}`,
209+
check: func(t *testing.T, r SemanticTokensRequestsCapabilities) {
210+
if r.Range == nil || !*r.Range {
211+
t.Fatalf("range {} should mean supported, got %+v", r.Range)
212+
}
213+
},
214+
},
215+
{
216+
name: "full as invalid",
217+
input: `{"requests":{"full":"nonsense"}}`,
218+
wantErr: true,
219+
},
220+
}
221+
222+
for _, tc := range tests {
223+
t.Run(tc.name, func(t *testing.T) {
224+
var caps SemanticTokensClientCapabilities
225+
err := json.Unmarshal([]byte(tc.input), &caps)
226+
if tc.wantErr {
227+
if err == nil {
228+
t.Fatal("expected error, got nil")
229+
}
230+
return
231+
}
232+
if err != nil {
233+
t.Fatalf("unmarshal: %v", err)
234+
}
235+
tc.check(t, caps.Requests)
236+
})
237+
}
238+
}
239+
160240
func TestPositionEncodingCapabilitiesMarshal(t *testing.T) {
161241
caps := ClientCapabilities{
162242
General: &GeneralClientCapabilities{

0 commit comments

Comments
 (0)