Extract Vue extension (#10486)

Marshall Bowers and Max created

This PR extracts Vue support into an extension and removes the built-in
C# support from Zed.

Release Notes:

- Removed built-in support for Vue, in favor of making it available as
an extension. The Vue extension will be suggested for download when you
open a `.vue` file.

---------

Co-authored-by: Max <max@zed.dev>

Change summary

Cargo.lock                                    |  18 
Cargo.toml                                    |   3 
crates/extension/src/extension_lsp_adapter.rs |  17 
crates/extension/src/extension_manifest.rs    |   2 
crates/extensions_ui/src/extension_suggest.rs |   1 
crates/languages/Cargo.toml                   |   2 
crates/languages/src/lib.rs                   |  13 
crates/languages/src/vue.rs                   | 242 ---------------------
extensions/terraform/extension.toml           |   3 
extensions/vue/Cargo.toml                     |  16 +
extensions/vue/LICENSE-APACHE                 |   1 
extensions/vue/extension.toml                 |  18 +
extensions/vue/languages/vue/brackets.scm     |   0 
extensions/vue/languages/vue/config.toml      |   0 
extensions/vue/languages/vue/highlights.scm   |   0 
extensions/vue/languages/vue/injections.scm   |   0 
extensions/vue/languages/vue/overrides.scm    |   0 
extensions/vue/src/vue.rs                     | 135 +++++++++++
18 files changed, 196 insertions(+), 275 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5487,7 +5487,6 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
- "parking_lot",
  "project",
  "regex",
  "rope",
@@ -5524,7 +5523,6 @@ dependencies = [
  "tree-sitter-ruby",
  "tree-sitter-rust",
  "tree-sitter-typescript",
- "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
  "util",
@@ -10664,15 +10662,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-vue"
-version = "0.0.1"
-source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-yaml"
 version = "0.0.1"
@@ -12810,6 +12799,13 @@ dependencies = [
  "zed_extension_api 0.0.4",
 ]
 
+[[package]]
+name = "zed_vue"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "zed_zig"
 version = "0.1.0"

Cargo.toml 🔗

@@ -121,6 +121,7 @@ members = [
     "extensions/terraform",
     "extensions/toml",
     "extensions/uiua",
+    "extensions/vue",
     "extensions/zig",
 
     "tooling/xtask",
@@ -338,9 +339,7 @@ tree-sitter-python = "0.20.2"
 tree-sitter-regex = "0.20.0"
 tree-sitter-ruby = "0.20.0"
 tree-sitter-rust = "0.20.3"
-tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
 tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
-tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
 unindent = "0.1.7"
 unicase = "2.6"

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -130,21 +130,20 @@ impl LspAdapter for ExtensionLspAdapter {
     }
 
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
-        if self.extension.manifest.id.as_ref() == "terraform" {
-            // This is taken from the original Terraform implementation, including
-            // the TODOs:
-            // TODO: file issue for server supported code actions
-            // TODO: reenable default actions / delete override
-            return Some(vec![]);
-        }
+        let code_action_kinds = self
+            .extension
+            .manifest
+            .language_servers
+            .get(&self.language_server_id)
+            .and_then(|server| server.code_action_kinds.clone());
 
-        Some(vec![
+        code_action_kinds.or(Some(vec![
             CodeActionKind::EMPTY,
             CodeActionKind::QUICKFIX,
             CodeActionKind::REFACTOR,
             CodeActionKind::REFACTOR_EXTRACT,
             CodeActionKind::SOURCE,
-        ])
+        ]))
     }
 
     fn language_ids(&self) -> HashMap<String, String> {

crates/extension/src/extension_manifest.rs 🔗

@@ -106,6 +106,8 @@ pub struct LanguageServerManifestEntry {
     languages: Vec<Arc<str>>,
     #[serde(default)]
     pub language_ids: HashMap<String, String>,
+    #[serde(default)]
+    pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
 }
 
 impl LanguageServerManifestEntry {

crates/extensions_ui/src/extension_suggest.rs 🔗

@@ -61,6 +61,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
     ("templ", &["templ"]),
     ("terraform", &["tf", "tfvars", "hcl"]),
     ("toml", &["Cargo.lock", "toml"]),
+    ("vue", &["vue"]),
     ("wgsl", &["wgsl"]),
     ("zig", &["zig"]),
 ];

crates/languages/Cargo.toml 🔗

@@ -22,7 +22,6 @@ lazy_static.workspace = true
 log.workspace = true
 lsp.workspace = true
 node_runtime.workspace = true
-parking_lot.workspace = true
 project.workspace = true
 regex.workspace = true
 rope.workspace = true
@@ -56,7 +55,6 @@ tree-sitter-regex.workspace = true
 tree-sitter-ruby.workspace = true
 tree-sitter-rust.workspace = true
 tree-sitter-typescript.workspace = true
-tree-sitter-vue.workspace = true
 tree-sitter-yaml.workspace = true
 tree-sitter.workspace = true
 util.workspace = true

crates/languages/src/lib.rs 🔗

@@ -24,7 +24,6 @@ mod ruby;
 mod rust;
 mod tailwind;
 mod typescript;
-mod vue;
 mod yaml;
 
 // 1. Add tree-sitter-{language} parser to zed crate
@@ -74,7 +73,6 @@ pub fn init(
         ("rust", tree_sitter_rust::language()),
         ("tsx", tree_sitter_typescript::language_tsx()),
         ("typescript", tree_sitter_typescript::language_typescript()),
-        ("vue", tree_sitter_vue::language()),
         ("yaml", tree_sitter_yaml::language()),
     ]);
 
@@ -270,13 +268,6 @@ pub fn init(
         vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
     );
     language!("nu", vec![Arc::new(nu::NuLanguageServer {})]);
-    language!(
-        "vue",
-        vec![
-            Arc::new(vue::VueLspAdapter::new(node_runtime.clone())),
-            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-        ]
-    );
     language!("proto");
 
     languages.register_secondary_lsp_adapter(
@@ -295,6 +286,10 @@ pub fn init(
         "Svelte".into(),
         Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
     );
+    languages.register_secondary_lsp_adapter(
+        "Vue.js".into(),
+        Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+    );
 
     let mut subscription = languages.subscribe();
     let mut prev_language_settings = languages.language_settings();

crates/languages/src/vue.rs 🔗

@@ -1,242 +0,0 @@
-use anyhow::{anyhow, ensure, Result};
-use async_trait::async_trait;
-use futures::StreamExt;
-pub use language::*;
-use lsp::{CodeActionKind, LanguageServerBinary};
-use node_runtime::NodeRuntime;
-use parking_lot::Mutex;
-use smol::fs::{self};
-use std::{
-    any::Any,
-    ffi::OsString,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
-use util::{maybe, ResultExt};
-
-pub struct VueLspVersion {
-    vue_version: String,
-    ts_version: String,
-}
-
-pub struct VueLspAdapter {
-    node: Arc<dyn NodeRuntime>,
-    typescript_install_path: Mutex<Option<PathBuf>>,
-}
-
-impl VueLspAdapter {
-    const SERVER_PATH: &'static str =
-        "node_modules/@vue/language-server/bin/vue-language-server.js";
-    // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
-    const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
-    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
-        let typescript_install_path = Mutex::new(None);
-        Self {
-            node,
-            typescript_install_path,
-        }
-    }
-}
-#[async_trait(?Send)]
-impl super::LspAdapter for VueLspAdapter {
-    fn name(&self) -> LanguageServerName {
-        LanguageServerName("vue-language-server".into())
-    }
-
-    async fn fetch_latest_server_version(
-        &self,
-        _: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        Ok(Box::new(VueLspVersion {
-            // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
-            vue_version: "1.8".to_string(),
-            ts_version: self.node.npm_package_latest_version("typescript").await?,
-        }) as Box<_>)
-    }
-    async fn initialization_options(
-        self: Arc<Self>,
-        _: &Arc<dyn LspAdapterDelegate>,
-    ) -> Result<Option<serde_json::Value>> {
-        let typescript_sdk_path = self.typescript_install_path.lock();
-        let typescript_sdk_path = typescript_sdk_path
-            .as_ref()
-            .expect("initialization_options called without a container_dir for typescript");
-
-        Ok(Some(serde_json::json!({
-            "typescript": {
-                "tsdk": typescript_sdk_path
-            }
-        })))
-    }
-    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
-        // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
-        // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
-        Some(vec![
-            CodeActionKind::EMPTY,
-            CodeActionKind::QUICKFIX,
-            CodeActionKind::REFACTOR_REWRITE,
-        ])
-    }
-    async fn fetch_server_binary(
-        &self,
-        latest_version: Box<dyn 'static + Send + Any>,
-        container_dir: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        let latest_version = latest_version.downcast::<VueLspVersion>().unwrap();
-        let server_path = container_dir.join(Self::SERVER_PATH);
-        let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
-
-        let vue_package_name = "@vue/language-server";
-        let should_install_vue_language_server = self
-            .node
-            .should_install_npm_package(
-                vue_package_name,
-                &server_path,
-                &container_dir,
-                &latest_version.vue_version,
-            )
-            .await;
-
-        if should_install_vue_language_server {
-            self.node
-                .npm_install_packages(
-                    &container_dir,
-                    &[(vue_package_name, latest_version.vue_version.as_str())],
-                )
-                .await?;
-        }
-        ensure!(
-            fs::metadata(&server_path).await.is_ok(),
-            "@vue/language-server package installation failed"
-        );
-
-        let ts_package_name = "typescript";
-        let should_install_ts_language_server = self
-            .node
-            .should_install_npm_package(
-                ts_package_name,
-                &server_path,
-                &container_dir,
-                &latest_version.ts_version,
-            )
-            .await;
-
-        if should_install_ts_language_server {
-            self.node
-                .npm_install_packages(
-                    &container_dir,
-                    &[(ts_package_name, latest_version.ts_version.as_str())],
-                )
-                .await?;
-        }
-
-        ensure!(
-            fs::metadata(&ts_path).await.is_ok(),
-            "typescript for Vue package installation failed"
-        );
-        *self.typescript_install_path.lock() = Some(ts_path);
-        Ok(LanguageServerBinary {
-            path: self.node.binary_path().await?,
-            env: None,
-            arguments: vue_server_binary_arguments(&server_path),
-        })
-    }
-
-    async fn cached_server_binary(
-        &self,
-        container_dir: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
-        *self.typescript_install_path.lock() = Some(ts_path);
-        Some(server)
-    }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
-            .await
-            .map(|(mut binary, ts_path)| {
-                binary.arguments = vec!["--help".into()];
-                (binary, ts_path)
-            })?;
-        *self.typescript_install_path.lock() = Some(ts_path);
-        Some(server)
-    }
-
-    async fn label_for_completion(
-        &self,
-        item: &lsp::CompletionItem,
-        language: &Arc<language::Language>,
-    ) -> Option<language::CodeLabel> {
-        use lsp::CompletionItemKind as Kind;
-        let len = item.label.len();
-        let grammar = language.grammar()?;
-        let highlight_id = match item.kind? {
-            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
-            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
-            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
-            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
-            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
-            Kind::VARIABLE => grammar.highlight_id_for_name("type"),
-            Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
-            Kind::VALUE => grammar.highlight_id_for_name("tag"),
-            _ => None,
-        }?;
-
-        let text = match &item.detail {
-            Some(detail) => format!("{} {}", item.label, detail),
-            None => item.label.clone(),
-        };
-
-        Some(language::CodeLabel {
-            text,
-            runs: vec![(0..len, highlight_id)],
-            filter_range: 0..len,
-        })
-    }
-}
-
-fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
-    vec![server_path.into(), "--stdio".into()]
-}
-
-type TypescriptPath = PathBuf;
-async fn get_cached_server_binary(
-    container_dir: PathBuf,
-    node: Arc<dyn NodeRuntime>,
-) -> Option<(LanguageServerBinary, TypescriptPath)> {
-    maybe!(async {
-        let mut last_version_dir = None;
-        let mut entries = fs::read_dir(&container_dir).await?;
-        while let Some(entry) = entries.next().await {
-            let entry = entry?;
-            if entry.file_type().await?.is_dir() {
-                last_version_dir = Some(entry.path());
-            }
-        }
-        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-        let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
-        let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
-        if server_path.exists() && typescript_path.exists() {
-            Ok((
-                LanguageServerBinary {
-                    path: node.binary_path().await?,
-                    env: None,
-                    arguments: vue_server_binary_arguments(&server_path),
-                },
-                typescript_path,
-            ))
-        } else {
-            Err(anyhow!(
-                "missing executable in directory {:?}",
-                last_version_dir
-            ))
-        }
-    })
-    .await
-    .log_err()
-}

