since_v0_0_7.rs

  1use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
  2use ::settings::Settings;
  3use anyhow::{anyhow, bail, Context, Result};
  4use async_compression::futures::bufread::GzipDecoder;
  5use async_tar::Archive;
  6use async_trait::async_trait;
  7use futures::AsyncReadExt;
  8use futures::{io::BufReader, FutureExt as _};
  9use http::AsyncBody;
 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::github::GithubRelease> for github::GithubRelease {
213    fn from(value: http::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::github::GithubReleaseAsset> for github::GithubReleaseAsset {
222    fn from(value: http::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::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 =
258                http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
259                    .await?;
260            Ok(release.into())
261        })
262        .await
263        .to_wasmtime_result()
264    }
265}
266
267#[async_trait]
268impl platform::Host for WasmState {
269    async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
270        Ok((
271            match env::consts::OS {
272                "macos" => platform::Os::Mac,
273                "linux" => platform::Os::Linux,
274                "windows" => platform::Os::Windows,
275                _ => panic!("unsupported os"),
276            },
277            match env::consts::ARCH {
278                "aarch64" => platform::Architecture::Aarch64,
279                "x86" => platform::Architecture::X86,
280                "x86_64" => platform::Architecture::X8664,
281                _ => panic!("unsupported architecture"),
282            },
283        ))
284    }
285}
286
287#[async_trait]
288impl slash_command::Host for WasmState {}
289
290#[async_trait]
291impl ExtensionImports for WasmState {
292    async fn get_settings(
293        &mut self,
294        location: Option<self::SettingsLocation>,
295        category: String,
296        key: Option<String>,
297    ) -> wasmtime::Result<Result<String, String>> {
298        self.on_main_thread(|cx| {
299            async move {
300                let location = location
301                    .as_ref()
302                    .map(|location| ::settings::SettingsLocation {
303                        worktree_id: location.worktree_id as usize,
304                        path: Path::new(&location.path),
305                    });
306
307                cx.update(|cx| match category.as_str() {
308                    "language" => {
309                        let settings =
310                            AllLanguageSettings::get(location, cx).language(key.as_deref());
311                        Ok(serde_json::to_string(&settings::LanguageSettings {
312                            tab_size: settings.tab_size,
313                        })?)
314                    }
315                    "lsp" => {
316                        let settings = key
317                            .and_then(|key| {
318                                ProjectSettings::get(location, cx)
319                                    .lsp
320                                    .get(&Arc::<str>::from(key))
321                            })
322                            .cloned()
323                            .unwrap_or_default();
324                        Ok(serde_json::to_string(&settings::LspSettings {
325                            binary: settings.binary.map(|binary| settings::BinarySettings {
326                                path: binary.path,
327                                arguments: binary.arguments,
328                            }),
329                            settings: settings.settings,
330                            initialization_options: settings.initialization_options,
331                        })?)
332                    }
333                    _ => {
334                        bail!("Unknown settings category: {}", category);
335                    }
336                })
337            }
338            .boxed_local()
339        })
340        .await?
341        .to_wasmtime_result()
342    }
343
344    async fn set_language_server_installation_status(
345        &mut self,
346        server_name: String,
347        status: LanguageServerInstallationStatus,
348    ) -> wasmtime::Result<()> {
349        let status = match status {
350            LanguageServerInstallationStatus::CheckingForUpdate => {
351                LanguageServerBinaryStatus::CheckingForUpdate
352            }
353            LanguageServerInstallationStatus::Downloading => {
354                LanguageServerBinaryStatus::Downloading
355            }
356            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
357            LanguageServerInstallationStatus::Failed(error) => {
358                LanguageServerBinaryStatus::Failed { error }
359            }
360        };
361
362        self.host
363            .language_registry
364            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
365        Ok(())
366    }
367
368    async fn download_file(
369        &mut self,
370        url: String,
371        path: String,
372        file_type: DownloadedFileType,
373    ) -> wasmtime::Result<Result<(), String>> {
374        maybe!(async {
375            let path = PathBuf::from(path);
376            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
377
378            self.host.fs.create_dir(&extension_work_dir).await?;
379
380            let destination_path = self
381                .host
382                .writeable_path_from_extension(&self.manifest.id, &path)?;
383
384            let mut response = self
385                .host
386                .http_client
387                .get(&url, Default::default(), true)
388                .await
389                .map_err(|err| anyhow!("error downloading release: {}", err))?;
390
391            if !response.status().is_success() {
392                Err(anyhow!(
393                    "download failed with status {}",
394                    response.status().to_string()
395                ))?;
396            }
397            let body = BufReader::new(response.body_mut());
398
399            match file_type {
400                DownloadedFileType::Uncompressed => {
401                    futures::pin_mut!(body);
402                    self.host
403                        .fs
404                        .create_file_with(&destination_path, body)
405                        .await?;
406                }
407                DownloadedFileType::Gzip => {
408                    let body = GzipDecoder::new(body);
409                    futures::pin_mut!(body);
410                    self.host
411                        .fs
412                        .create_file_with(&destination_path, body)
413                        .await?;
414                }
415                DownloadedFileType::GzipTar => {
416                    let body = GzipDecoder::new(body);
417                    futures::pin_mut!(body);
418                    self.host
419                        .fs
420                        .extract_tar_file(&destination_path, Archive::new(body))
421                        .await?;
422                }
423                DownloadedFileType::Zip => {
424                    futures::pin_mut!(body);
425                    node_runtime::extract_zip(&destination_path, body)
426                        .await
427                        .with_context(|| format!("failed to unzip {} archive", path.display()))?;
428                }
429            }
430
431            Ok(())
432        })
433        .await
434        .to_wasmtime_result()
435    }
436
437    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
438        #[allow(unused)]
439        let path = self
440            .host
441            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
442
443        #[cfg(unix)]
444        {
445            use std::fs::{self, Permissions};
446            use std::os::unix::fs::PermissionsExt;
447
448            return fs::set_permissions(&path, Permissions::from_mode(0o755))
449                .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
450                .to_wasmtime_result();
451        }
452
453        #[cfg(not(unix))]
454        Ok(Ok(()))
455    }
456}