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