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