1use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
2use ::http_client::{AsyncBody, HttpRequestExt};
3use ::settings::{Settings, WorktreeId};
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 language::{
12 language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
13};
14use language::{LanguageName, LanguageServerName};
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, 2, 0);
26pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 2, 0);
27
28wasmtime::component::bindgen!({
29 async: true,
30 trappable_imports: true,
31 path: "../extension_api/wit/since_v0.2.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.2.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().to_proto())
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 .follow_redirects(match extension_request.redirect_policy {
216 http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
217 http_client::RedirectPolicy::FollowLimit(limit) => {
218 ::http_client::RedirectPolicy::FollowLimit(limit)
219 }
220 http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
221 });
222 for (key, value) in &extension_request.headers {
223 request = request.header(key, value);
224 }
225 let body = extension_request
226 .body
227 .clone()
228 .map(AsyncBody::from)
229 .unwrap_or_default();
230 request.body(body).map_err(anyhow::Error::from)
231}
232
233async fn convert_response(
234 response: &mut ::http_client::Response<AsyncBody>,
235) -> Result<http_client::HttpResponse, anyhow::Error> {
236 let mut extension_response = http_client::HttpResponse {
237 body: Vec::new(),
238 headers: Vec::new(),
239 };
240
241 for (key, value) in response.headers() {
242 extension_response
243 .headers
244 .push((key.to_string(), value.to_str().unwrap_or("").to_string()));
245 }
246
247 response
248 .body_mut()
249 .read_to_end(&mut extension_response.body)
250 .await?;
251
252 Ok(extension_response)
253}
254
255#[async_trait]
256impl nodejs::Host for WasmState {
257 async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
258 self.host
259 .node_runtime
260 .binary_path()
261 .await
262 .map(|path| path.to_string_lossy().to_string())
263 .to_wasmtime_result()
264 }
265
266 async fn npm_package_latest_version(
267 &mut self,
268 package_name: String,
269 ) -> wasmtime::Result<Result<String, String>> {
270 self.host
271 .node_runtime
272 .npm_package_latest_version(&package_name)
273 .await
274 .to_wasmtime_result()
275 }
276
277 async fn npm_package_installed_version(
278 &mut self,
279 package_name: String,
280 ) -> wasmtime::Result<Result<Option<String>, String>> {
281 self.host
282 .node_runtime
283 .npm_package_installed_version(&self.work_dir(), &package_name)
284 .await
285 .to_wasmtime_result()
286 }
287
288 async fn npm_install_package(
289 &mut self,
290 package_name: String,
291 version: String,
292 ) -> wasmtime::Result<Result<(), String>> {
293 self.host
294 .node_runtime
295 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
296 .await
297 .to_wasmtime_result()
298 }
299}
300
301#[async_trait]
302impl lsp::Host for WasmState {}
303
304impl From<::http_client::github::GithubRelease> for github::GithubRelease {
305 fn from(value: ::http_client::github::GithubRelease) -> Self {
306 Self {
307 version: value.tag_name,
308 assets: value.assets.into_iter().map(Into::into).collect(),
309 }
310 }
311}
312
313impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
314 fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
315 Self {
316 name: value.name,
317 download_url: value.browser_download_url,
318 }
319 }
320}
321
322#[async_trait]
323impl github::Host for WasmState {
324 async fn latest_github_release(
325 &mut self,
326 repo: String,
327 options: github::GithubReleaseOptions,
328 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
329 maybe!(async {
330 let release = ::http_client::github::latest_github_release(
331 &repo,
332 options.require_assets,
333 options.pre_release,
334 self.host.http_client.clone(),
335 )
336 .await?;
337 Ok(release.into())
338 })
339 .await
340 .to_wasmtime_result()
341 }
342
343 async fn github_release_by_tag_name(
344 &mut self,
345 repo: String,
346 tag: String,
347 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
348 maybe!(async {
349 let release = ::http_client::github::get_release_by_tag_name(
350 &repo,
351 &tag,
352 self.host.http_client.clone(),
353 )
354 .await?;
355 Ok(release.into())
356 })
357 .await
358 .to_wasmtime_result()
359 }
360}
361
362#[async_trait]
363impl platform::Host for WasmState {
364 async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
365 Ok((
366 match env::consts::OS {
367 "macos" => platform::Os::Mac,
368 "linux" => platform::Os::Linux,
369 "windows" => platform::Os::Windows,
370 _ => panic!("unsupported os"),
371 },
372 match env::consts::ARCH {
373 "aarch64" => platform::Architecture::Aarch64,
374 "x86" => platform::Architecture::X86,
375 "x86_64" => platform::Architecture::X8664,
376 _ => panic!("unsupported architecture"),
377 },
378 ))
379 }
380}
381
382#[async_trait]
383impl slash_command::Host for WasmState {}
384
385#[async_trait]
386impl ExtensionImports for WasmState {
387 async fn get_settings(
388 &mut self,
389 location: Option<self::SettingsLocation>,
390 category: String,
391 key: Option<String>,
392 ) -> wasmtime::Result<Result<String, String>> {
393 self.on_main_thread(|cx| {
394 async move {
395 let location = location
396 .as_ref()
397 .map(|location| ::settings::SettingsLocation {
398 worktree_id: WorktreeId::from_proto(location.worktree_id),
399 path: Path::new(&location.path),
400 });
401
402 cx.update(|cx| match category.as_str() {
403 "language" => {
404 let key = key.map(|k| LanguageName::new(&k));
405 let settings =
406 AllLanguageSettings::get(location, cx).language(key.as_ref());
407 Ok(serde_json::to_string(&settings::LanguageSettings {
408 tab_size: settings.tab_size,
409 })?)
410 }
411 "lsp" => {
412 let settings = key
413 .and_then(|key| {
414 ProjectSettings::get(location, cx)
415 .lsp
416 .get(&LanguageServerName::from_proto(key))
417 })
418 .cloned()
419 .unwrap_or_default();
420 Ok(serde_json::to_string(&settings::LspSettings {
421 binary: settings.binary.map(|binary| settings::BinarySettings {
422 path: binary.path,
423 arguments: binary.arguments,
424 }),
425 settings: settings.settings,
426 initialization_options: settings.initialization_options,
427 })?)
428 }
429 _ => {
430 bail!("Unknown settings category: {}", category);
431 }
432 })
433 }
434 .boxed_local()
435 })
436 .await?
437 .to_wasmtime_result()
438 }
439
440 async fn set_language_server_installation_status(
441 &mut self,
442 server_name: String,
443 status: LanguageServerInstallationStatus,
444 ) -> wasmtime::Result<()> {
445 let status = match status {
446 LanguageServerInstallationStatus::CheckingForUpdate => {
447 LanguageServerBinaryStatus::CheckingForUpdate
448 }
449 LanguageServerInstallationStatus::Downloading => {
450 LanguageServerBinaryStatus::Downloading
451 }
452 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
453 LanguageServerInstallationStatus::Failed(error) => {
454 LanguageServerBinaryStatus::Failed { error }
455 }
456 };
457
458 self.host
459 .language_registry
460 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
461 Ok(())
462 }
463
464 async fn download_file(
465 &mut self,
466 url: String,
467 path: String,
468 file_type: DownloadedFileType,
469 ) -> wasmtime::Result<Result<(), String>> {
470 maybe!(async {
471 let path = PathBuf::from(path);
472 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
473
474 self.host.fs.create_dir(&extension_work_dir).await?;
475
476 let destination_path = self
477 .host
478 .writeable_path_from_extension(&self.manifest.id, &path)?;
479
480 let mut response = self
481 .host
482 .http_client
483 .get(&url, Default::default(), true)
484 .await
485 .map_err(|err| anyhow!("error downloading release: {}", err))?;
486
487 if !response.status().is_success() {
488 Err(anyhow!(
489 "download failed with status {}",
490 response.status().to_string()
491 ))?;
492 }
493 let body = BufReader::new(response.body_mut());
494
495 match file_type {
496 DownloadedFileType::Uncompressed => {
497 futures::pin_mut!(body);
498 self.host
499 .fs
500 .create_file_with(&destination_path, body)
501 .await?;
502 }
503 DownloadedFileType::Gzip => {
504 let body = GzipDecoder::new(body);
505 futures::pin_mut!(body);
506 self.host
507 .fs
508 .create_file_with(&destination_path, body)
509 .await?;
510 }
511 DownloadedFileType::GzipTar => {
512 let body = GzipDecoder::new(body);
513 futures::pin_mut!(body);
514 self.host
515 .fs
516 .extract_tar_file(&destination_path, Archive::new(body))
517 .await?;
518 }
519 DownloadedFileType::Zip => {
520 futures::pin_mut!(body);
521 node_runtime::extract_zip(&destination_path, body)
522 .await
523 .with_context(|| format!("failed to unzip {} archive", path.display()))?;
524 }
525 }
526
527 Ok(())
528 })
529 .await
530 .to_wasmtime_result()
531 }
532
533 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
534 #[allow(unused)]
535 let path = self
536 .host
537 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
538
539 #[cfg(unix)]
540 {
541 use std::fs::{self, Permissions};
542 use std::os::unix::fs::PermissionsExt;
543
544 return fs::set_permissions(&path, Permissions::from_mode(0o755))
545 .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
546 .to_wasmtime_result();
547 }
548
549 #[cfg(not(unix))]
550 Ok(Ok(()))
551 }
552}