1use crate::ExtensionSettings;
2use crate::wasm_host::wit::since_v0_8_0::{
3 dap::{
4 AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest,
5 StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate,
6 },
7 lsp::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind},
8 slash_command::SlashCommandOutputSection,
9};
10use crate::wasm_host::{WasmState, wit::ToWasmtimeResult};
11use ::http_client::{AsyncBody, HttpRequestExt};
12use ::settings::{Settings, WorktreeId};
13use anyhow::{Context as _, Result, bail};
14use async_compression::futures::bufread::GzipDecoder;
15use async_tar::Archive;
16use async_trait::async_trait;
17use credentials_provider::CredentialsProvider;
18use extension::{
19 ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
20};
21use futures::{AsyncReadExt, lock::Mutex};
22use futures::{FutureExt as _, io::BufReader};
23use gpui::{BackgroundExecutor, SharedString};
24use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
25use project::project_settings::ProjectSettings;
26use semver::Version;
27use smol::net::TcpListener;
28use std::{
29 env,
30 io::{BufRead, Write},
31 net::Ipv4Addr,
32 path::{Path, PathBuf},
33 str::FromStr,
34 sync::{Arc, OnceLock},
35 time::Duration,
36};
37use task::{SpawnInTerminal, ZedDebugConfig};
38use url::Url;
39use util::{
40 archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath,
41};
42use wasmtime::component::{Linker, Resource};
43
44pub const MIN_VERSION: Version = Version::new(0, 8, 0);
45pub const MAX_VERSION: Version = Version::new(0, 8, 0);
46
47wasmtime::component::bindgen!({
48 async: true,
49 trappable_imports: true,
50 path: "../extension_api/wit/since_v0.8.0",
51 with: {
52 "worktree": ExtensionWorktree,
53 "project": ExtensionProject,
54 "key-value-store": ExtensionKeyValueStore,
55 "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
56 },
57});
58
59pub use self::zed::extension::*;
60
61mod settings {
62 #![allow(dead_code)]
63 include!(concat!(env!("OUT_DIR"), "/since_v0.8.0/settings.rs"));
64}
65
66pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
67pub type ExtensionProject = Arc<dyn ProjectDelegate>;
68pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
69pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
70
71pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
72 static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
73 LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
74}
75
76impl From<Range> for std::ops::Range<usize> {
77 fn from(range: Range) -> Self {
78 let start = range.start as usize;
79 let end = range.end as usize;
80 start..end
81 }
82}
83
84impl From<Command> for extension::Command {
85 fn from(value: Command) -> Self {
86 Self {
87 command: value.command.into(),
88 args: value.args,
89 env: value.env,
90 }
91 }
92}
93
94impl From<StartDebuggingRequestArgumentsRequest>
95 for extension::StartDebuggingRequestArgumentsRequest
96{
97 fn from(value: StartDebuggingRequestArgumentsRequest) -> Self {
98 match value {
99 StartDebuggingRequestArgumentsRequest::Launch => Self::Launch,
100 StartDebuggingRequestArgumentsRequest::Attach => Self::Attach,
101 }
102 }
103}
104impl TryFrom<StartDebuggingRequestArguments> for extension::StartDebuggingRequestArguments {
105 type Error = anyhow::Error;
106
107 fn try_from(value: StartDebuggingRequestArguments) -> Result<Self, Self::Error> {
108 Ok(Self {
109 configuration: serde_json::from_str(&value.configuration)?,
110 request: value.request.into(),
111 })
112 }
113}
114impl From<TcpArguments> for extension::TcpArguments {
115 fn from(value: TcpArguments) -> Self {
116 Self {
117 host: value.host.into(),
118 port: value.port,
119 timeout: value.timeout,
120 }
121 }
122}
123
124impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
125 fn from(value: extension::TcpArgumentsTemplate) -> Self {
126 Self {
127 host: value.host.map(Ipv4Addr::to_bits),
128 port: value.port,
129 timeout: value.timeout,
130 }
131 }
132}
133
134impl From<TcpArgumentsTemplate> for extension::TcpArgumentsTemplate {
135 fn from(value: TcpArgumentsTemplate) -> Self {
136 Self {
137 host: value.host.map(Ipv4Addr::from_bits),
138 port: value.port,
139 timeout: value.timeout,
140 }
141 }
142}
143
144impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
145 type Error = anyhow::Error;
146 fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
147 Ok(Self {
148 label: value.label.to_string(),
149 adapter: value.adapter.to_string(),
150 config: value.config.to_string(),
151 tcp_connection: value.tcp_connection.map(Into::into),
152 })
153 }
154}
155
156impl From<task::DebugRequest> for DebugRequest {
157 fn from(value: task::DebugRequest) -> Self {
158 match value {
159 task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
160 task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
161 }
162 }
163}
164
165impl From<DebugRequest> for task::DebugRequest {
166 fn from(value: DebugRequest) -> Self {
167 match value {
168 DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
169 DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
170 }
171 }
172}
173
174impl From<task::LaunchRequest> for LaunchRequest {
175 fn from(value: task::LaunchRequest) -> Self {
176 Self {
177 program: value.program,
178 cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()),
179 args: value.args,
180 envs: value.env.into_iter().collect(),
181 }
182 }
183}
184
185impl From<task::AttachRequest> for AttachRequest {
186 fn from(value: task::AttachRequest) -> Self {
187 Self {
188 process_id: value.process_id,
189 }
190 }
191}
192
193impl From<LaunchRequest> for task::LaunchRequest {
194 fn from(value: LaunchRequest) -> Self {
195 Self {
196 program: value.program,
197 cwd: value.cwd.map(|p| p.into()),
198 args: value.args,
199 env: value.envs.into_iter().collect(),
200 }
201 }
202}
203impl From<AttachRequest> for task::AttachRequest {
204 fn from(value: AttachRequest) -> Self {
205 Self {
206 process_id: value.process_id,
207 }
208 }
209}
210
211impl From<ZedDebugConfig> for DebugConfig {
212 fn from(value: ZedDebugConfig) -> Self {
213 Self {
214 label: value.label.into(),
215 adapter: value.adapter.into(),
216 request: value.request.into(),
217 stop_on_entry: value.stop_on_entry,
218 }
219 }
220}
221impl TryFrom<DebugAdapterBinary> for extension::DebugAdapterBinary {
222 type Error = anyhow::Error;
223 fn try_from(value: DebugAdapterBinary) -> Result<Self, Self::Error> {
224 Ok(Self {
225 command: value.command,
226 arguments: value.arguments,
227 envs: value.envs.into_iter().collect(),
228 cwd: value.cwd.map(|s| s.into()),
229 connection: value.connection.map(Into::into),
230 request_args: value.request_args.try_into()?,
231 })
232 }
233}
234
235impl From<BuildTaskDefinition> for extension::BuildTaskDefinition {
236 fn from(value: BuildTaskDefinition) -> Self {
237 match value {
238 BuildTaskDefinition::ByName(name) => Self::ByName(name.into()),
239 BuildTaskDefinition::Template(build_task_template) => Self::Template {
240 task_template: build_task_template.template.into(),
241 locator_name: build_task_template.locator_name.map(SharedString::from),
242 },
243 }
244 }
245}
246
247impl From<extension::BuildTaskDefinition> for BuildTaskDefinition {
248 fn from(value: extension::BuildTaskDefinition) -> Self {
249 match value {
250 extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()),
251 extension::BuildTaskDefinition::Template {
252 task_template,
253 locator_name,
254 } => Self::Template(BuildTaskDefinitionTemplatePayload {
255 template: task_template.into(),
256 locator_name: locator_name.map(String::from),
257 }),
258 }
259 }
260}
261impl From<BuildTaskTemplate> for extension::BuildTaskTemplate {
262 fn from(value: BuildTaskTemplate) -> Self {
263 Self {
264 label: value.label,
265 command: value.command,
266 args: value.args,
267 env: value.env.into_iter().collect(),
268 cwd: value.cwd,
269 ..Default::default()
270 }
271 }
272}
273impl From<extension::BuildTaskTemplate> for BuildTaskTemplate {
274 fn from(value: extension::BuildTaskTemplate) -> Self {
275 Self {
276 label: value.label,
277 command: value.command,
278 args: value.args,
279 env: value.env.into_iter().collect(),
280 cwd: value.cwd,
281 }
282 }
283}
284
285impl TryFrom<DebugScenario> for extension::DebugScenario {
286 type Error = anyhow::Error;
287
288 fn try_from(value: DebugScenario) -> std::result::Result<Self, Self::Error> {
289 Ok(Self {
290 adapter: value.adapter.into(),
291 label: value.label.into(),
292 build: value.build.map(Into::into),
293 config: serde_json::Value::from_str(&value.config)?,
294 tcp_connection: value.tcp_connection.map(Into::into),
295 })
296 }
297}
298
299impl From<extension::DebugScenario> for DebugScenario {
300 fn from(value: extension::DebugScenario) -> Self {
301 Self {
302 adapter: value.adapter.into(),
303 label: value.label.into(),
304 build: value.build.map(Into::into),
305 config: value.config.to_string(),
306 tcp_connection: value.tcp_connection.map(Into::into),
307 }
308 }
309}
310
311impl TryFrom<SpawnInTerminal> for ResolvedTask {
312 type Error = anyhow::Error;
313
314 fn try_from(value: SpawnInTerminal) -> Result<Self, Self::Error> {
315 Ok(Self {
316 label: value.label,
317 command: value.command.context("missing command")?,
318 args: value.args,
319 env: value.env.into_iter().collect(),
320 cwd: value.cwd.map(|s| {
321 let s = s.to_string_lossy();
322 if cfg!(target_os = "windows") {
323 s.replace('\\', "/")
324 } else {
325 s.into_owned()
326 }
327 }),
328 })
329 }
330}
331
332impl From<CodeLabel> for extension::CodeLabel {
333 fn from(value: CodeLabel) -> Self {
334 Self {
335 code: value.code,
336 spans: value.spans.into_iter().map(Into::into).collect(),
337 filter_range: value.filter_range.into(),
338 }
339 }
340}
341
342impl From<CodeLabelSpan> for extension::CodeLabelSpan {
343 fn from(value: CodeLabelSpan) -> Self {
344 match value {
345 CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
346 CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
347 }
348 }
349}
350
351impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
352 fn from(value: CodeLabelSpanLiteral) -> Self {
353 Self {
354 text: value.text,
355 highlight_name: value.highlight_name,
356 }
357 }
358}
359
360impl From<extension::Completion> for Completion {
361 fn from(value: extension::Completion) -> Self {
362 Self {
363 label: value.label,
364 label_details: value.label_details.map(Into::into),
365 detail: value.detail,
366 kind: value.kind.map(Into::into),
367 insert_text_format: value.insert_text_format.map(Into::into),
368 }
369 }
370}
371
372impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
373 fn from(value: extension::CompletionLabelDetails) -> Self {
374 Self {
375 detail: value.detail,
376 description: value.description,
377 }
378 }
379}
380
381impl From<extension::CompletionKind> for CompletionKind {
382 fn from(value: extension::CompletionKind) -> Self {
383 match value {
384 extension::CompletionKind::Text => Self::Text,
385 extension::CompletionKind::Method => Self::Method,
386 extension::CompletionKind::Function => Self::Function,
387 extension::CompletionKind::Constructor => Self::Constructor,
388 extension::CompletionKind::Field => Self::Field,
389 extension::CompletionKind::Variable => Self::Variable,
390 extension::CompletionKind::Class => Self::Class,
391 extension::CompletionKind::Interface => Self::Interface,
392 extension::CompletionKind::Module => Self::Module,
393 extension::CompletionKind::Property => Self::Property,
394 extension::CompletionKind::Unit => Self::Unit,
395 extension::CompletionKind::Value => Self::Value,
396 extension::CompletionKind::Enum => Self::Enum,
397 extension::CompletionKind::Keyword => Self::Keyword,
398 extension::CompletionKind::Snippet => Self::Snippet,
399 extension::CompletionKind::Color => Self::Color,
400 extension::CompletionKind::File => Self::File,
401 extension::CompletionKind::Reference => Self::Reference,
402 extension::CompletionKind::Folder => Self::Folder,
403 extension::CompletionKind::EnumMember => Self::EnumMember,
404 extension::CompletionKind::Constant => Self::Constant,
405 extension::CompletionKind::Struct => Self::Struct,
406 extension::CompletionKind::Event => Self::Event,
407 extension::CompletionKind::Operator => Self::Operator,
408 extension::CompletionKind::TypeParameter => Self::TypeParameter,
409 extension::CompletionKind::Other(value) => Self::Other(value),
410 }
411 }
412}
413
414impl From<extension::InsertTextFormat> for InsertTextFormat {
415 fn from(value: extension::InsertTextFormat) -> Self {
416 match value {
417 extension::InsertTextFormat::PlainText => Self::PlainText,
418 extension::InsertTextFormat::Snippet => Self::Snippet,
419 extension::InsertTextFormat::Other(value) => Self::Other(value),
420 }
421 }
422}
423
424impl From<extension::Symbol> for Symbol {
425 fn from(value: extension::Symbol) -> Self {
426 Self {
427 kind: value.kind.into(),
428 name: value.name,
429 }
430 }
431}
432
433impl From<extension::SymbolKind> for SymbolKind {
434 fn from(value: extension::SymbolKind) -> Self {
435 match value {
436 extension::SymbolKind::File => Self::File,
437 extension::SymbolKind::Module => Self::Module,
438 extension::SymbolKind::Namespace => Self::Namespace,
439 extension::SymbolKind::Package => Self::Package,
440 extension::SymbolKind::Class => Self::Class,
441 extension::SymbolKind::Method => Self::Method,
442 extension::SymbolKind::Property => Self::Property,
443 extension::SymbolKind::Field => Self::Field,
444 extension::SymbolKind::Constructor => Self::Constructor,
445 extension::SymbolKind::Enum => Self::Enum,
446 extension::SymbolKind::Interface => Self::Interface,
447 extension::SymbolKind::Function => Self::Function,
448 extension::SymbolKind::Variable => Self::Variable,
449 extension::SymbolKind::Constant => Self::Constant,
450 extension::SymbolKind::String => Self::String,
451 extension::SymbolKind::Number => Self::Number,
452 extension::SymbolKind::Boolean => Self::Boolean,
453 extension::SymbolKind::Array => Self::Array,
454 extension::SymbolKind::Object => Self::Object,
455 extension::SymbolKind::Key => Self::Key,
456 extension::SymbolKind::Null => Self::Null,
457 extension::SymbolKind::EnumMember => Self::EnumMember,
458 extension::SymbolKind::Struct => Self::Struct,
459 extension::SymbolKind::Event => Self::Event,
460 extension::SymbolKind::Operator => Self::Operator,
461 extension::SymbolKind::TypeParameter => Self::TypeParameter,
462 extension::SymbolKind::Other(value) => Self::Other(value),
463 }
464 }
465}
466
467impl From<extension::SlashCommand> for SlashCommand {
468 fn from(value: extension::SlashCommand) -> Self {
469 Self {
470 name: value.name,
471 description: value.description,
472 tooltip_text: value.tooltip_text,
473 requires_argument: value.requires_argument,
474 }
475 }
476}
477
478impl From<SlashCommandOutput> for extension::SlashCommandOutput {
479 fn from(value: SlashCommandOutput) -> Self {
480 Self {
481 text: value.text,
482 sections: value.sections.into_iter().map(Into::into).collect(),
483 }
484 }
485}
486
487impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
488 fn from(value: SlashCommandOutputSection) -> Self {
489 Self {
490 range: value.range.start as usize..value.range.end as usize,
491 label: value.label,
492 }
493 }
494}
495
496impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
497 fn from(value: SlashCommandArgumentCompletion) -> Self {
498 Self {
499 label: value.label,
500 new_text: value.new_text,
501 run_command: value.run_command,
502 }
503 }
504}
505
506impl TryFrom<ContextServerConfiguration> for extension::ContextServerConfiguration {
507 type Error = anyhow::Error;
508
509 fn try_from(value: ContextServerConfiguration) -> Result<Self, Self::Error> {
510 let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema)
511 .context("Failed to parse settings_schema")?;
512
513 Ok(Self {
514 installation_instructions: value.installation_instructions,
515 default_settings: value.default_settings,
516 settings_schema,
517 })
518 }
519}
520
521impl HostKeyValueStore for WasmState {
522 async fn insert(
523 &mut self,
524 kv_store: Resource<ExtensionKeyValueStore>,
525 key: String,
526 value: String,
527 ) -> wasmtime::Result<Result<(), String>> {
528 let kv_store = self.table.get(&kv_store)?;
529 kv_store.insert(key, value).await.to_wasmtime_result()
530 }
531
532 async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
533 // We only ever hand out borrows of key-value stores.
534 Ok(())
535 }
536}
537
538impl HostProject for WasmState {
539 async fn worktree_ids(
540 &mut self,
541 project: Resource<ExtensionProject>,
542 ) -> wasmtime::Result<Vec<u64>> {
543 let project = self.table.get(&project)?;
544 Ok(project.worktree_ids())
545 }
546
547 async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
548 // We only ever hand out borrows of projects.
549 Ok(())
550 }
551}
552
553impl HostWorktree for WasmState {
554 async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
555 let delegate = self.table.get(&delegate)?;
556 Ok(delegate.id())
557 }
558
559 async fn root_path(
560 &mut self,
561 delegate: Resource<Arc<dyn WorktreeDelegate>>,
562 ) -> wasmtime::Result<String> {
563 let delegate = self.table.get(&delegate)?;
564 Ok(delegate.root_path())
565 }
566
567 async fn read_text_file(
568 &mut self,
569 delegate: Resource<Arc<dyn WorktreeDelegate>>,
570 path: String,
571 ) -> wasmtime::Result<Result<String, String>> {
572 let delegate = self.table.get(&delegate)?;
573 Ok(delegate
574 .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?)
575 .await
576 .map_err(|error| error.to_string()))
577 }
578
579 async fn shell_env(
580 &mut self,
581 delegate: Resource<Arc<dyn WorktreeDelegate>>,
582 ) -> wasmtime::Result<EnvVars> {
583 let delegate = self.table.get(&delegate)?;
584 Ok(delegate.shell_env().await.into_iter().collect())
585 }
586
587 async fn which(
588 &mut self,
589 delegate: Resource<Arc<dyn WorktreeDelegate>>,
590 binary_name: String,
591 ) -> wasmtime::Result<Option<String>> {
592 let delegate = self.table.get(&delegate)?;
593 Ok(delegate.which(binary_name).await)
594 }
595
596 async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
597 // We only ever hand out borrows of worktrees.
598 Ok(())
599 }
600}
601
602impl common::Host for WasmState {}
603
604impl http_client::Host for WasmState {
605 async fn fetch(
606 &mut self,
607 request: http_client::HttpRequest,
608 ) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
609 maybe!(async {
610 let url = &request.url;
611 let request = convert_request(&request)?;
612 let mut response = self.host.http_client.send(request).await?;
613
614 if response.status().is_client_error() || response.status().is_server_error() {
615 bail!("failed to fetch '{url}': status code {}", response.status())
616 }
617 convert_response(&mut response).await
618 })
619 .await
620 .to_wasmtime_result()
621 }
622
623 async fn fetch_stream(
624 &mut self,
625 request: http_client::HttpRequest,
626 ) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
627 let request = convert_request(&request)?;
628 let response = self.host.http_client.send(request);
629 maybe!(async {
630 let response = response.await?;
631 let stream = Arc::new(Mutex::new(response));
632 let resource = self.table.push(stream)?;
633 Ok(resource)
634 })
635 .await
636 .to_wasmtime_result()
637 }
638}
639
640impl http_client::HostHttpResponseStream for WasmState {
641 async fn next_chunk(
642 &mut self,
643 resource: Resource<ExtensionHttpResponseStream>,
644 ) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
645 let stream = self.table.get(&resource)?.clone();
646 maybe!(async move {
647 let mut response = stream.lock().await;
648 let mut buffer = vec![0; 8192]; // 8KB buffer
649 let bytes_read = response.body_mut().read(&mut buffer).await?;
650 if bytes_read == 0 {
651 Ok(None)
652 } else {
653 buffer.truncate(bytes_read);
654 Ok(Some(buffer))
655 }
656 })
657 .await
658 .to_wasmtime_result()
659 }
660
661 async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
662 Ok(())
663 }
664}
665
666impl From<http_client::HttpMethod> for ::http_client::Method {
667 fn from(value: http_client::HttpMethod) -> Self {
668 match value {
669 http_client::HttpMethod::Get => Self::GET,
670 http_client::HttpMethod::Post => Self::POST,
671 http_client::HttpMethod::Put => Self::PUT,
672 http_client::HttpMethod::Delete => Self::DELETE,
673 http_client::HttpMethod::Head => Self::HEAD,
674 http_client::HttpMethod::Options => Self::OPTIONS,
675 http_client::HttpMethod::Patch => Self::PATCH,
676 }
677 }
678}
679
680fn convert_request(
681 extension_request: &http_client::HttpRequest,
682) -> anyhow::Result<::http_client::Request<AsyncBody>> {
683 let mut request = ::http_client::Request::builder()
684 .method(::http_client::Method::from(extension_request.method))
685 .uri(&extension_request.url)
686 .follow_redirects(match extension_request.redirect_policy {
687 http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
688 http_client::RedirectPolicy::FollowLimit(limit) => {
689 ::http_client::RedirectPolicy::FollowLimit(limit)
690 }
691 http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
692 });
693 for (key, value) in &extension_request.headers {
694 request = request.header(key, value);
695 }
696 let body = extension_request
697 .body
698 .clone()
699 .map(AsyncBody::from)
700 .unwrap_or_default();
701 request.body(body).map_err(anyhow::Error::from)
702}
703
704async fn convert_response(
705 response: &mut ::http_client::Response<AsyncBody>,
706) -> anyhow::Result<http_client::HttpResponse> {
707 let mut extension_response = http_client::HttpResponse {
708 body: Vec::new(),
709 headers: Vec::new(),
710 };
711
712 for (key, value) in response.headers() {
713 extension_response
714 .headers
715 .push((key.to_string(), value.to_str().unwrap_or("").to_string()));
716 }
717
718 response
719 .body_mut()
720 .read_to_end(&mut extension_response.body)
721 .await?;
722
723 Ok(extension_response)
724}
725
726impl nodejs::Host for WasmState {
727 async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
728 self.host
729 .node_runtime
730 .binary_path()
731 .await
732 .map(|path| path.to_string_lossy().into_owned())
733 .to_wasmtime_result()
734 }
735
736 async fn npm_package_latest_version(
737 &mut self,
738 package_name: String,
739 ) -> wasmtime::Result<Result<String, String>> {
740 self.host
741 .node_runtime
742 .npm_package_latest_version(&package_name)
743 .await
744 .to_wasmtime_result()
745 }
746
747 async fn npm_package_installed_version(
748 &mut self,
749 package_name: String,
750 ) -> wasmtime::Result<Result<Option<String>, String>> {
751 self.host
752 .node_runtime
753 .npm_package_installed_version(&self.work_dir(), &package_name)
754 .await
755 .to_wasmtime_result()
756 }
757
758 async fn npm_install_package(
759 &mut self,
760 package_name: String,
761 version: String,
762 ) -> wasmtime::Result<Result<(), String>> {
763 self.capability_granter
764 .grant_npm_install_package(&package_name)?;
765
766 self.host
767 .node_runtime
768 .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
769 .await
770 .to_wasmtime_result()
771 }
772}
773
774#[async_trait]
775impl lsp::Host for WasmState {}
776
777impl From<::http_client::github::GithubRelease> for github::GithubRelease {
778 fn from(value: ::http_client::github::GithubRelease) -> Self {
779 Self {
780 version: value.tag_name,
781 assets: value.assets.into_iter().map(Into::into).collect(),
782 }
783 }
784}
785
786impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
787 fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
788 Self {
789 name: value.name,
790 download_url: value.browser_download_url,
791 }
792 }
793}
794
795impl github::Host for WasmState {
796 async fn latest_github_release(
797 &mut self,
798 repo: String,
799 options: github::GithubReleaseOptions,
800 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
801 maybe!(async {
802 let release = ::http_client::github::latest_github_release(
803 &repo,
804 options.require_assets,
805 options.pre_release,
806 self.host.http_client.clone(),
807 )
808 .await?;
809 Ok(release.into())
810 })
811 .await
812 .to_wasmtime_result()
813 }
814
815 async fn github_release_by_tag_name(
816 &mut self,
817 repo: String,
818 tag: String,
819 ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
820 maybe!(async {
821 let release = ::http_client::github::get_release_by_tag_name(
822 &repo,
823 &tag,
824 self.host.http_client.clone(),
825 )
826 .await?;
827 Ok(release.into())
828 })
829 .await
830 .to_wasmtime_result()
831 }
832}
833
834impl platform::Host for WasmState {
835 async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
836 Ok((
837 match env::consts::OS {
838 "macos" => platform::Os::Mac,
839 "linux" => platform::Os::Linux,
840 "windows" => platform::Os::Windows,
841 _ => panic!("unsupported os"),
842 },
843 match env::consts::ARCH {
844 "aarch64" => platform::Architecture::Aarch64,
845 "x86" => platform::Architecture::X86,
846 "x86_64" => platform::Architecture::X8664,
847 _ => panic!("unsupported architecture"),
848 },
849 ))
850 }
851}
852
853impl From<std::process::Output> for process::Output {
854 fn from(output: std::process::Output) -> Self {
855 Self {
856 status: output.status.code(),
857 stdout: output.stdout,
858 stderr: output.stderr,
859 }
860 }
861}
862
863impl process::Host for WasmState {
864 async fn run_command(
865 &mut self,
866 command: process::Command,
867 ) -> wasmtime::Result<Result<process::Output, String>> {
868 maybe!(async {
869 self.capability_granter
870 .grant_exec(&command.command, &command.args)?;
871
872 let output = util::command::new_smol_command(command.command.as_str())
873 .args(&command.args)
874 .envs(command.env)
875 .output()
876 .await?;
877
878 Ok(output.into())
879 })
880 .await
881 .to_wasmtime_result()
882 }
883}
884
885#[async_trait]
886impl slash_command::Host for WasmState {}
887
888#[async_trait]
889impl context_server::Host for WasmState {}
890
891impl dap::Host for WasmState {
892 async fn resolve_tcp_template(
893 &mut self,
894 template: TcpArgumentsTemplate,
895 ) -> wasmtime::Result<Result<TcpArguments, String>> {
896 maybe!(async {
897 let (host, port, timeout) =
898 ::dap::configure_tcp_connection(task::TcpArgumentsTemplate {
899 port: template.port,
900 host: template.host.map(Ipv4Addr::from_bits),
901 timeout: template.timeout,
902 })
903 .await?;
904 Ok(TcpArguments {
905 port,
906 host: host.to_bits(),
907 timeout,
908 })
909 })
910 .await
911 .to_wasmtime_result()
912 }
913}
914
915impl ExtensionImports for WasmState {
916 async fn get_settings(
917 &mut self,
918 location: Option<self::SettingsLocation>,
919 category: String,
920 key: Option<String>,
921 ) -> wasmtime::Result<Result<String, String>> {
922 self.on_main_thread(|cx| {
923 async move {
924 let path = location.as_ref().and_then(|location| {
925 RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
926 });
927 let location = path
928 .as_ref()
929 .zip(location.as_ref())
930 .map(|(path, location)| ::settings::SettingsLocation {
931 worktree_id: WorktreeId::from_proto(location.worktree_id),
932 path,
933 });
934
935 cx.update(|cx| match category.as_str() {
936 "language" => {
937 let key = key.map(|k| LanguageName::new(&k));
938 let settings = AllLanguageSettings::get(location, cx).language(
939 location,
940 key.as_ref(),
941 cx,
942 );
943 Ok(serde_json::to_string(&settings::LanguageSettings {
944 tab_size: settings.tab_size,
945 })?)
946 }
947 "lsp" => {
948 let settings = key
949 .and_then(|key| {
950 ProjectSettings::get(location, cx)
951 .lsp
952 .get(&::lsp::LanguageServerName::from_proto(key))
953 })
954 .cloned()
955 .unwrap_or_default();
956 Ok(serde_json::to_string(&settings::LspSettings {
957 binary: settings.binary.map(|binary| settings::CommandSettings {
958 path: binary.path,
959 arguments: binary.arguments,
960 env: binary.env.map(|env| env.into_iter().collect()),
961 }),
962 settings: settings.settings,
963 initialization_options: settings.initialization_options,
964 })?)
965 }
966 "context_servers" => {
967 let settings = key
968 .and_then(|key| {
969 ProjectSettings::get(location, cx)
970 .context_servers
971 .get(key.as_str())
972 })
973 .cloned()
974 .unwrap_or_else(|| {
975 project::project_settings::ContextServerSettings::default_extension(
976 )
977 });
978
979 match settings {
980 project::project_settings::ContextServerSettings::Stdio {
981 enabled: _,
982 command,
983 } => Ok(serde_json::to_string(&settings::ContextServerSettings {
984 command: Some(settings::CommandSettings {
985 path: command.path.to_str().map(|path| path.to_string()),
986 arguments: Some(command.args),
987 env: command.env.map(|env| env.into_iter().collect()),
988 }),
989 settings: None,
990 })?),
991 project::project_settings::ContextServerSettings::Extension {
992 enabled: _,
993 settings,
994 } => Ok(serde_json::to_string(&settings::ContextServerSettings {
995 command: None,
996 settings: Some(settings),
997 })?),
998 project::project_settings::ContextServerSettings::Http { .. } => {
999 bail!("remote context server settings not supported in 0.6.0")
1000 }
1001 }
1002 }
1003 _ => {
1004 bail!("Unknown settings category: {}", category);
1005 }
1006 })
1007 }
1008 .boxed_local()
1009 })
1010 .await?
1011 .to_wasmtime_result()
1012 }
1013
1014 async fn set_language_server_installation_status(
1015 &mut self,
1016 server_name: String,
1017 status: LanguageServerInstallationStatus,
1018 ) -> wasmtime::Result<()> {
1019 let status = match status {
1020 LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
1021 LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
1022 LanguageServerInstallationStatus::None => BinaryStatus::None,
1023 LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
1024 };
1025
1026 self.host
1027 .proxy
1028 .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
1029
1030 Ok(())
1031 }
1032
1033 async fn download_file(
1034 &mut self,
1035 url: String,
1036 path: String,
1037 file_type: DownloadedFileType,
1038 ) -> wasmtime::Result<Result<(), String>> {
1039 maybe!(async {
1040 let parsed_url = Url::parse(&url)?;
1041 self.capability_granter.grant_download_file(&parsed_url)?;
1042
1043 let path = PathBuf::from(path);
1044 let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
1045
1046 self.host.fs.create_dir(&extension_work_dir).await?;
1047
1048 let destination_path = self
1049 .host
1050 .writeable_path_from_extension(&self.manifest.id, &path)?;
1051
1052 let mut response = self
1053 .host
1054 .http_client
1055 .get(&url, Default::default(), true)
1056 .await
1057 .context("downloading release")?;
1058
1059 anyhow::ensure!(
1060 response.status().is_success(),
1061 "download failed with status {}",
1062 response.status()
1063 );
1064 let body = BufReader::new(response.body_mut());
1065
1066 match file_type {
1067 DownloadedFileType::Uncompressed => {
1068 futures::pin_mut!(body);
1069 self.host
1070 .fs
1071 .create_file_with(&destination_path, body)
1072 .await?;
1073 }
1074 DownloadedFileType::Gzip => {
1075 let body = GzipDecoder::new(body);
1076 futures::pin_mut!(body);
1077 self.host
1078 .fs
1079 .create_file_with(&destination_path, body)
1080 .await?;
1081 }
1082 DownloadedFileType::GzipTar => {
1083 let body = GzipDecoder::new(body);
1084 futures::pin_mut!(body);
1085 self.host
1086 .fs
1087 .extract_tar_file(&destination_path, Archive::new(body))
1088 .await?;
1089 }
1090 DownloadedFileType::Zip => {
1091 futures::pin_mut!(body);
1092 extract_zip(&destination_path, body)
1093 .await
1094 .with_context(|| format!("unzipping {path:?} archive"))?;
1095 }
1096 }
1097
1098 Ok(())
1099 })
1100 .await
1101 .to_wasmtime_result()
1102 }
1103
1104 async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
1105 let path = self
1106 .host
1107 .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
1108
1109 make_file_executable(&path)
1110 .await
1111 .with_context(|| format!("setting permissions for path {path:?}"))
1112 .to_wasmtime_result()
1113 }
1114
1115 // =========================================================================
1116 // LLM Provider Import Implementations
1117 // =========================================================================
1118
1119 async fn llm_request_credential(
1120 &mut self,
1121 _provider_id: String,
1122 _credential_type: llm_provider::CredentialType,
1123 _label: String,
1124 _placeholder: String,
1125 ) -> wasmtime::Result<Result<bool, String>> {
1126 // For now, credential requests return false (not provided)
1127 // Extensions should use llm_get_env_var to check for env vars first,
1128 // then llm_store_credential/llm_get_credential for manual storage
1129 // Full UI credential prompting will be added in a future phase
1130 Ok(Ok(false))
1131 }
1132
1133 async fn llm_get_credential(
1134 &mut self,
1135 provider_id: String,
1136 ) -> wasmtime::Result<Option<String>> {
1137 let extension_id = self.manifest.id.clone();
1138 let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id);
1139
1140 self.on_main_thread(move |cx| {
1141 async move {
1142 let credentials_provider = cx.update(|cx| <dyn CredentialsProvider>::global(cx))?;
1143 let result = credentials_provider
1144 .read_credentials(&credential_key, cx)
1145 .await
1146 .ok()
1147 .flatten();
1148 Ok(result.map(|(_, password)| String::from_utf8_lossy(&password).to_string()))
1149 }
1150 .boxed_local()
1151 })
1152 .await
1153 }
1154
1155 async fn llm_store_credential(
1156 &mut self,
1157 provider_id: String,
1158 value: String,
1159 ) -> wasmtime::Result<Result<(), String>> {
1160 let extension_id = self.manifest.id.clone();
1161 let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id);
1162
1163 self.on_main_thread(move |cx| {
1164 async move {
1165 let credentials_provider = cx.update(|cx| <dyn CredentialsProvider>::global(cx))?;
1166 credentials_provider
1167 .write_credentials(&credential_key, "api_key", value.as_bytes(), cx)
1168 .await
1169 .map_err(|e| anyhow::anyhow!("{}", e))
1170 }
1171 .boxed_local()
1172 })
1173 .await
1174 .to_wasmtime_result()
1175 }
1176
1177 async fn llm_delete_credential(
1178 &mut self,
1179 provider_id: String,
1180 ) -> wasmtime::Result<Result<(), String>> {
1181 let extension_id = self.manifest.id.clone();
1182 let credential_key = format!("extension-llm-{}:{}", extension_id, provider_id);
1183
1184 self.on_main_thread(move |cx| {
1185 async move {
1186 let credentials_provider = cx.update(|cx| <dyn CredentialsProvider>::global(cx))?;
1187 credentials_provider
1188 .delete_credentials(&credential_key, cx)
1189 .await
1190 .map_err(|e| anyhow::anyhow!("{}", e))
1191 }
1192 .boxed_local()
1193 })
1194 .await
1195 .to_wasmtime_result()
1196 }
1197
1198 async fn llm_get_env_var(&mut self, name: String) -> wasmtime::Result<Option<String>> {
1199 let extension_id = self.manifest.id.clone();
1200
1201 // Find which provider (if any) declares this env var in its auth config
1202 let mut allowed_provider_id: Option<Arc<str>> = None;
1203 for (provider_id, provider_entry) in &self.manifest.language_model_providers {
1204 if let Some(auth_config) = &provider_entry.auth {
1205 if auth_config.env_var.as_deref() == Some(&name) {
1206 allowed_provider_id = Some(provider_id.clone());
1207 break;
1208 }
1209 }
1210 }
1211
1212 // If no provider declares this env var, deny access
1213 let Some(provider_id) = allowed_provider_id else {
1214 log::warn!(
1215 "Extension {} attempted to read env var {} which is not declared in any provider auth config",
1216 extension_id,
1217 name
1218 );
1219 return Ok(None);
1220 };
1221
1222 // Check if the user has allowed this provider to read env vars
1223 let full_provider_id = format!("{}:{}", extension_id, provider_id);
1224 let is_allowed = self
1225 .on_main_thread(move |cx| {
1226 async move {
1227 cx.update(|cx| {
1228 ExtensionSettings::get_global(cx)
1229 .allowed_env_var_providers
1230 .contains(full_provider_id.as_str())
1231 })
1232 .unwrap_or(false)
1233 }
1234 .boxed_local()
1235 })
1236 .await;
1237
1238 if !is_allowed {
1239 log::debug!(
1240 "Extension {} provider {} is not allowed to read env var {}",
1241 extension_id,
1242 provider_id,
1243 name
1244 );
1245 return Ok(None);
1246 }
1247
1248 Ok(env::var(&name).ok())
1249 }
1250
1251 async fn llm_oauth_start_web_auth(
1252 &mut self,
1253 config: llm_provider::OauthWebAuthConfig,
1254 ) -> wasmtime::Result<Result<llm_provider::OauthWebAuthResult, String>> {
1255 let auth_url = config.auth_url;
1256 let callback_path = config.callback_path;
1257 let timeout_secs = config.timeout_secs.unwrap_or(300);
1258
1259 self.on_main_thread(move |cx| {
1260 async move {
1261 let listener = TcpListener::bind("127.0.0.1:0")
1262 .await
1263 .map_err(|e| anyhow::anyhow!("Failed to bind localhost server: {}", e))?;
1264 let port = listener
1265 .local_addr()
1266 .map_err(|e| anyhow::anyhow!("Failed to get local address: {}", e))?
1267 .port();
1268
1269 cx.update(|cx| {
1270 cx.open_url(&auth_url);
1271 })?;
1272
1273 let accept_future = async {
1274 let (stream, _) = listener
1275 .accept()
1276 .await
1277 .map_err(|e| anyhow::anyhow!("Failed to accept connection: {}", e))?;
1278
1279 let mut reader = smol::io::BufReader::new(&stream);
1280 let mut request_line = String::new();
1281 smol::io::AsyncBufReadExt::read_line(&mut reader, &mut request_line)
1282 .await
1283 .map_err(|e| anyhow::anyhow!("Failed to read request: {}", e))?;
1284
1285 let callback_url = if let Some(path_start) = request_line.find(' ') {
1286 if let Some(path_end) = request_line[path_start + 1..].find(' ') {
1287 let path = &request_line[path_start + 1..path_start + 1 + path_end];
1288 if path.starts_with(&callback_path) || path.starts_with(&format!("/{}", callback_path.trim_start_matches('/'))) {
1289 format!("http://localhost:{}{}", port, path)
1290 } else {
1291 return Err(anyhow::anyhow!(
1292 "Unexpected callback path: {}",
1293 path
1294 ));
1295 }
1296 } else {
1297 return Err(anyhow::anyhow!("Malformed HTTP request"));
1298 }
1299 } else {
1300 return Err(anyhow::anyhow!("Malformed HTTP request"));
1301 };
1302
1303 let response = "HTTP/1.1 200 OK\r\n\
1304 Content-Type: text/html\r\n\
1305 Connection: close\r\n\
1306 \r\n\
1307 <!DOCTYPE html>\
1308 <html><head><title>Authentication Complete</title></head>\
1309 <body style=\"font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;\">\
1310 <div style=\"text-align: center;\">\
1311 <h1>Authentication Complete</h1>\
1312 <p>You can close this window and return to Zed.</p>\
1313 </div></body></html>";
1314
1315 let mut writer = &stream;
1316 smol::io::AsyncWriteExt::write_all(&mut writer, response.as_bytes())
1317 .await
1318 .ok();
1319 smol::io::AsyncWriteExt::flush(&mut writer).await.ok();
1320
1321 Ok(callback_url)
1322 };
1323
1324 let timeout_duration = Duration::from_secs(timeout_secs as u64);
1325 let callback_url = smol::future::or(
1326 accept_future,
1327 async {
1328 smol::Timer::after(timeout_duration).await;
1329 Err(anyhow::anyhow!(
1330 "OAuth callback timed out after {} seconds",
1331 timeout_secs
1332 ))
1333 },
1334 )
1335 .await?;
1336
1337 Ok(llm_provider::OauthWebAuthResult {
1338 callback_url,
1339 port: port as u32,
1340 })
1341 }
1342 .boxed_local()
1343 })
1344 .await
1345 .to_wasmtime_result()
1346 }
1347
1348 async fn llm_oauth_http_request(
1349 &mut self,
1350 request: llm_provider::OauthHttpRequest,
1351 ) -> wasmtime::Result<Result<llm_provider::OauthHttpResponse, String>> {
1352 let http_client = self.http_client.clone();
1353
1354 self.on_main_thread(move |_cx| {
1355 async move {
1356 let method = match request.method.to_uppercase().as_str() {
1357 "GET" => ::http_client::Method::GET,
1358 "POST" => ::http_client::Method::POST,
1359 "PUT" => ::http_client::Method::PUT,
1360 "DELETE" => ::http_client::Method::DELETE,
1361 "PATCH" => ::http_client::Method::PATCH,
1362 _ => {
1363 return Err(anyhow::anyhow!(
1364 "Unsupported HTTP method: {}",
1365 request.method
1366 ));
1367 }
1368 };
1369
1370 let mut builder = ::http_client::HttpRequest::builder()
1371 .method(method)
1372 .uri(&request.url);
1373
1374 for (key, value) in &request.headers {
1375 builder = builder.header(key.as_str(), value.as_str());
1376 }
1377
1378 let body = if request.body.is_empty() {
1379 AsyncBody::empty()
1380 } else {
1381 AsyncBody::from(request.body.into_bytes())
1382 };
1383
1384 let http_request = builder
1385 .body(body)
1386 .map_err(|e| anyhow::anyhow!("Failed to build request: {}", e))?;
1387
1388 let mut response = http_client
1389 .send(http_request)
1390 .await
1391 .map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?;
1392
1393 let status = response.status().as_u16();
1394 let headers: Vec<(String, String)> = response
1395 .headers()
1396 .iter()
1397 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
1398 .collect();
1399
1400 let mut body_bytes = Vec::new();
1401 futures::AsyncReadExt::read_to_end(response.body_mut(), &mut body_bytes)
1402 .await
1403 .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))?;
1404
1405 let body = String::from_utf8_lossy(&body_bytes).to_string();
1406
1407 Ok(llm_provider::OauthHttpResponse {
1408 status,
1409 headers,
1410 body,
1411 })
1412 }
1413 .boxed_local()
1414 })
1415 .await
1416 .to_wasmtime_result()
1417 }
1418
1419 async fn llm_oauth_open_browser(
1420 &mut self,
1421 url: String,
1422 ) -> wasmtime::Result<Result<(), String>> {
1423 self.on_main_thread(move |cx| {
1424 async move {
1425 cx.update(|cx| {
1426 cx.open_url(&url);
1427 })?;
1428 Ok(())
1429 }
1430 .boxed_local()
1431 })
1432 .await
1433 .to_wasmtime_result()
1434 }
1435}
1436
1437// =============================================================================
1438// LLM Provider Host Implementations
1439// =============================================================================
1440
1441impl llm_provider::Host for WasmState {}