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