Repository: autozimu/LanguageClient-neovim (language: rust)

Issue:
Feature Request: new autocmd event `LanguageClientInitialized` for registering handlers before anything happens?
## Is your feature request related to a problem? Please describe.

In our language server implementation, we have some customize events that I'd like to register handlers for since it would crash if the language client does not handle these events at all. The tricky part is that it sends some of these custom events at the very beginning of a session.

My understanding is that we can only register handler after the language server is started, so I came up with the following:

```vim
augroup LanguageClient_config
  autocmd User LanguageClientStarted call RegisterEventHandlers()
augroup END
```

However, this doesn't really work since by the time languageClient-neovim triggers the LanguageClientStarted event, the server already sent some of the custom events and crashed since they are not handled.

## Describe the solution you'd like

To solve this issue, I tried to add a new event `LagnuageClientInitialized` right after the server is started and this does register the custom event handlers in time:

```diff
diff --git a/src/language_server_protocol.rs b/src/language_server_protocol.rs
index b65868e..d2ac969 100644
--- a/src/language_server_protocol.rs
+++ b/src/language_server_protocol.rs
@@ -3885,6 +3885,10 @@ impl LanguageClient {
         self.sync_settings()?;
         info!("settings synced");

+        self.vim()?
+            .rpcclient
+            .notify("s:ExecuteAutocmd", "LanguageClientInitialized")?;
+
         let command = self
             .get(|state| state.server_commands.get(&language_id).cloned())?
             .ok_or_else(|| {
```

I wonder if this is acceptable to be merged to this repository. Thanks!



Discussion / hints:
I think something like that could be merged yeah, although I would change the name of the autocmd, as that could easily be mistaken for `LSP` initialized, which happens after that.

Out of curiosity, are you using `LanguageClient#registerHandlers()` inside that `RegisterEventHandlers` function?

I think a better strategy would be to include a dictionary of handlers in the server config, so that different handlers can be configured for each server independently, and also so that the handlers are registered before the server starts. This is obviously a bigger change, but now that we allow for more detailed server config it should be rather straight forward.
> I think something like that could be merged yeah, although I would change the name of the autocmd, as that could easily be mistaken for LSP initialized, which happens after that.

Great! Maybe something like `LanguageClientCreated` is a better name?

> Out of curiosity, are you using `LanguageClient#registerHandlers()` inside that `RegisterEventHandlers` function?

Yep.

>  ... so that different handlers can be configured for each server independently, and also so that the handlers are registered before the server starts.

Agree. This should be a better solution. nvim_lsp does allow you to configure a custom handler in configuration directly without registering after the server is started. Having something like that in LanguageClient-neovim would be quite nice. Is there an issue tracking this? 

There's no issue tracking this, nope. If you are happy to create one I can try to open a PR for this in the next few days. Or if anyone else wants to tackle this one that would also be great!

Fix patch:
diff --git a/.vim/settings.json b/.vim/settings.json
index af92717b1..f931be30b 100644
--- a/.vim/settings.json
+++ b/.vim/settings.json
@@ -4,5 +4,16 @@
         "all_targets": false,
         "build_on_save": true,
         "wait_to_build": 0
+    },
+    "rust-analyzer": {
+      "diagnostics": {
+        "disabled": ["macro-error"]
+      },
+      "procMacro": {
+        "enable": true
+      },
+      "cargo": {
+        "loadOutDirsFromCheck": true
+      }
     }
 }
diff --git a/doc/LanguageClient.txt b/doc/LanguageClient.txt
index d25a49dc5..0714b059d 100644
--- a/doc/LanguageClient.txt
+++ b/doc/LanguageClient.txt
@@ -78,11 +78,11 @@ To use the language server with Vim's formatting operator |gq|, set 'formatexpr'
 Dictionary to specify the servers to be used for each filetype. The keys are
 strings representing the filetypes and the values are either
   1 - a list of strings that form a command
-  2 - a dictionary with keys name, command and initializationOptions, where:
-    - name: is the name of the server, which should match the root node of the
+  2 - a dictionary with the following keys:
+    - name: the name of the server, which should match the root node of the
       configuration options specified in the settings.json file for this server
-    - command: is the same list of strings in option 1
-    - initializationOptions: is an optional dictionary with options to be passed
+    - command: the same list of strings in option 1
+    - initializationOptions: an optional dictionary with options to be passed
       to the server. The values to be set here are highly dependent on each
       server so you should see the documentation for each server to find out
       what to configure in here (if anything).  The options set in
@@ -93,6 +93,13 @@ strings representing the filetypes and the values are either
       then the server settings configured in this section and lastly the
       contents of the files in the `LanguageClient_settingsPath` variable in
       the order in which they were listed.
+    - handlers: an optional dictionary of custom handlers to register for this
+      server. The keys for this dictionary should be (LSP) method names, and
+      the values should be vim functions to be executed upon receiving an event
+      with that method. The function will be called with a single param which
+      is the JSON-RPC message itself. Note that any events will only be handled
+      once, so if you register a handler for an event, the function you specify
+      will dictate all that happens with that event.
 
 For example: >
 
@@ -108,14 +115,21 @@ For example: >
         \       'test': v:true,
         \     },
         \   },
+        \   'handlers': {
+        \    'window/logMessage': 'LogMessage'
+        \   }
         \ },
         \}
 
 In the configuration for the rust filetype above, 'run', 'stable', and 'rls'
