Add emmet-language-server support (#9910)

Piotr Osiewicz created

Note that I want to move this into an extension before merging.

Fixes: #4992 

Release Notes:

- Added Emmet snippets support

Change summary

Cargo.lock                      |  7 ++
Cargo.toml                      |  1 
extensions/emmet/.gitignore     |  3 +
extensions/emmet/Cargo.toml     | 16 ++++++
extensions/emmet/LICENSE-APACHE |  1 
extensions/emmet/extension.toml | 11 ++++
extensions/emmet/src/emmet.rs   | 93 +++++++++++++++++++++++++++++++++++
7 files changed, 132 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -12490,6 +12490,13 @@ dependencies = [
  "zed_extension_api 0.0.4",
 ]
 
+[[package]]
+name = "zed_emmet"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api 0.0.4",
+]
+
 [[package]]
 name = "zed_erlang"
 version = "0.0.1"

Cargo.toml 🔗

@@ -102,6 +102,7 @@ members = [
     "extensions/astro",
     "extensions/clojure",
     "extensions/csharp",
+    "extensions/emmet",
     "extensions/erlang",
     "extensions/gleam",
     "extensions/haskell",

extensions/emmet/Cargo.toml 🔗

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

extensions/emmet/extension.toml 🔗

@@ -0,0 +1,11 @@
+id = "emmet"
+name = "Emmet"
+description = "Emmet support"
+version = "0.0.1"
+schema_version = 1
+authors = []
+repository = "https://github.com/zed-industries/zed"
+
+[language_servers.emmet-language-server]
+name = "Emmet Language Server"
+language = "HTML"

extensions/emmet/src/emmet.rs 🔗

@@ -0,0 +1,93 @@
+use std::{env, fs};
+use zed_extension_api::{self as zed, Result};
+
+struct EmmetExtension {
+    did_find_server: bool,
+}
+
+const SERVER_PATH: &str = "node_modules/.bin/emmet-language-server";
+const PACKAGE_NAME: &str = "@olrtg/emmet-language-server";
+
+impl EmmetExtension {
+    fn server_exists(&self) -> bool {
+        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
+    }
+
+    fn server_script_path(&mut self, config: zed::LanguageServerConfig) -> 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(
+            &config.name,
+            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
+        );
+        let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
+
+        if !server_exists
+            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
+        {
+            zed::set_language_server_installation_status(
+                &config.name,
+                &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 EmmetExtension {
+    fn new() -> Self {
+        Self {
+            did_find_server: false,
+        }
+    }
+
+    fn language_server_command(
+        &mut self,
+        config: zed::LanguageServerConfig,
+        _: &zed::Worktree,
+    ) -> Result<zed::Command> {
+        let server_path = self.server_script_path(config)?;
+        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::LanguageServerConfig,
+        _: &zed::Worktree,
+    ) -> Result<Option<String>> {
+        Ok(None)
+    }
+}
+
+zed::register_extension!(EmmetExtension);