dap_store.rs

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