Add Vue language server v3 support (#40651)

Smit Barmase , MrSubidubi , and Piotr Osiewicz created

Closes https://github.com/zed-extensions/vue/issues/48

Migration guide:
https://github.com/vuejs/language-tools/discussions/5456

PR to remove tdsk: https://github.com/zed-extensions/vue/pull/61

Release Notes:

- Added support for Vue language server version 3. Know more
[here](https://github.com/vuejs/language-tools/releases/tag/v3.0.0).

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

assets/settings/default.json                            |   2 
crates/project/src/lsp_store.rs                         |   2 
crates/project/src/lsp_store/vue_language_server_ext.rs | 124 +++++++++++
3 files changed, 127 insertions(+), 1 deletion(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1813,7 +1813,7 @@
       "use_on_type_format": false
     },
     "Vue.js": {
-      "language_servers": ["vue-language-server", "..."],
+      "language_servers": ["vue-language-server", "vtsls", "..."],
       "prettier": {
         "allowed": true
       }

crates/project/src/lsp_store.rs 🔗

@@ -14,6 +14,7 @@ pub mod json_language_server_ext;
 pub mod log_store;
 pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
+pub mod vue_language_server_ext;
 
 use crate::{
     CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
@@ -990,6 +991,7 @@ impl LocalLspStore {
             })
             .detach();
 
+        vue_language_server_ext::register_requests(this.clone(), language_server);
         json_language_server_ext::register_requests(this.clone(), language_server);
         rust_analyzer_ext::register_notifications(this.clone(), language_server);
         clangd_ext::register_notifications(this, language_server, adapter);

crates/project/src/lsp_store/vue_language_server_ext.rs 🔗

@@ -0,0 +1,124 @@
+use anyhow::Context as _;
+use gpui::{AppContext, WeakEntity};
+use lsp::{LanguageServer, LanguageServerName};
+use serde_json::Value;
+
+use crate::LspStore;
+
+struct VueServerRequest;
+struct TypescriptServerResponse;
+
+impl lsp::notification::Notification for VueServerRequest {
+    type Params = Vec<(u64, String, serde_json::Value)>;
+
+    const METHOD: &'static str = "tsserver/request";
+}
+
+impl lsp::notification::Notification for TypescriptServerResponse {
+    type Params = Vec<(u64, serde_json::Value)>;
+
+    const METHOD: &'static str = "tsserver/response";
+}
+
+const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server");
+const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls");
+const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server");
+
+pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
+    let language_server_name = language_server.name();
+    if language_server_name == VUE_SERVER_NAME {
+        let vue_server_id = language_server.server_id();
+        language_server
+            .on_notification::<VueServerRequest, _>({
+                move |params, cx| {
+                    let lsp_store = lsp_store.clone();
+                    let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| {
+                        this.language_server_for_id(vue_server_id)
+                    }) else {
+                        return;
+                    };
+
+                    let requests = params;
+                    let target_server = match lsp_store.read_with(cx, |this, _| {
+                        let language_server_id = this
+                            .as_local()
+                            .and_then(|local| {
+                                local.language_server_ids.iter().find_map(|(seed, v)| {
+                                    [VTSLS, TS_LS].contains(&seed.name).then_some(v.id)
+                                })
+                            })
+                            .context("Could not find language server")?;
+
+                        this.language_server_for_id(language_server_id)
+                            .context("language server not found")
+                    }) {
+                        Ok(Ok(server)) => server,
+                        other => {
+                            log::warn!(
+                                "vue-language-server forwarding skipped: {other:?}. \
+                                 Returning null tsserver responses"
+                            );
+                            if !requests.is_empty() {
+                                let null_responses = requests
+                                    .into_iter()
+                                    .map(|(id, _, _)| (id, Value::Null))
+                                    .collect::<Vec<_>>();
+                                let _ = vue_server
+                                    .notify::<TypescriptServerResponse>(null_responses);
+                            }
+                            return;
+                        }
+                    };
+
+                    let cx = cx.clone();
+                    for (request_id, command, payload) in requests.into_iter() {
+                        let target_server = target_server.clone();
+                        let vue_server = vue_server.clone();
+                        cx.background_spawn(async move {
+                            let response = target_server
+                                .request::<lsp::request::ExecuteCommand>(
+                                    lsp::ExecuteCommandParams {
+                                        command: "typescript.tsserverRequest".to_owned(),
+                                        arguments: vec![Value::String(command), payload],
+                                        ..Default::default()
+                                    },
+                                )
+                                .await;
+
+                            let response_body = match response {
+                                util::ConnectionResult::Result(Ok(result)) => match result {
+                                    Some(Value::Object(mut map)) => map
+                                        .remove("body")
+                                        .unwrap_or(Value::Object(map)),
+                                    Some(other) => other,
+                                    None => Value::Null,
+                                },
+                                util::ConnectionResult::Result(Err(error)) => {
+                                    log::warn!(
+                                        "typescript.tsserverRequest failed: {error:?} for request {request_id}"
+                                    );
+                                    Value::Null
+                                }
+                                other => {
+                                    log::warn!(
+                                        "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}"
+                                    );
+                                    Value::Null
+                                }
+                            };
+
+                            if let Err(err) = vue_server
+                                .notify::<TypescriptServerResponse>(vec![(request_id, response_body)])
+                            {
+                                log::warn!(
+                                    "Failed to notify vue-language-server of tsserver response: {err:?}"
+                                );
+                            }
+                        })
+                        .detach();
+                    }
+                }
+            })
+            .detach();
+    }
+}