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