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