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