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