Skip to content

Commit 9900522

Browse files
authored
lsp: Wait for client to send initialized at boot (#1909)
Now the server will wait for the client to send the initialized (empty) message before starting work. Previously we got away with sending diagnostics but now we have a custom test location handler, we need to ensure that we wait for that to be registered before sending test location notifications. So we have moved the load workspace contents to the post initialized state, and added a gate to block the test locations worker from running if the client does not support it. Signed-off-by: Charlie Egan <charlie_egan@apple.com>
1 parent 5ff449b commit 9900522

6 files changed

Lines changed: 89 additions & 41 deletions

File tree

cmd/languageserver.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ func init() {
4242
verbose = true
4343
}
4444

45+
logLevel := log.LevelMessage
46+
if verbose {
47+
logLevel = log.LevelDebug
48+
}
49+
4550
opts := &lsp.LanguageServerOptions{
46-
Logger: log.NewLogger(log.LevelMessage, os.Stderr),
51+
Logger: log.NewLogger(logLevel, os.Stderr),
4752
FeatureFlags: lsp.DefaultServerFeatureFlags(),
4853
}
54+
4955
ls := lsp.NewLanguageServer(ctx, opts)
5056

5157
conf := connection.LoggingConfig{Logger: opts.Logger, LogInbound: verbose, LogOutbound: verbose}

internal/lsp/server.go

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ type LanguageServer struct {
146146
regoRouter *rego.RegoRouter
147147
testingCompiler *ast.Compiler
148148

149+
// initializationGate blocks workers until the initialized notification is received
150+
initializationGate chan struct{}
151+
149152
commandRequest chan types.ExecuteCommandParams
150153
lintWorkspaceJobs chan lintWorkspaceJob
151154
lintFileJobs chan lintFileJob
@@ -213,6 +216,7 @@ func NewLanguageServerMinimal(ctx context.Context, opts *LanguageServerOptions,
213216
regoStore: store,
214217
log: opts.Logger,
215218
featureFlags: *featureFlags,
219+
initializationGate: make(chan struct{}),
216220
lintFileJobs: make(chan lintFileJob, 10),
217221
lintWorkspaceJobs: make(chan lintWorkspaceJob, 10),
218222
builtinsPositionJobs: make(chan lintFileJob, 10),
@@ -260,13 +264,13 @@ func (l *LanguageServer) Handle(ctx context.Context, _ *jsonrpc2.Conn, req *json
260264
case "initialize":
261265
return handler.WithContextAndParams(ctx, req, l.handleInitialize)
262266
case "initialized":
263-
return l.handleInitialized()
267+
return l.handleInitialized(ctx)
264268
case "textDocument/definition":
265269
return handler.WithParams(req, l.handleTextDocumentDefinition)
266270
case "textDocument/diagnostic":
267271
return l.handleTextDocumentDiagnostic()
268272
case "textDocument/didOpen":
269-
return handler.WithContextAndParams(ctx, req, l.handleTextDocumentDidOpen)
273+
return handler.WithParams(req, l.handleTextDocumentDidOpen)
270274
case "textDocument/didClose":
271275
return handler.WithParams(req, l.handleTextDocumentDidClose)
272276
case "textDocument/didSave":
@@ -365,13 +369,15 @@ func (l *LanguageServer) StartDiagnosticsWorker(ctx context.Context) {
365369
continue
366370
}
367371

368-
// Send test locations update after parse completes
369-
if parseSuccess {
370-
l.testLocationJobs <- lintFileJob{Reason: job.Reason, URI: job.URI}
371-
} else {
372-
// Parse failed, send empty test locations
373-
if err := l.sendTestLocations(ctx, job.URI, []any{}); err != nil {
374-
l.log.Message("failed to send empty test locations after parse failure: %s", err)
372+
// Send test locations update after parse completes (if client supports it)
373+
if l.client.SupportsOPATestProvider() {
374+
if parseSuccess {
375+
l.testLocationJobs <- lintFileJob{Reason: job.Reason, URI: job.URI}
376+
} else {
377+
// Parse failed, send empty test locations
378+
if err := l.sendTestLocations(ctx, job.URI, []any{}); err != nil {
379+
l.log.Message("failed to send empty test locations after parse failure: %s", err)
380+
}
375381
}
376382
}
377383

@@ -1516,12 +1522,11 @@ func (l *LanguageServer) handleTextDocumentDefinition(params types.DefinitionPar
15161522
}
15171523

15181524
func (l *LanguageServer) handleTextDocumentDidOpen(
1519-
ctx context.Context,
15201525
params types.DidOpenTextDocumentParams,
15211526
) (any, error) {
15221527
// then we have started the server, and not yet received a suitable root to use.
15231528
if l.workspaceRootURI == "" {
1524-
err := l.updateRootURI(ctx,
1529+
err := l.updateRootURI(
15251530
// get the URI of the file's immediate parent
15261531
l.fromPath(filepath.Dir(uri.ToPath(params.TextDocument.URI))),
15271532
)
@@ -2034,7 +2039,7 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init
20342039
}
20352040

20362041
if params.RootURI != "" {
2037-
err := l.updateRootURI(ctx, params.RootURI)
2042+
err := l.updateRootURI(params.RootURI)
20382043
if err != nil {
20392044
l.log.Message("failed to set rootURI: %w", err)
20402045
}
@@ -2044,7 +2049,7 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init
20442049
l.log.Message("cannot operate with more than one workspace folder, using: %s", (*params.WorkspaceFolders)[0].URI)
20452050
}
20462051

2047-
err := l.updateRootURI(ctx, (*params.WorkspaceFolders)[0].URI)
2052+
err := l.updateRootURI((*params.WorkspaceFolders)[0].URI)
20482053
if err != nil {
20492054
l.log.Message("failed to set rootURI to workspace folder: %w", err)
20502055
}
@@ -2053,7 +2058,7 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init
20532058
return initializeResult, nil
20542059
}
20552060

2056-
func (l *LanguageServer) updateRootURI(ctx context.Context, rootURI string) error {
2061+
func (l *LanguageServer) updateRootURI(rootURI string) error {
20572062
// rootURI not expected to have a trailing slash, remove if present for
20582063
// consistency
20592064
normalizedRootURI := strings.TrimSuffix(rootURI, string(os.PathSeparator))
@@ -2105,19 +2110,6 @@ func (l *LanguageServer) updateRootURI(ctx context.Context, rootURI string) erro
21052110
l.log.Message("no config file found for workspace")
21062111
}
21072112

2108-
_, failed, err := l.loadWorkspaceContents(ctx, false)
2109-
for _, f := range failed {
2110-
l.log.Message("failed to load file %s: %s", f.URI, f.Error)
2111-
}
2112-
2113-
if err != nil {
2114-
l.log.Message("failed to load workspace contents: %s", err)
2115-
}
2116-
2117-
// 'OverwriteAggregates' is set to populate the cache's initial aggregate state.
2118-
// Subsequent runs of lintWorkspaceJobs will not set this and use the cached state.
2119-
l.lintWorkspaceJobs <- lintWorkspaceJob{Reason: "server initialize", OverwriteAggregates: true}
2120-
21212113
return nil
21222114
}
21232115

@@ -2161,13 +2153,15 @@ func (l *LanguageServer) loadWorkspaceContents(ctx context.Context, newOnly bool
21612153
return nil // continue processing other files
21622154
}
21632155

2164-
if parseSuccess {
2165-
l.testLocationJobs <- lintFileJob{Reason: "server initialized", URI: fileURI}
2166-
} else {
2167-
// this is covering the case where the client starts and the state
2168-
// is different (the client remembers test locations and state).
2169-
if err := l.sendTestLocations(ctx, fileURI, []any{}); err != nil {
2170-
l.log.Message("failed to send empty test locations after parse failure: %s", err)
2156+
if l.client.SupportsOPATestProvider() {
2157+
if parseSuccess {
2158+
l.testLocationJobs <- lintFileJob{Reason: "server initialized", URI: fileURI}
2159+
} else {
2160+
// this is covering the case where the client starts and the state
2161+
// is different (the client remembers test locations and state).
2162+
if err := l.sendTestLocations(ctx, fileURI, []any{}); err != nil {
2163+
l.log.Message("failed to send empty test locations after parse failure: %s", err)
2164+
}
21712165
}
21722166
}
21732167

@@ -2187,12 +2181,33 @@ func (l *LanguageServer) loadWorkspaceContents(ctx context.Context, newOnly bool
21872181
return changedOrNewURIs, failed, nil
21882182
}
21892183

2190-
func (l *LanguageServer) handleInitialized() (any, error) {
2191-
// if running without config, then we should send the diagnostic request now
2192-
// otherwise it'll happen when the config is loaded
2193-
if !l.configWatcher.IsWatching() {
2194-
l.lintWorkspaceJobs <- lintWorkspaceJob{Reason: "server initialized"}
2195-
}
2184+
func (l *LanguageServer) handleInitialized(ctx context.Context) (any, error) {
2185+
// Signal workers that initialization handshake is complete
2186+
close(l.initializationGate)
2187+
2188+
// Load workspace contents and start jobs asynchronously
2189+
// This allows us to respond to the client immediately while workspace
2190+
// loading happens in the background
2191+
go func() {
2192+
_, failed, err := l.loadWorkspaceContents(ctx, false)
2193+
for _, f := range failed {
2194+
l.log.Message("failed to load file %s: %s", f.URI, f.Error)
2195+
}
2196+
2197+
if err != nil {
2198+
l.log.Message("failed to load workspace contents: %s", err)
2199+
}
2200+
2201+
// 'OverwriteAggregates' is set to populate the cache's initial aggregate state.
2202+
// Subsequent runs of lintWorkspaceJobs will not set this and use the cached state.
2203+
l.lintWorkspaceJobs <- lintWorkspaceJob{Reason: "server initialize", OverwriteAggregates: true}
2204+
2205+
// if running without config, then we should send the diagnostic request now
2206+
// otherwise it'll happen when the config is loaded
2207+
if !l.configWatcher.IsWatching() {
2208+
l.lintWorkspaceJobs <- lintWorkspaceJob{Reason: "server initialized"}
2209+
}
2210+
}()
21962211

21972212
return emptyStruct, nil
21982213
}

internal/lsp/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func createAndInitServer(t *testing.T, ctx context.Context, tempDir string, clie
9898
EnableDebugCodelens: new(true),
9999
EnableExplorer: new(true),
100100
EvalCodelensDisplayInline: new(true),
101+
EnableServerTesting: new(true),
101102
},
102103
}
103104

internal/lsp/testing.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ import (
1111
)
1212

1313
func (l *LanguageServer) StartTestLocationsWorker(ctx context.Context) {
14+
// Wait for initialization to complete before starting to check worker is needed
15+
<-l.initializationGate
16+
17+
if !l.client.SupportsOPATestProvider() {
18+
l.log.Debug("Test locations worker exiting - client does not support opaTestProvider")
19+
20+
return
21+
}
22+
23+
l.log.Debug("Test locations worker starting")
24+
1425
for {
1526
select {
1627
case <-ctx.Done():
@@ -27,6 +38,12 @@ func (l *LanguageServer) StartTestLocationsWorker(ctx context.Context) {
2738
// This is called after the parse has completed in the lintFileJobs worker to
2839
// ensure we send the latest.
2940
func (l *LanguageServer) processTestLocationsUpdate(ctx context.Context, fileURI string) error {
41+
if !l.client.SupportsOPATestProvider() {
42+
l.log.Message("processTestLocationsUpdate called but client does not support opaTestProvider")
43+
44+
return nil
45+
}
46+
3047
if l.ignoreURI(fileURI) {
3148
return nil
3249
}

internal/lsp/types/internal.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ func (c Client) SupportsEvalCodelensDisplayInline() bool {
8585
*c.InitOptions.EvalCodelensDisplayInline
8686
}
8787

88+
func (c Client) SupportsOPATestProvider() bool {
89+
return c.InitOptions != nil &&
90+
c.InitOptions.EnableServerTesting != nil &&
91+
*c.InitOptions.EnableServerTesting
92+
}
93+
8894
// ServerContext is a type which is used to contain things from the server's
8995
// state that is needed in RegalContext.
9096
type ServerContext struct {

internal/lsp/types/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type (
3434
// EnableExplorer, if set, will enable the regal.explorer command
3535
// and related functionality.
3636
EnableExplorer *bool `json:"enableExplorer,omitempty"`
37+
// EnableServerTesting, if set, will enable test location notifications
38+
// via the regal/testLocations and test running handler.
39+
EnableServerTesting *bool `json:"enableServerTesting,omitempty"`
3740
}
3841

3942
InitializeParams struct {

0 commit comments

Comments
 (0)