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 let registry = DapRegistry::global(cx);
105 registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
106 registry.add_locator(Arc::new(locators::python::PythonLocator));
107 });
108 client.add_entity_request_handler(Self::handle_run_debug_locator);
109 client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
110 client.add_entity_message_handler(Self::handle_log_to_debug_console);
111 }
112
113 #[expect(clippy::too_many_arguments)]
114 pub fn new_local(
115 http_client: Arc<dyn HttpClient>,
116 node_runtime: NodeRuntime,
117 fs: Arc<dyn Fs>,
118 environment: Entity<ProjectEnvironment>,
119 toolchain_store: Arc<dyn LanguageToolchainStore>,
120 worktree_store: Entity<WorktreeStore>,
121 breakpoint_store: Entity<BreakpointStore>,
122 cx: &mut Context<Self>,
123 ) -> Self {
124 let mode = DapStoreMode::Local(LocalDapStore {
125 fs,
126 environment,
127 http_client,
128 node_runtime,
129 toolchain_store,
130 });
131
132 Self::new(mode, breakpoint_store, worktree_store, cx)
133 }
134
135 pub fn new_ssh(
136 project_id: u64,
137 ssh_client: Entity<SshRemoteClient>,
138 breakpoint_store: Entity<BreakpointStore>,
139 worktree_store: Entity<WorktreeStore>,
140 cx: &mut Context<Self>,
141 ) -> Self {
142 let mode = DapStoreMode::Ssh(SshDapStore {
143 upstream_client: ssh_client.read(cx).proto_client(),
144 ssh_client,
145 upstream_project_id: project_id,
146 });
147
148 Self::new(mode, breakpoint_store, worktree_store, cx)
149 }
150
151 pub fn new_collab(
152 _project_id: u64,
153 _upstream_client: AnyProtoClient,
154 breakpoint_store: Entity<BreakpointStore>,
155 worktree_store: Entity<WorktreeStore>,
156 cx: &mut Context<Self>,
157 ) -> Self {
158 Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
159 }
160
161 fn new(
162 mode: DapStoreMode,
163 breakpoint_store: Entity<BreakpointStore>,
164 worktree_store: Entity<WorktreeStore>,
165 _cx: &mut Context<Self>,
166 ) -> Self {
167 Self {
168 mode,
169 next_session_id: 0,
170 downstream_client: None,
171 breakpoint_store,
172 worktree_store,
173 sessions: Default::default(),
174 }
175 }
176
177 pub fn get_debug_adapter_binary(
178 &mut self,
179 definition: DebugTaskDefinition,
180 session_id: SessionId,
181 console: UnboundedSender<String>,
182 cx: &mut Context<Self>,
183 ) -> Task<Result<DebugAdapterBinary>> {
184 match &self.mode {
185 DapStoreMode::Local(_) => {
186 let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
187 else {
188 return Task::ready(Err(anyhow!("Failed to find a worktree")));
189 };
190 let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
191 return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
192 };
193
194 let user_installed_path = ProjectSettings::get_global(cx)
195 .dap
196 .get(&adapter.name())
197 .and_then(|s| s.binary.as_ref().map(PathBuf::from));
198
199 let delegate = self.delegate(&worktree, console, cx);
200 let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
201
202 cx.spawn(async move |this, cx| {
203 let mut binary = adapter
204 .get_binary(&delegate, &definition, user_installed_path, cx)
205 .await?;
206
207 let env = this
208 .update(cx, |this, cx| {
209 this.as_local()
210 .unwrap()
211 .environment
212 .update(cx, |environment, cx| {
213 environment.get_directory_environment(cwd, cx)
214 })
215 })?
216 .await;
217
218 if let Some(mut env) = env {
219 env.extend(std::mem::take(&mut binary.envs));
220 binary.envs = env;
221 }
222
223 Ok(binary)
224 })
225 }
226 DapStoreMode::Ssh(ssh) => {
227 let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
228 session_id: session_id.to_proto(),
229 project_id: ssh.upstream_project_id,
230 definition: Some(definition.to_proto()),
231 });
232 let ssh_client = ssh.ssh_client.clone();
233
234 cx.spawn(async move |_, cx| {
235 let response = request.await?;
236 let binary = DebugAdapterBinary::from_proto(response)?;
237 let mut ssh_command = ssh_client.read_with(cx, |ssh, _| {
238 anyhow::Ok(SshCommand {
239 arguments: ssh.ssh_args().context("SSH arguments not found")?,
240 })
241 })??;
242
243 let mut connection = None;
244 if let Some(c) = binary.connection {
245 let local_bind_addr = Ipv4Addr::LOCALHOST;
246 let port =
247 dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
248
249 ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
250 connection = Some(TcpArguments {
251 port,
252 host: local_bind_addr,
253 timeout: c.timeout,
254 })
255 }
256
257 let (program, args) = wrap_for_ssh(
258 &ssh_command,
259 Some((&binary.command, &binary.arguments)),
260 binary.cwd.as_deref(),
261 binary.envs,
262 None,
263 );
264
265 Ok(DebugAdapterBinary {
266 command: program,
267 arguments: args,
268 envs: HashMap::default(),
269 cwd: None,
270 connection,
271 request_args: binary.request_args,
272 })
273 })
274 }
275 DapStoreMode::Collab => {
276 Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
277 }
278 }
279 }
280
281 pub fn debug_scenario_for_build_task(
282 &self,
283 build: TaskTemplate,
284 adapter: DebugAdapterName,
285 label: SharedString,
286 cx: &mut App,
287 ) -> Option<DebugScenario> {
288 DapRegistry::global(cx)
289 .locators()
290 .values()
291 .find_map(|locator| locator.create_scenario(&build, &label, adapter.clone()))
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 anyhow::bail!(
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 binary = this
413 .update(cx, |this, cx| {
414 this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
415 })?
416 .await?;
417 session
418 .update(cx, |session, cx| {
419 session.boot(binary, worktree, dap_store, cx)
420 })?
421 .await
422 }
423 })
424 }
425
426 pub fn session_by_id(
427 &self,
428 session_id: impl Borrow<SessionId>,
429 ) -> Option<Entity<session::Session>> {
430 let session_id = session_id.borrow();
431 let client = self.sessions.get(session_id).cloned();
432
433 client
434 }
435 pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
436 self.sessions.values()
437 }
438
439 pub fn capabilities_by_id(
440 &self,
441 session_id: impl Borrow<SessionId>,
442 cx: &App,
443 ) -> Option<Capabilities> {
444 let session_id = session_id.borrow();
445 self.sessions
446 .get(session_id)
447 .map(|client| client.read(cx).capabilities.clone())
448 }
449
450 pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
451 &self.breakpoint_store
452 }
453
454 pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
455 &self.worktree_store
456 }
457
458 #[allow(dead_code)]
459 async fn handle_ignore_breakpoint_state(
460 this: Entity<Self>,
461 envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
462 mut cx: AsyncApp,
463 ) -> Result<()> {
464 let session_id = SessionId::from_proto(envelope.payload.session_id);
465
466 this.update(&mut cx, |this, cx| {
467 if let Some(session) = this.session_by_id(&session_id) {
468 session.update(cx, |session, cx| {
469 session.set_ignore_breakpoints(envelope.payload.ignore, cx)
470 })
471 } else {
472 Task::ready(HashMap::default())
473 }
474 })?
475 .await;
476
477 Ok(())
478 }
479
480 fn delegate(
481 &self,
482 worktree: &Entity<Worktree>,
483 console: UnboundedSender<String>,
484 cx: &mut App,
485 ) -> Arc<dyn DapDelegate> {
486 let Some(local_store) = self.as_local() else {
487 unimplemented!("Starting session on remote side");
488 };
489
490 Arc::new(DapAdapterDelegate::new(
491 local_store.fs.clone(),
492 worktree.read(cx).snapshot(),
493 console,
494 local_store.node_runtime.clone(),
495 local_store.http_client.clone(),
496 local_store.toolchain_store.clone(),
497 local_store.environment.update(cx, |env, cx| {
498 env.get_worktree_environment(worktree.clone(), cx)
499 }),
500 ))
501 }
502
503 pub fn evaluate(
504 &self,
505 session_id: &SessionId,
506 stack_frame_id: u64,
507 expression: String,
508 context: EvaluateArgumentsContext,
509 source: Option<Source>,
510 cx: &mut Context<Self>,
511 ) -> Task<Result<EvaluateResponse>> {
512 let Some(client) = self
513 .session_by_id(session_id)
514 .and_then(|client| client.read(cx).adapter_client())
515 else {
516 return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
517 };
518
519 cx.background_executor().spawn(async move {
520 client
521 .request::<Evaluate>(EvaluateArguments {
522 expression: expression.clone(),
523 frame_id: Some(stack_frame_id),
524 context: Some(context),
525 format: None,
526 line: None,
527 column: None,
528 source,
529 })
530 .await
531 })
532 }
533
534 pub fn completions(
535 &self,
536 session_id: &SessionId,
537 stack_frame_id: u64,
538 text: String,
539 completion_column: u64,
540 cx: &mut Context<Self>,
541 ) -> Task<Result<Vec<CompletionItem>>> {
542 let Some(client) = self
543 .session_by_id(session_id)
544 .and_then(|client| client.read(cx).adapter_client())
545 else {
546 return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
547 };
548
549 cx.background_executor().spawn(async move {
550 Ok(client
551 .request::<Completions>(CompletionsArguments {
552 frame_id: Some(stack_frame_id),
553 line: None,
554 text,
555 column: completion_column,
556 })
557 .await?
558 .targets)
559 })
560 }
561
562 pub fn resolve_inline_value_locations(
563 &self,
564 session: Entity<Session>,
565 stack_frame_id: StackFrameId,
566 buffer_handle: Entity<Buffer>,
567 inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
568 cx: &mut Context<Self>,
569 ) -> Task<Result<Vec<InlayHint>>> {
570 let snapshot = buffer_handle.read(cx).snapshot();
571 let all_variables = session.read(cx).variables_by_stack_frame_id(stack_frame_id);
572
573 fn format_value(mut value: String) -> String {
574 const LIMIT: usize = 100;
575
576 if value.len() > LIMIT {
577 value.truncate(LIMIT);
578 value.push_str("...");
579 }
580
581 format!(": {}", value)
582 }
583
584 cx.spawn(async move |_, cx| {
585 let mut inlay_hints = Vec::with_capacity(inline_value_locations.len());
586 for inline_value_location in inline_value_locations.iter() {
587 let point = snapshot.point_to_point_utf16(language::Point::new(
588 inline_value_location.row as u32,
589 inline_value_location.column as u32,
590 ));
591 let position = snapshot.anchor_after(point);
592
593 match inline_value_location.lookup {
594 VariableLookupKind::Variable => {
595 let Some(variable) = all_variables
596 .iter()
597 .find(|variable| variable.name == inline_value_location.variable_name)
598 else {
599 continue;
600 };
601
602 inlay_hints.push(InlayHint {
603 position,
604 label: InlayHintLabel::String(format_value(variable.value.clone())),
605 kind: Some(InlayHintKind::Type),
606 padding_left: false,
607 padding_right: false,
608 tooltip: None,
609 resolve_state: ResolveState::Resolved,
610 });
611 }
612 VariableLookupKind::Expression => {
613 let Ok(eval_task) = session.read_with(cx, |session, _| {
614 session.mode.request_dap(EvaluateCommand {
615 expression: inline_value_location.variable_name.clone(),
616 frame_id: Some(stack_frame_id),
617 source: None,
618 context: Some(EvaluateArgumentsContext::Variables),
619 })
620 }) else {
621 continue;
622 };
623
624 if let Some(response) = eval_task.await.log_err() {
625 inlay_hints.push(InlayHint {
626 position,
627 label: InlayHintLabel::String(format_value(response.result)),
628 kind: Some(InlayHintKind::Type),
629 padding_left: false,
630 padding_right: false,
631 tooltip: None,
632 resolve_state: ResolveState::Resolved,
633 });
634 };
635 }
636 };
637 }
638
639 Ok(inlay_hints)
640 })
641 }
642
643 pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
644 let mut tasks = vec![];
645 for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
646 tasks.push(self.shutdown_session(session_id, cx));
647 }
648
649 cx.background_executor().spawn(async move {
650 futures::future::join_all(tasks).await;
651 })
652 }
653
654 pub fn shutdown_session(
655 &mut self,
656 session_id: SessionId,
657 cx: &mut Context<Self>,
658 ) -> Task<Result<()>> {
659 let Some(session) = self.sessions.remove(&session_id) else {
660 return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
661 };
662
663 let shutdown_children = session
664 .read(cx)
665 .child_session_ids()
666 .iter()
667 .map(|session_id| self.shutdown_session(*session_id, cx))
668 .collect::<Vec<_>>();
669
670 let shutdown_parent_task = if let Some(parent_session) = session
671 .read(cx)
672 .parent_id(cx)
673 .and_then(|session_id| self.session_by_id(session_id))
674 {
675 let shutdown_id = parent_session.update(cx, |parent_session, _| {
676 parent_session.remove_child_session_id(session_id);
677
678 if parent_session.child_session_ids().len() == 0 {
679 Some(parent_session.session_id())
680 } else {
681 None
682 }
683 });
684
685 shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
686 } else {
687 None
688 };
689
690 let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
691
692 cx.background_spawn(async move {
693 if shutdown_children.len() > 0 {
694 let _ = join_all(shutdown_children).await;
695 }
696
697 shutdown_task.await;
698
699 if let Some(parent_task) = shutdown_parent_task {
700 parent_task.await?;
701 }
702
703 Ok(())
704 })
705 }
706
707 pub fn shared(
708 &mut self,
709 project_id: u64,
710 downstream_client: AnyProtoClient,
711 _: &mut Context<Self>,
712 ) {
713 self.downstream_client = Some((downstream_client.clone(), project_id));
714 }
715
716 pub fn unshared(&mut self, cx: &mut Context<Self>) {
717 self.downstream_client.take();
718
719 cx.notify();
720 }
721
722 async fn handle_run_debug_locator(
723 this: Entity<Self>,
724 envelope: TypedEnvelope<proto::RunDebugLocators>,
725 mut cx: AsyncApp,
726 ) -> Result<proto::DebugRequest> {
727 let task = envelope
728 .payload
729 .build_command
730 .context("missing definition")?;
731 let build_task = SpawnInTerminal::from_proto(task);
732 let locator = envelope.payload.locator;
733 let request = this
734 .update(&mut cx, |this, cx| {
735 this.run_debug_locator(&locator, build_task, cx)
736 })?
737 .await?;
738
739 Ok(request.to_proto())
740 }
741
742 async fn handle_get_debug_adapter_binary(
743 this: Entity<Self>,
744 envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
745 mut cx: AsyncApp,
746 ) -> Result<proto::DebugAdapterBinary> {
747 let definition = DebugTaskDefinition::from_proto(
748 envelope.payload.definition.context("missing definition")?,
749 )?;
750 let (tx, mut rx) = mpsc::unbounded();
751 let session_id = envelope.payload.session_id;
752 cx.spawn({
753 let this = this.clone();
754 async move |cx| {
755 while let Some(message) = rx.next().await {
756 this.read_with(cx, |this, _| {
757 if let Some((downstream, project_id)) = this.downstream_client.clone() {
758 downstream
759 .send(proto::LogToDebugConsole {
760 project_id,
761 session_id,
762 message,
763 })
764 .ok();
765 }
766 })
767 .ok();
768 }
769 }
770 })
771 .detach();
772
773 let binary = this
774 .update(&mut cx, |this, cx| {
775 this.get_debug_adapter_binary(definition, SessionId::from_proto(session_id), tx, cx)
776 })?
777 .await?;
778 Ok(binary.to_proto())
779 }
780
781 async fn handle_log_to_debug_console(
782 this: Entity<Self>,
783 envelope: TypedEnvelope<proto::LogToDebugConsole>,
784 mut cx: AsyncApp,
785 ) -> Result<()> {
786 let session_id = SessionId::from_proto(envelope.payload.session_id);
787 this.update(&mut cx, |this, cx| {
788 let Some(session) = this.sessions.get(&session_id) else {
789 return;
790 };
791 session.update(cx, |session, cx| {
792 session
793 .console_output(cx)
794 .unbounded_send(envelope.payload.message)
795 .ok();
796 })
797 })
798 }
799}
800
801#[derive(Clone)]
802pub struct DapAdapterDelegate {
803 fs: Arc<dyn Fs>,
804 console: mpsc::UnboundedSender<String>,
805 worktree: worktree::Snapshot,
806 node_runtime: NodeRuntime,
807 http_client: Arc<dyn HttpClient>,
808 toolchain_store: Arc<dyn LanguageToolchainStore>,
809 load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
810}
811
812impl DapAdapterDelegate {
813 pub fn new(
814 fs: Arc<dyn Fs>,
815 worktree: worktree::Snapshot,
816 status: mpsc::UnboundedSender<String>,
817 node_runtime: NodeRuntime,
818 http_client: Arc<dyn HttpClient>,
819 toolchain_store: Arc<dyn LanguageToolchainStore>,
820 load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
821 ) -> Self {
822 Self {
823 fs,
824 console: status,
825 worktree,
826 http_client,
827 node_runtime,
828 toolchain_store,
829 load_shell_env_task,
830 }
831 }
832}
833
834#[async_trait]
835impl dap::adapters::DapDelegate for DapAdapterDelegate {
836 fn worktree_id(&self) -> WorktreeId {
837 self.worktree.id()
838 }
839
840 fn worktree_root_path(&self) -> &Path {
841 &self.worktree.abs_path()
842 }
843 fn http_client(&self) -> Arc<dyn HttpClient> {
844 self.http_client.clone()
845 }
846
847 fn node_runtime(&self) -> NodeRuntime {
848 self.node_runtime.clone()
849 }
850
851 fn fs(&self) -> Arc<dyn Fs> {
852 self.fs.clone()
853 }
854
855 fn output_to_console(&self, msg: String) {
856 self.console.unbounded_send(msg).ok();
857 }
858
859 async fn which(&self, command: &OsStr) -> Option<PathBuf> {
860 which::which(command).ok()
861 }
862
863 async fn shell_env(&self) -> HashMap<String, String> {
864 let task = self.load_shell_env_task.clone();
865 task.await.unwrap_or_default()
866 }
867
868 fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
869 self.toolchain_store.clone()
870 }
871 async fn read_text_file(&self, path: PathBuf) -> Result<String> {
872 let entry = self
873 .worktree
874 .entry_for_path(&path)
875 .with_context(|| format!("no worktree entry for path {path:?}"))?;
876 let abs_path = self
877 .worktree
878 .absolutize(&entry.path)
879 .with_context(|| format!("cannot absolutize path {path:?}"))?;
880
881 self.fs.load(&abs_path).await
882 }
883}