Skip to content

Commit d891bd2

Browse files
authored
LSP ensure check diagnostics are always published (#3709)
1 parent f69f068 commit d891bd2

File tree

1 file changed

+62
-26
lines changed

1 file changed

+62
-26
lines changed

private/buf/buflsp/file.go

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,17 @@ import (
4646
)
4747

4848
const (
49-
descriptorPath = "google/protobuf/descriptor.proto"
50-
refreshCheckStagger = 5 * time.Millisecond
49+
descriptorPath = "google/protobuf/descriptor.proto"
50+
checkRefreshPeriod = 3 * time.Second
5151
)
5252

5353
// file is a file that has been opened by the client.
5454
//
5555
// Mutating a file is thread-safe.
5656
type file struct {
57-
lsp *lsp
58-
uri protocol.URI
57+
lsp *lsp
58+
uri protocol.URI
59+
checkWork chan<- struct{}
5960

6061
text string
6162
// Version is an opaque version identifier given to us by the LSP client. This
@@ -135,6 +136,10 @@ func (f *file) Reset(ctx context.Context) {
135136
// for this file.
136137
func (f *file) Close(ctx context.Context) {
137138
f.lsp.fileManager.Close(ctx, f.uri)
139+
if f.checkWork != nil {
140+
close(f.checkWork)
141+
f.checkWork = nil
142+
}
138143
}
139144

140145
// IsOpenInEditor returns whether this file was opened in the LSP client's
@@ -291,28 +296,7 @@ func (f *file) Refresh(ctx context.Context) {
291296
f.FindModule(ctx)
292297

293298
progress.Report(ctx, "Running Checks", 4.0/6)
294-
// Since checks are a more expensive operation, we do not want to run a check on every
295-
// Refresh call. Instead, we can stagger the checks and only run them periodically by
296-
// spinning them off into a go routine. Then we attempt to lock using the top-level LSP
297-
// lock. It is safe to use because if another LSP call is made, we allow checks to finish
298-
// before resolving a subsequent LSP request.
299-
go func() {
300-
// We stagger the check operation by 5ms and run it for the latest Refresh state.
301-
time.Sleep(refreshCheckStagger)
302-
// Call TryLock, if unnsuccessful, then another thread holds the lock, so we provide a
303-
// debug log and move on.
304-
if !f.lsp.lock.TryLock() {
305-
f.lsp.logger.Debug(
306-
fmt.Sprintf("another thread holds the LSP lock, no new checks started for %v", f.uri),
307-
)
308-
return
309-
}
310-
// We have successfully obtained the lock, we can now run the checks.
311-
defer f.lsp.lock.Unlock()
312-
f.BuildImages(ctx)
313-
f.RunLints(ctx)
314-
f.RunBreaking(ctx)
315-
}()
299+
f.RunChecks(ctx)
316300

317301
progress.Report(ctx, "Indexing Symbols", 5.0/6)
318302
f.IndexSymbols(ctx)
@@ -325,6 +309,58 @@ func (f *file) Refresh(ctx context.Context) {
325309
f.PublishDiagnostics(ctx)
326310
}
327311

312+
// RunChecks initiates background checks (lint and breaking) on this file and
313+
// returns immediately.
314+
//
315+
// Checks are executed in a background goroutine to avoid blocking the LSP
316+
// call. Each call to RunChecks invalidates any ongoing checks, triggering a
317+
// fresh run. However, previous checks are not interrupted. The checks acquire
318+
// the LSP mutex. Subsequent LSP calls will wait for the current check to
319+
// complete before proceeding.
320+
//
321+
// Checks are debounce (with the delay defined by checkRefreshPeriod) to avoid
322+
// overwhelming the client with expensive checks. If the file is not open in the
323+
// editor, checks are skipped. Diagnostics are published after checks are run.
324+
func (f *file) RunChecks(ctx context.Context) {
325+
// If we have not yet started a goroutine to run checks, start one.
326+
// This goroutine will run checks in the background and publish diagnostics.
327+
// We debounce checks to avoid spamming the client.
328+
if f.checkWork == nil {
329+
// We use a buffered channel of length one as the check invalidation mechanism.
330+
work := make(chan struct{}, 1)
331+
f.checkWork = work
332+
runChecks := func(ctx context.Context) {
333+
f.lsp.lock.Lock()
334+
defer f.lsp.lock.Unlock()
335+
if !f.IsOpenInEditor() {
336+
// Skip checks if the file is not open in the editor.
337+
return
338+
}
339+
f.lsp.logger.Info(fmt.Sprintf("running checks for %v, %v", f.uri, f.version))
340+
f.BuildImages(ctx)
341+
f.RunLints(ctx)
342+
f.RunBreaking(ctx)
343+
f.PublishDiagnostics(ctx) // Publish the latest diagnostics.
344+
}
345+
// Start a goroutine to process checks.
346+
go func() {
347+
// Detach from the parent RPC context.
348+
ctx := context.WithoutCancel(ctx)
349+
for range work {
350+
runChecks(ctx)
351+
// Debounce checks to prevent thrashing expensive checks.
352+
time.Sleep(checkRefreshPeriod)
353+
}
354+
}()
355+
}
356+
// Signal the goroutine to invalidate and rerun checks.
357+
select {
358+
case f.checkWork <- struct{}{}:
359+
default:
360+
// Channel is full, checks are already invalidated and will be rerun.
361+
}
362+
}
363+
328364
// RefreshAST reparses the file and generates diagnostics if necessary.
329365
//
330366
// Returns whether a reparse was necessary.

0 commit comments

Comments
 (0)