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