mirror of
https://github.com/helix-editor/helix
synced 2026-07-21 21:24:55 +02:00
ad6a1b1bdf
* fix(lsp): break shutdown deadlock and use fire-and-forget exit Two related problems caused `Error: Timed out waiting for language servers to shutdown`, most visibly with gopls on large Go monorepos. **Problem 1 — deadlock** When helix sends `shutdown`, some servers (gopls, vscode-languageserver- node family) send `client/registerCapability` and `workspace/configuration` requests *back* to helix before responding. The application event loop is blocked at `close_language_servers().await` and cannot consume these requests. The server waits for helix to respond; helix waits for the server's shutdown response. Classic deadlock broken only by the 3-second timeout. Fix: add `shutdown_requested: AtomicBool` and an `inject_tx` channel to `Transport`. When the flag is set and a server-to-client `MethodCall` arrives, the `recv` task immediately sends a null-success response via `inject_tx` rather than forwarding to the dead application queue. The flag is set *before* writing shutdown to stdin to close the race window. Null success is used rather than an error because `application.rs` already documents that an error response to `registerCapability` causes vscode-languageserver-node servers to abort rather than complete the shutdown handshake. **Problem 2 — blocking design** Even with the deadlock broken, `force_shutdown` blocked on `shutdown().await` (up to 20 s per-request timeout) while the outer `join_all` had only a 3 s budget. On large monorepos gopls flushes ~1 300 `window/logMessage` notifications before responding, pushing the total past 3 s. The deeper issue is that blocking the application task during shutdown prevents it from handling any server-to-client traffic, making the system fragile against any slow or chatty server. Fix: make `force_shutdown` a plain (non-async) fire-and-forget function that queues `shutdown` + `exit` in the send channel and returns immediately. The response channel receiver is dropped on purpose; any late response is discarded at debug level. `close_language_servers` is simplified to a synchronous loop + a 50 ms sleep, which gives the send tasks one scheduling quantum to flush the queued bytes before the runtime drops. Servers that do not receive the bytes before process exit will see stdin EOF and exit cleanly. This matches the approach already used for uninitialized servers since #7449, and mirrors Neovim's default `exit_timeout = false` behaviour. Fixes #14724 Fixes #14679 See also #5117, #5174, #4551, #11542 * fix linter issues * transfer child to background thread on shutdown * lsp: Immediately terminate on quit, wait until servers have been notified * clippy lint --------- Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>