@@ -125,19 +125,38 @@ func (c *Client) Initialize(ctx context.Context, workspaceDir string) (*protocol
125125 return result, nil
126126}
127127
128+// closeTimeout is the maximum time to wait for a graceful LSP shutdown.
129+const closeTimeout = 5 * time.Second
130+
128131// Kill kills the client without doing anything else.
129132func (c *Client) Kill() { c.client.Kill() }
130133
131−// Close closes all open files in the client, then the client.
134+// Close closes all open files in the client, then shuts down gracefully.
135+// If shutdown takes longer than closeTimeout, it falls back to Kill().
132136func (c *Client) Close(ctx context.Context) error {
133137 c.CloseAllFiles(ctx)
134138
135− // Shutdown and exit the client
136− if err := c.client.Shutdown(ctx); err != nil {
137− slog.Warn("Failed to shutdown LSP client", "error", err)
138− }
139+ // Use a timeout to prevent hanging on unresponsive LSP servers.
140+ // jsonrpc2's send lock doesn't respect context cancellation, so we
141+ // need to fall back to Kill() which closes the underlying connection.
142+ closeCtx, cancel := context.WithTimeout(ctx, closeTimeout)
143+ defer cancel()
139144
140− return c.client.Exit()
145+ done := make(chan error, 1)
146+ go func() {
147+ if err := c.client.Shutdown(closeCtx); err != nil {
148+ slog.Warn("Failed to shutdown LSP client", "error", err)
149+ }
150+ done <- c.client.Exit()
151+ }()
152+
153+ select {
154+ case err := <-done:
155+ return err
156+ case <-closeCtx.Done():
157+ c.client.Kill()
158+ return closeCtx.Err()
159+ }
141160}
142161
143162// createPowernapClient creates a new powernap client with the current configuration.