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