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::{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::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments},
 20    client::SessionId,
 21    messages::Message,
 22    requests::{Completions, Evaluate},
 23};
 24use fs::Fs;
 25use futures::{
 26    StreamExt,
 27    channel::mpsc::{self, UnboundedSender},
 28    future::{Shared, join_all},
 29};
 30use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 31use http_client::HttpClient;
 32use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind, range_from_lsp};
 33use node_runtime::NodeRuntime;
 34
 35use remote::SshRemoteClient;
 36use rpc::{
 37    AnyProtoClient, TypedEnvelope,
 38    proto::{self},
 39};
 40use settings::{Settings, WorktreeId};
 41use std::{
 42    borrow::Borrow,
 43    collections::BTreeMap,
 44    ffi::OsStr,
 45    net::Ipv4Addr,
 46    path::{Path, PathBuf},
 47    sync::{Arc, Once},
 48};
 49use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
 50use util::{ResultExt as _, merge_json_value_into};
 51use worktree::Worktree;
 52
 53#[derive(Debug)]
 54pub enum DapStoreEvent {
 55    DebugClientStarted(SessionId),
 56    DebugSessionInitialized(SessionId),
 57    DebugClientShutdown(SessionId),
 58    DebugClientEvent {
 59        session_id: SessionId,
 60        message: Message,
 61    },
 62    Notification(String),
 63    RemoteHasInitialized,
 64}
 65
 66#[allow(clippy::large_enum_variant)]
 67enum DapStoreMode {
 68    Local(LocalDapStore),
 69    Ssh(SshDapStore),
 70    Collab,
 71}
 72
 73pub struct LocalDapStore {
 74    fs: Arc<dyn Fs>,
 75    node_runtime: NodeRuntime,
 76    http_client: Arc<dyn HttpClient>,
 77    environment: Entity<ProjectEnvironment>,
 78    toolchain_store: Arc<dyn LanguageToolchainStore>,
 79}
 80
 81pub struct SshDapStore {
 82    ssh_client: Entity<SshRemoteClient>,
 83    upstream_client: AnyProtoClient,
 84    upstream_project_id: u64,
 85}
 86
 87pub struct DapStore {
 88    mode: DapStoreMode,
 89    downstream_client: Option<(AnyProtoClient, u64)>,
 90    breakpoint_store: Entity<BreakpointStore>,
 91    worktree_store: Entity<WorktreeStore>,
 92    sessions: BTreeMap<SessionId, Entity<Session>>,
 93    next_session_id: u32,
 94}
 95
 96impl EventEmitter<DapStoreEvent> for DapStore {}
 97
 98impl DapStore {
 99    pub fn init(client: &AnyProtoClient, cx: &mut App) {
100        static ADD_LOCATORS: Once = Once::new();
101        ADD_LOCATORS.call_once(|| {
102            DapRegistry::global(cx).add_locator(Arc::new(locators::cargo::CargoLocator {}))
103        });
104        client.add_entity_request_handler(Self::handle_run_debug_locator);
105        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
106        client.add_entity_message_handler(Self::handle_log_to_debug_console);
107    }
108
109    #[expect(clippy::too_many_arguments)]
110    pub fn new_local(
111        http_client: Arc<dyn HttpClient>,
112        node_runtime: NodeRuntime,
113        fs: Arc<dyn Fs>,
114        environment: Entity<ProjectEnvironment>,
115        toolchain_store: Arc<dyn LanguageToolchainStore>,
116        worktree_store: Entity<WorktreeStore>,
117        breakpoint_store: Entity<BreakpointStore>,
118        cx: &mut Context<Self>,
119    ) -> Self {
120        let mode = DapStoreMode::Local(LocalDapStore {
121            fs,
122            environment,
123            http_client,
124            node_runtime,
125            toolchain_store,
126        });
127
128        Self::new(mode, breakpoint_store, worktree_store, cx)
129    }
130
131    pub fn new_ssh(
132        project_id: u64,
133        ssh_client: Entity<SshRemoteClient>,
134        breakpoint_store: Entity<BreakpointStore>,
135        worktree_store: Entity<WorktreeStore>,
136        cx: &mut Context<Self>,
137    ) -> Self {
138        let mode = DapStoreMode::Ssh(SshDapStore {
139            upstream_client: ssh_client.read(cx).proto_client(),
140            ssh_client,
141            upstream_project_id: project_id,
142        });
143
144        Self::new(mode, breakpoint_store, worktree_store, cx)
145    }
146
147    pub fn new_collab(
148        _project_id: u64,
149        _upstream_client: AnyProtoClient,
150        breakpoint_store: Entity<BreakpointStore>,
151        worktree_store: Entity<WorktreeStore>,
152        cx: &mut Context<Self>,
153    ) -> Self {
154        Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
155    }
156
157    fn new(
158        mode: DapStoreMode,
159        breakpoint_store: Entity<BreakpointStore>,
160        worktree_store: Entity<WorktreeStore>,
161        _cx: &mut Context<Self>,
162    ) -> Self {
163        Self {
164            mode,
165            next_session_id: 0,
166            downstream_client: None,
167            breakpoint_store,
168            worktree_store,
169            sessions: Default::default(),
170        }
171    }
172
173    pub fn get_debug_adapter_binary(
174        &mut self,
175        definition: DebugTaskDefinition,
176        session_id: SessionId,
177        console: UnboundedSender<String>,
178        cx: &mut Context<Self>,
179    ) -> Task<Result<DebugAdapterBinary>> {
180        match &self.mode {
181            DapStoreMode::Local(_) => {
182                let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
183                else {
184                    return Task::ready(Err(anyhow!("Failed to find a worktree")));
185                };
186                let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
187                    return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
188                };
189
190                let user_installed_path = ProjectSettings::get_global(cx)
191                    .dap
192                    .get(&adapter.name())
193                    .and_then(|s| s.binary.as_ref().map(PathBuf::from));
194
195                let delegate = self.delegate(&worktree, console, cx);
196                let cwd: Arc<Path> = definition
197                    .cwd()
198                    .unwrap_or(worktree.read(cx).abs_path().as_ref())
199                    .into();
200
201                cx.spawn(async move |this, cx| {
202                    let mut binary = adapter
203                        .get_binary(&delegate, &definition, user_installed_path, cx)
204                        .await?;
205
206                    let env = this
207                        .update(cx, |this, cx| {
208                            this.as_local()
209                                .unwrap()
210                                .environment
211                                .update(cx, |environment, cx| {
212                                    environment.get_directory_environment(cwd, cx)
213                                })
214                        })?
215                        .await;
216
217                    if let Some(mut env) = env {
218                        env.extend(std::mem::take(&mut binary.envs));
219                        binary.envs = env;
220                    }
221
222                    Ok(binary)
223                })
224            }
225            DapStoreMode::Ssh(ssh) => {
226                let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
227                    session_id: session_id.to_proto(),
228                    project_id: ssh.upstream_project_id,
229                    definition: Some(definition.to_proto()),
230                });
231                let ssh_client = ssh.ssh_client.clone();
232
233                cx.spawn(async move |_, cx| {
234                    let response = request.await?;
235                    let binary = DebugAdapterBinary::from_proto(response)?;
236                    let mut ssh_command = ssh_client.update(cx, |ssh, _| {
237                        anyhow::Ok(SshCommand {
238                            arguments: ssh
239                                .ssh_args()
240                                .ok_or_else(|| anyhow!("SSH arguments not found"))?,
241                        })
242                    })??;
243
244                    let mut connection = None;
245                    if let Some(c) = binary.connection {
246                        let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
247                        let port =
248                            dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
249
250                        ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
251                        connection = Some(TcpArguments {
252                            port: c.port,
253                            host: local_bind_addr,
254                            timeout: c.timeout,
255                        })
256                    }
257
258                    let (program, args) = wrap_for_ssh(
259                        &ssh_command,
260                        Some((&binary.command, &binary.arguments)),
261                        binary.cwd.as_deref(),
262                        binary.envs,
263                        None,
264                    );
265
266                    Ok(DebugAdapterBinary {
267                        command: program,
268                        arguments: args,
269                        envs: HashMap::default(),
270                        cwd: None,
271                        connection,
272                        request_args: binary.request_args,
273                    })
274                })
275            }
276            DapStoreMode::Collab => {
277                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
278            }
279        }
280    }
281
282    pub fn debug_scenario_for_build_task(
283        &self,
284        build: TaskTemplate,
285        adapter: SharedString,
286        cx: &mut App,
287    ) -> Option<DebugScenario> {
288        DapRegistry::global(cx)
289            .locators()
290            .values()
291            .find_map(|locator| locator.create_scenario(&build, &adapter))
292    }
293
294    pub fn run_debug_locator(
295        &mut self,
296        locator_name: &str,
297        build_command: SpawnInTerminal,
298        cx: &mut Context<Self>,
299    ) -> Task<Result<DebugRequest>> {
300        match &self.mode {
301            DapStoreMode::Local(_) => {
302                // Pre-resolve args with existing environment.
303                let locators = DapRegistry::global(cx).locators();
304                let locator = locators.get(locator_name);
305
306                if let Some(locator) = locator.cloned() {
307                    cx.background_spawn(async move {
308                        let result = locator
309                            .run(build_command.clone())
310                            .await
311                            .log_with_level(log::Level::Error);
312                        if let Some(result) = result {
313                            return Ok(result);
314                        }
315
316                        Err(anyhow!(
317                            "None of the locators for task `{}` completed successfully",
318                            build_command.label
319                        ))
320                    })
321                } else {
322                    Task::ready(Err(anyhow!(
323                        "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
324                        build_command.label
325                    )))
326                }
327            }
328            DapStoreMode::Ssh(ssh) => {
329                let request = ssh.upstream_client.request(proto::RunDebugLocators {
330                    project_id: ssh.upstream_project_id,
331                    build_command: Some(build_command.to_proto()),
332                    locator: locator_name.to_owned(),
333                });
334                cx.background_spawn(async move {
335                    let response = request.await?;
336                    DebugRequest::from_proto(response)
337                })
338            }
339            DapStoreMode::Collab => {
340                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
341            }
342        }
343    }
344
345    fn as_local(&self) -> Option<&LocalDapStore> {
346        match &self.mode {
347            DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
348            _ => None,
349        }
350    }
351
352    pub fn new_session(
353        &mut self,
354        label: SharedString,
355        adapter: DebugAdapterName,
356        parent_session: Option<Entity<Session>>,
357        cx: &mut Context<Self>,
358    ) -> Entity<Session> {
359        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
360
361        if let Some(session) = &parent_session {
362            session.update(cx, |session, _| {
363                session.add_child_session_id(session_id);
364            });
365        }
366
367        let session = Session::new(
368            self.breakpoint_store.clone(),
369            session_id,
370            parent_session,
371            label,
372            adapter,
373            cx,
374        );
375
376        self.sessions.insert(session_id, session.clone());
377        cx.notify();
378
379        cx.subscribe(&session, {
380            move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
381                SessionStateEvent::Shutdown => {
382                    this.shutdown_session(session_id, cx).detach_and_log_err(cx);
383                }
384                SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
385                SessionStateEvent::Running => {
386                    cx.emit(DapStoreEvent::DebugClientStarted(session_id));
387                }
388            }
389        })
390        .detach();
391
392        session
393    }
394
395    pub fn boot_session(
396        &self,
397        session: Entity<Session>,
398        definition: DebugTaskDefinition,
399        cx: &mut Context<Self>,
400    ) -> Task<Result<()>> {
401        let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next() else {
402            return Task::ready(Err(anyhow!("Failed to find a worktree")));
403        };
404
405        let dap_store = cx.weak_entity();
406        let console = session.update(cx, |session, cx| session.console_output(cx));
407        let session_id = session.read(cx).session_id();
408
409        cx.spawn({
410            let session = session.clone();
411            async move |this, cx| {
412                let mut binary = this
413                    .update(cx, |this, cx| {
414                        this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
415                    })?
416                    .await?;
417
418                if let Some(args) = definition.initialize_args {
419                    merge_json_value_into(args, &mut binary.request_args.configuration);
420                }
421
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    ) -> DapAdapterDelegate {
491        let Some(local_store) = self.as_local() else {
492            unimplemented!("Starting session on remote side");
493        };
494
495        DapAdapterDelegate::new(
496            local_store.fs.clone(),
497            worktree.read(cx).id(),
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_values(
568        &self,
569        session: Entity<Session>,
570        stack_frame_id: StackFrameId,
571        buffer_handle: Entity<Buffer>,
572        inline_values: Vec<lsp::InlineValue>,
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        cx.spawn(async move |_, cx| {
579            let mut inlay_hints = Vec::with_capacity(inline_values.len());
580            for inline_value in inline_values.iter() {
581                match inline_value {
582                    lsp::InlineValue::Text(text) => {
583                        inlay_hints.push(InlayHint {
584                            position: snapshot.anchor_after(range_from_lsp(text.range).end),
585                            label: InlayHintLabel::String(format!(": {}", text.text)),
586                            kind: Some(InlayHintKind::Type),
587                            padding_left: false,
588                            padding_right: false,
589                            tooltip: None,
590                            resolve_state: ResolveState::Resolved,
591                        });
592                    }
593                    lsp::InlineValue::VariableLookup(variable_lookup) => {
594                        let range = range_from_lsp(variable_lookup.range);
595
596                        let mut variable_name = variable_lookup
597                            .variable_name
598                            .clone()
599                            .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect());
600
601                        if !variable_lookup.case_sensitive_lookup {
602                            variable_name = variable_name.to_ascii_lowercase();
603                        }
604
605                        let Some(variable) = all_variables.iter().find(|variable| {
606                            if variable_lookup.case_sensitive_lookup {
607                                variable.name == variable_name
608                            } else {
609                                variable.name.to_ascii_lowercase() == variable_name
610                            }
611                        }) else {
612                            continue;
613                        };
614
615                        inlay_hints.push(InlayHint {
616                            position: snapshot.anchor_after(range.end),
617                            label: InlayHintLabel::String(format!(": {}", variable.value)),
618                            kind: Some(InlayHintKind::Type),
619                            padding_left: false,
620                            padding_right: false,
621                            tooltip: None,
622                            resolve_state: ResolveState::Resolved,
623                        });
624                    }
625                    lsp::InlineValue::EvaluatableExpression(expression) => {
626                        let range = range_from_lsp(expression.range);
627
628                        let expression = expression
629                            .expression
630                            .clone()
631                            .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect());
632
633                        let Ok(eval_task) = session.update(cx, |session, _| {
634                            session.mode.request_dap(EvaluateCommand {
635                                expression,
636                                frame_id: Some(stack_frame_id),
637                                source: None,
638                                context: Some(EvaluateArgumentsContext::Variables),
639                            })
640                        }) else {
641                            continue;
642                        };
643
644                        if let Some(response) = eval_task.await.log_err() {
645                            inlay_hints.push(InlayHint {
646                                position: snapshot.anchor_after(range.end),
647                                label: InlayHintLabel::String(format!(": {}", response.result)),
648                                kind: Some(InlayHintKind::Type),
649                                padding_left: false,
650                                padding_right: false,
651                                tooltip: None,
652                                resolve_state: ResolveState::Resolved,
653                            });
654                        };
655                    }
656                };
657            }
658
659            Ok(inlay_hints)
660        })
661    }
662
663    pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
664        let mut tasks = vec![];
665        for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
666            tasks.push(self.shutdown_session(session_id, cx));
667        }
668
669        cx.background_executor().spawn(async move {
670            futures::future::join_all(tasks).await;
671        })
672    }
673
674    pub fn shutdown_session(
675        &mut self,
676        session_id: SessionId,
677        cx: &mut Context<Self>,
678    ) -> Task<Result<()>> {
679        let Some(session) = self.sessions.remove(&session_id) else {
680            return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
681        };
682
683        let shutdown_children = session
684            .read(cx)
685            .child_session_ids()
686            .iter()
687            .map(|session_id| self.shutdown_session(*session_id, cx))
688            .collect::<Vec<_>>();
689
690        let shutdown_parent_task = if let Some(parent_session) = session
691            .read(cx)
692            .parent_id(cx)
693            .and_then(|session_id| self.session_by_id(session_id))
694        {
695            let shutdown_id = parent_session.update(cx, |parent_session, _| {
696                parent_session.remove_child_session_id(session_id);
697
698                if parent_session.child_session_ids().len() == 0 {
699                    Some(parent_session.session_id())
700                } else {
701                    None
702                }
703            });
704
705            shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
706        } else {
707            None
708        };
709
710        let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
711
712        cx.background_spawn(async move {
713            if shutdown_children.len() > 0 {
714                let _ = join_all(shutdown_children).await;
715            }
716
717            shutdown_task.await;
718
719            if let Some(parent_task) = shutdown_parent_task {
720                parent_task.await?;
721            }
722
723            Ok(())
724        })
725    }
726
727    pub fn shared(
728        &mut self,
729        project_id: u64,
730        downstream_client: AnyProtoClient,
731        _: &mut Context<Self>,
732    ) {
733        self.downstream_client = Some((downstream_client.clone(), project_id));
734    }
735
736    pub fn unshared(&mut self, cx: &mut Context<Self>) {
737        self.downstream_client.take();
738
739        cx.notify();
740    }
741
742    async fn handle_run_debug_locator(
743        this: Entity<Self>,
744        envelope: TypedEnvelope<proto::RunDebugLocators>,
745        mut cx: AsyncApp,
746    ) -> Result<proto::DebugRequest> {
747        let task = envelope
748            .payload
749            .build_command
750            .ok_or_else(|| anyhow!("missing definition"))?;
751        let build_task = SpawnInTerminal::from_proto(task);
752        let locator = envelope.payload.locator;
753        let request = this
754            .update(&mut cx, |this, cx| {
755                this.run_debug_locator(&locator, build_task, cx)
756            })?
757            .await?;
758
759        Ok(request.to_proto())
760    }
761
762    async fn handle_get_debug_adapter_binary(
763        this: Entity<Self>,
764        envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
765        mut cx: AsyncApp,
766    ) -> Result<proto::DebugAdapterBinary> {
767        let definition = DebugTaskDefinition::from_proto(
768            envelope
769                .payload
770                .definition
771                .ok_or_else(|| anyhow!("missing definition"))?,
772        )?;
773        let (tx, mut rx) = mpsc::unbounded();
774        let session_id = envelope.payload.session_id;
775        cx.spawn({
776            let this = this.clone();
777            async move |cx| {
778                while let Some(message) = rx.next().await {
779                    this.update(cx, |this, _| {
780                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
781                            downstream
782                                .send(proto::LogToDebugConsole {
783                                    project_id,
784                                    session_id,
785                                    message,
786                                })
787                                .ok();
788                        }
789                    })
790                    .ok();
791                }
792            }
793        })
794        .detach();
795
796        let binary = this
797            .update(&mut cx, |this, cx| {
798                this.get_debug_adapter_binary(definition, SessionId::from_proto(session_id), tx, cx)
799            })?
800            .await?;
801        Ok(binary.to_proto())
802    }
803
804    async fn handle_log_to_debug_console(
805        this: Entity<Self>,
806        envelope: TypedEnvelope<proto::LogToDebugConsole>,
807        mut cx: AsyncApp,
808    ) -> Result<()> {
809        let session_id = SessionId::from_proto(envelope.payload.session_id);
810        this.update(&mut cx, |this, cx| {
811            let Some(session) = this.sessions.get(&session_id) else {
812                return;
813            };
814            session.update(cx, |session, cx| {
815                session
816                    .console_output(cx)
817                    .unbounded_send(envelope.payload.message)
818                    .ok();
819            })
820        })
821    }
822}
823
824#[derive(Clone)]
825pub struct DapAdapterDelegate {
826    fs: Arc<dyn Fs>,
827    console: mpsc::UnboundedSender<String>,
828    worktree_id: WorktreeId,
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}
834
835impl DapAdapterDelegate {
836    pub fn new(
837        fs: Arc<dyn Fs>,
838        worktree_id: WorktreeId,
839        status: mpsc::UnboundedSender<String>,
840        node_runtime: NodeRuntime,
841        http_client: Arc<dyn HttpClient>,
842        toolchain_store: Arc<dyn LanguageToolchainStore>,
843        load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
844    ) -> Self {
845        Self {
846            fs,
847            console: status,
848            worktree_id,
849            http_client,
850            node_runtime,
851            toolchain_store,
852            load_shell_env_task,
853        }
854    }
855}
856
857#[async_trait(?Send)]
858impl dap::adapters::DapDelegate for DapAdapterDelegate {
859    fn worktree_id(&self) -> WorktreeId {
860        self.worktree_id
861    }
862
863    fn http_client(&self) -> Arc<dyn HttpClient> {
864        self.http_client.clone()
865    }
866
867    fn node_runtime(&self) -> NodeRuntime {
868        self.node_runtime.clone()
869    }
870
871    fn fs(&self) -> Arc<dyn Fs> {
872        self.fs.clone()
873    }
874
875    fn output_to_console(&self, msg: String) {
876        self.console.unbounded_send(msg).ok();
877    }
878
879    fn which(&self, command: &OsStr) -> Option<PathBuf> {
880        which::which(command).ok()
881    }
882
883    async fn shell_env(&self) -> HashMap<String, String> {
884        let task = self.load_shell_env_task.clone();
885        task.await.unwrap_or_default()
886    }
887
888    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
889        self.toolchain_store.clone()
890    }
891}