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