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