@@ -125,19 +125,38 @@ func (c *Client) Initialize(ctx context.Context, workspaceDir string) (*protocol
125 return result, nil
125 return result, nil
128+// closeTimeout is the maximum time to wait for a graceful LSP shutdown.
129+const closeTimeout = 5 * time.Second
128// Kill kills the client without doing anything else.
131// Kill kills the client without doing anything else.
129func (c *Client) Kill() { c.client.Kill() }
132func (c *Client) Kill() { c.client.Kill() }
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().
132func (c *Client) Close(ctx context.Context) error {
136func (c *Client) Close(ctx context.Context) error {
133 c.CloseAllFiles(ctx)
137 c.CloseAllFiles(ctx)
135− // Shutdown and exit the client
139+ // Use a timeout to prevent hanging on unresponsive LSP servers.
136− if err := c.client.Shutdown(ctx); err != nil {
140+ // jsonrpc2's send lock doesn't respect context cancellation, so we
137− slog.Warn("Failed to shutdown LSP client", "error", err)
141+ // need to fall back to Kill() which closes the underlying connection.
138− }
142+ closeCtx, cancel := context.WithTimeout(ctx, closeTimeout)
145+ done := make(chan error, 1)
147+ if err := c.client.Shutdown(closeCtx); err != nil {
148+ slog.Warn("Failed to shutdown LSP client", "error", err)
140− return c.client.Exit()
150+ done <- c.client.Exit()
156+ case <-closeCtx.Done():
158+ return closeCtx.Err()
143// createPowernapClient creates a new powernap client with the current configuration.
162// createPowernapClient creates a new powernap client with the current configuration.