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