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