vue.rs

  1use std::{env, fs};
  2use zed::lsp::{Completion, CompletionKind};
  3use zed::CodeLabelSpan;
  4use zed_extension_api::{self as zed, serde_json, Result};
  5
  6struct VueExtension {
  7    did_find_server: bool,
  8}
  9
 10const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js";
 11const PACKAGE_NAME: &str = "@vue/language-server";
 12
 13impl VueExtension {
 14    fn server_exists(&self) -> bool {
 15        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
 16    }
 17
 18    fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result<String> {
 19        let server_exists = self.server_exists();
 20        if self.did_find_server && server_exists {
 21            return Ok(SERVER_PATH.to_string());
 22        }
 23
 24        zed::set_language_server_installation_status(
 25            id,
 26            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
 27        );
 28        // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
 29        let version = "1.8".to_string();
 30
 31        if !server_exists
 32            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
 33        {
 34            zed::set_language_server_installation_status(
 35                id,
 36                &zed::LanguageServerInstallationStatus::Downloading,
 37            );
 38            let result = zed::npm_install_package(PACKAGE_NAME, &version);
 39            match result {
 40                Ok(()) => {
 41                    if !self.server_exists() {
 42                        Err(format!(
 43                            "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
 44                        ))?;
 45                    }
 46                }
 47                Err(error) => {
 48                    if !self.server_exists() {
 49                        Err(error)?;
 50                    }
 51                }
 52            }
 53        }
 54
 55        self.did_find_server = true;
 56        Ok(SERVER_PATH.to_string())
 57    }
 58}
 59
 60impl zed::Extension for VueExtension {
 61    fn new() -> Self {
 62        Self {
 63            did_find_server: false,
 64        }
 65    }
 66
 67    fn language_server_command(
 68        &mut self,
 69        id: &zed::LanguageServerId,
 70        _: &zed::Worktree,
 71    ) -> Result<zed::Command> {
 72        let server_path = self.server_script_path(id)?;
 73        Ok(zed::Command {
 74            command: zed::node_binary_path()?,
 75            args: vec![
 76                env::current_dir()
 77                    .unwrap()
 78                    .join(&server_path)
 79                    .to_string_lossy()
 80                    .to_string(),
 81                "--stdio".to_string(),
 82            ],
 83            env: Default::default(),
 84        })
 85    }
 86
 87    fn language_server_initialization_options(
 88        &mut self,
 89        _: &zed::LanguageServerId,
 90        _: &zed::Worktree,
 91    ) -> Result<Option<serde_json::Value>> {
 92        Ok(Some(serde_json::json!({
 93            "typescript": {
 94                "tsdk": "node_modules/typescript/lib"
 95            }
 96        })))
 97    }
 98
 99    fn label_for_completion(
100        &self,
101        _language_server_id: &zed::LanguageServerId,
102        completion: Completion,
103    ) -> Option<zed::CodeLabel> {
104        let highlight_name = match completion.kind? {
105            CompletionKind::Class | CompletionKind::Interface => "type",
106            CompletionKind::Constructor => "type",
107            CompletionKind::Constant => "constant",
108            CompletionKind::Function | CompletionKind::Method => "function",
109            CompletionKind::Property | CompletionKind::Field => "tag",
110            CompletionKind::Variable => "type",
111            CompletionKind::Keyword => "keyword",
112            CompletionKind::Value => "tag",
113            _ => return None,
114        };
115
116        let len = completion.label.len();
117        let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string()));
118
119        Some(zed::CodeLabel {
120            code: Default::default(),
121            spans: if let Some(detail) = completion.detail {
122                vec![
123                    name_span,
124                    CodeLabelSpan::literal(" ", None),
125                    CodeLabelSpan::literal(detail, None),
126                ]
127            } else {
128                vec![name_span]
129            },
130            filter_range: (0..len).into(),
131        })
132    }
133}
134
135zed::register_extension!(VueExtension);