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