since_v0_0_7.rs

  1use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
  2use ::http_client::AsyncBody;
  3use ::settings::Settings;
  4use anyhow::{anyhow, bail, Context, Result};
  5use async_compression::futures::bufread::GzipDecoder;
  6use async_tar::Archive;
  7use async_trait::async_trait;
  8use futures::AsyncReadExt;
  9use futures::{io::BufReader, FutureExt as _};
 10use indexed_docs::IndexedDocsDatabase;
 11use language::{
 12    language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
 13};
 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, 0, 7);
 25pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
 26
 27wasmtime::component::bindgen!({
 28    async: true,
 29    path: "../extension_api/wit/since_v0.0.7",
 30    with: {
 31         "worktree": ExtensionWorktree,
 32         "key-value-store": ExtensionKeyValueStore
 33    },
 34});
 35
 36pub use self::zed::extension::*;
 37
 38mod settings {
 39    include!("../../../../extension_api/wit/since_v0.0.7/settings.rs");
 40}
 41
 42pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
 43
 44pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
 45
 46pub fn linker() -> &'static Linker<WasmState> {
 47    static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
 48    LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
 49}
 50
 51#[async_trait]
 52impl HostKeyValueStore for WasmState {
 53    async fn insert(
 54        &mut self,
 55        kv_store: Resource<ExtensionKeyValueStore>,
 56        key: String,
 57        value: String,
 58    ) -> wasmtime::Result<Result<(), String>> {
 59        let kv_store = self.table.get(&kv_store)?;
 60        kv_store.insert(key, value).await.to_wasmtime_result()
 61    }
 62
 63    fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
 64        // We only ever hand out borrows of key-value stores.
 65        Ok(())
 66    }
 67}
 68
 69#[async_trait]
 70impl HostWorktree for WasmState {
 71    async fn id(
 72        &mut self,
 73        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 74    ) -> wasmtime::Result<u64> {
 75        let delegate = self.table.get(&delegate)?;
 76        Ok(delegate.worktree_id())
 77    }
 78
 79    async fn root_path(
 80        &mut self,
 81        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 82    ) -> wasmtime::Result<String> {
 83        let delegate = self.table.get(&delegate)?;
 84        Ok(delegate.worktree_root_path().to_string_lossy().to_string())
 85    }
 86
 87    async fn read_text_file(
 88        &mut self,
 89        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 90        path: String,
 91    ) -> wasmtime::Result<Result<String, String>> {
 92        let delegate = self.table.get(&delegate)?;
 93        Ok(delegate
 94            .read_text_file(path.into())
 95            .await
 96            .map_err(|error| error.to_string()))
 97    }
 98
 99    async fn shell_env(
100        &mut self,
101        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
102    ) -> wasmtime::Result<EnvVars> {
103        let delegate = self.table.get(&delegate)?;
104        Ok(delegate.shell_env().await.into_iter().collect())
105    }
106
107    async fn which(
108        &mut self,
109        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
110        binary_name: String,
111    ) -> wasmtime::Result<Option<String>> {
112        let delegate = self.table.get(&delegate)?;
113        Ok(delegate
114            .which(binary_name.as_ref())
115            .await
116            .map(|path| path.to_string_lossy().to_string()))
117    }
118
119    fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
120        // We only ever hand out borrows of worktrees.
121        Ok(())
122    }
123}
124
125#[async_trait]
126impl common::Host for WasmState {}
127
128#[async_trait]
129impl http_client::Host for WasmState {
130    async fn fetch(
131        &mut self,
132        req: http_client::HttpRequest,
133    ) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
134        maybe!(async {
135            let url = &req.url;
136
137            let mut response = self
138                .host
139                .http_client
140                .get(url, AsyncBody::default(), true)
141                .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
147            let mut body = Vec::new();
148            response
149                .body_mut()
150                .read_to_end(&mut body)
151                .await
152                .with_context(|| format!("failed to read response body from '{url}'"))?;
153
154            Ok(http_client::HttpResponse {
155                body: String::from_utf8(body)?,
156            })
157        })
158        .await
159        .to_wasmtime_result()
160    }
161}
162
163#[async_trait]
164impl nodejs::Host for WasmState {
165    async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
166        self.host
167            .node_runtime
168            .binary_path()
169            .await
170            .map(|path| path.to_string_lossy().to_string())
171            .to_wasmtime_result()
172    }
173
174    async fn npm_package_latest_version(
175        &mut self,
176        package_name: String,
177    ) -> wasmtime::Result<Result<String, String>> {
178        self.host
179            .node_runtime
180            .npm_package_latest_version(&package_name)
181            .await
182            .to_wasmtime_result()
183    }
184
185    async fn npm_package_installed_version(
186        &mut self,
187        package_name: String,
188    ) -> wasmtime::Result<Result<Option<String>, String>> {
189        self.host
190            .node_runtime
191            .npm_package_installed_version(&self.work_dir(), &package_name)
192            .await
193            .to_wasmtime_result()
194    }
195
196    async fn npm_install_package(
197        &mut self,
198        package_name: String,
199        version: String,
200    ) -> wasmtime::Result<Result<(), String>> {
201        self.host
202            .node_runtime
203            .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
204            .await
205            .to_wasmtime_result()
206    }
207}
208
209#[async_trait]
210impl lsp::Host for WasmState {}
211
212impl From<::http_client::github::GithubRelease> for github::GithubRelease {
213    fn from(value: ::http_client::github::GithubRelease) -> Self {
214        Self {
215            version: value.tag_name,
216            assets: value.assets.into_iter().map(Into::into).collect(),
217        }
218    }
219}
220
221impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
222    fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
223        Self {
224            name: value.name,
225            download_url: value.browser_download_url,
226        }
227    }
228}
229
230#[async_trait]
231impl github::Host for WasmState {
232    async fn latest_github_release(
233        &mut self,
234        repo: String,
235        options: github::GithubReleaseOptions,
236    ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
237        maybe!(async {
238            let release = ::http_client::github::latest_github_release(
239                &repo,
240                options.require_assets,
241                options.pre_release,
242                self.host.http_client.clone(),
243            )
244            .await?;
245            Ok(release.into())
246        })
247        .await
248        .to_wasmtime_result()
249    }
250
251    async fn github_release_by_tag_name(
252        &mut self,
253        repo: String,
254        tag: String,
255    ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
256        maybe!(async {
257            let release = ::http_client::github::get_release_by_tag_name(
258                &repo,
259                &tag,
260                self.host.http_client.clone(),
261            )
262            .await?;
263            Ok(release.into())
264        })
265        .await
266        .to_wasmtime_result()
267    }
268}
269
270#[async_trait]
271impl platform::Host for WasmState {
272    async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
273        Ok((
274            match env::consts::OS {
275                "macos" => platform::Os::Mac,
276                "linux" => platform::Os::Linux,
277                "windows" => platform::Os::Windows,
278                _ => panic!("unsupported os"),
279            },
280            match env::consts::ARCH {
281                "aarch64" => platform::Architecture::Aarch64,
282                "x86" => platform::Architecture::X86,
283                "x86_64" => platform::Architecture::X8664,
284                _ => panic!("unsupported architecture"),
285            },
286        ))
287    }
288}
289
290#[async_trait]
291impl slash_command::Host for WasmState {}
292
293#[async_trait]
294impl ExtensionImports for WasmState {
295    async fn get_settings(
296        &mut self,
297        location: Option<self::SettingsLocation>,
298        category: String,
299        key: Option<String>,
300    ) -> wasmtime::Result<Result<String, String>> {
301        self.on_main_thread(|cx| {
302            async move {
303                let location = location
304                    .as_ref()
305                    .map(|location| ::settings::SettingsLocation {
306                        worktree_id: location.worktree_id as usize,
307                        path: Path::new(&location.path),
308                    });
309
310                cx.update(|cx| match category.as_str() {
311                    "language" => {
312                        let settings =
313                            AllLanguageSettings::get(location, cx).language(key.as_deref());
314                        Ok(serde_json::to_string(&settings::LanguageSettings {
315                            tab_size: settings.tab_size,
316                        })?)
317                    }
318                    "lsp" => {
319                        let settings = key
320                            .and_then(|key| {
321                                ProjectSettings::get(location, cx)
322                                    .lsp
323                                    .get(&Arc::<str>::from(key))
324                            })
325                            .cloned()
326                            .unwrap_or_default();
327                        Ok(serde_json::to_string(&settings::LspSettings {
328                            binary: settings.binary.map(|binary| settings::BinarySettings {
329                                path: binary.path,
330                                arguments: binary.arguments,
331                            }),
332                            settings: settings.settings,
333                            initialization_options: settings.initialization_options,
334                        })?)
335                    }
336                    _ => {
337                        bail!("Unknown settings category: {}", category);
338                    }
339                })
340            }
341            .boxed_local()
342        })
343        .await?
344        .to_wasmtime_result()
345    }
346
347    async fn set_language_server_installation_status(
348        &mut self,
349        server_name: String,
350        status: LanguageServerInstallationStatus,
351    ) -> wasmtime::Result<()> {
352        let status = match status {
353            LanguageServerInstallationStatus::CheckingForUpdate => {
354                LanguageServerBinaryStatus::CheckingForUpdate
355            }
356            LanguageServerInstallationStatus::Downloading => {
357                LanguageServerBinaryStatus::Downloading
358            }
359            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
360            LanguageServerInstallationStatus::Failed(error) => {
361                LanguageServerBinaryStatus::Failed { error }
362            }
363        };
364
365        self.host
366            .language_registry
367            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
368        Ok(())
369    }
370
371    async fn download_file(
372        &mut self,
373        url: String,
374        path: String,
375        file_type: DownloadedFileType,
376    ) -> wasmtime::Result<Result<(), String>> {
377        maybe!(async {
378            let path = PathBuf::from(path);
379            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
380
381            self.host.fs.create_dir(&extension_work_dir).await?;
382
383            let destination_path = self
384                .host
385                .writeable_path_from_extension(&self.manifest.id, &path)?;
386
387            let mut response = self
388                .host
389                .http_client
390                .get(&url, Default::default(), true)
391                .await
392                .map_err(|err| anyhow!("error downloading release: {}", err))?;
393
394            if !response.status().is_success() {
395                Err(anyhow!(
396                    "download failed with status {}",
397                    response.status().to_string()
398                ))?;
399            }
400            let body = BufReader::new(response.body_mut());
401
402            match file_type {
403                DownloadedFileType::Uncompressed => {
404                    futures::pin_mut!(body);
405                    self.host
406                        .fs
407                        .create_file_with(&destination_path, body)
408                        .await?;
409                }
410                DownloadedFileType::Gzip => {
411                    let body = GzipDecoder::new(body);
412                    futures::pin_mut!(body);
413                    self.host
414                        .fs
415                        .create_file_with(&destination_path, body)
416                        .await?;
417                }
418                DownloadedFileType::GzipTar => {
419                    let body = GzipDecoder::new(body);
420                    futures::pin_mut!(body);
421                    self.host
422                        .fs
423                        .extract_tar_file(&destination_path, Archive::new(body))
424                        .await?;
425                }
426                DownloadedFileType::Zip => {
427                    futures::pin_mut!(body);
428                    node_runtime::extract_zip(&destination_path, body)
429                        .await
430                        .with_context(|| format!("failed to unzip {} archive", path.display()))?;
431                }
432            }
433
434            Ok(())
435        })
436        .await
437        .to_wasmtime_result()
438    }
439
440    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
441        #[allow(unused)]
442        let path = self
443            .host
444            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
445
446        #[cfg(unix)]
447        {
448            use std::fs::{self, Permissions};
449            use std::os::unix::fs::PermissionsExt;
450
451            return fs::set_permissions(&path, Permissions::from_mode(0o755))
452                .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
453                .to_wasmtime_result();
454        }
455
456        #[cfg(not(unix))]
457        Ok(Ok(()))
458    }
459}