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