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