-are arguments to the 'rustup' command line tool. And in the configuration for
-the go filetype the server is configured to run the command `gopls` with no
-additional arguments, and with the initialization options set in the
-`initializationOptions` key.
+are arguments to the 'rustup' command line tool.
+
+In the configuration for the go filetype the server is configured to run the
+command `gopls` with no additional arguments, and with the initialization
+options set in the `initializationOptions` key. Also, the go server is
+configured to run the vim function LogMessage upon receiving an event with
+method `window/logMessage`.  Remember that both initializationOptions and
+handlers are optional, so you can omit specifying those if you wish.
 
 You can also use a tcp connection to the server, for example: >
     let g:LanguageClient_serverCommands = {
diff --git a/src/config/server_command.rs b/src/config/server_command.rs
index 2c2bc8ec0..6d12f2d85 100644
--- a/src/config/server_command.rs
+++ b/src/config/server_command.rs
@@ -1,3 +1,5 @@
+use std::collections::HashMap;
+
 use crate::utils::expand_json_path;
 use jsonrpc_core::Value;
 use serde::{Deserialize, Serialize};
@@ -8,6 +10,7 @@ pub struct ServerDetails {
     pub command: Vec<String>,
     pub name: String,
     pub initialization_options: Option<Value>,
+    pub handlers: Option<HashMap<String, String>>,
 }
 
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -48,6 +51,13 @@ impl ServerCommand {
         }
     }
 
+    pub fn handlers(&self) -> HashMap<String, String> {
+        match self {
+            ServerCommand::Simple(_) => HashMap::new(),
+            ServerCommand::Detailed(cmd) => cmd.handlers.clone().unwrap_or_default(),
+        }
+    }
+
     #[cfg(not(target_os = "windows"))]
     fn name_from_command(cmd: &[String]) -> String {
         // it's safe to assume there is at least one item in cmd, otherwise
diff --git a/src/language_server_protocol.rs b/src/language_server_protocol.rs
index 0003959e8..fd425d0ae 100644
--- a/src/language_server_protocol.rs
+++ b/src/language_server_protocol.rs
@@ -858,6 +858,17 @@ impl LanguageClient {
         }
         let command = command.unwrap();
 
+        let handlers = command.handlers();
+        if !handlers.is_empty() {
+            self.update_state(|s| {
+                s.custom_handlers
+                    .entry(language_id.clone())
+                    .or_insert_with(HashMap::new)
+                    .extend(handlers);
+                Ok(())
+            })?;
+        }
+
         let settings = self.get_workspace_settings(&root).unwrap_or_default();
         // warn the user that they are using a deprecated workspace settings
         // file format and direct them to the documentation about the new one
@@ -3982,6 +3993,7 @@ mod test {
             initialization_options: Some(json!({
                 "inlayHints.enable": true,
             })),
+            handlers: None,
         });
 
         let options = merged_initialization_options(&command, &settings)
@@ -4014,6 +4026,7 @@ mod test {
             name: "gopls".into(),
             command: vec!["gopls".into()],
             initialization_options: None,
+            handlers: None,
         });
 
         let options = merged_initialization_options(&command, &settings)
@@ -4040,6 +4053,7 @@ mod test {
             initialization_options: Some(json!({
                 "usePlaceholders": true,
             })),
+            handlers: None,
         });
 
         let options = merged_initialization_options(&command, &settings)
diff --git a/src/rpchandler.rs b/src/rpchandler.rs
index f4c623b3b..00c5f5708 100644
--- a/src/rpchandler.rs
+++ b/src/rpchandler.rs
@@ -158,6 +158,19 @@ impl LanguageClient {
     ) -> Result<()> {
         let params = serde_json::to_value(notification.params.clone())?;
 
+        // handle custom, server specific handlers
+        let custom_handler = self.get_state(|state| {
+            state
+                .custom_handlers
+                .get(language_id.unwrap_or_default())
+                .map(|h| h.get(&notification.method).cloned())
+                .unwrap_or_default()
+        })?;
+        if let Some(handler) = custom_handler {
+            return self.vim()?.rpcclient.notify(&handler, params);
+        }
+
+        // handle custom, server agnostic handlers
         let user_handler =
             self.get_state(|state| state.user_handlers.get(&notification.method).cloned())?;
         if let Some(user_handler) = user_handler {
diff --git a/src/types.rs b/src/types.rs
index 1eb39548d..071b13500 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -175,6 +175,12 @@ pub struct State {
     // TODO: make file specific.
     pub highlight_match_ids: Vec<u32>,
     pub user_handlers: HashMap<String, String>,
+    /// server_handlers stores a hashmap keyed by filetype with values being another hashmap, keyed
+    /// by method and values being a vim function to call when an LSP event with that method is
+    /// received.
+    /// The difference between user_handlers and server_handlers is that this one is filetype
+    /// specific, and user_handlers is global, the handlers are ran for all language servers.
+    pub custom_handlers: HashMap<String, HashMap<String, String>>,
     #[serde(skip_serializing)]
     pub watchers: HashMap<String, FSWatch>,
     #[serde(skip_serializing)]
@@ -233,6 +239,7 @@ impl State {
             highlights_placed: HashMap::new(),
             highlight_match_ids: Vec::new(),
             user_handlers: HashMap::new(),
+            custom_handlers: HashMap::new(),
             watchers: HashMap::new(),
             watcher_rxs: HashMap::new(),
             last_cursor_line: 0,


Test patch: