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