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