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