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