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