diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index cc1c4ce8f..7c6bf3729 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -17,7 +17,7 @@ use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; use std::sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU64, AtomicU8, Ordering}, Arc, }; use std::{collections::HashMap, path::PathBuf}; @@ -59,6 +59,7 @@ pub struct Client { initialize_notify: Arc, /// workspace folders added while the server is still initializing req_timeout: u64, + restarts_left: AtomicU8, } impl Client { @@ -231,6 +232,7 @@ impl Client { root_uri, workspace_folders: Mutex::new(workspace_folders), initialize_notify: initialize_notify.clone(), + restarts_left: AtomicU8::new(2), }; Ok((client, server_rx, initialize_notify)) @@ -244,6 +246,14 @@ impl Client { self.id } + pub fn set_restarts_left(&self, x: u8) { + self.restarts_left.store(x, Ordering::Relaxed); + } + + pub fn restarts_left(&self) -> u8 { + self.restarts_left.load(Ordering::Relaxed) + } + fn next_request_id(&self) -> jsonrpc::Id { let id = self.request_counter.fetch_add(1, Ordering::Relaxed); jsonrpc::Id::Num(id) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 47f38bcf2..b8cd33806 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -673,10 +673,10 @@ impl Registry { self.inner.get(id) } - pub fn remove_by_id(&mut self, id: LanguageServerId) { + pub fn remove_by_id(&mut self, id: LanguageServerId) -> Option> { let Some(client) = self.inner.remove(id) else { - log::debug!("client was already removed"); - return; + log::error!("client was already removed"); + return None; }; self.file_event_handler.remove_client(id); let instances = self @@ -687,22 +687,23 @@ impl Registry { if instances.is_empty() { self.inner_by_name.remove(client.name()); } + Some(client) } - fn start_client( + pub fn start( &mut self, name: String, ls_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result, StartupError> { + ) -> Result>, Error> { let syn_loader = self.syn_loader.load(); let config = syn_loader .language_server_configs() .get(&name) .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; - let id = self.inner.try_insert_with_key(|id| { + match self.inner.try_insert_with_key(|id| { start_client( id, name, @@ -716,8 +717,11 @@ impl Registry { self.incoming.push(UnboundedReceiverStream::new(client.1)); client.0 }) - })?; - Ok(self.inner[id].clone()) + }) { + Ok(id) => Ok(Some(self.inner[id].clone())), + Err(StartupError::NoRequiredRootFound) => Ok(None), + Err(StartupError::Error(err)) => Err(err), + } } /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, @@ -748,16 +752,18 @@ impl Registry { }); } } - let client = match self.start_client( - name.clone(), - language_config, - doc_path, - root_dirs, - enable_snippets, - ) { + let client = match self + .start( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) + .transpose()? + { Ok(client) => client, - Err(StartupError::NoRequiredRootFound) => return None, - Err(StartupError::Error(err)) => return Some(Err(err)), + Err(err) => return Some(Err(err)), }; self.inner_by_name .insert(name.to_owned(), vec![client.clone()]); @@ -808,23 +814,22 @@ impl Registry { return Some((name.to_owned(), Ok(client.clone()))); } } - match self.start_client( - name.clone(), - language_config, - doc_path, - root_dirs, - enable_snippets, - ) { - Ok(client) => { - self.inner_by_name - .entry(name.to_owned()) - .or_default() - .push(client.clone()); - Some((name.clone(), Ok(client))) - } - Err(StartupError::NoRequiredRootFound) => None, - Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))), + let client = self + .start( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) + .transpose()?; + if let Ok(client) = &client { + self.inner_by_name + .entry(name.to_owned()) + .or_default() + .push(client.clone()); } + Some((name.to_owned(), client)) }, ) } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a567815fc..6e603df7b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -25,7 +25,7 @@ use crate::{ compositor::{Compositor, Event}, config::Config, handlers, - job::Jobs, + job::{Callback, Jobs}, keymap::Keymaps, ui::{self, overlay::overlaid}, }; @@ -963,7 +963,47 @@ impl Application { } // Remove the language server from the registry. - self.editor.language_servers.remove_by_id(server_id); + let client = self.editor.language_servers.remove_by_id(server_id); + + if let Some(client) = client { + let name = client.name().to_owned(); + let restarts = client.restarts_left(); + if let Some(restarts) = restarts.checked_sub(1) { + let job = async move { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + Ok(Callback::Editor(Box::new(move |editor: &mut Editor| { + let editor_config = &editor.config(); + let (_, doc) = { + let view = view_mut!(editor); + let id = view.doc; + let doc = doc_mut!(editor, &id); + (view, doc) + }; + let doc_path = doc.path(); + let Some(lang_config) = doc.language_config() else { + log::warn!("at LSP restart config is missing"); + return; + }; + + match editor.language_servers.start( + name, + lang_config, + doc_path, + &editor_config.workspace_lsp_roots, + editor_config.lsp.snippets, + ) { + Ok(Some(client)) => client.set_restarts_left(restarts), + Ok(None) => {} + Err(err) => { + log::warn!("failed to restart LSP: {:?}", err); + } + } + }))) + }; + + self.jobs.callback(job); + } + } } } }