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::{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::python::PythonLocator));
107            registry.add_locator(Arc::new(locators::go::GoLocator {}));
108        });
109        client.add_entity_request_handler(Self::handle_run_debug_locator);
110        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
111        client.add_entity_message_handler(Self::handle_log_to_debug_console);
112    }
113
114    #[expect(clippy::too_many_arguments)]
115    pub fn new_local(
116        http_client: Arc<dyn HttpClient>,
117        node_runtime: NodeRuntime,
118        fs: Arc<dyn Fs>,
119        environment: Entity<ProjectEnvironment>,
120        toolchain_store: Arc<dyn LanguageToolchainStore>,
121        worktree_store: Entity<WorktreeStore>,
122        breakpoint_store: Entity<BreakpointStore>,
123        cx: &mut Context<Self>,
124    ) -> Self {
125        let mode = DapStoreMode::Local(LocalDapStore {
126            fs,
127            environment,
128            http_client,
129            node_runtime,
130            toolchain_store,
131        });
132
133        Self::new(mode, breakpoint_store, worktree_store, cx)
134    }
135
136    pub fn new_ssh(
137        project_id: u64,
138        ssh_client: Entity<SshRemoteClient>,
139        breakpoint_store: Entity<BreakpointStore>,
140        worktree_store: Entity<WorktreeStore>,
141        cx: &mut Context<Self>,
142    ) -> Self {
143        let mode = DapStoreMode::Ssh(SshDapStore {
144            upstream_client: ssh_client.read(cx).proto_client(),
145            ssh_client,
146            upstream_project_id: project_id,
147        });
148
149        Self::new(mode, breakpoint_store, worktree_store, cx)
150    }
151
152    pub fn new_collab(
153        _project_id: u64,
154        _upstream_client: AnyProtoClient,
155        breakpoint_store: Entity<BreakpointStore>,
156        worktree_store: Entity<WorktreeStore>,
157        cx: &mut Context<Self>,
158    ) -> Self {
159        Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
160    }
161
162    fn new(
163        mode: DapStoreMode,
164        breakpoint_store: Entity<BreakpointStore>,
165        worktree_store: Entity<WorktreeStore>,
166        _cx: &mut Context<Self>,
167    ) -> Self {
168        Self {
169            mode,
170            next_session_id: 0,
171            downstream_client: None,
172            breakpoint_store,
173            worktree_store,
174            sessions: Default::default(),
175        }
176    }
177
178    pub fn get_debug_adapter_binary(
179        &mut self,
180        definition: DebugTaskDefinition,
181        session_id: SessionId,
182        console: UnboundedSender<String>,
183        cx: &mut Context<Self>,
184    ) -> Task<Result<DebugAdapterBinary>> {
185        match &self.mode {
186            DapStoreMode::Local(_) => {
187                let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
188                else {
189                    return Task::ready(Err(anyhow!("Failed to find a worktree")));
190                };
191                let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
192                    return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
193                };
194
195                let user_installed_path = ProjectSettings::get_global(cx)
196                    .dap
197                    .get(&adapter.name())
198                    .and_then(|s| s.binary.as_ref().map(PathBuf::from));
199
200                let delegate = self.delegate(&worktree, console, cx);
201                let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
202
203                cx.spawn(async move |this, cx| {
204                    let mut binary = adapter
205                        .get_binary(&delegate, &definition, user_installed_path, cx)
206                        .await?;
207
208                    let env = this
209                        .update(cx, |this, cx| {
210                            this.as_local()
211                                .unwrap()
212                                .environment
213                                .update(cx, |environment, cx| {
214                                    environment.get_directory_environment(cwd, cx)
215                                })
216                        })?
217                        .await;
218
219                    if let Some(mut env) = env {
220                        env.extend(std::mem::take(&mut binary.envs));
221                        binary.envs = env;
222                    }
223
224                    Ok(binary)
225                })
226            }
227            DapStoreMode::Ssh(ssh) => {
228                let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
229                    session_id: session_id.to_proto(),
230                    project_id: ssh.upstream_project_id,
231                    definition: Some(definition.to_proto()),
232                });
233                let ssh_client = ssh.ssh_client.clone();
234
235                cx.spawn(async move |_, cx| {
236                    let response = request.await?;
237                    let binary = DebugAdapterBinary::from_proto(response)?;
238                    let mut ssh_command = ssh_client.read_with(cx, |ssh, _| {
239                        anyhow::Ok(SshCommand {
240                            arguments: ssh.ssh_args().context("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::LOCALHOST;
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,
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                        anyhow::bail!(
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 binary = this
414                    .update(cx, |this, cx| {
415                        this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
416                    })?
417                    .await?;
418                session
419                    .update(cx, |session, cx| {
420                        session.boot(binary, worktree, dap_store, cx)
421                    })?
422                    .await
423            }
424        })
425    }
426
427    pub fn session_by_id(
428        &self,
429        session_id: impl Borrow<SessionId>,
430    ) -> Option<Entity<session::Session>> {
431        let session_id = session_id.borrow();
432        let client = self.sessions.get(session_id).cloned();
433
434        client
435    }
436    pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
437        self.sessions.values()
438    }
439
440    pub fn capabilities_by_id(
441        &self,
442        session_id: impl Borrow<SessionId>,
443        cx: &App,
444    ) -> Option<Capabilities> {
445        let session_id = session_id.borrow();
446        self.sessions
447            .get(session_id)
448            .map(|client| client.read(cx).capabilities.clone())
449    }
450
451    pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
452        &self.breakpoint_store
453    }
454
455    pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
456        &self.worktree_store
457    }
458
459    #[allow(dead_code)]
460    async fn handle_ignore_breakpoint_state(
461        this: Entity<Self>,
462        envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
463        mut cx: AsyncApp,
464    ) -> Result<()> {
465        let session_id = SessionId::from_proto(envelope.payload.session_id);
466
467        this.update(&mut cx, |this, cx| {
468            if let Some(session) = this.session_by_id(&session_id) {
469                session.update(cx, |session, cx| {
470                    session.set_ignore_breakpoints(envelope.payload.ignore, cx)
471                })
472            } else {
473                Task::ready(HashMap::default())
474            }
475        })?
476        .await;
477
478        Ok(())
479    }
480
481    fn delegate(
482        &self,
483        worktree: &Entity<Worktree>,
484        console: UnboundedSender<String>,
485        cx: &mut App,
486    ) -> Arc<dyn DapDelegate> {
487        let Some(local_store) = self.as_local() else {
488            unimplemented!("Starting session on remote side");
489        };
490
491        Arc::new(DapAdapterDelegate::new(
492            local_store.fs.clone(),
493            worktree.read(cx).snapshot(),
494            console,
495            local_store.node_runtime.clone(),
496            local_store.http_client.clone(),
497            local_store.toolchain_store.clone(),
498            local_store.environment.update(cx, |env, cx| {
499                env.get_worktree_environment(worktree.clone(), cx)
500            }),
501        ))
502    }
503
504    pub fn evaluate(
505        &self,
506        session_id: &SessionId,
507        stack_frame_id: u64,
508        expression: String,
509        context: EvaluateArgumentsContext,
510        source: Option<Source>,
511        cx: &mut Context<Self>,
512    ) -> Task<Result<EvaluateResponse>> {
513        let Some(client) = self
514            .session_by_id(session_id)
515            .and_then(|client| client.read(cx).adapter_client())
516        else {
517            return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
518        };
519
520        cx.background_executor().spawn(async move {
521            client
522                .request::<Evaluate>(EvaluateArguments {
523                    expression: expression.clone(),
524                    frame_id: Some(stack_frame_id),
525                    context: Some(context),
526                    format: None,
527                    line: None,
528                    column: None,
529                    source,
530                })
531                .await
532        })
533    }
534
535    pub fn completions(
536        &self,
537        session_id: &SessionId,
538        stack_frame_id: u64,
539        text: String,
540        completion_column: u64,
541        cx: &mut Context<Self>,
542    ) -> Task<Result<Vec<CompletionItem>>> {
543        let Some(client) = self
544            .session_by_id(session_id)
545            .and_then(|client| client.read(cx).adapter_client())
546        else {
547            return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
548        };
549
550        cx.background_executor().spawn(async move {
551            Ok(client
552                .request::<Completions>(CompletionsArguments {
553                    frame_id: Some(stack_frame_id),
554                    line: None,
555                    text,
556                    column: completion_column,
557                })
558                .await?
559                .targets)
560        })
561    }
562
563    pub fn resolve_inline_value_locations(
564        &self,
565        session: Entity<Session>,
566        stack_frame_id: StackFrameId,
567        buffer_handle: Entity<Buffer>,
568        inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
569        cx: &mut Context<Self>,
570    ) -> Task<Result<Vec<InlayHint>>> {
571        let snapshot = buffer_handle.read(cx).snapshot();
572        let all_variables = session.read(cx).variables_by_stack_frame_id(stack_frame_id);
573
574        fn format_value(mut value: String) -> String {
575            const LIMIT: usize = 100;
576
577            if value.len() > LIMIT {
578                value.truncate(LIMIT);
579                value.push_str("...");
580            }
581
582            format!(": {}", value)
583        }
584
585        cx.spawn(async move |_, cx| {
586            let mut inlay_hints = Vec::with_capacity(inline_value_locations.len());
587            for inline_value_location in inline_value_locations.iter() {
588                let point = snapshot.point_to_point_utf16(language::Point::new(
589                    inline_value_location.row as u32,
590                    inline_value_location.column as u32,
591                ));
592                let position = snapshot.anchor_after(point);
593
594                match inline_value_location.lookup {
595                    VariableLookupKind::Variable => {
596                        let Some(variable) = all_variables
597                            .iter()
598                            .find(|variable| variable.name == inline_value_location.variable_name)
599                        else {
600                            continue;
601                        };
602
603                        inlay_hints.push(InlayHint {
604                            position,
605                            label: InlayHintLabel::String(format_value(variable.value.clone())),
606                            kind: Some(InlayHintKind::Type),
607                            padding_left: false,
608                            padding_right: false,
609                            tooltip: None,
610                            resolve_state: ResolveState::Resolved,
611                        });
612                    }
613                    VariableLookupKind::Expression => {
614                        let Ok(eval_task) = session.read_with(cx, |session, _| {
615                            session.mode.request_dap(EvaluateCommand {
616                                expression: inline_value_location.variable_name.clone(),
617                                frame_id: Some(stack_frame_id),
618                                source: None,
619                                context: Some(EvaluateArgumentsContext::Variables),
620                            })
621                        }) else {
622                            continue;
623                        };
624
625                        if let Some(response) = eval_task.await.log_err() {
626                            inlay_hints.push(InlayHint {
627                                position,
628                                label: InlayHintLabel::String(format_value(response.result)),
629                                kind: Some(InlayHintKind::Type),
630                                padding_left: false,
631                                padding_right: false,
632                                tooltip: None,
633                                resolve_state: ResolveState::Resolved,
634                            });
635                        };
636                    }
637                };
638            }
639
640            Ok(inlay_hints)
641        })
642    }
643
644    pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
645        let mut tasks = vec![];
646        for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
647            tasks.push(self.shutdown_session(session_id, cx));
648        }
649
650        cx.background_executor().spawn(async move {
651            futures::future::join_all(tasks).await;
652        })
653    }
654
655    pub fn shutdown_session(
656        &mut self,
657        session_id: SessionId,
658        cx: &mut Context<Self>,
659    ) -> Task<Result<()>> {
660        let Some(session) = self.sessions.remove(&session_id) else {
661            return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
662        };
663
664        let shutdown_children = session
665            .read(cx)
666            .child_session_ids()
667            .iter()
668            .map(|session_id| self.shutdown_session(*session_id, cx))
669            .collect::<Vec<_>>();
670
671        let shutdown_parent_task = if let Some(parent_session) = session
672            .read(cx)
673            .parent_id(cx)
674            .and_then(|session_id| self.session_by_id(session_id))
675        {
676            let shutdown_id = parent_session.update(cx, |parent_session, _| {
677                parent_session.remove_child_session_id(session_id);
678
679                if parent_session.child_session_ids().len() == 0 {
680                    Some(parent_session.session_id())
681                } else {
682                    None
683                }
684            });
685
686            shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
687        } else {
688            None
689        };
690
691        let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
692
693        cx.background_spawn(async move {
694            if shutdown_children.len() > 0 {
695                let _ = join_all(shutdown_children).await;
696            }
697
698            shutdown_task.await;
699
700            if let Some(parent_task) = shutdown_parent_task {
701                parent_task.await?;
702            }
703
704            Ok(())
705        })
706    }
707
708    pub fn shared(
709        &mut self,
710        project_id: u64,
711        downstream_client: AnyProtoClient,
712        _: &mut Context<Self>,
713    ) {
714        self.downstream_client = Some((downstream_client.clone(), project_id));
715    }
716
717    pub fn unshared(&mut self, cx: &mut Context<Self>) {
718        self.downstream_client.take();
719
720        cx.notify();
721    }
722
723    async fn handle_run_debug_locator(
724        this: Entity<Self>,
725        envelope: TypedEnvelope<proto::RunDebugLocators>,
726        mut cx: AsyncApp,
727    ) -> Result<proto::DebugRequest> {
728        let task = envelope
729            .payload
730            .build_command
731            .context("missing definition")?;
732        let build_task = SpawnInTerminal::from_proto(task);
733        let locator = envelope.payload.locator;
734        let request = this
735            .update(&mut cx, |this, cx| {
736                this.run_debug_locator(&locator, build_task, cx)
737            })?
738            .await?;
739
740        Ok(request.to_proto())
741    }
742
743    async fn handle_get_debug_adapter_binary(
744        this: Entity<Self>,
745        envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
746        mut cx: AsyncApp,
747    ) -> Result<proto::DebugAdapterBinary> {
748        let definition = DebugTaskDefinition::from_proto(
749            envelope.payload.definition.context("missing definition")?,
750        )?;
751        let (tx, mut rx) = mpsc::unbounded();
752        let session_id = envelope.payload.session_id;
753        cx.spawn({
754            let this = this.clone();
755            async move |cx| {
756                while let Some(message) = rx.next().await {
757                    this.read_with(cx, |this, _| {
758                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
759                            downstream
760                                .send(proto::LogToDebugConsole {
761                                    project_id,
762                                    session_id,
763                                    message,
764                                })
765                                .ok();
766                        }
767                    })
768                    .ok();
769                }
770            }
771        })
772        .detach();
773
774        let binary = this
775            .update(&mut cx, |this, cx| {
776                this.get_debug_adapter_binary(definition, SessionId::from_proto(session_id), tx, cx)
777            })?
778            .await?;
779        Ok(binary.to_proto())
780    }
781
782    async fn handle_log_to_debug_console(
783        this: Entity<Self>,
784        envelope: TypedEnvelope<proto::LogToDebugConsole>,
785        mut cx: AsyncApp,
786    ) -> Result<()> {
787        let session_id = SessionId::from_proto(envelope.payload.session_id);
788        this.update(&mut cx, |this, cx| {
789            let Some(session) = this.sessions.get(&session_id) else {
790                return;
791            };
792            session.update(cx, |session, cx| {
793                session
794                    .console_output(cx)
795                    .unbounded_send(envelope.payload.message)
796                    .ok();
797            })
798        })
799    }
800}
801
802#[derive(Clone)]
803pub struct DapAdapterDelegate {
804    fs: Arc<dyn Fs>,
805    console: mpsc::UnboundedSender<String>,
806    worktree: worktree::Snapshot,
807    node_runtime: NodeRuntime,
808    http_client: Arc<dyn HttpClient>,
809    toolchain_store: Arc<dyn LanguageToolchainStore>,
810    load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
811}
812
813impl DapAdapterDelegate {
814    pub fn new(
815        fs: Arc<dyn Fs>,
816        worktree: worktree::Snapshot,
817        status: mpsc::UnboundedSender<String>,
818        node_runtime: NodeRuntime,
819        http_client: Arc<dyn HttpClient>,
820        toolchain_store: Arc<dyn LanguageToolchainStore>,
821        load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
822    ) -> Self {
823        Self {
824            fs,
825            console: status,
826            worktree,
827            http_client,
828            node_runtime,
829            toolchain_store,
830            load_shell_env_task,
831        }
832    }
833}
834
835#[async_trait]
836impl dap::adapters::DapDelegate for DapAdapterDelegate {
837    fn worktree_id(&self) -> WorktreeId {
838        self.worktree.id()
839    }
840
841    fn worktree_root_path(&self) -> &Path {
842        &self.worktree.abs_path()
843    }
844    fn http_client(&self) -> Arc<dyn HttpClient> {
845        self.http_client.clone()
846    }
847
848    fn node_runtime(&self) -> NodeRuntime {
849        self.node_runtime.clone()
850    }
851
852    fn fs(&self) -> Arc<dyn Fs> {
853        self.fs.clone()
854    }
855
856    fn output_to_console(&self, msg: String) {
857        self.console.unbounded_send(msg).ok();
858    }
859
860    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
861        which::which(command).ok()
862    }
863
864    async fn shell_env(&self) -> HashMap<String, String> {
865        let task = self.load_shell_env_task.clone();
866        task.await.unwrap_or_default()
867    }
868
869    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
870        self.toolchain_store.clone()
871    }
872    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
873        let entry = self
874            .worktree
875            .entry_for_path(&path)
876            .with_context(|| format!("no worktree entry for path {path:?}"))?;
877        let abs_path = self
878            .worktree
879            .absolutize(&entry.path)
880            .with_context(|| format!("cannot absolutize path {path:?}"))?;
881
882        self.fs.load(&abs_path).await
883    }
884}