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 = http::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}