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 common::Host for WasmState {}
103
104#[async_trait]
105impl nodejs::Host for WasmState {
106 async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
107 self.host
108 .node_runtime
109 .binary_path()
110 .await
111 .map(|path| path.to_string_lossy().to_string())
112 .to_wasmtime_result()
113 }
114
115 async fn npm_package_latest_version(
116 &mut self,
117 package_name: String,
118 ) -> wasmtime::Result<Result<String, String>> {
119 self.host
120 .node_runtime
121 .npm_package_latest_version(&package_name)
122 .await
123 .to_wasmtime_result()
124 }
125
126 async fn npm_package_installed_version(
127 &mut self,
128 package_name: String,
129 ) -> wasmtime::Result<Result<Option<String>, String>> {
130 self.host
131 .node_runtime
132 .npm_package_installed_version(&self.work_dir(), &package_name)
133 .await
134 .to_wasmtime_result()
135 }
136
137 async fn npm_install_package(
138 &mut self,
139 package_name: String,
140 version: String,
141 ) -> wasmtime::Result<Result<(), String>> {
142 self.host
143 .node_runtime
144 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
145 .await
146 .to_wasmtime_result()
147 }
148}
149
150#[async_trait]
151impl lsp::Host for WasmState {}
152
153impl From<http::github::GithubRelease> for github::GithubRelease {
154 fn from(value: http::github::GithubRelease) -> Self {
155 Self {
156 version: value.tag_name,
157 assets: value.assets.into_iter().map(Into::into).collect(),
158 }
159 }
160}
161
162impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
163 fn from(value: http::github::GithubReleaseAsset) -> Self {
164 Self {
165 name: value.name,
166 download_url: value.browser_download_url,
167 }
168 }
169}
170
171#[async_trait]
172impl github::Host for WasmState {
173 async fn latest_github_release(
174 &mut self,
175 repo: String,
176 options: github::GithubReleaseOptions,
177 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
178 maybe!(async {
179 let release = http::github::latest_github_release(
180 &repo,
181 options.require_assets,
182 options.pre_release,
183 self.host.http_client.clone(),
184 )
185 .await?;
186 Ok(release.into())
187 })
188 .await
189 .to_wasmtime_result()
190 }
191
192 async fn github_release_by_tag_name(
193 &mut self,
194 repo: String,
195 tag: String,
196 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
197 maybe!(async {
198 let release =
199 http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
200 .await?;
201 Ok(release.into())
202 })
203 .await
204 .to_wasmtime_result()
205 }
206}
207
208#[async_trait]
209impl platform::Host for WasmState {
210 async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
211 Ok((
212 match env::consts::OS {
213 "macos" => platform::Os::Mac,
214 "linux" => platform::Os::Linux,
215 "windows" => platform::Os::Windows,
216 _ => panic!("unsupported os"),
217 },
218 match env::consts::ARCH {
219 "aarch64" => platform::Architecture::Aarch64,
220 "x86" => platform::Architecture::X86,
221 "x86_64" => platform::Architecture::X8664,
222 _ => panic!("unsupported architecture"),
223 },
224 ))
225 }
226}
227
228#[async_trait]
229impl slash_command::Host for WasmState {}
230
231#[async_trait]
232impl ExtensionImports for WasmState {
233 async fn get_settings(
234 &mut self,
235 location: Option<self::SettingsLocation>,
236 category: String,
237 key: Option<String>,
238 ) -> wasmtime::Result<Result<String, String>> {
239 self.on_main_thread(|cx| {
240 async move {
241 let location = location
242 .as_ref()
243 .map(|location| ::settings::SettingsLocation {
244 worktree_id: location.worktree_id as usize,
245 path: Path::new(&location.path),
246 });
247
248 cx.update(|cx| match category.as_str() {
249 "language" => {
250 let settings =
251 AllLanguageSettings::get(location, cx).language(key.as_deref());
252 Ok(serde_json::to_string(&settings::LanguageSettings {
253 tab_size: settings.tab_size,
254 })?)
255 }
256 "lsp" => {
257 let settings = key
258 .and_then(|key| {
259 ProjectSettings::get(location, cx)
260 .lsp
261 .get(&Arc::<str>::from(key))
262 })
263 .cloned()
264 .unwrap_or_default();
265 Ok(serde_json::to_string(&settings::LspSettings {
266 binary: settings.binary.map(|binary| settings::BinarySettings {
267 path: binary.path,
268 arguments: binary.arguments,
269 }),
270 settings: settings.settings,
271 initialization_options: settings.initialization_options,
272 })?)
273 }
274 _ => {
275 bail!("Unknown settings category: {}", category);
276 }
277 })
278 }
279 .boxed_local()
280 })
281 .await?
282 .to_wasmtime_result()
283 }
284
285 async fn set_language_server_installation_status(
286 &mut self,
287 server_name: String,
288 status: LanguageServerInstallationStatus,
289 ) -> wasmtime::Result<()> {
290 let status = match status {
291 LanguageServerInstallationStatus::CheckingForUpdate => {
292 LanguageServerBinaryStatus::CheckingForUpdate
293 }
294 LanguageServerInstallationStatus::Downloading => {
295 LanguageServerBinaryStatus::Downloading
296 }
297 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
298 LanguageServerInstallationStatus::Failed(error) => {
299 LanguageServerBinaryStatus::Failed { error }
300 }
301 };
302
303 self.host
304 .language_registry
305 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
306 Ok(())
307 }
308
309 async fn download_file(
310 &mut self,
311 url: String,
312 path: String,
313 file_type: DownloadedFileType,
314 ) -> wasmtime::Result<Result<(), String>> {
315 maybe!(async {
316 let path = PathBuf::from(path);
317 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
318
319 self.host.fs.create_dir(&extension_work_dir).await?;
320
321 let destination_path = self
322 .host
323 .writeable_path_from_extension(&self.manifest.id, &path)?;
324
325 let mut response = self
326 .host
327 .http_client
328 .get(&url, Default::default(), true)
329 .await
330 .map_err(|err| anyhow!("error downloading release: {}", err))?;
331
332 if !response.status().is_success() {
333 Err(anyhow!(
334 "download failed with status {}",
335 response.status().to_string()
336 ))?;
337 }
338 let body = BufReader::new(response.body_mut());
339
340 match file_type {
341 DownloadedFileType::Uncompressed => {
342 futures::pin_mut!(body);
343 self.host
344 .fs
345 .create_file_with(&destination_path, body)
346 .await?;
347 }
348 DownloadedFileType::Gzip => {
349 let body = GzipDecoder::new(body);
350 futures::pin_mut!(body);
351 self.host
352 .fs
353 .create_file_with(&destination_path, body)
354 .await?;
355 }
356 DownloadedFileType::GzipTar => {
357 let body = GzipDecoder::new(body);
358 futures::pin_mut!(body);
359 self.host
360 .fs
361 .extract_tar_file(&destination_path, Archive::new(body))
362 .await?;
363 }
364 DownloadedFileType::Zip => {
365 let file_name = destination_path
366 .file_name()
367 .ok_or_else(|| anyhow!("invalid download path"))?
368 .to_string_lossy();
369 let zip_filename = format!("{file_name}.zip");
370 let mut zip_path = destination_path.clone();
371 zip_path.set_file_name(zip_filename);
372
373 futures::pin_mut!(body);
374 self.host.fs.create_file_with(&zip_path, body).await?;
375
376 let unzip_status = std::process::Command::new("unzip")
377 .current_dir(&extension_work_dir)
378 .arg("-d")
379 .arg(&destination_path)
380 .arg(&zip_path)
381 .output()?
382 .status;
383 if !unzip_status.success() {
384 Err(anyhow!("failed to unzip {} archive", path.display()))?;
385 }
386 }
387 }
388
389 Ok(())
390 })
391 .await
392 .to_wasmtime_result()
393 }
394
395 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
396 #[allow(unused)]
397 let path = self
398 .host
399 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
400
401 #[cfg(unix)]
402 {
403 use std::fs::{self, Permissions};
404 use std::os::unix::fs::PermissionsExt;
405
406 return fs::set_permissions(&path, Permissions::from_mode(0o755))
407 .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
408 .to_wasmtime_result();
409 }
410
411 #[cfg(not(unix))]
412 Ok(Ok(()))
413 }
414}