1use super::{
2 breakpoint_store::BreakpointStore,
3 dap_command::EvaluateCommand,
4 locators,
5 session::{self, Session, SessionStateEvent},
6};
7use crate::{
8 InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
9 project_settings::ProjectSettings,
10 terminals::{SshCommand, wrap_for_ssh},
11 worktree_store::WorktreeStore,
12};
13use anyhow::{Result, anyhow};
14use async_trait::async_trait;
15use collections::HashMap;
16use dap::{
17 Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
18 EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
19 adapters::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments},
20 client::SessionId,
21 messages::Message,
22 requests::{Completions, Evaluate},
23};
24use fs::Fs;
25use futures::{
26 StreamExt,
27 channel::mpsc::{self, UnboundedSender},
28 future::{Shared, join_all},
29};
30use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
31use http_client::HttpClient;
32use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind, range_from_lsp};
33use node_runtime::NodeRuntime;
34
35use remote::SshRemoteClient;
36use rpc::{
37 AnyProtoClient, TypedEnvelope,
38 proto::{self},
39};
40use settings::{Settings, WorktreeId};
41use std::{
42 borrow::Borrow,
43 collections::BTreeMap,
44 ffi::OsStr,
45 net::Ipv4Addr,
46 path::{Path, PathBuf},
47 sync::{Arc, Once},
48};
49use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
50use util::{ResultExt as _, merge_json_value_into};
51use worktree::Worktree;
52
53#[derive(Debug)]
54pub enum DapStoreEvent {
55 DebugClientStarted(SessionId),
56 DebugSessionInitialized(SessionId),
57 DebugClientShutdown(SessionId),
58 DebugClientEvent {
59 session_id: SessionId,
60 message: Message,
61 },
62 Notification(String),
63 RemoteHasInitialized,
64}
65
66#[allow(clippy::large_enum_variant)]
67enum DapStoreMode {
68 Local(LocalDapStore),
69 Ssh(SshDapStore),
70 Collab,
71}
72
73pub struct LocalDapStore {
74 fs: Arc<dyn Fs>,
75 node_runtime: NodeRuntime,
76 http_client: Arc<dyn HttpClient>,
77 environment: Entity<ProjectEnvironment>,
78 toolchain_store: Arc<dyn LanguageToolchainStore>,
79}
80
81pub struct SshDapStore {
82 ssh_client: Entity<SshRemoteClient>,
83 upstream_client: AnyProtoClient,
84 upstream_project_id: u64,
85}
86
87pub struct DapStore {
88 mode: DapStoreMode,
89 downstream_client: Option<(AnyProtoClient, u64)>,
90 breakpoint_store: Entity<BreakpointStore>,
91 worktree_store: Entity<WorktreeStore>,
92 sessions: BTreeMap<SessionId, Entity<Session>>,
93 next_session_id: u32,
94}
95
96impl EventEmitter<DapStoreEvent> for DapStore {}
97
98impl DapStore {
99 pub fn init(client: &AnyProtoClient, cx: &mut App) {
100 static ADD_LOCATORS: Once = Once::new();
101 ADD_LOCATORS.call_once(|| {
102 DapRegistry::global(cx).add_locator(Arc::new(locators::cargo::CargoLocator {}))
103 });
104 client.add_entity_request_handler(Self::handle_run_debug_locator);
105 client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
106 client.add_entity_message_handler(Self::handle_log_to_debug_console);
107 }
108
109 #[expect(clippy::too_many_arguments)]
110 pub fn new_local(
111 http_client: Arc<dyn HttpClient>,
112 node_runtime: NodeRuntime,
113 fs: Arc<dyn Fs>,
114 environment: Entity<ProjectEnvironment>,
115 toolchain_store: Arc<dyn LanguageToolchainStore>,
116 worktree_store: Entity<WorktreeStore>,
117 breakpoint_store: Entity<BreakpointStore>,
118 cx: &mut Context<Self>,
119 ) -> Self {
120 let mode = DapStoreMode::Local(LocalDapStore {
121 fs,
122 environment,
123 http_client,
124 node_runtime,
125 toolchain_store,
126 });
127
128 Self::new(mode, breakpoint_store, worktree_store, cx)
129 }
130
131 pub fn new_ssh(
132 project_id: u64,
133 ssh_client: Entity<SshRemoteClient>,
134 breakpoint_store: Entity<BreakpointStore>,
135 worktree_store: Entity<WorktreeStore>,
136 cx: &mut Context<Self>,
137 ) -> Self {
138 let mode = DapStoreMode::Ssh(SshDapStore {
139 upstream_client: ssh_client.read(cx).proto_client(),
140 ssh_client,
141 upstream_project_id: project_id,
142 });
143
144 Self::new(mode, breakpoint_store, worktree_store, cx)
145 }
146
147 pub fn new_collab(
148 _project_id: u64,
149 _upstream_client: AnyProtoClient,
150 breakpoint_store: Entity<BreakpointStore>,
151 worktree_store: Entity<WorktreeStore>,
152 cx: &mut Context<Self>,
153 ) -> Self {
154 Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
155 }
156
157 fn new(
158 mode: DapStoreMode,
159 breakpoint_store: Entity<BreakpointStore>,
160 worktree_store: Entity<WorktreeStore>,
161 _cx: &mut Context<Self>,
162 ) -> Self {
163 Self {
164 mode,
165 next_session_id: 0,
166 downstream_client: None,
167 breakpoint_store,
168 worktree_store,
169 sessions: Default::default(),
170 }
171 }
172
173 pub fn get_debug_adapter_binary(
174 &mut self,
175 definition: DebugTaskDefinition,
176 session_id: SessionId,
177 console: UnboundedSender<String>,
178 cx: &mut Context<Self>,
179 ) -> Task<Result<DebugAdapterBinary>> {
180 match &self.mode {
181 DapStoreMode::Local(_) => {
182 let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
183 else {
184 return Task::ready(Err(anyhow!("Failed to find a worktree")));
185 };
186 let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
187 return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
188 };
189
190 let user_installed_path = ProjectSettings::get_global(cx)
191 .dap
192 .get(&adapter.name())
193 .and_then(|s| s.binary.as_ref().map(PathBuf::from));
194
195 let delegate = self.delegate(&worktree, console, cx);
196 let cwd: Arc<Path> = definition
197 .cwd()
198 .unwrap_or(worktree.read(cx).abs_path().as_ref())
199 .into();
200
201 cx.spawn(async move |this, cx| {
202 let mut binary = adapter
203 .get_binary(&delegate, &definition, user_installed_path, cx)
204 .await?;
205
206 let env = this
207 .update(cx, |this, cx| {
208 this.as_local()
209 .unwrap()
210 .environment
211 .update(cx, |environment, cx| {
212 environment.get_directory_environment(cwd, cx)
213 })
214 })?
215 .await;
216
217 if let Some(mut env) = env {
218 env.extend(std::mem::take(&mut binary.envs));
219 binary.envs = env;
220 }
221
222 Ok(binary)
223 })
224 }
225 DapStoreMode::Ssh(ssh) => {
226 let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
227 session_id: session_id.to_proto(),
228 project_id: ssh.upstream_project_id,
229 definition: Some(definition.to_proto()),
230 });
231 let ssh_client = ssh.ssh_client.clone();
232
233 cx.spawn(async move |_, cx| {
234 let response = request.await?;
235 let binary = DebugAdapterBinary::from_proto(response)?;
236 let mut ssh_command = ssh_client.update(cx, |ssh, _| {
237 anyhow::Ok(SshCommand {
238 arguments: ssh
239 .ssh_args()
240 .ok_or_else(|| anyhow!("SSH arguments not found"))?,
241 })
242 })??;
243
244 let mut connection = None;
245 if let Some(c) = binary.connection {
246 let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
247 let port =
248 dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
249
250 ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
251 connection = Some(TcpArguments {
252 port: c.port,
253 host: local_bind_addr,
254 timeout: c.timeout,
255 })
256 }
257
258 let (program, args) = wrap_for_ssh(
259 &ssh_command,
260 Some((&binary.command, &binary.arguments)),
261 binary.cwd.as_deref(),
262 binary.envs,
263 None,
264 );
265
266 Ok(DebugAdapterBinary {
267 command: program,
268 arguments: args,
269 envs: HashMap::default(),
270 cwd: None,
271 connection,
272 request_args: binary.request_args,
273 })
274 })
275 }
276 DapStoreMode::Collab => {
277 Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
278 }
279 }
280 }
281
282 pub fn debug_scenario_for_build_task(
283 &self,
284 build: TaskTemplate,
285 adapter: SharedString,
286 cx: &mut App,
287 ) -> Option<DebugScenario> {
288 DapRegistry::global(cx)
289 .locators()
290 .values()
291 .find_map(|locator| locator.create_scenario(&build, &adapter))
292 }
293
294 pub fn run_debug_locator(
295 &mut self,
296 locator_name: &str,
297 build_command: SpawnInTerminal,
298 cx: &mut Context<Self>,
299 ) -> Task<Result<DebugRequest>> {
300 match &self.mode {
301 DapStoreMode::Local(_) => {
302 // Pre-resolve args with existing environment.
303 let locators = DapRegistry::global(cx).locators();
304 let locator = locators.get(locator_name);
305
306 if let Some(locator) = locator.cloned() {
307 cx.background_spawn(async move {
308 let result = locator
309 .run(build_command.clone())
310 .await
311 .log_with_level(log::Level::Error);
312 if let Some(result) = result {
313 return Ok(result);
314 }
315
316 Err(anyhow!(
317 "None of the locators for task `{}` completed successfully",
318 build_command.label
319 ))
320 })
321 } else {
322 Task::ready(Err(anyhow!(
323 "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
324 build_command.label
325 )))
326 }
327 }
328 DapStoreMode::Ssh(ssh) => {
329 let request = ssh.upstream_client.request(proto::RunDebugLocators {
330 project_id: ssh.upstream_project_id,
331 build_command: Some(build_command.to_proto()),
332 locator: locator_name.to_owned(),
333 });
334 cx.background_spawn(async move {
335 let response = request.await?;
336 DebugRequest::from_proto(response)
337 })
338 }
339 DapStoreMode::Collab => {
340 Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
341 }
342 }
343 }
344
345 fn as_local(&self) -> Option<&LocalDapStore> {
346 match &self.mode {
347 DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
348 _ => None,
349 }
350 }
351
352 pub fn new_session(
353 &mut self,
354 label: SharedString,
355 adapter: DebugAdapterName,
356 parent_session: Option<Entity<Session>>,
357 cx: &mut Context<Self>,
358 ) -> Entity<Session> {
359 let session_id = SessionId(util::post_inc(&mut self.next_session_id));
360
361 if let Some(session) = &parent_session {
362 session.update(cx, |session, _| {
363 session.add_child_session_id(session_id);
364 });
365 }
366
367 let session = Session::new(
368 self.breakpoint_store.clone(),
369 session_id,
370 parent_session,
371 label,
372 adapter,
373 cx,
374 );
375
376 self.sessions.insert(session_id, session.clone());
377 cx.notify();
378
379 cx.subscribe(&session, {
380 move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
381 SessionStateEvent::Shutdown => {
382 this.shutdown_session(session_id, cx).detach_and_log_err(cx);
383 }
384 SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
385 SessionStateEvent::Running => {
386 cx.emit(DapStoreEvent::DebugClientStarted(session_id));
387 }
388 }
389 })
390 .detach();
391
392 session
393 }
394
395 pub fn boot_session(
396 &self,
397 session: Entity<Session>,
398 definition: DebugTaskDefinition,
399 cx: &mut Context<Self>,
400 ) -> Task<Result<()>> {
401 let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next() else {
402 return Task::ready(Err(anyhow!("Failed to find a worktree")));
403 };
404
405 let dap_store = cx.weak_entity();
406 let console = session.update(cx, |session, cx| session.console_output(cx));
407 let session_id = session.read(cx).session_id();
408
409 cx.spawn({
410 let session = session.clone();
411 async move |this, cx| {
412 let mut binary = this
413 .update(cx, |this, cx| {
414 this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
415 })?
416 .await?;
417
418 if let Some(args) = definition.initialize_args {
419 merge_json_value_into(args, &mut binary.request_args.configuration);
420 }
421
422 session
423 .update(cx, |session, cx| {
424 session.boot(binary, worktree, dap_store, cx)
425 })?
426 .await
427 }
428 })
429 }
430
431 pub fn session_by_id(
432 &self,
433 session_id: impl Borrow<SessionId>,
434 ) -> Option<Entity<session::Session>> {
435 let session_id = session_id.borrow();
436 let client = self.sessions.get(session_id).cloned();
437
438 client
439 }
440 pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
441 self.sessions.values()
442 }
443
444 pub fn capabilities_by_id(
445 &self,
446 session_id: impl Borrow<SessionId>,
447 cx: &App,
448 ) -> Option<Capabilities> {
449 let session_id = session_id.borrow();
450 self.sessions
451 .get(session_id)
452 .map(|client| client.read(cx).capabilities.clone())
453 }
454
455 pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
456 &self.breakpoint_store
457 }
458
459 pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
460 &self.worktree_store
461 }
462
463 #[allow(dead_code)]
464 async fn handle_ignore_breakpoint_state(
465 this: Entity<Self>,
466 envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
467 mut cx: AsyncApp,
468 ) -> Result<()> {
469 let session_id = SessionId::from_proto(envelope.payload.session_id);
470
471 this.update(&mut cx, |this, cx| {
472 if let Some(session) = this.session_by_id(&session_id) {
473 session.update(cx, |session, cx| {
474 session.set_ignore_breakpoints(envelope.payload.ignore, cx)
475 })
476 } else {
477 Task::ready(HashMap::default())
478 }
479 })?
480 .await;
481
482 Ok(())
483 }
484
485 fn delegate(
486 &self,
487 worktree: &Entity<Worktree>,
488 console: UnboundedSender<String>,
489 cx: &mut App,
490 ) -> DapAdapterDelegate {
491 let Some(local_store) = self.as_local() else {
492 unimplemented!("Starting session on remote side");
493 };
494
495 DapAdapterDelegate::new(
496 local_store.fs.clone(),
497 worktree.read(cx).id(),
498 console,
499 local_store.node_runtime.clone(),
500 local_store.http_client.clone(),
501 local_store.toolchain_store.clone(),
502 local_store.environment.update(cx, |env, cx| {
503 env.get_worktree_environment(worktree.clone(), cx)
504 }),
505 )
506 }
507
508 pub fn evaluate(
509 &self,
510 session_id: &SessionId,
511 stack_frame_id: u64,
512 expression: String,
513 context: EvaluateArgumentsContext,
514 source: Option<Source>,
515 cx: &mut Context<Self>,
516 ) -> Task<Result<EvaluateResponse>> {
517 let Some(client) = self
518 .session_by_id(session_id)
519 .and_then(|client| client.read(cx).adapter_client())
520 else {
521 return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
522 };
523
524 cx.background_executor().spawn(async move {
525 client
526 .request::<Evaluate>(EvaluateArguments {
527 expression: expression.clone(),
528 frame_id: Some(stack_frame_id),
529 context: Some(context),
530 format: None,
531 line: None,
532 column: None,
533 source,
534 })
535 .await
536 })
537 }
538
539 pub fn completions(
540 &self,
541 session_id: &SessionId,
542 stack_frame_id: u64,
543 text: String,
544 completion_column: u64,
545 cx: &mut Context<Self>,
546 ) -> Task<Result<Vec<CompletionItem>>> {
547 let Some(client) = self
548 .session_by_id(session_id)
549 .and_then(|client| client.read(cx).adapter_client())
550 else {
551 return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
552 };
553
554 cx.background_executor().spawn(async move {
555 Ok(client
556 .request::<Completions>(CompletionsArguments {
557 frame_id: Some(stack_frame_id),
558 line: None,
559 text,
560 column: completion_column,
561 })
562 .await?
563 .targets)
564 })
565 }
566
567 pub fn resolve_inline_values(
568 &self,
569 session: Entity<Session>,
570 stack_frame_id: StackFrameId,
571 buffer_handle: Entity<Buffer>,
572 inline_values: Vec<lsp::InlineValue>,
573 cx: &mut Context<Self>,
574 ) -> Task<Result<Vec<InlayHint>>> {
575 let snapshot = buffer_handle.read(cx).snapshot();
576 let all_variables = session.read(cx).variables_by_stack_frame_id(stack_frame_id);
577
578 cx.spawn(async move |_, cx| {
579 let mut inlay_hints = Vec::with_capacity(inline_values.len());
580 for inline_value in inline_values.iter() {
581 match inline_value {
582 lsp::InlineValue::Text(text) => {
583 inlay_hints.push(InlayHint {
584 position: snapshot.anchor_after(range_from_lsp(text.range).end),
585 label: InlayHintLabel::String(format!(": {}", text.text)),
586 kind: Some(InlayHintKind::Type),
587 padding_left: false,
588 padding_right: false,
589 tooltip: None,
590 resolve_state: ResolveState::Resolved,
591 });
592 }
593 lsp::InlineValue::VariableLookup(variable_lookup) => {
594 let range = range_from_lsp(variable_lookup.range);
595
596 let mut variable_name = variable_lookup
597 .variable_name
598 .clone()
599 .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect());
600
601 if !variable_lookup.case_sensitive_lookup {
602 variable_name = variable_name.to_ascii_lowercase();
603 }
604
605 let Some(variable) = all_variables.iter().find(|variable| {
606 if variable_lookup.case_sensitive_lookup {
607 variable.name == variable_name
608 } else {
609 variable.name.to_ascii_lowercase() == variable_name
610 }
611 }) else {
612 continue;
613 };
614
615 inlay_hints.push(InlayHint {
616 position: snapshot.anchor_after(range.end),
617 label: InlayHintLabel::String(format!(": {}", variable.value)),
618 kind: Some(InlayHintKind::Type),
619 padding_left: false,
620 padding_right: false,
621 tooltip: None,
622 resolve_state: ResolveState::Resolved,
623 });
624 }
625 lsp::InlineValue::EvaluatableExpression(expression) => {
626 let range = range_from_lsp(expression.range);
627
628 let expression = expression
629 .expression
630 .clone()
631 .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect());
632
633 let Ok(eval_task) = session.update(cx, |session, _| {
634 session.mode.request_dap(EvaluateCommand {
635 expression,
636 frame_id: Some(stack_frame_id),
637 source: None,
638 context: Some(EvaluateArgumentsContext::Variables),
639 })
640 }) else {
641 continue;
642 };
643
644 if let Some(response) = eval_task.await.log_err() {
645 inlay_hints.push(InlayHint {
646 position: snapshot.anchor_after(range.end),
647 label: InlayHintLabel::String(format!(": {}", response.result)),
648 kind: Some(InlayHintKind::Type),
649 padding_left: false,
650 padding_right: false,
651 tooltip: None,
652 resolve_state: ResolveState::Resolved,
653 });
654 };
655 }
656 };
657 }
658
659 Ok(inlay_hints)
660 })
661 }
662
663 pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
664 let mut tasks = vec![];
665 for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
666 tasks.push(self.shutdown_session(session_id, cx));
667 }
668
669 cx.background_executor().spawn(async move {
670 futures::future::join_all(tasks).await;
671 })
672 }
673
674 pub fn shutdown_session(
675 &mut self,
676 session_id: SessionId,
677 cx: &mut Context<Self>,
678 ) -> Task<Result<()>> {
679 let Some(session) = self.sessions.remove(&session_id) else {
680 return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
681 };
682
683 let shutdown_children = session
684 .read(cx)
685 .child_session_ids()
686 .iter()
687 .map(|session_id| self.shutdown_session(*session_id, cx))
688 .collect::<Vec<_>>();
689
690 let shutdown_parent_task = if let Some(parent_session) = session
691 .read(cx)
692 .parent_id(cx)
693 .and_then(|session_id| self.session_by_id(session_id))
694 {
695 let shutdown_id = parent_session.update(cx, |parent_session, _| {
696 parent_session.remove_child_session_id(session_id);
697
698 if parent_session.child_session_ids().len() == 0 {
699 Some(parent_session.session_id())
700 } else {
701 None
702 }
703 });
704
705 shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
706 } else {
707 None
708 };
709
710 let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
711
712 cx.background_spawn(async move {
713 if shutdown_children.len() > 0 {
714 let _ = join_all(shutdown_children).await;
715 }
716
717 shutdown_task.await;
718
719 if let Some(parent_task) = shutdown_parent_task {
720 parent_task.await?;
721 }
722
723 Ok(())
724 })
725 }
726
727 pub fn shared(
728 &mut self,
729 project_id: u64,
730 downstream_client: AnyProtoClient,
731 _: &mut Context<Self>,
732 ) {
733 self.downstream_client = Some((downstream_client.clone(), project_id));
734 }
735
736 pub fn unshared(&mut self, cx: &mut Context<Self>) {
737 self.downstream_client.take();
738
739 cx.notify();
740 }
741
742 async fn handle_run_debug_locator(
743 this: Entity<Self>,
744 envelope: TypedEnvelope<proto::RunDebugLocators>,
745 mut cx: AsyncApp,
746 ) -> Result<proto::DebugRequest> {
747 let task = envelope
748 .payload
749 .build_command
750 .ok_or_else(|| anyhow!("missing definition"))?;
751 let build_task = SpawnInTerminal::from_proto(task);
752 let locator = envelope.payload.locator;
753 let request = this
754 .update(&mut cx, |this, cx| {
755 this.run_debug_locator(&locator, build_task, cx)
756 })?
757 .await?;
758
759 Ok(request.to_proto())
760 }
761
762 async fn handle_get_debug_adapter_binary(
763 this: Entity<Self>,
764 envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
765 mut cx: AsyncApp,
766 ) -> Result<proto::DebugAdapterBinary> {
767 let definition = DebugTaskDefinition::from_proto(
768 envelope
769 .payload
770 .definition
771 .ok_or_else(|| anyhow!("missing definition"))?,
772 )?;
773 let (tx, mut rx) = mpsc::unbounded();
774 let session_id = envelope.payload.session_id;
775 cx.spawn({
776 let this = this.clone();
777 async move |cx| {
778 while let Some(message) = rx.next().await {
779 this.update(cx, |this, _| {
780 if let Some((downstream, project_id)) = this.downstream_client.clone() {
781 downstream
782 .send(proto::LogToDebugConsole {
783 project_id,
784 session_id,
785 message,
786 })
787 .ok();
788 }
789 })
790 .ok();
791 }
792 }
793 })
794 .detach();
795
796 let binary = this
797 .update(&mut cx, |this, cx| {
798 this.get_debug_adapter_binary(definition, SessionId::from_proto(session_id), tx, cx)
799 })?
800 .await?;
801 Ok(binary.to_proto())
802 }
803
804 async fn handle_log_to_debug_console(
805 this: Entity<Self>,
806 envelope: TypedEnvelope<proto::LogToDebugConsole>,
807 mut cx: AsyncApp,
808 ) -> Result<()> {
809 let session_id = SessionId::from_proto(envelope.payload.session_id);
810 this.update(&mut cx, |this, cx| {
811 let Some(session) = this.sessions.get(&session_id) else {
812 return;
813 };
814 session.update(cx, |session, cx| {
815 session
816 .console_output(cx)
817 .unbounded_send(envelope.payload.message)
818 .ok();
819 })
820 })
821 }
822}
823
824#[derive(Clone)]
825pub struct DapAdapterDelegate {
826 fs: Arc<dyn Fs>,
827 console: mpsc::UnboundedSender<String>,
828 worktree_id: WorktreeId,
829 node_runtime: NodeRuntime,
830 http_client: Arc<dyn HttpClient>,
831 toolchain_store: Arc<dyn LanguageToolchainStore>,
832 load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
833}
834
835impl DapAdapterDelegate {
836 pub fn new(
837 fs: Arc<dyn Fs>,
838 worktree_id: WorktreeId,
839 status: mpsc::UnboundedSender<String>,
840 node_runtime: NodeRuntime,
841 http_client: Arc<dyn HttpClient>,
842 toolchain_store: Arc<dyn LanguageToolchainStore>,
843 load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
844 ) -> Self {
845 Self {
846 fs,
847 console: status,
848 worktree_id,
849 http_client,
850 node_runtime,
851 toolchain_store,
852 load_shell_env_task,
853 }
854 }
855}
856
857#[async_trait(?Send)]
858impl dap::adapters::DapDelegate for DapAdapterDelegate {
859 fn worktree_id(&self) -> WorktreeId {
860 self.worktree_id
861 }
862
863 fn http_client(&self) -> Arc<dyn HttpClient> {
864 self.http_client.clone()
865 }
866
867 fn node_runtime(&self) -> NodeRuntime {
868 self.node_runtime.clone()
869 }
870
871 fn fs(&self) -> Arc<dyn Fs> {
872 self.fs.clone()
873 }
874
875 fn output_to_console(&self, msg: String) {
876 self.console.unbounded_send(msg).ok();
877 }
878
879 fn which(&self, command: &OsStr) -> Option<PathBuf> {
880 which::which(command).ok()
881 }
882
883 async fn shell_env(&self) -> HashMap<String, String> {
884 let task = self.load_shell_env_task.clone();
885 task.await.unwrap_or_default()
886 }
887
888 fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
889 self.toolchain_store.clone()
890 }
891}