vue.rs

  1use std::collections::HashMap;
  2use std::{env, fs};
  3
  4use serde::Deserialize;
  5use zed::lsp::{Completion, CompletionKind};
  6use zed::CodeLabelSpan;
  7use zed_extension_api::{self as zed, serde_json, Result};
  8
  9const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js";
 10const PACKAGE_NAME: &str = "@vue/language-server";
 11
 12const TYPESCRIPT_PACKAGE_NAME: &str = "typescript";
 13
 14/// The relative path to TypeScript's SDK.
 15const TYPESCRIPT_TSDK_PATH: &str = "node_modules/typescript/lib";
 16
 17#[derive(Debug, Deserialize)]
 18#[serde(rename_all = "camelCase")]
 19struct PackageJson {
 20    #[serde(default)]
 21    dependencies: HashMap<String, String>,
 22    #[serde(default)]
 23    dev_dependencies: HashMap<String, String>,
 24}
 25
 26struct VueExtension {
 27    did_find_server: bool,
 28    typescript_tsdk_path: String,
 29}
 30
 31impl VueExtension {
 32    fn server_exists(&self) -> bool {
 33        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
 34    }
 35
 36    fn server_script_path(
 37        &mut self,
 38        language_server_id: &zed::LanguageServerId,
 39        worktree: &zed::Worktree,
 40    ) -> Result<String> {
 41        let server_exists = self.server_exists();
 42        if self.did_find_server && server_exists {
 43            self.install_typescript_if_needed(worktree)?;
 44            return Ok(SERVER_PATH.to_string());
 45        }
 46
 47        zed::set_language_server_installation_status(
 48            language_server_id,
 49            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
 50        );
 51        // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
 52        let version = "1.8".to_string();
 53
 54        if !server_exists
 55            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
 56        {
 57            zed::set_language_server_installation_status(
 58                language_server_id,
 59                &zed::LanguageServerInstallationStatus::Downloading,
 60            );
 61            let result = zed::npm_install_package(PACKAGE_NAME, &version);
 62            match result {
 63                Ok(()) => {
 64                    if !self.server_exists() {
 65                        Err(format!(
 66                            "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
 67                        ))?;
 68                    }
 69                }
 70                Err(error) => {
 71                    if !self.server_exists() {
 72                        Err(error)?;
 73                    }
 74                }
 75            }
 76        }
 77
 78        self.install_typescript_if_needed(worktree)?;
 79        self.did_find_server = true;
 80        Ok(SERVER_PATH.to_string())
 81    }
 82
 83    /// Returns whether a local copy of TypeScript exists in the worktree.
 84    fn typescript_exists_for_worktree(&self, worktree: &zed::Worktree) -> Result<bool> {
 85        let package_json = worktree.read_text_file("package.json")?;
 86        let package_json: PackageJson = serde_json::from_str(&package_json)
 87            .map_err(|err| format!("failed to parse package.json: {err}"))?;
 88
 89        let dev_dependencies = &package_json.dev_dependencies;
 90        let dependencies = &package_json.dependencies;
 91
 92        // Since the extension is not allowed to read the filesystem within the project
 93        // except through the worktree (which does not contains `node_modules`), we check
 94        // the `package.json` to see if `typescript` is listed in the dependencies.
 95        Ok(dev_dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME)
 96            || dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME))
 97    }
 98
 99    fn install_typescript_if_needed(&mut self, worktree: &zed::Worktree) -> Result<()> {
100        if self
101            .typescript_exists_for_worktree(worktree)
102            .unwrap_or_default()
103        {
104            println!("found local TypeScript installation at '{TYPESCRIPT_TSDK_PATH}'");
105            return Ok(());
106        }
107
108        let installed_typescript_version =
109            zed::npm_package_installed_version(TYPESCRIPT_PACKAGE_NAME)?;
110        let latest_typescript_version = zed::npm_package_latest_version(TYPESCRIPT_PACKAGE_NAME)?;
111
112        if installed_typescript_version.as_ref() != Some(&latest_typescript_version) {
113            println!("installing {TYPESCRIPT_PACKAGE_NAME}@{latest_typescript_version}");
114            zed::npm_install_package(TYPESCRIPT_PACKAGE_NAME, &latest_typescript_version)?;
115        } else {
116            println!("typescript already installed");
117        }
118
119        self.typescript_tsdk_path = env::current_dir()
120            .unwrap()
121            .join(TYPESCRIPT_TSDK_PATH)
122            .to_string_lossy()
123            .to_string();
124
125        Ok(())
126    }
127}
128
129impl zed::Extension for VueExtension {
130    fn new() -> Self {
131        Self {
132            did_find_server: false,
133            typescript_tsdk_path: TYPESCRIPT_TSDK_PATH.to_owned(),
134        }
135    }
136
137    fn language_server_command(
138        &mut self,
139        language_server_id: &zed::LanguageServerId,
140        worktree: &zed::Worktree,
141    ) -> Result<zed::Command> {
142        let server_path = self.server_script_path(language_server_id, worktree)?;
143        Ok(zed::Command {
144            command: zed::node_binary_path()?,
145            args: vec![
146                env::current_dir()
147                    .unwrap()
148                    .join(&server_path)
149                    .to_string_lossy()
150                    .to_string(),
151                "--stdio".to_string(),
152            ],
153            env: Default::default(),
154        })
155    }
156
157    fn language_server_initialization_options(
158        &mut self,
159        _language_server_id: &zed::LanguageServerId,
160        _worktree: &zed::Worktree,
161    ) -> Result<Option<serde_json::Value>> {
162        Ok(Some(serde_json::json!({
163            "typescript": {
164                "tsdk": self.typescript_tsdk_path
165            }
166        })))
167    }
168
169    fn label_for_completion(
170        &self,
171        _language_server_id: &zed::LanguageServerId,
172        completion: Completion,
173    ) -> Option<zed::CodeLabel> {
174        let highlight_name = match completion.kind? {
175            CompletionKind::Class | CompletionKind::Interface => "type",
176            CompletionKind::Constructor => "type",
177            CompletionKind::Constant => "constant",
178            CompletionKind::Function | CompletionKind::Method => "function",
179            CompletionKind::Property | CompletionKind::Field => "tag",
180            CompletionKind::Variable => "type",
181            CompletionKind::Keyword => "keyword",
182            CompletionKind::Value => "tag",
183            _ => return None,
184        };
185
186        let len = completion.label.len();
187        let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string()));
188
189        Some(zed::CodeLabel {
190            code: Default::default(),
191            spans: if let Some(detail) = completion.detail {
192                vec![
193                    name_span,
194                    CodeLabelSpan::literal(" ", None),
195                    CodeLabelSpan::literal(detail, None),
196                ]
197            } else {
198                vec![name_span]
199            },
200            filter_range: (0..len).into(),
201        })
202    }
203}
204
205zed::register_extension!(VueExtension);