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