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