emmet.rs

  1use std::{env, fs};
  2use zed_extension_api::{self as zed, Result};
  3
  4struct EmmetExtension {
  5    did_find_server: bool,
  6}
  7
  8const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js";
  9const PACKAGE_NAME: &str = "@olrtg/emmet-language-server";
 10
 11impl EmmetExtension {
 12    fn server_exists(&self) -> bool {
 13        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
 14    }
 15
 16    fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result<String> {
 17        let server_exists = self.server_exists();
 18        if self.did_find_server && server_exists {
 19            return Ok(SERVER_PATH.to_string());
 20        }
 21
 22        zed::set_language_server_installation_status(
 23            language_server_id,
 24            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
 25        );
 26        let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
 27
 28        if !server_exists
 29            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
 30        {
 31            zed::set_language_server_installation_status(
 32                language_server_id,
 33                &zed::LanguageServerInstallationStatus::Downloading,
 34            );
 35            let result = zed::npm_install_package(PACKAGE_NAME, &version);
 36            match result {
 37                Ok(()) => {
 38                    if !self.server_exists() {
 39                        Err(format!(
 40                            "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
 41                        ))?;
 42                    }
 43                }
 44                Err(error) => {
 45                    if !self.server_exists() {
 46                        Err(error)?;
 47                    }
 48                }
 49            }
 50        }
 51
 52        self.did_find_server = true;
 53        Ok(SERVER_PATH.to_string())
 54    }
 55}
 56
 57impl zed::Extension for EmmetExtension {
 58    fn new() -> Self {
 59        Self {
 60            did_find_server: false,
 61        }
 62    }
 63
 64    fn language_server_command(
 65        &mut self,
 66        language_server_id: &zed::LanguageServerId,
 67        _worktree: &zed::Worktree,
 68    ) -> Result<zed::Command> {
 69        let server_path = self.server_script_path(language_server_id)?;
 70        Ok(zed::Command {
 71            command: zed::node_binary_path()?,
 72            args: vec![
 73                zed_ext::sanitize_windows_path(env::current_dir().unwrap())
 74                    .join(&server_path)
 75                    .to_string_lossy()
 76                    .to_string(),
 77                "--stdio".to_string(),
 78            ],
 79            env: Default::default(),
 80        })
 81    }
 82}
 83
 84zed::register_extension!(EmmetExtension);
 85
 86/// Extensions to the Zed extension API that have not yet stabilized.
 87mod zed_ext {
 88    /// Sanitizes the given path to remove the leading `/` on Windows.
 89    ///
 90    /// On macOS and Linux this is a no-op.
 91    ///
 92    /// This is a workaround for https://github.com/bytecodealliance/wasmtime/issues/10415.
 93    pub fn sanitize_windows_path(path: std::path::PathBuf) -> std::path::PathBuf {
 94        use zed_extension_api::{Os, current_platform};
 95
 96        let (os, _arch) = current_platform();
 97        match os {
 98            Os::Mac | Os::Linux => path,
 99            Os::Windows => path
100                .to_string_lossy()
101                .to_string()
102                .trim_start_matches('/')
103                .into(),
104        }
105    }
106}