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