@@ -46,16 +46,17 @@ import (
4646)
4747
4848const (
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.
5656type 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.
136137func (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