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