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