v0_0_4.rs

  1use crate::wasm_host::WasmState;
  2use anyhow::{anyhow, Result};
  3use async_compression::futures::bufread::GzipDecoder;
  4use async_tar::Archive;
  5use async_trait::async_trait;
  6use futures::io::BufReader;
  7use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
  8use std::{
  9    env,
 10    path::PathBuf,
 11    sync::{Arc, OnceLock},
 12};
 13use util::{maybe, SemanticVersion};
 14use wasmtime::component::{Linker, Resource};
 15
 16pub const VERSION: SemanticVersion = SemanticVersion {
 17    major: 0,
 18    minor: 0,
 19    patch: 4,
 20};
 21
 22wasmtime::component::bindgen!({
 23    async: true,
 24    path: "../extension_api/wit/0.0.4",
 25    with: {
 26         "worktree": ExtensionWorktree,
 27    },
 28});
 29
 30pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
 31
 32pub fn linker() -> &'static Linker<WasmState> {
 33    static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
 34    LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
 35}
 36
 37#[async_trait]
 38impl HostWorktree for WasmState {
 39    async fn read_text_file(
 40        &mut self,
 41        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 42        path: String,
 43    ) -> wasmtime::Result<Result<String, String>> {
 44        let delegate = self.table.get(&delegate)?;
 45        Ok(delegate
 46            .read_text_file(path.into())
 47            .await
 48            .map_err(|error| error.to_string()))
 49    }
 50
 51    async fn shell_env(
 52        &mut self,
 53        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 54    ) -> wasmtime::Result<EnvVars> {
 55        let delegate = self.table.get(&delegate)?;
 56        Ok(delegate.shell_env().await.into_iter().collect())
 57    }
 58
 59    async fn which(
 60        &mut self,
 61        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 62        binary_name: String,
 63    ) -> wasmtime::Result<Option<String>> {
 64        let delegate = self.table.get(&delegate)?;
 65        Ok(delegate
 66            .which(binary_name.as_ref())
 67            .await
 68            .map(|path| path.to_string_lossy().to_string()))
 69    }
 70
 71    fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
 72        // we only ever hand out borrows of worktrees
 73        Ok(())
 74    }
 75}
 76
 77#[async_trait]
 78impl ExtensionImports for WasmState {
 79    async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
 80        convert_result(
 81            self.host
 82                .node_runtime
 83                .binary_path()
 84                .await
 85                .map(|path| path.to_string_lossy().to_string()),
 86        )
 87    }
 88
 89    async fn npm_package_latest_version(
 90        &mut self,
 91        package_name: String,
 92    ) -> wasmtime::Result<Result<String, String>> {
 93        convert_result(
 94            self.host
 95                .node_runtime
 96                .npm_package_latest_version(&package_name)
 97                .await,
 98        )
 99    }
