| @@ -1,28 +1,51 @@ |
| 1 | +use std::collections::HashMap; |
| 1 | 2 | use std::{env, fs}; |
| 3 | + |
| 4 | +use serde::Deserialize; |
| 2 | 5 | use zed::lsp::{Completion, CompletionKind}; |
| 3 | 6 | use zed::CodeLabelSpan; |
| 4 | 7 | use zed_extension_api::{self as zed, serde_json, Result}; |
| 5 | 8 | |
| 9 | +const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js"; |
| 10 | +const PACKAGE_NAME: &str = "@vue/language-server"; |
| 11 | + |
| 12 | +const TYPESCRIPT_PACKAGE_NAME: &str = "typescript"; |
| 13 | + |
| 14 | +/// The relative path to TypeScript's SDK. |
| 15 | +const TYPESCRIPT_TSDK_PATH: &str = "node_modules/typescript/lib"; |
| 16 | + |
| 17 | +#[derive(Debug, Deserialize)] |
| 18 | +#[serde(rename_all = "camelCase")] |
| 19 | +struct PackageJson { |
| 20 | + #[serde(default)] |
| 21 | + dependencies: HashMap<String, String>, |
| 22 | + #[serde(default)] |
| 23 | + dev_dependencies: HashMap<String, String>, |
| 24 | +} |
| 25 | + |
| 6 | 26 | struct VueExtension { |
| 7 | 27 | did_find_server: bool, |
| 28 | + typescript_tsdk_path: String, |
| 8 | 29 | } |
| 9 | 30 | |
| 10 | | −const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js"; |
| 11 | | −const PACKAGE_NAME: &str = "@vue/language-server"; |
| 12 | | − |
| 13 | 31 | impl VueExtension { |
| 14 | 32 | fn server_exists(&self) -> bool { |
| 15 | 33 | fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) |
| 16 | 34 | } |
| 17 | 35 | |
| 18 | | − fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result<String> { |
| 36 | + fn server_script_path( |
| 37 | + &mut self, |
| 38 | + language_server_id: &zed::LanguageServerId, |
| 39 | + worktree: &zed::Worktree, |
| 40 | + ) -> Result<String> { |
| 19 | 41 | let server_exists = self.server_exists(); |
| 20 | 42 | if self.did_find_server && server_exists { |
| 43 | + self.install_typescript_if_needed(worktree)?; |
| 21 | 44 | return Ok(SERVER_PATH.to_string()); |
| 22 | 45 | } |
| 23 | 46 | |
| 24 | 47 | zed::set_language_server_installation_status( |
| 25 | | − id, |
| 48 | + language_server_id, |
| 26 | 49 | &zed::LanguageServerInstallationStatus::CheckingForUpdate, |
| 27 | 50 | ); |
| 28 | 51 | // We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet. |
| @@ -32,7 +55,7 @@ impl VueExtension { · 3 unmodified lines |
| 32 | 55 | || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) |
| 33 | 56 | { |
| 34 | 57 | zed::set_language_server_installation_status( |
| 35 | | − id, |
| 58 | + language_server_id, |
| 36 | 59 | &zed::LanguageServerInstallationStatus::Downloading, |
| 37 | 60 | ); |
| 38 | 61 | let result = zed::npm_install_package(PACKAGE_NAME, &version); |
| @@ -52,24 +75,71 @@ impl VueExtension { · 13 unmodified lines |
| 52 | 75 | } |
| 53 | 76 | } |
| 54 | 77 | |
| 78 | + self.install_typescript_if_needed(worktree)?; |
| 55 | 79 | self.did_find_server = true; |
| 56 | 80 | Ok(SERVER_PATH.to_string()) |
| 57 | 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 | + } |
| 58 | 127 | } |
| 59 | 128 | |
| 60 | 129 | impl zed::Extension for VueExtension { |
| 61 | 130 | fn new() -> Self { |
| 62 | 131 | Self { |
| 63 | 132 | did_find_server: false, |
| 133 | + typescript_tsdk_path: TYPESCRIPT_TSDK_PATH.to_owned(), |
| 64 | 134 | } |
| 65 | 135 | } |
| 66 | 136 | |
| 67 | 137 | fn language_server_command( |
| 68 | 138 | &mut self, |
| 69 | | − id: &zed::LanguageServerId, |
| 70 | | − _: &zed::Worktree, |
| 139 | + language_server_id: &zed::LanguageServerId, |
| 140 | + worktree: &zed::Worktree, |
| 71 | 141 | ) -> Result<zed::Command> { |
| 72 | | − let server_path = self.server_script_path(id)?; |
| 142 | + let server_path = self.server_script_path(language_server_id, worktree)?; |
| 73 | 143 | Ok(zed::Command { |
| 74 | 144 | command: zed::node_binary_path()?, |
| 75 | 145 | args: vec![ |
| @@ -86,12 +156,12 @@ impl zed::Extension for VueExtension { · 10 unmodified lines |
| 86 | 156 | |
| 87 | 157 | fn language_server_initialization_options( |
| 88 | 158 | &mut self, |
| 89 | | − _: &zed::LanguageServerId, |
| 90 | | − _: &zed::Worktree, |
| 159 | + _language_server_id: &zed::LanguageServerId, |
| 160 | + _worktree: &zed::Worktree, |
| 91 | 161 | ) -> Result<Option<serde_json::Value>> { |
| 92 | 162 | Ok(Some(serde_json::json!({ |
| 93 | 163 | "typescript": { |
| 94 | | − "tsdk": "node_modules/typescript/lib" |
| 164 | + "tsdk": self.typescript_tsdk_path |
| 95 | 165 | } |
| 96 | 166 | }))) |
| 97 | 167 | } |