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