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