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