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