extensions/terraform/extension.toml 🔗

@@ -10,6 +10,9 @@ repository = "https://github.com/zed-industries/zed"
 name = "Terraform Language Server"
 languages = ["Terraform", "Terraform Vars"]
 language_ids = { Terraform = "terraform", "Terraform Vars" = "terraform-vars" }
+# TODO: file issue for server supported code actions
+# TODO: reenable default actions / delete override
+code_action_kinds = []
 
 [grammars.hcl]
 repository = "https://github.com/MichaHoffmann/tree-sitter-hcl"

extensions/vue/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "zed_vue"
+version = "0.0.1"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/vue.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = "0.0.6"

extensions/vue/extension.toml 🔗

@@ -0,0 +1,18 @@
+id = "vue"
+name = "Vue"
+description = "Vue support."
+version = "0.0.1"
+schema_version = 1
+authors = ["Piotr Osiewicz <piotr@zed.dev>"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_servers.vue-language-server]
+name = "Vue Language Server"
+language = "Vue.js"
+# REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
+# sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
+code_action_kinds = ["", "quickfix", "refactor.rewrite"]
+
+[grammars.vue]
+repository = "https://github.com/tree-sitter-grammars/tree-sitter-vue"
+commit = "7e48557b903a9db9c38cea3b7839ef7e1f36c693"

