From 385ccdfc9c4f04cbe33c7cfdd668a1f694b6d870 Mon Sep 17 00:00:00 2001
From: Filipe Azevedo <filipe@azevedo.io>
Date: Tue, 20 Sep 2022 08:44:36 +0100
Subject: [PATCH] add :lsp-restart command (#3435)

---
 book/src/generated/typable-cmd.md |   1 +
 helix-lsp/src/lib.rs              | 118 ++++++++++++++++++++----------
 helix-term/src/commands/typed.rs  |  41 +++++++++++
 3 files changed, 123 insertions(+), 37 deletions(-)

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<Option<Arc<Client>>> {
+        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<Option<Arc<Client>>> {
         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<initialized>
-                    _client
-                        .notify::<lsp::notification::Initialized>(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<Client>, 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<NewClientResult> {
+    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<initialized>
+        _client
+            .notify::<lsp::notification::Initialized>(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<str>],
+    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<DocumentId> = 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<str>],
@@ -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: &[],