since_v0_0_6.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, 6);
 22pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
 23
 24wasmtime::component::bindgen!({
 25    async: true,
 26    path: "../extension_api/wit/since_v0.0.6",
 27    with: {
 28         "worktree": ExtensionWorktree,
 29    },
 30});
 31
 32pub use self::zed::extension::*;
 33
 34mod settings {
 35    include!("../../../../extension_api/wit/since_v0.0.6/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
150#[async_trait]
151impl github::Host for WasmState {
152    async fn latest_github_release(
153        &mut self,
154        repo: String,
155        options: github::GithubReleaseOptions,
156    ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
157        maybe!(async {
158            let release = util::github::latest_github_release(
159                &repo,
160                options.require_assets,
161                options.pre_release,
162                self.host.http_client.clone(),
163            )
164            .await?;
165            Ok(github::GithubRelease {
166                version: release.tag_name,
167                assets: release
168                    .assets
169                    .into_iter()
170                    .map(|asset| github::GithubReleaseAsset {
171                        name: asset.name,
172                        download_url: asset.browser_download_url,
173                    })
174                    .collect(),
175            })
176        })
177        .await
178        .to_wasmtime_result()
179    }
180}
181
182#[async_trait]
183impl platform::Host for WasmState {
184    async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
185        Ok((
186            match env::consts::OS {
187                "macos" => platform::Os::Mac,
188                "linux" => platform::Os::Linux,
189                "windows" => platform::Os::Windows,
190                _ => panic!("unsupported os"),
191            },
192            match env::consts::ARCH {
193                "aarch64" => platform::Architecture::Aarch64,
194                "x86" => platform::Architecture::X86,
195                "x86_64" => platform::Architecture::X8664,
196                _ => panic!("unsupported architecture"),
197            },
198        ))
199    }
200}
201
202#[async_trait]
203impl ExtensionImports for WasmState {
204    async fn get_settings(
205        &mut self,
206        location: Option<self::SettingsLocation>,
207        category: String,
208        key: Option<String>,
209    ) -> wasmtime::Result<Result<String, String>> {
210        self.on_main_thread(|cx| {
211            async move {
212                let location = location
213                    .as_ref()
214                    .map(|location| ::settings::SettingsLocation {
215                        worktree_id: location.worktree_id as usize,
216                        path: Path::new(&location.path),
217                    });
218
219                cx.update(|cx| match category.as_str() {
220                    "language" => {
221                        let settings =
222                            AllLanguageSettings::get(location, cx).language(key.as_deref());
223                        Ok(serde_json::to_string(&settings::LanguageSettings {
224                            tab_size: settings.tab_size,
225                        })?)
226                    }
227                    "lsp" => {
228                        let settings = key
229                            .and_then(|key| {
230                                ProjectSettings::get(location, cx)
231                                    .lsp
232                                    .get(&Arc::<str>::from(key))
233                            })
234                            .cloned()
235                            .unwrap_or_default();
236                        Ok(serde_json::to_string(&settings::LspSettings {
237                            binary: settings.binary.map(|binary| settings::BinarySettings {
238                                path: binary.path,
239                                arguments: binary.arguments,
240                            }),
241                            settings: settings.settings,
242                            initialization_options: settings.initialization_options,
243                        })?)
244                    }
245                    _ => {
246                        bail!("Unknown settings category: {}", category);
247                    }
248                })
249            }
250            .boxed_local()
251        })
252        .await?
253        .to_wasmtime_result()
254    }
255
256    async fn set_language_server_installation_status(
257        &mut self,
258        server_name: String,
259        status: LanguageServerInstallationStatus,
260    ) -> wasmtime::Result<()> {
261        let status = match status {
262            LanguageServerInstallationStatus::CheckingForUpdate => {
263                LanguageServerBinaryStatus::CheckingForUpdate
264            }
265            LanguageServerInstallationStatus::Downloading => {
266                LanguageServerBinaryStatus::Downloading
267            }
268            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
269            LanguageServerInstallationStatus::Failed(error) => {
270                LanguageServerBinaryStatus::Failed { error }
271            }
272        };
273
274        self.host
275            .language_registry
276            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
277        Ok(())
278    }
279
280    async fn download_file(
281        &mut self,
282        url: String,
283        path: String,
284        file_type: DownloadedFileType,
285    ) -> wasmtime::Result<Result<(), String>> {
286        maybe!(async {
287            let path = PathBuf::from(path);
288            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
289
290            self.host.fs.create_dir(&extension_work_dir).await?;
291
292            let destination_path = self
293                .host
294                .writeable_path_from_extension(&self.manifest.id, &path)?;
295
296            let mut response = self
297                .host
298                .http_client
299                .get(&url, Default::default(), true)
300                .await
301                .map_err(|err| anyhow!("error downloading release: {}", err))?;
302
303            if !response.status().is_success() {
304                Err(anyhow!(
305                    "download failed with status {}",
306                    response.status().to_string()
307                ))?;
308            }
309            let body = BufReader::new(response.body_mut());
310
311            match file_type {
312                DownloadedFileType::Uncompressed => {
313                    futures::pin_mut!(body);
314                    self.host
315                        .fs
316                        .create_file_with(&destination_path, body)
317                        .await?;
318                }
319                DownloadedFileType::Gzip => {
320                    let body = GzipDecoder::new(body);
321                    futures::pin_mut!(body);
322                    self.host
323                        .fs
324                        .create_file_with(&destination_path, body)
325                        .await?;
326                }
327                DownloadedFileType::GzipTar => {
328                    let body = GzipDecoder::new(body);
329                    futures::pin_mut!(body);
330                    self.host
331                        .fs
332                        .extract_tar_file(&destination_path, Archive::new(body))
333                        .await?;
334                }
335                DownloadedFileType::Zip => {
336                    let file_name = destination_path
337                        .file_name()
338                        .ok_or_else(|| anyhow!("invalid download path"))?
339                        .to_string_lossy();
340                    let zip_filename = format!("{file_name}.zip");
341                    let mut zip_path = destination_path.clone();
342                    zip_path.set_file_name(zip_filename);
343
344                    futures::pin_mut!(body);
345                    self.host.fs.create_file_with(&zip_path, body).await?;
346
347                    let unzip_status = std::process::Command::new("unzip")
348                        .current_dir(&extension_work_dir)
349                        .arg("-d")
350                        .arg(&destination_path)
351                        .arg(&zip_path)
352                        .output()?
353                        .status;
354                    if !unzip_status.success() {
355                        Err(anyhow!("failed to unzip {} archive", path.display()))?;
356                    }
357                }
358            }
359
360            Ok(())
361        })
362        .await
363        .to_wasmtime_result()
364    }
365
366    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
367        #[allow(unused)]
368        let path = self
369            .host
370            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
371
372        #[cfg(unix)]
373        {
374            use std::fs::{self, Permissions};
375            use std::os::unix::fs::PermissionsExt;
376
377            return fs::set_permissions(&path, Permissions::from_mode(0o755))
378                .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
379                .to_wasmtime_result();
380        }
381
382        #[cfg(not(unix))]
383        Ok(Ok(()))
384    }
385}