1use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
2use ::http_client::AsyncBody;
3use ::settings::{Settings, WorktreeId};
4use anyhow::{anyhow, bail, Context, Result};
5use async_compression::futures::bufread::GzipDecoder;
6use async_tar::Archive;
7use async_trait::async_trait;
8use futures::{io::BufReader, FutureExt as _};
9use futures::{lock::Mutex, AsyncReadExt};
10use indexed_docs::IndexedDocsDatabase;
11use isahc::config::{Configurable, RedirectPolicy};
12use language::LanguageName;
13use language::{
14 language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
15};
16use project::project_settings::ProjectSettings;
17use semantic_version::SemanticVersion;
18use std::{
19 path::{Path, PathBuf},
20 sync::{Arc, OnceLock},
21};
22use util::maybe;
23use wasmtime::component::{Linker, Resource};
24
25use super::latest;
26
27pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
28pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
29
30wasmtime::component::bindgen!({
31 async: true,
32 trappable_imports: true,
33 path: "../extension_api/wit/since_v0.1.0",
34 with: {
35 "worktree": ExtensionWorktree,
36 "key-value-store": ExtensionKeyValueStore,
37 "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream,
38 "zed:extension/github": latest::zed::extension::github,
39 "zed:extension/lsp": latest::zed::extension::lsp,
40 "zed:extension/nodejs": latest::zed::extension::nodejs,
41 "zed:extension/platform": latest::zed::extension::platform,
42 "zed:extension/slash-command": latest::zed::extension::slash_command,
43 },
44});
45
46pub use self::zed::extension::*;
47
48mod settings {
49 include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs"));
50}
51
52pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
53pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
54pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
55
56pub fn linker() -> &'static Linker<WasmState> {
57 static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
58 LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
59}
60
61impl From<Command> for latest::Command {
62 fn from(value: Command) -> Self {
63 Self {
64 command: value.command,
65 args: value.args,
66 env: value.env,
67 }
68 }
69}
70
71impl From<SettingsLocation> for latest::SettingsLocation {
72 fn from(value: SettingsLocation) -> Self {
73 Self {
74 worktree_id: value.worktree_id,
75 path: value.path,
76 }
77 }
78}
79
80impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
81 fn from(value: LanguageServerInstallationStatus) -> Self {
82 match value {
83 LanguageServerInstallationStatus::None => Self::None,
84 LanguageServerInstallationStatus::Downloading => Self::Downloading,
85 LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
86 LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
87 }
88 }
89}
90
91impl From<DownloadedFileType> for latest::DownloadedFileType {
92 fn from(value: DownloadedFileType) -> Self {
93 match value {
94 DownloadedFileType::Gzip => Self::Gzip,
95 DownloadedFileType::GzipTar => Self::GzipTar,
96 DownloadedFileType::Zip => Self::Zip,
97 DownloadedFileType::Uncompressed => Self::Uncompressed,
98 }
99 }
100}
101
102impl From<Range> for latest::Range {
103 fn from(value: Range) -> Self {
104 Self {
105 start: value.start,
106 end: value.end,
107 }
108 }
109}
110
111impl From<CodeLabelSpan> for latest::CodeLabelSpan {
112 fn from(value: CodeLabelSpan) -> Self {
113 match value {
114 CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
115 CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
116 }
117 }
118}
119
120impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
121 fn from(value: CodeLabelSpanLiteral) -> Self {
122 Self {
123 text: value.text,
124 highlight_name: value.highlight_name,
125 }
126 }
127}
128
129impl From<CodeLabel> for latest::CodeLabel {
130 fn from(value: CodeLabel) -> Self {
131 Self {
132 code: value.code,
133 spans: value.spans.into_iter().map(Into::into).collect(),
134 filter_range: value.filter_range.into(),
135 }
136 }
137}
138
139#[async_trait]
140impl HostKeyValueStore for WasmState {
141 async fn insert(
142 &mut self,
143 kv_store: Resource<ExtensionKeyValueStore>,
144 key: String,
145 value: String,
146 ) -> wasmtime::Result<Result<(), String>> {
147 let kv_store = self.table.get(&kv_store)?;
148 kv_store.insert(key, value).await.to_wasmtime_result()
149 }
150
151 fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
152 // We only ever hand out borrows of key-value stores.
153 Ok(())
154 }
155}
156
157#[async_trait]
158impl HostWorktree for WasmState {
159 async fn id(
160 &mut self,
161 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
162 ) -> wasmtime::Result<u64> {
163 let delegate = self.table.get(&delegate)?;
164 Ok(delegate.worktree_id().to_proto())
165 }
166
167 async fn root_path(
168 &mut self,
169 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
170 ) -> wasmtime::Result<String> {
171 let delegate = self.table.get(&delegate)?;
172 Ok(delegate.worktree_root_path().to_string_lossy().to_string())
173 }
174
175 async fn read_text_file(
176 &mut self,
177 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
178 path: String,
179 ) -> wasmtime::Result<Result<String, String>> {
180 let delegate = self.table.get(&delegate)?;
181 Ok(delegate
182 .read_text_file(path.into())
183 .await
184 .map_err(|error| error.to_string()))
185 }
186
187 async fn shell_env(
188 &mut self,
189 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
190 ) -> wasmtime::Result<EnvVars> {
191 let delegate = self.table.get(&delegate)?;
192 Ok(delegate.shell_env().await.into_iter().collect())
193 }
194
195 async fn which(
196 &mut self,
197 delegate: Resource<Arc<dyn LspAdapterDelegate>>,
198 binary_name: String,
199 ) -> wasmtime::Result<Option<String>> {
200 let delegate = self.table.get(&delegate)?;
201 Ok(delegate
202 .which(binary_name.as_ref())
203 .await
204 .map(|path| path.to_string_lossy().to_string()))
205 }
206
207 fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
208 // We only ever hand out borrows of worktrees.
209 Ok(())
210 }
211}
212
213#[async_trait]
214impl common::Host for WasmState {}
215
216#[async_trait]
217impl http_client::Host for WasmState {
218 async fn fetch(
219 &mut self,
220 request: http_client::HttpRequest,
221 ) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
222 maybe!(async {
223 let url = &request.url;
224 let request = convert_request(&request)?;
225 let mut response = self.host.http_client.send(request).await?;
226
227 if response.status().is_client_error() || response.status().is_server_error() {
228 bail!("failed to fetch '{url}': status code {}", response.status())
229 }
230 convert_response(&mut response).await
231 })
232 .await
233 .to_wasmtime_result()
234 }
235
236 async fn fetch_stream(
237 &mut self,
238 request: http_client::HttpRequest,
239 ) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
240 let request = convert_request(&request)?;
241 let response = self.host.http_client.send(request);
242 maybe!(async {
243 let response = response.await?;
244 let stream = Arc::new(Mutex::new(response));
245 let resource = self.table.push(stream)?;
246 Ok(resource)
247 })
248 .await
249 .to_wasmtime_result()
250 }
251}
252
253#[async_trait]
254impl http_client::HostHttpResponseStream for WasmState {
255 async fn next_chunk(
256 &mut self,
257 resource: Resource<ExtensionHttpResponseStream>,
258 ) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
259 let stream = self.table.get(&resource)?.clone();
260 maybe!(async move {
261 let mut response = stream.lock().await;
262 let mut buffer = vec![0; 8192]; // 8KB buffer
263 let bytes_read = response.body_mut().read(&mut buffer).await?;
264 if bytes_read == 0 {
265 Ok(None)
266 } else {
267 buffer.truncate(bytes_read);
268 Ok(Some(buffer))
269 }
270 })
271 .await
272 .to_wasmtime_result()
273 }
274
275 fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
276 Ok(())
277 }
278}
279
280impl From<http_client::HttpMethod> for ::http_client::Method {
281 fn from(value: http_client::HttpMethod) -> Self {
282 match value {
283 http_client::HttpMethod::Get => Self::GET,
284 http_client::HttpMethod::Post => Self::POST,
285 http_client::HttpMethod::Put => Self::PUT,
286 http_client::HttpMethod::Delete => Self::DELETE,
287 http_client::HttpMethod::Head => Self::HEAD,
288 http_client::HttpMethod::Options => Self::OPTIONS,
289 http_client::HttpMethod::Patch => Self::PATCH,
290 }
291 }
292}
293
294fn convert_request(
295 extension_request: &http_client::HttpRequest,
296) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
297 let mut request = ::http_client::Request::builder()
298 .method(::http_client::Method::from(extension_request.method))
299 .uri(&extension_request.url)
300 .redirect_policy(match extension_request.redirect_policy {
301 http_client::RedirectPolicy::NoFollow => RedirectPolicy::None,
302 http_client::RedirectPolicy::FollowLimit(limit) => RedirectPolicy::Limit(limit),
303 http_client::RedirectPolicy::FollowAll => RedirectPolicy::Follow,
304 });
305 for (key, value) in &extension_request.headers {
306 request = request.header(key, value);
307 }
308 let body = extension_request
309 .body
310 .clone()
311 .map(AsyncBody::from)
312 .unwrap_or_default();
313 request.body(body).map_err(anyhow::Error::from)
314}
315
316async fn convert_response(
317 response: &mut ::http_client::Response<AsyncBody>,
318) -> Result<http_client::HttpResponse, anyhow::Error> {
319 let mut extension_response = http_client::HttpResponse {
320 body: Vec::new(),
321 headers: Vec::new(),
322 };
323
324 for (key, value) in response.headers() {
325 extension_response
326 .headers
327 .push((key.to_string(), value.to_str().unwrap_or("").to_string()));
328 }
329
330 response
331 .body_mut()
332 .read_to_end(&mut extension_response.body)
333 .await?;
334
335 Ok(extension_response)
336}
337
338#[async_trait]
339impl ExtensionImports for WasmState {
340 async fn get_settings(
341 &mut self,
342 location: Option<self::SettingsLocation>,
343 category: String,
344 key: Option<String>,
345 ) -> wasmtime::Result<Result<String, String>> {
346 self.on_main_thread(|cx| {
347 async move {
348 let location = location
349 .as_ref()
350 .map(|location| ::settings::SettingsLocation {
351 worktree_id: WorktreeId::from_proto(location.worktree_id),
352 path: Path::new(&location.path),
353 });
354
355 cx.update(|cx| match category.as_str() {
356 "language" => {
357 let key = key.map(|k| LanguageName::new(&k));
358 let settings =
359 AllLanguageSettings::get(location, cx).language(key.as_ref());
360 Ok(serde_json::to_string(&settings::LanguageSettings {
361 tab_size: settings.tab_size,
362 })?)
363 }
364 "lsp" => {
365 let settings = key
366 .and_then(|key| {
367 ProjectSettings::get(location, cx)
368 .lsp
369 .get(&Arc::<str>::from(key))
370 })
371 .cloned()
372 .unwrap_or_default();
373 Ok(serde_json::to_string(&settings::LspSettings {
374 binary: settings.binary.map(|binary| settings::BinarySettings {
375 path: binary.path,
376 arguments: binary.arguments,
377 }),
378 settings: settings.settings,
379 initialization_options: settings.initialization_options,
380 })?)
381 }
382 _ => {
383 bail!("Unknown settings category: {}", category);
384 }
385 })
386 }
387 .boxed_local()
388 })
389 .await?
390 .to_wasmtime_result()
391 }
392
393 async fn set_language_server_installation_status(
394 &mut self,
395 server_name: String,
396 status: LanguageServerInstallationStatus,
397 ) -> wasmtime::Result<()> {
398 let status = match status {
399 LanguageServerInstallationStatus::CheckingForUpdate => {
400 LanguageServerBinaryStatus::CheckingForUpdate
401 }
402 LanguageServerInstallationStatus::Downloading => {
403 LanguageServerBinaryStatus::Downloading
404 }
405 LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
406 LanguageServerInstallationStatus::Failed(error) => {
407 LanguageServerBinaryStatus::Failed { error }
408 }
409 };
410
411 self.host
412 .language_registry
413 .update_lsp_status(language::LanguageServerName(server_name.into()), status);
414 Ok(())
415 }
416
417 async fn download_file(
418 &mut self,
419 url: String,
420 path: String,
421 file_type: DownloadedFileType,
422 ) -> wasmtime::Result<Result<(), String>> {
423 maybe!(async {
424 let path = PathBuf::from(path);
425 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
426
427 self.host.fs.create_dir(&extension_work_dir).await?;
428
429 let destination_path = self
430 .host
431 .writeable_path_from_extension(&self.manifest.id, &path)?;
432
433 let mut response = self
434 .host
435 .http_client
436 .get(&url, Default::default(), true)
437 .await
438 .map_err(|err| anyhow!("error downloading release: {}", err))?;
439
440 if !response.status().is_success() {
441 Err(anyhow!(
442 "download failed with status {}",
443 response.status().to_string()
444 ))?;
445 }
446 let body = BufReader::new(response.body_mut());
447
448 match file_type {
449 DownloadedFileType::Uncompressed => {
450 futures::pin_mut!(body);
451 self.host
452 .fs
453 .create_file_with(&destination_path, body)
454 .await?;
455 }
456 DownloadedFileType::Gzip => {
457 let body = GzipDecoder::new(body);
458 futures::pin_mut!(body);
459 self.host
460 .fs
461 .create_file_with(&destination_path, body)
462 .await?;
463 }
464 DownloadedFileType::GzipTar => {
465 let body = GzipDecoder::new(body);
466 futures::pin_mut!(body);
467 self.host
468 .fs
469 .extract_tar_file(&destination_path, Archive::new(body))
470 .await?;
471 }
472 DownloadedFileType::Zip => {
473 futures::pin_mut!(body);
474 node_runtime::extract_zip(&destination_path, body)
475 .await
476 .with_context(|| format!("failed to unzip {} archive", path.display()))?;
477 }
478 }
479
480 Ok(())
481 })
482 .await
483 .to_wasmtime_result()
484 }
485
486 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
487 #[allow(unused)]
488 let path = self
489 .host
490 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
491
492 #[cfg(unix)]
493 {
494 use std::fs::{self, Permissions};
495 use std::os::unix::fs::PermissionsExt;
496
497 return fs::set_permissions(&path, Permissions::from_mode(0o755))
498 .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
499 .to_wasmtime_result();
500 }
501
502 #[cfg(not(unix))]
503 Ok(Ok(()))
504 }
505}