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 common::Host for WasmState {}
103
104#[async_trait]
105impl nodejs::Host for WasmState {
106    async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
107        self.host
108            .node_runtime
109            .binary_path()
110            .await
111            .map(|path| path.to_string_lossy().to_string())
112            .to_wasmtime_result()
113    }
114
115    async fn npm_package_latest_version(
116        &mut self,
117        package_name: String,
118    ) -> wasmtime::Result<Result<String, String>> {
119        self.host
120            .node_runtime
121            .npm_package_latest_version(&package_name)
122            .await
123            .to_wasmtime_result()
124    }
125
126    async fn npm_package_installed_version(
127        &mut self,
128        package_name: String,
129    ) -> wasmtime::Result<Result<Option<String>, String>> {
130        self.host
131            .node_runtime
132            .npm_package_installed_version(&self.work_dir(), &package_name)
133            .await
134            .to_wasmtime_result()
135    }
136
137    async fn npm_install_package(
138        &mut self,
139        package_name: String,
140        version: String,
141    ) -> wasmtime::Result<Result<(), String>> {
142        self.host
143            .node_runtime
144            .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
145            .await
146            .to_wasmtime_result()
147    }
148}
149
150#[async_trait]
151impl lsp::Host for WasmState {}
152
153impl From<http::github::GithubRelease> for github::GithubRelease {
154    fn from(value: http::github::GithubRelease) -> Self {
155        Self {
156            version: value.tag_name,
157            assets: value.assets.into_iter().map(Into::into).collect(),
158        }
159    }
160}
161
162impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
163    fn from(value: http::github::GithubReleaseAsset) -> Self {
164        Self {
165            name: value.name,
166            download_url: value.browser_download_url,
167        }
168    }
169}
170
171#[async_trait]
172impl github::Host for WasmState {
173    async fn latest_github_release(
174        &mut self,
175        repo: String,
176        options: github::GithubReleaseOptions,
177    ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
178        maybe!(async {
179            let release = http::github::latest_github_release(
180                &repo,
181                options.require_assets,
182                options.pre_release,
183                self.host.http_client.clone(),
184            )
185            .await?;
186            Ok(release.into())
187        })
188        .await
189        .to_wasmtime_result()
190    }
191
192    async fn github_release_by_tag_name(
193        &mut self,
194        repo: String,
195        tag: String,
196    ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
197        maybe!(async {
198            let release =
199                http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
200                    .await?;
201            Ok(release.into())
202        })
203        .await
204        .to_wasmtime_result()
205    }
206}
207
208#[async_trait]
209impl platform::Host for WasmState {
210    async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
211        Ok((
212            match env::consts::OS {
213                "macos" => platform::Os::Mac,
214                "linux" => platform::Os::Linux,
215                "windows" => platform::Os::Windows,
216                _ => panic!("unsupported os"),
217            },
218            match env::consts::ARCH {
219                "aarch64" => platform::Architecture::Aarch64,
220                "x86" => platform::Architecture::X86,
221                "x86_64" => platform::Architecture::X8664,
222                _ => panic!("unsupported architecture"),
223            },
224        ))
225    }
226}
227
228#[async_trait]
229impl slash_command::Host for WasmState {}
230
231#[async_trait]
232impl ExtensionImports for WasmState {
233    async fn get_settings(
234        &mut self,
235        location: Option<self::SettingsLocation>,
236        category: String,
237        key: Option<String>,
238    ) -> wasmtime::Result<Result<String, String>> {
239        self.on_main_thread(|cx| {
240            async move {
241                let location = location
242                    .as_ref()
243                    .map(|location| ::settings::SettingsLocation {
244                        worktree_id: location.worktree_id as usize,
245                        path: Path::new(&location.path),
246                    });
247
248                cx.update(|cx| match category.as_str() {
249                    "language" => {
250                        let settings =
251                            AllLanguageSettings::get(location, cx).language(key.as_deref());
252                        Ok(serde_json::to_string(&settings::LanguageSettings {
253                            tab_size: settings.tab_size,
254                        })?)
255                    }
256                    "lsp" => {
257                        let settings = key
258                            .and_then(|key| {
259                                ProjectSettings::get(location, cx)
260                                    .lsp
261                                    .get(&Arc::<str>::from(key))
262                            })
263                            .cloned()
264                            .unwrap_or_default();
265                        Ok(serde_json::to_string(&settings::LspSettings {
266                            binary: settings.binary.map(|binary| settings::BinarySettings {
267                                path: binary.path,
268                                arguments: binary.arguments,
269                            }),
270                            settings: settings.settings,
271                            initialization_options: settings.initialization_options,
272                        })?)
273                    }
274                    _ => {
275                        bail!("Unknown settings category: {}", category);
276                    }
277                })
278            }
279            .boxed_local()
280        })
281        .await?
282        .to_wasmtime_result()
283    }
284
285    async fn set_language_server_installation_status(
286        &mut self,
287        server_name: String,
288        status: LanguageServerInstallationStatus,
289    ) -> wasmtime::Result<()> {
290        let status = match status {
291            LanguageServerInstallationStatus::CheckingForUpdate => {
292                LanguageServerBinaryStatus::CheckingForUpdate
293            }
294            LanguageServerInstallationStatus::Downloading => {
295                LanguageServerBinaryStatus::Downloading
296            }
297            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
298            LanguageServerInstallationStatus::Failed(error) => {
299                LanguageServerBinaryStatus::Failed { error }
300            }
301        };
302
303        self.host
304            .language_registry
305            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
306        Ok(())
307    }
308
309    async fn download_file(
310        &mut self,
311        url: String,
312        path: String,
313        file_type: DownloadedFileType,
314    ) -> wasmtime::Result<Result<(), String>> {
315        maybe!(async {
316            let path = PathBuf::from(path);
317            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
318
319            self.host.fs.create_dir(&extension_work_dir).await?;
320
321            let destination_path = self
322                .host
323                .writeable_path_from_extension(&self.manifest.id, &path)?;
324
325            let mut response = self
326                .host
327                .http_client
328                .get(&url, Default::default(), true)
329                .await
330                .map_err(|err| anyhow!("error downloading release: {}", err))?;
331
332            if !response.status().is_success() {
333                Err(anyhow!(
334                    "download failed with status {}",
335                    response.status().to_string()
336                ))?;
337            }
338            let body = BufReader::new(response.body_mut());
339
340            match file_type {
341                DownloadedFileType::Uncompressed => {
342                    futures::pin_mut!(body);
343                    self.host
344                        .fs
345                        .create_file_with(&destination_path, body)
346                        .await?;
347                }
348                DownloadedFileType::Gzip => {
349                    let body = GzipDecoder::new(body);
350                    futures::pin_mut!(body);
351                    self.host
352                        .fs
353                        .create_file_with(&destination_path, body)
354                        .await?;
355                }
356                DownloadedFileType::GzipTar => {
357                    let body = GzipDecoder::new(body);
358                    futures::pin_mut!(body);
359                    self.host
360                        .fs
361                        .extract_tar_file(&destination_path, Archive::new(body))
362                        .await?;
363                }
364                DownloadedFileType::Zip => {
365                    let file_name = destination_path
366                        .file_name()
367                        .ok_or_else(|| anyhow!("invalid download path"))?
368                        .to_string_lossy();
369                    let zip_filename = format!("{file_name}.zip");
370                    let mut zip_path = destination_path.clone();
371                    zip_path.set_file_name(zip_filename);
372
373                    futures::pin_mut!(body);
374                    self.host.fs.create_file_with(&zip_path, body).await?;
375
376                    let unzip_status = std::process::Command::new("unzip")
377                        .current_dir(&extension_work_dir)
378                        .arg("-d")
379                        .arg(&destination_path)
380                        .arg(&zip_path)
381                        .output()?
382                        .status;
383                    if !unzip_status.success() {
384                        Err(anyhow!("failed to unzip {} archive", path.display()))?;
385                    }
386                }
387            }
388
389            Ok(())
390        })
391        .await
392        .to_wasmtime_result()
393    }
394
395    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
396        #[allow(unused)]
397        let path = self
398            .host
399            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
400
401        #[cfg(unix)]
402        {
403            use std::fs::{self, Permissions};
404            use std::os::unix::fs::PermissionsExt;
405
406            return fs::set_permissions(&path, Permissions::from_mode(0o755))
407                .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
408                .to_wasmtime_result();
409        }
410
411        #[cfg(not(unix))]
412        Ok(Ok(()))
413    }
414}