since_v0_0_7.rs

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