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