1use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
2use ::settings::Settings;
3use anyhow::{anyhow, bail, Result};
4use async_compression::futures::bufread::GzipDecoder;
5use async_tar::Archive;
6use async_trait::async_trait;
7use futures::{io::BufReader, FutureExt as _};
8use language::{
9 language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
10};
11use project::project_settings::ProjectSettings;
12use semantic_version::SemanticVersion;
13use std::{
14 env,
15 path::{Path, PathBuf},
16 sync::{Arc, OnceLock},
17};
18use util::maybe;
19use wasmtime::component::{Linker, Resource};
20
21pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
22pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
23
24wasmtime::component::bindgen!({
25 async: true,
26 path: "../extension_api/wit/since_v0.0.7",
27 with: {
28 "worktree": ExtensionWorktree,
29 },
30});
31
32pub use self::zed::extension::*;
33
34mod settings {
35 include!("../../../../extension_api/wit/since_v0.0.7/settings.rs");
36}
37
38pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
39
40pub fn linker() -> &'static Linker<WasmState> {
41 static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
42 LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
43}
44
45#[async_trait]
46impl HostWorktree for WasmState {
47 async fn id(
48 &mut self,
49 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
50 ) -> wasmtime::Result<u64> {
51 let delegate = self.table.get(&delegate)?;
52 Ok(delegate.worktree_id())
53 }
54
55 async fn root_path(
56 &mut self,
57 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
58 ) -> wasmtime::Result<String> {
59 let delegate = self.table.get(&delegate)?;
60 Ok(delegate.worktree_root_path().to_string_lossy().to_string())
61 }
62
63 async fn read_text_file(
64 &mut self,
65 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
66 path: String,
67 ) -> wasmtime::Result<Result<String, String>> {
68 let delegate = self.table.get(&delegate)?;
69 Ok(delegate
70 .read_text_file(path.into())
71 .await
72 .map_err(|error| error.to_string()))
73 }
74
75 async fn shell_env(
76 &mut self,
77 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
78 ) -> wasmtime::Result<EnvVars> {
79 let delegate = self.table.get(&delegate)?;
80 Ok(delegate.shell_env().await.into_iter().collect())
81 }
82
83 async fn which(
84 &mut self,
85 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
86 binary_name: String,
87 ) -> wasmtime::Result<Option<String>> {
88 let delegate = self.table.get(&delegate)?;
89 Ok(delegate
90 .which(binary_name.as_ref())
91 .await
92 .map(|path| path.to_string_lossy().to_string()))
93 }
94
95 fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
96 // We only ever hand out borrows of worktrees.
97 Ok(())
98 }
99}
100
101#[async_trait]
102impl nodejs::Host for WasmState {
103 async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
104 self.host
105 .node_runtime
106 .binary_path()
107 .await
108 .map(|path| path.to_string_lossy().to_string())
109 .to_wasmtime_result()
110 }
111
112 async fn npm_package_latest_version(
113 &mut self,
114 package_name: String,
115 ) -> wasmtime::Result<Result<String, String>> {
116 self.host
117 .node_runtime
118 .npm_package_latest_version(&package_name)
119 .await
120 .to_wasmtime_result()
121 }
122
123 async fn npm_package_installed_version(
124 &mut self,
125 package_name: String,
126 ) -> wasmtime::Result<Result<Option<String>, String>> {
127 self.host
128 .node_runtime
129 .npm_package_installed_version(&self.work_dir(), &package_name)
130 .await
131 .to_wasmtime_result()
132 }
133
134 async fn npm_install_package(
135 &mut self,
136 package_name: String,
137 version: String,
138 ) -> wasmtime::Result<Result<(), String>> {
139 self.host
140 .node_runtime
141 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
142 .await
143 .to_wasmtime_result()
144 }
145}
146
147#[async_trait]
148impl lsp::Host for WasmState {}
149
150impl From<http::github::GithubRelease> for github::GithubRelease {
151 fn from(value: http::github::GithubRelease) -> Self {
152 Self {
153 version: value.tag_name,
154 assets: value.assets.into_iter().map(Into::into).collect(),
155 }
156 }
157}
158
159impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
160 fn from(value: http::github::GithubReleaseAsset) -> Self {
161 Self {
162 name: value.name,
163 download_url: value.browser_download_url,
164 }
165 }
166}
167
168#[async_trait]
169impl github::Host for WasmState {
170 async fn latest_github_release(
171 &mut self,
172 repo: String,
173 options: github::GithubReleaseOptions,
174 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
175 maybe!(async {
176 let release = http::github::latest_github_release(
177 &repo,
178 options.require_assets,
179 options.pre_release,
180 self.host.http_client.clone(),
181 )
182 .await?;
183 Ok(release.into())
184 })
185 .await
186 .to_wasmtime_result()
187 }
188
189 async fn github_release_by_tag_name(
190 &mut self,
191 repo: String,
192 tag: String,
193 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
194 maybe!(async {
195 let release =
196 http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
197 .await?;
198 Ok(release.into())
199 })
200 .await
201 .to_wasmtime_result()
202 }
203}
204
205#[async_trait]
206impl platform::Host for WasmState {
207 async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
208 Ok((
209 match env::consts::OS {
210 "macos" => platform::Os::Mac,
211 "linux" => platform::Os::Linux,
212 "windows" => platform::Os::Windows,
213 _ => panic!("unsupported os"),
214 },
215 match env::consts::ARCH {
216 "aarch64" => platform::Architecture::Aarch64,
217 "x86" => platform::Architecture::X86,
218 "x86_64" => platform::Architecture::X8664,
219 _ => panic!("unsupported architecture"),
220 },
221 ))
222 }
223}
224
225#[async_trait]
226impl slash_command::Host for WasmState {}
227
228#[async_trait]
229impl ExtensionImports for WasmState {
230 async fn get_settings(
231 &mut self,
232 location: Option<self::SettingsLocation>,
233 category: String,
234 key: Option<String>,
235 ) -> wasmtime::Result<Result<String, String>> {
236 self.on_main_thread(|cx| {
237 async move {
238 let location = location
239 .as_ref()
240 .map(|location| ::settings::SettingsLocation {
241 worktree_id: location.worktree_id as usize,
242 path: Path::new(&location.path),
243 });
244
245 cx.update(|cx| match category.as_str() {
246 "language" => {
247 let settings =
248 AllLanguageSettings::get(location, cx).language(key.as_deref());
249 Ok(serde_json::to_string(&settings::LanguageSettings {
250 tab_size: settings.tab_size,
251 })?)
252 }
253 "lsp" => {
254 let settings = key
255 .and_then(|key| {
256 ProjectSettings::get(location, cx)
257 .lsp
258 .get(&Arc::<str>::from(key))
259 })
260 .cloned()
261 .unwrap_or_default();
262 Ok(serde_json::to_string(&settings::LspSettings {
263 binary: settings.binary.map(|binary| settings::BinarySettings {
264 path: binary.path,
265 arguments: binary.arguments,
266 }),
267 settings: settings.settings,
268 initialization_options: settings.initialization_options,
269 })?)
270 }
271 _ => {
272 bail!("Unknown settings category: {}", category);
273 }
274 })
275 }
276 .boxed_local()
277 })
278 .await?
279 .to_wasmtime_result()
280 }
281
282 async fn set_language_server_installation_status(
283 &mut self,
284 server_name: String,
285 status: LanguageServerInstallationStatus,
286 ) -> wasmtime::Result<()> {
287 let status = match status {
288 LanguageServerInstallationStatus::CheckingForUpdate => {
289 LanguageServerBinaryStatus::CheckingForUpdate
290 }
291 LanguageServerInstallationStatus::Downloading => {
292 LanguageServerBinaryStatus::Downloading
293 }
294 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
295 LanguageServerInstallationStatus::Failed(error) => {
296 LanguageServerBinaryStatus::Failed { error }
297 }
298 };
299
300 self.host
301 .language_registry
302 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
303 Ok(())
304 }
305
306 async fn download_file(
307 &mut self,
308 url: String,
309 path: String,
310 file_type: DownloadedFileType,
311 ) -> wasmtime::Result<Result<(), String>> {
312 maybe!(async {
313 let path = PathBuf::from(path);
314 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
315
316 self.host.fs.create_dir(&extension_work_dir).await?;
317
318 let destination_path = self
319 .host
320 .writeable_path_from_extension(&self.manifest.id, &path)?;
321
322 let mut response = self
323 .host
324 .http_client
325 .get(&url, Default::default(), true)
326 .await
327 .map_err(|err| anyhow!("error downloading release: {}", err))?;
328
329 if !response.status().is_success() {
330 Err(anyhow!(
331 "download failed with status {}",
332 response.status().to_string()
333 ))?;
334 }
335 let body = BufReader::new(response.body_mut());
336
337 match file_type {
338 DownloadedFileType::Uncompressed => {
339 futures::pin_mut!(body);
340 self.host
341 .fs
342 .create_file_with(&destination_path, body)
343 .await?;
344 }
345 DownloadedFileType::Gzip => {
346 let body = GzipDecoder::new(body);
347 futures::pin_mut!(body);
348 self.host
349 .fs
350 .create_file_with(&destination_path, body)
351 .await?;
352 }
353 DownloadedFileType::GzipTar => {
354 let body = GzipDecoder::new(body);
355 futures::pin_mut!(body);
356 self.host
357 .fs
358 .extract_tar_file(&destination_path, Archive::new(body))
359 .await?;
360 }
361 DownloadedFileType::Zip => {
362 let file_name = destination_path
363 .file_name()
364 .ok_or_else(|| anyhow!("invalid download path"))?
365 .to_string_lossy();
366 let zip_filename = format!("{file_name}.zip");
367 let mut zip_path = destination_path.clone();
368 zip_path.set_file_name(zip_filename);
369
370 futures::pin_mut!(body);
371 self.host.fs.create_file_with(&zip_path, body).await?;
372
373 let unzip_status = std::process::Command::new("unzip")
374 .current_dir(&extension_work_dir)
375 .arg("-d")
376 .arg(&destination_path)
377 .arg(&zip_path)
378 .output()?
379 .status;
380 if !unzip_status.success() {
381 Err(anyhow!("failed to unzip {} archive", path.display()))?;
382 }
383 }
384 }
385
386 Ok(())
387 })
388 .await
389 .to_wasmtime_result()
390 }
391
392 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
393 #[allow(unused)]
394 let path = self
395 .host
396 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
397
398 #[cfg(unix)]
399 {
400 use std::fs::{self, Permissions};
401 use std::os::unix::fs::PermissionsExt;
402
403 return fs::set_permissions(&path, Permissions::from_mode(0o755))
404 .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
405 .to_wasmtime_result();
406 }
407
408 #[cfg(not(unix))]
409 Ok(Ok(()))
410 }
411}