100
101    async fn npm_package_installed_version(
102        &mut self,
103        package_name: String,
104    ) -> wasmtime::Result<Result<Option<String>, String>> {
105        convert_result(
106            self.host
107                .node_runtime
108                .npm_package_installed_version(&self.work_dir(), &package_name)
109                .await,
110        )
111    }
112
113    async fn npm_install_package(
114        &mut self,
115        package_name: String,
116        version: String,
117    ) -> wasmtime::Result<Result<(), String>> {
118        convert_result(
119            self.host
120                .node_runtime
121                .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
122                .await,
123        )
124    }
125
126    async fn latest_github_release(
127        &mut self,
128        repo: String,
129        options: GithubReleaseOptions,
130    ) -> wasmtime::Result<Result<GithubRelease, String>> {
131        convert_result(
132            maybe!(async {
133                let release = util::github::latest_github_release(
134                    &repo,
135                    options.require_assets,
136                    options.pre_release,
137                    self.host.http_client.clone(),
138                )
139                .await?;
140                Ok(GithubRelease {
141                    version: release.tag_name,
142                    assets: release
143                        .assets
144                        .into_iter()
145                        .map(|asset| GithubReleaseAsset {
146                            name: asset.name,
147                            download_url: asset.browser_download_url,
148                        })
149                        .collect(),
150                })
151            })
152            .await,
153        )
154    }
155
156    async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
157        Ok((
158            match env::consts::OS {
159                "macos" => Os::Mac,
160                "linux" => Os::Linux,
161                "windows" => Os::Windows,
162                _ => panic!("unsupported os"),
163            },
164            match env::consts::ARCH {
165                "aarch64" => Architecture::Aarch64,
166                "x86" => Architecture::X86,
167                "x86_64" => Architecture::X8664,
168                _ => panic!("unsupported architecture"),
169            },
170        ))
171    }
172
173    async fn set_language_server_installation_status(
174        &mut self,
175        server_name: String,
176        status: LanguageServerInstallationStatus,
177    ) -> wasmtime::Result<()> {
178        let status = match status {
179            LanguageServerInstallationStatus::CheckingForUpdate => {
180                LanguageServerBinaryStatus::CheckingForUpdate
181            }
182            LanguageServerInstallationStatus::Downloading => {
183                LanguageServerBinaryStatus::Downloading
184            }
185            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
186            LanguageServerInstallationStatus::Failed(error) => {
187                LanguageServerBinaryStatus::Failed { error }
188            }
189        };
190
191        self.host
192            .language_registry
193            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
194        Ok(())
195    }
196
197    async fn download_file(
198        &mut self,
199        url: String,
200        path: String,
201        file_type: DownloadedFileType,
202    ) -> wasmtime::Result<Result<(), String>> {
203        let result = maybe!(async {
204            let path = PathBuf::from(path);
205            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
206
207            self.host.fs.create_dir(&extension_work_dir).await?;
208
209            let destination_path = self
210                .host
211                .writeable_path_from_extension(&self.manifest.id, &path)?;
212
213            let mut response = self
214                .host
215                .http_client
216                .get(&url, Default::default(), true)
217                .await
218                .map_err(|err| anyhow!("error downloading release: {}", err))?;
219
220            if !response.status().is_success() {
221                Err(anyhow!(
222                    "download failed with status {}",
223                    response.status().to_string()
224                ))?;
225            }
226            let body = BufReader::new(response.body_mut());
227
228            match file_type {
229                DownloadedFileType::Uncompressed => {
230                    futures::pin_mut!(body);
231                    self.host
232                        .fs
233                        .create_file_with(&destination_path, body)
234                        .await?;
235                }
236                DownloadedFileType::Gzip => {
237                    let body = GzipDecoder::new(body);
238                    futures::pin_mut!(body);
239                    self.host
240                        .fs
241                        .create_file_with(&destination_path, body)
242                        .await?;
243                }
244                DownloadedFileType::GzipTar => {
245                    let body = GzipDecoder::new(body);
246                    futures::pin_mut!(body);
247                    self.host
248                        .fs
249                        .extract_tar_file(&destination_path, Archive::new(body))
250                        .await?;
251                }
252                DownloadedFileType::Zip => {
253                    let file_name = destination_path
254                        .file_name()
255                        .ok_or_else(|| anyhow!("invalid download path"))?
256                        .to_string_lossy();
257                    let zip_filename = format!("{file_name}.zip");
258                    let mut zip_path = destination_path.clone();
259                    zip_path.set_file_name(zip_filename);
260
261                    futures::pin_mut!(body);
262                    self.host.fs.create_file_with(&zip_path, body).await?;
263
264                    let unzip_status = std::process::Command::new("unzip")
265                        .current_dir(&extension_work_dir)
266                        .arg(&zip_path)
267                        .output()?
268                        .status;
269                    if !unzip_status.success() {
270                        Err(anyhow!("failed to unzip {} archive", path.display()))?;
271                    }
272                }
273            }
274
275            Ok(())
276        })
277        .await;
278        convert_result(result)
279    }
280}
281
282fn convert_result<T>(result: Result<T>) -> wasmtime::Result<Result<T, String>> {
283    Ok(result.map_err(|error| error.to_string()))
284}