diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 79d9609e1c5f7..1300d648f834b 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -9,7 +9,8 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::LanguageConfiguration; +use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; +use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, @@ -320,56 +321,51 @@ impl Registry { .map(|(_, client)| client.as_ref()) } + pub fn restart(&mut self, language_config: &LanguageConfiguration) -> Result> { + let config = match &language_config.language_server { + Some(config) => config, + None => { + return Err(Error::LspNotDefined); + } + }; + + let scope = language_config.scope.clone(); + + match self.inner.entry(scope) { + Entry::Vacant(_) => Err(Error::LspNotDefined), + Entry::Occupied(mut entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + + let (client, incoming) = start_client(id, language_config, config)?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + + entry.insert((id, client.clone())); + + Ok(client) + } + } + } + pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result> { let config = match &language_config.language_server { Some(config) => config, None => return Err(Error::LspNotDefined), }; - match self.inner.entry(language_config.scope.clone()) { + let scope = language_config.scope.clone(); + + match self.inner.entry(scope) { Entry::Occupied(entry) => Ok(entry.get().1.clone()), Entry::Vacant(entry) => { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (client, incoming, initialize_notify) = Client::start( - &config.command, - &config.args, - language_config.config.clone(), - &language_config.roots, - id, - config.timeout, - )?; + + let NewClientResult(client, incoming) = start_client(id, language_config, config)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - // Initialize the client asynchronously - let _client = client.clone(); - tokio::spawn(async move { - use futures_util::TryFutureExt; - let value = _client - .capabilities - .get_or_try_init(|| { - _client - .initialize() - .map_ok(|response| response.capabilities) - }) - .await; - - if let Err(e) = value { - log::error!("failed to initialize language server: {}", e); - return; - } - - // next up, notify - _client - .notify::(lsp::InitializedParams {}) - .await - .unwrap(); - - initialize_notify.notify_one(); - }); entry.insert((id, client.clone())); + Ok(client) } } @@ -380,6 +376,55 @@ impl Registry { } } +struct NewClientResult(Arc, UnboundedReceiver<(usize, Call)>); + +/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that +/// it is only called when it makes sense, specifically, start_client shouldn't care whether. +fn start_client( + id: usize, + config: &LanguageConfiguration, + ls_config: &LanguageServerConfiguration, +) -> Result { + let (client, incoming, initialize_notify) = Client::start( + &ls_config.command, + &ls_config.args, + config.config.clone(), + &config.roots, + id, + ls_config.timeout, + )?; + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + if let Err(e) = value { + log::error!("failed to initialize language server: {}", e); + return; + } + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + Ok((client, incoming)) +} + #[derive(Debug)] pub enum ProgressStatus { Created, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ad4e7f4ce9da6..81ca3b9ec5e5e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use super::*; +use helix_core::surround::Error; use helix_view::editor::{Action, ConfigEvent}; use ui::completers::{self, Completer}; @@ -982,6 +983,39 @@ fn reload( }) } +fn lsp_restart( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let (_view, doc) = current!(cx.editor); + let config = doc + .language_config() + .ok_or(anyhow!("LSP not defined for current document"))?; + + let scope = config.scope.clone(); + cx.editor.language_servers.restart(config)?; + + let to_refresh: Vec = cx + .editor + .documents_mut() + .filter_map(|doc| match doc.language_config() { + Some(config) if config.scope.eq(&scope) => Some(doc.id()), + _ => None, + }) + .collect(); + + to_refresh.into_iter().for_each(|id| { + cx.editor.refresh_language_server(id); + }); + + Ok(()) +} + fn tree_sitter_scopes( cx: &mut compositor::Context, _args: &[Cow], @@ -1827,6 +1861,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reload, completer: None, }, + TypableCommand { + name: "lsp-reload", + aliases: &[], + doc: "Reload the LSP the is in use by the current doc", + fun: lsp_restart, + completer: None, + }, TypableCommand { name: "tree-sitter-scopes", aliases: &[],