since_v0_2_0.rs

  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}