diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 653acf60..4cbff306 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -44,6 +44,7 @@ | `:show-directory`, `:pwd` | Show the current working directory. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:reload` | Discard changes and reload from the source file. | +| `:lsp-restart` | Restarts the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8d43410a..cb234357 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}, @@ -335,6 +336,33 @@ 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 Ok(None), + }; + + let scope = language_config.scope.clone(); + + match self.inner.entry(scope) { + Entry::Vacant(_) => Ok(None), + Entry::Occupied(mut entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + + let NewClientResult(client, incoming) = start_client(id, language_config, config)?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + + entry.insert((id, client.clone())); + + Ok(Some(client)) + } + } + } + pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -346,43 +374,9 @@ impl Registry { 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(Some(client)) @@ -473,6 +467,56 @@ impl LspProgressMap { } } +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. +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(NewClientResult(client, incoming)) +} + #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f49fff30..6d0ced65 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -985,6 +985,40 @@ 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() + .context("LSP not defined for the current document")?; + + let scope = config.scope.clone(); + cx.editor.language_servers.restart(config)?; + + // This collect is needed because refresh_language_server would need to re-borrow editor. + let document_ids_to_refresh: Vec = cx + .editor + .documents() + .filter_map(|doc| match doc.language_config() { + Some(config) if config.scope.eq(&scope) => Some(doc.id()), + _ => None, + }) + .collect(); + + for document_id in document_ids_to_refresh { + cx.editor.refresh_language_server(document_id); + } + + Ok(()) +} + fn tree_sitter_scopes( cx: &mut compositor::Context, _args: &[Cow], @@ -1837,6 +1871,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reload, completer: None, }, + TypableCommand { + name: "lsp-restart", + aliases: &[], + doc: "Restarts the Language Server that is in use by the current doc", + fun: lsp_restart, + completer: None, + }, TypableCommand { name: "tree-sitter-scopes", aliases: &[],