Skip to content

Commit 8be2d1d

Browse files
Handle language server termination (#4797)
This change handles a language server exiting. This was a UX sore-spot: if a language server crashed, Helix did not recognize the exit and continued to send requests to it. All requests would timeout since they would not receive responses. This would also hold-up Helix closing itself down since it would try to gracefully shutdown the server which is implemented in the LSP spec as a request. We could attempt to automatically restart the language server on crash. I left this for future work since that change will need to be slightly complicated: it will need to cover the case of a language server repeatedly crashing.
1 parent 598bd8b commit 8be2d1d

File tree

3 files changed

+53
-0
lines changed

3 files changed

+53
-0
lines changed

helix-lsp/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ impl MethodCall {
282282
pub enum Notification {
283283
// we inject this notification to signal the LSP is ready
284284
Initialized,
285+
// and this notification to signal that the LSP exited
286+
Exit,
285287
PublishDiagnostics(lsp::PublishDiagnosticsParams),
286288
ShowMessage(lsp::ShowMessageParams),
287289
LogMessage(lsp::LogMessageParams),
@@ -294,6 +296,7 @@ impl Notification {
294296

295297
let notification = match method {
296298
lsp::notification::Initialized::METHOD => Self::Initialized,
299+
lsp::notification::Exit::METHOD => Self::Exit,
297300
lsp::notification::PublishDiagnostics::METHOD => {
298301
let params: lsp::PublishDiagnosticsParams = params.parse()?;
299302
Self::PublishDiagnostics(params)
@@ -350,6 +353,10 @@ impl Registry {
350353
.map(|(_, client)| client.as_ref())
351354
}
352355

356+
pub fn remove_by_id(&mut self, id: usize) {
357+
self.inner.retain(|_, (client_id, _)| client_id != &id)
358+
}
359+
353360
pub fn restart(
354361
&mut self,
355362
language_config: &LanguageConfiguration,

helix-lsp/src/transport.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,26 @@ impl Transport {
250250
}
251251
};
252252
}
253+
Err(Error::StreamClosed) => {
254+
// Hack: inject a terminated notification so we trigger code that needs to happen after exit
255+
use lsp_types::notification::Notification as _;
256+
let notification =
257+
ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification {
258+
jsonrpc: None,
259+
method: lsp_types::notification::Exit::METHOD.to_string(),
260+
params: jsonrpc::Params::None,
261+
}));
262+
match transport
263+
.process_server_message(&client_tx, notification)
264+
.await
265+
{
266+
Ok(_) => {}
267+
Err(err) => {
268+
error!("err: <- {:?}", err);
269+
}
270+
}
271+
break;
272+
}
253273
Err(err) => {
254274
error!("err: <- {:?}", err);
255275
break;

helix-term/src/application.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,32 @@ impl Application {
871871
Notification::ProgressMessage(_params) => {
872872
// do nothing
873873
}
874+
Notification::Exit => {
875+
self.editor.set_status("Language server exited");
876+
877+
// Clear any diagnostics for documents with this server open.
878+
let urls: Vec<_> = self
879+
.editor
880+
.documents_mut()
881+
.filter_map(|doc| {
882+
if doc.language_server().map(|server| server.id())
883+
== Some(server_id)
884+
{
885+
doc.set_diagnostics(Vec::new());
886+
doc.url()
887+
} else {
888+
None
889+
}
890+
})
891+
.collect();
892+
893+
for url in urls {
894+
self.editor.diagnostics.remove(&url);
895+
}
896+
897+
// Remove the language server from the registry.
898+
self.editor.language_servers.remove_by_id(server_id);
899+
}
874900
}
875901
}
876902
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {

0 commit comments

Comments
 (0)