Skip to content

Commit 1a7d9bf

Browse files
committed
fix(lsp): prevent duplicate diagnostics when client supports pull diagnostics
Correct ClientCapabilities struct to match LSP spec, detecting pull diagnostics support via textDocument.diagnostic presence. Fixes authzed/spicedb-vscode#41
1 parent 8e2054a commit 1a7d9bf

File tree

5 files changed

+49
-13
lines changed

5 files changed

+49
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## [Unreleased]
7+
### Fixed
8+
- Fix duplicate diagnostics in LSP server when VS Code pulls diagnostics (https://github.com/authzed/spicedb/pull/2977)
79

810
## [1.50.0] - 2026-03-19
911
### Added

internal/lsp/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error)
335335
return nil, err
336336
}
337337

338-
s.requestsDiagnostics = ip.Capabilities.Diagnostics.RefreshSupport
338+
s.requestsDiagnostics = ip.Capabilities.SupportsPullDiagnostics()
339339
log.Debug().
340340
Bool("requestsDiagnostics", s.requestsDiagnostics).
341341
Msg("initialize")

internal/lsp/lsp_test.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,32 +287,42 @@ func TestRequestAfterShutdown(t *testing.T) {
287287
}
288288

289289
func TestDiagnosticsRefreshSupport(t *testing.T) {
290+
// Initialize with textDocument diagnostic support enabled
290291
tester := newLSPTester(t)
291-
292-
// Initialize with diagnostic refresh support enabled
293292
resp, serverState := sendAndReceive[lsp.InitializeResult](tester, "initialize", InitializeParams{
294293
Capabilities: ClientCapabilities{
295-
Diagnostics: DiagnosticWorkspaceClientCapabilities{
296-
RefreshSupport: true,
294+
TextDocument: &TextDocumentClientCapabilities{
295+
Diagnostic: &DiagnosticClientCapabilities{},
297296
},
298297
},
299298
})
300299
require.Equal(t, serverStateInitialized, serverState)
301300
require.True(t, resp.Capabilities.DocumentFormattingProvider)
302301
require.True(t, tester.server.requestsDiagnostics)
303302

304-
// Initialize without diagnostic refresh support
303+
// Initialize with only workspace diagnostic refresh support (not pull diagnostics)
305304
tester2 := newLSPTester(t)
306305
resp2, serverState2 := sendAndReceive[lsp.InitializeResult](tester2, "initialize", InitializeParams{
307306
Capabilities: ClientCapabilities{
308-
Diagnostics: DiagnosticWorkspaceClientCapabilities{
309-
RefreshSupport: false,
307+
Workspace: &WorkspaceClientCapabilities{
308+
Diagnostics: &DiagnosticWorkspaceClientCapabilities{
309+
RefreshSupport: true,
310+
},
310311
},
311312
},
312313
})
313314
require.Equal(t, serverStateInitialized, serverState2)
314315
require.True(t, resp2.Capabilities.DocumentFormattingProvider)
315316
require.False(t, tester2.server.requestsDiagnostics)
317+
318+
// Initialize without any diagnostic support
319+
tester3 := newLSPTester(t)
320+
resp3, serverState3 := sendAndReceive[lsp.InitializeResult](tester3, "initialize", InitializeParams{
321+
Capabilities: ClientCapabilities{},
322+
})
323+
require.Equal(t, serverStateInitialized, serverState3)
324+
require.True(t, resp3.Capabilities.DocumentFormattingProvider)
325+
require.False(t, tester3.server.requestsDiagnostics)
316326
}
317327

318328
func TestLogJSONPtr(t *testing.T) {

internal/lsp/lspdefs.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,32 @@ type InitializeParams struct {
5353
}
5454

5555
type ClientCapabilities struct {
56-
Diagnostics DiagnosticWorkspaceClientCapabilities `json:"diagnostics"`
56+
TextDocument *TextDocumentClientCapabilities `json:"textDocument,omitempty"`
57+
Workspace *WorkspaceClientCapabilities `json:"workspace,omitempty"`
58+
}
59+
60+
type TextDocumentClientCapabilities struct {
61+
Diagnostic *DiagnosticClientCapabilities `json:"diagnostic,omitempty"`
62+
}
63+
64+
type DiagnosticClientCapabilities struct {
65+
DynamicRegistration bool `json:"dynamicRegistration,omitempty"`
66+
}
67+
68+
type WorkspaceClientCapabilities struct {
69+
Diagnostics *DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"`
5770
}
5871

5972
type DiagnosticWorkspaceClientCapabilities struct {
60-
// RefreshSupport indicates whether the client supports the new
61-
// `textDocument/diagnostic` request.
6273
RefreshSupport bool `json:"refreshSupport,omitempty"`
6374
}
6475

76+
// SupportsPullDiagnostics returns true if the client indicated support for
77+
// pull-based diagnostics by including the textDocument.diagnostic capability.
78+
func (c ClientCapabilities) SupportsPullDiagnostics() bool {
79+
return c.TextDocument != nil && c.TextDocument.Diagnostic != nil
80+
}
81+
6582
type Hover struct {
6683
Contents MarkupContent `json:"contents"`
6784
Range *baselsp.Range `json:"range,omitempty"`

internal/lsp/testutil.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ type lspTester struct {
4848
func (lt *lspTester) initialize() {
4949
resp, serverState := sendAndReceive[lsp.InitializeResult](lt, "initialize", InitializeParams{
5050
Capabilities: ClientCapabilities{
51-
Diagnostics: DiagnosticWorkspaceClientCapabilities{
52-
RefreshSupport: true,
51+
TextDocument: &TextDocumentClientCapabilities{
52+
Diagnostic: &DiagnosticClientCapabilities{
53+
DynamicRegistration: true,
54+
},
55+
},
56+
Workspace: &WorkspaceClientCapabilities{
57+
Diagnostics: &DiagnosticWorkspaceClientCapabilities{
58+
RefreshSupport: true,
59+
},
5360
},
5461
},
5562
})

0 commit comments

Comments
 (0)