1use crate::wasm_host::wit::ToWasmtimeResult;
2use crate::wasm_host::WasmState;
3use anyhow::{anyhow, Result};
4use async_compression::futures::bufread::GzipDecoder;
5use async_tar::Archive;
6use async_trait::async_trait;
7use futures::io::BufReader;
8use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
9use semantic_version::SemanticVersion;
10use std::path::Path;
11use std::{
12 env,
13 path::PathBuf,
14 sync::{Arc, OnceLock},
15};
16use util::maybe;
17use wasmtime::component::{Linker, Resource};
18
19pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
20pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5);
21
22wasmtime::component::bindgen!({
23 async: true,
24 path: "../extension_api/wit/since_v0.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 self.host
81 .node_runtime
82 .binary_path()
83 .await
84 .map(|path| path.to_string_lossy().to_string())
85 .to_wasmtime_result()
86 }
87
88 async fn npm_package_latest_version(
89 &mut self,
90 package_name: String,
91 ) -> wasmtime::Result<Result<String, String>> {
92 self.host
93 .node_runtime
94 .npm_package_latest_version(&package_name)
95 .await
96 .to_wasmtime_result()
97 }
98
99 async fn npm_package_installed_version(
100 &mut self,
101 package_name: String,
102 ) -> wasmtime::Result<Result<Option<String>, String>> {
103 self.host
104 .node_runtime
105 .npm_package_installed_version(&self.work_dir(), &package_name)
106 .await
107 .to_wasmtime_result()
108 }
109
110 async fn npm_install_package(
111 &mut self,
112 package_name: String,
113 version: String,
114 ) -> wasmtime::Result<Result<(), String>> {
115 self.host
116 .node_runtime
117 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
118 .await
119 .to_wasmtime_result()
120 }
121
122 async fn latest_github_release(
123 &mut self,
124 repo: String,
125 options: GithubReleaseOptions,
126 ) -> wasmtime::Result<Result<GithubRelease, String>> {
127 maybe!(async {
128 let release = util::github::latest_github_release(
129 &repo,
130 options.require_assets,
131 options.pre_release,
132 self.host.http_client.clone(),
133 )
134 .await?;
135 Ok(GithubRelease {
136 version: release.tag_name,
137 assets: release
138 .assets
139 .into_iter()
140 .map(|asset| GithubReleaseAsset {
141 name: asset.name,
142 download_url: asset.browser_download_url,
143 })
144 .collect(),
145 })
146 })
147 .await
148 .to_wasmtime_result()
149 }
150
151 async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
152 Ok((
153 match env::consts::OS {
154 "macos" => Os::Mac,
155 "linux" => Os::Linux,
156 "windows" => Os::Windows,
157 _ => panic!("unsupported os"),
158 },
159 match env::consts::ARCH {
160 "aarch64" => Architecture::Aarch64,
161 "x86" => Architecture::X86,
162 "x86_64" => Architecture::X8664,
163 _ => panic!("unsupported architecture"),
164 },
165 ))
166 }
167
168 async fn set_language_server_installation_status(
169 &mut self,
170 server_name: String,
171 status: LanguageServerInstallationStatus,
172 ) -> wasmtime::Result<()> {
173 let status = match status {
174 LanguageServerInstallationStatus::CheckingForUpdate => {
175 LanguageServerBinaryStatus::CheckingForUpdate
176 }
177 LanguageServerInstallationStatus::Downloading => {
178 LanguageServerBinaryStatus::Downloading
179 }
180 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
181 LanguageServerInstallationStatus::Failed(error) => {
182 LanguageServerBinaryStatus::Failed { error }
183 }
184 };
185
186 self.host
187 .language_registry
188 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
189 Ok(())
190 }
191
192 async fn download_file(
193 &mut self,
194 url: String,
195 path: String,
196 file_type: DownloadedFileType,
197 ) -> wasmtime::Result<Result<(), String>> {
198 maybe!(async {
199 let path = PathBuf::from(path);
200 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
201
202 self.host.fs.create_dir(&extension_work_dir).await?;
203
204 let destination_path = self
205 .host
206 .writeable_path_from_extension(&self.manifest.id, &path)?;
207
208 let mut response = self
209 .host
210 .http_client
211 .get(&url, Default::default(), true)
212 .await
213 .map_err(|err| anyhow!("error downloading release: {}", err))?;
214
215 if !response.status().is_success() {
216 Err(anyhow!(
217 "download failed with status {}",
218 response.status().to_string()
219 ))?;
220 }
221 let body = BufReader::new(response.body_mut());
222
223 match file_type {
224 DownloadedFileType::Uncompressed => {
225 futures::pin_mut!(body);
226 self.host
227 .fs
228 .create_file_with(&destination_path, body)
229 .await?;
230 }
231 DownloadedFileType::Gzip => {
232 let body = GzipDecoder::new(body);
233 futures::pin_mut!(body);
234 self.host
235 .fs
236 .create_file_with(&destination_path, body)
237 .await?;
238 }
239 DownloadedFileType::GzipTar => {
240 let body = GzipDecoder::new(body);
241 futures::pin_mut!(body);
242 self.host
243 .fs
244 .extract_tar_file(&destination_path, Archive::new(body))
245 .await?;
246 }
247 DownloadedFileType::Zip => {
248 let file_name = destination_path
249 .file_name()
250 .ok_or_else(|| anyhow!("invalid download path"))?
251 .to_string_lossy();
252 let zip_filename = format!("{file_name}.zip");
253 let mut zip_path = destination_path.clone();
254 zip_path.set_file_name(zip_filename);
255
256 futures::pin_mut!(body);
257 self.host.fs.create_file_with(&zip_path, body).await?;
258
259 let unzip_status = std::process::Command::new("unzip")
260 .current_dir(&extension_work_dir)
261 .arg("-d")
262 .arg(&destination_path)
263 .arg(&zip_path)
264 .output()?
265 .status;
266 if !unzip_status.success() {
267 Err(anyhow!("failed to unzip {} archive", path.display()))?;
268 }
269 }
270 }
271
272 Ok(())
273 })
274 .await
275 .to_wasmtime_result()
276 }
277
278 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
279 #[allow(unused)]
280 let path = self
281 .host
282 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
283
284 #[cfg(unix)]
285 {
286 use std::fs::{self, Permissions};
287 use std::os::unix::fs::PermissionsExt;
288
289 return fs::set_permissions(&path, Permissions::from_mode(0o755))
290 .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
291 .to_wasmtime_result();
292 }
293
294 #[cfg(not(unix))]
295 Ok(Ok(()))
296 }
297}