extensions/vue/src/vue.rs 🔗

@@ -0,0 +1,135 @@
+use std::{env, fs};
+use zed::lsp::{Completion, CompletionKind};
+use zed::CodeLabelSpan;
+use zed_extension_api::{self as zed, serde_json, Result};
+
+struct VueExtension {
+    did_find_server: bool,
+}
+
+const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js";
+const PACKAGE_NAME: &str = "@vue/language-server";
+
+impl VueExtension {
+    fn server_exists(&self) -> bool {
+        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
+    }
+
+    fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result<String> {
+        let server_exists = self.server_exists();
+        if self.did_find_server && server_exists {
+            return Ok(SERVER_PATH.to_string());
+        }
+
+        zed::set_language_server_installation_status(
+            id,
+            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
+        );
+        // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
+        let version = "1.8".to_string();
+
+        if !server_exists
+            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
+        {
+            zed::set_language_server_installation_status(
+                id,
+                &zed::LanguageServerInstallationStatus::Downloading,
+            );
+            let result = zed::npm_install_package(PACKAGE_NAME, &version);
+            match result {
+                Ok(()) => {
+                    if !self.server_exists() {
+                        Err(format!(
+                            "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
+                        ))?;
+                    }
+                }
+                Err(error) => {
+                    if !self.server_exists() {
+                        Err(error)?;
+                    }
+                }
+            }
+        }
+
+        self.did_find_server = true;
+        Ok(SERVER_PATH.to_string())
+    }
+}
+
+impl zed::Extension for VueExtension {
+    fn new() -> Self {
+        Self {
+            did_find_server: false,
+        }
+    }
+
+    fn language_server_command(
+        &mut self,
+        id: &zed::LanguageServerId,
+        _: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        let server_path = self.server_script_path(id)?;
+        Ok(zed::Command {
+            command: zed::node_binary_path()?,
+            args: vec![
+                env::current_dir()
+                    .unwrap()
+                    .join(&server_path)
+                    .to_string_lossy()
+                    .to_string(),
+                "--stdio".to_string(),
+            ],
+            env: Default::default(),
+        })
+    }
+
+    fn language_server_initialization_options(
+        &mut self,
+        _: &zed::LanguageServerId,
+        _: &zed::Worktree,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(serde_json::json!({
+            "typescript": {
+                "tsdk": "node_modules/typescript/lib"
+            }
+        })))
+    }
+
+    fn label_for_completion(
+        &self,
+        _language_server_id: &zed::LanguageServerId,
+        completion: Completion,
+    ) -> Option<zed::CodeLabel> {
+        let highlight_name = match completion.kind? {
+            CompletionKind::Class | CompletionKind::Interface => "type",
+            CompletionKind::Constructor => "type",
+            CompletionKind::Constant => "constant",
+            CompletionKind::Function | CompletionKind::Method => "function",
+            CompletionKind::Property | CompletionKind::Field => "tag",
+            CompletionKind::Variable => "type",
+            CompletionKind::Keyword => "keyword",
+            CompletionKind::Value => "tag",
+            _ => return None,
+        };
+
+        let len = completion.label.len();
+        let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string()));
+
+        Some(zed::CodeLabel {
+            code: Default::default(),
+            spans: if let Some(detail) = completion.detail {
+                vec![
+                    name_span,
+                    CodeLabelSpan::literal(" ", None),
+                    CodeLabelSpan::literal(detail, None),
+                ]
+            } else {
+                vec![name_span]
+            },
+            filter_range: (0..len).into(),
+        })
+    }
+}
+
+zed::register_extension!(VueExtension);