From 6786119903e64e5b54e5829afda367666dd1b956 Mon Sep 17 00:00:00 2001 From: SibtainOcn <224004177+SibtainOcn@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:45:20 +0530 Subject: [PATCH] fix: move GetLanguageService to async closure to prevent dispatch loop blocking Fixes #2857 The formatter gets stuck because GetLanguageService runs synchronously on the dispatch loop thread and blocks on snapshotUpdateMu. When this mutex is held by another operation (e.g. DidOpenFile, idle cache clean timer, or concurrent snapshot updates), the dispatch loop stalls and no responses can be sent. This is most visible with format-on-save, since the save notification triggers snapshot updates that compete for the same lock the formatting handler needs. Moving GetLanguageService into the async closure allows the dispatch loop to remain responsive while the language service is being acquired in a goroutine. --- internal/lsp/server.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 2d59cf427be..cbc490820b7 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -741,12 +741,17 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR if req.Params != nil { params = req.Params.(Req) } - ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) - if err != nil { - return nil, err - } + // GetLanguageService is moved into the async closure to avoid blocking the + // dispatch loop. Previously, acquiring the language service synchronously could + // block on snapshotUpdateMu if another operation (e.g. DidOpenFile, idle cache + // clean, or a concurrent snapshot update) was holding the lock, causing the + // dispatch loop to stall and preventing responses from being sent. See #2857. return func() error { defer s.recover(req) + ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) + if err != nil { + return err + } resp, lsErr := fn(s, ctx, ls, params) if lsErr != nil { return lsErr