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