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