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