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