1use crate::wasm_host::WasmState;
2use anyhow::{anyhow, Result};
3use async_compression::futures::bufread::GzipDecoder;
4use async_tar::Archive;
5use async_trait::async_trait;
6use futures::io::BufReader;
7use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
8use std::{
9 env,
10 path::PathBuf,
11 sync::{Arc, OnceLock},
12};
13use util::{maybe, SemanticVersion};
14use wasmtime::component::{Linker, Resource};
15
16pub const VERSION: SemanticVersion = SemanticVersion {
17 major: 0,
18 minor: 0,
19 patch: 4,
20};
21
22wasmtime::component::bindgen!({
23 async: true,
24 path: "../extension_api/wit/0.0.4",
25 with: {
26 "worktree": ExtensionWorktree,
27 },
28});
29
30pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
31
32pub fn linker() -> &'static Linker<WasmState> {
33 static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
34 LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
35}
36
37#[async_trait]
38impl HostWorktree for WasmState {
39 async fn read_text_file(
40 &mut self,
41 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
42 path: String,
43 ) -> wasmtime::Result<Result<String, String>> {
44 let delegate = self.table.get(&delegate)?;
45 Ok(delegate
46 .read_text_file(path.into())
47 .await
48 .map_err(|error| error.to_string()))
49 }
50
51 async fn shell_env(
52 &mut self,
53 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
54 ) -> wasmtime::Result<EnvVars> {
55 let delegate = self.table.get(&delegate)?;
56 Ok(delegate.shell_env().await.into_iter().collect())
57 }
58
59 async fn which(
60 &mut self,
61 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
62 binary_name: String,
63 ) -> wasmtime::Result<Option<String>> {
64 let delegate = self.table.get(&delegate)?;
65 Ok(delegate
66 .which(binary_name.as_ref())
67 .await
68 .map(|path| path.to_string_lossy().to_string()))
69 }
70
71 fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
72 // we only ever hand out borrows of worktrees
73 Ok(())
74 }
75}
76
77#[async_trait]
78impl ExtensionImports for WasmState {
79 async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
80 convert_result(
81 self.host
82 .node_runtime
83 .binary_path()
84 .await
85 .map(|path| path.to_string_lossy().to_string()),
86 )
87 }
88
89 async fn npm_package_latest_version(
90 &mut self,
91 package_name: String,
92 ) -> wasmtime::Result<Result<String, String>> {
93 convert_result(
94 self.host
95 .node_runtime
96 .npm_package_latest_version(&package_name)
97 .await,
98 )
99 }
100
101 async fn npm_package_installed_version(
102 &mut self,
103 package_name: String,
104 ) -> wasmtime::Result<Result<Option<String>, String>> {
105 convert_result(
106 self.host
107 .node_runtime
108 .npm_package_installed_version(&self.work_dir(), &package_name)
109 .await,
110 )
111 }
112
113 async fn npm_install_package(
114 &mut self,
115 package_name: String,
116 version: String,
117 ) -> wasmtime::Result<Result<(), String>> {
118 convert_result(
119 self.host
120 .node_runtime
121 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
122 .await,
123 )
124 }
125
126 async fn latest_github_release(
127 &mut self,
128 repo: String,
129 options: GithubReleaseOptions,
130 ) -> wasmtime::Result<Result<GithubRelease, String>> {
131 convert_result(
132 maybe!(async {
133 let release = util::github::latest_github_release(
134 &repo,
135 options.require_assets,
136 options.pre_release,
137 self.host.http_client.clone(),
138 )
139 .await?;
140 Ok(GithubRelease {
141 version: release.tag_name,
142 assets: release
143 .assets
144 .into_iter()
145 .map(|asset| GithubReleaseAsset {
146 name: asset.name,
147 download_url: asset.browser_download_url,
148 })
149 .collect(),
150 })
151 })
152 .await,
153 )
154 }
155
156 async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
157 Ok((
158 match env::consts::OS {
159 "macos" => Os::Mac,
160 "linux" => Os::Linux,
161 "windows" => Os::Windows,
162 _ => panic!("unsupported os"),
163 },
164 match env::consts::ARCH {
165 "aarch64" => Architecture::Aarch64,
166 "x86" => Architecture::X86,
167 "x86_64" => Architecture::X8664,
168 _ => panic!("unsupported architecture"),
169 },
170 ))
171 }
172
173 async fn set_language_server_installation_status(
174 &mut self,
175 server_name: String,
176 status: LanguageServerInstallationStatus,
177 ) -> wasmtime::Result<()> {
178 let status = match status {
179 LanguageServerInstallationStatus::CheckingForUpdate => {
180 LanguageServerBinaryStatus::CheckingForUpdate
181 }
182 LanguageServerInstallationStatus::Downloading => {
183 LanguageServerBinaryStatus::Downloading
184 }
185 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
186 LanguageServerInstallationStatus::Failed(error) => {
187 LanguageServerBinaryStatus::Failed { error }
188 }
189 };
190
191 self.host
192 .language_registry
193 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
194 Ok(())
195 }
196
197 async fn download_file(
198 &mut self,
199 url: String,
200 path: String,
201 file_type: DownloadedFileType,
202 ) -> wasmtime::Result<Result<(), String>> {
203 let result = maybe!(async {
204 let path = PathBuf::from(path);
205 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
206
207 self.host.fs.create_dir(&extension_work_dir).await?;
208
209 let destination_path = self
210 .host
211 .writeable_path_from_extension(&self.manifest.id, &path)?;
212
213 let mut response = self
214 .host
215 .http_client
216 .get(&url, Default::default(), true)
217 .await
218 .map_err(|err| anyhow!("error downloading release: {}", err))?;
219
220 if !response.status().is_success() {
221 Err(anyhow!(
222 "download failed with status {}",
223 response.status().to_string()
224 ))?;
225 }
226 let body = BufReader::new(response.body_mut());
227
228 match file_type {
229 DownloadedFileType::Uncompressed => {
230 futures::pin_mut!(body);
231 self.host
232 .fs
233 .create_file_with(&destination_path, body)
234 .await?;
235 }
236 DownloadedFileType::Gzip => {
237 let body = GzipDecoder::new(body);
238 futures::pin_mut!(body);
239 self.host
240 .fs
241 .create_file_with(&destination_path, body)
242 .await?;
243 }
244 DownloadedFileType::GzipTar => {
245 let body = GzipDecoder::new(body);
246 futures::pin_mut!(body);
247 self.host
248 .fs
249 .extract_tar_file(&destination_path, Archive::new(body))
250 .await?;
251 }
252 DownloadedFileType::Zip => {
253 let file_name = destination_path
254 .file_name()
255 .ok_or_else(|| anyhow!("invalid download path"))?
256 .to_string_lossy();
257 let zip_filename = format!("{file_name}.zip");
258 let mut zip_path = destination_path.clone();
259 zip_path.set_file_name(zip_filename);
260
261 futures::pin_mut!(body);
262 self.host.fs.create_file_with(&zip_path, body).await?;
263
264 let unzip_status = std::process::Command::new("unzip")
265 .current_dir(&extension_work_dir)
266 .arg(&zip_path)
267 .output()?
268 .status;
269 if !unzip_status.success() {
270 Err(anyhow!("failed to unzip {} archive", path.display()))?;
271 }
272 }
273 }
274
275 Ok(())
276 })
277 .await;
278 convert_result(result)
279 }
280}
281
282fn convert_result<T>(result: Result<T>) -> wasmtime::Result<Result<T, String>> {
283 Ok(result.map_err(|error| error.to_string()))
284}