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