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: &[],